Coder Social home page Coder Social logo

fetch-event-source's Introduction

Fetch Event Source

This package provides a better API for making Event Source requests - also known as server-sent events - with all the features available in the Fetch API.

The default browser EventSource API imposes several restrictions on the type of request you're allowed to make: the only parameters you're allowed to pass in are the url and withCredentials, so:

  • You cannot pass in a request body: you have to encode all the information necessary to execute the request inside the URL, which is limited to 2000 characters in most browsers.
  • You cannot pass in custom request headers
  • You can only make GET requests - there is no way to specify another method.
  • If the connection is cut, you don't have any control over the retry strategy: the browser will silently retry for you a few times and then stop, which is not good enough for any sort of robust application.

This library provides an alternate interface for consuming server-sent events, based on the Fetch API. It is fully compatible with the Event Stream format, so if you already have a server emitting these events, you can consume it just like before. However, you now have greater control over the request and response so:

  • You can use any request method/headers/body, plus all the other functionality exposed by fetch(). You can even provide an alternate fetch() implementation, if the default browser implementation doesn't work for you.
  • You have access to the response object if you want to do some custom validation/processing before parsing the event source. This is useful in case you have API gateways (like nginx) in front of your application server: if the gateway returns an error, you might want to handle it correctly.
  • If the connection gets cut or an error occurs, you have full control over the retry strategy.

In addition, this library also plugs into the browser's Page Visibility API so the connection closes if the document is hidden (e.g., the user minimizes the window), and automatically retries with the last event ID when it becomes visible again. This reduces the load on your server by not having open connections unnecessarily (but you can opt out of this behavior if you want.)

Install

npm install @microsoft/fetch-event-source

Usage

// BEFORE:
const sse = new EventSource('/api/sse');
sse.onmessage = (ev) => {
    console.log(ev.data);
};

// AFTER:
import { fetchEventSource } from '@microsoft/fetch-event-source';

await fetchEventSource('/api/sse', {
    onmessage(ev) {
        console.log(ev.data);
    }
});

You can pass in all the other parameters exposed by the default fetch API, for example:

const ctrl = new AbortController();
fetchEventSource('/api/sse', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        foo: 'bar'
    }),
    signal: ctrl.signal,
});

You can add better error handling, for example:

class RetriableError extends Error { }
class FatalError extends Error { }

fetchEventSource('/api/sse', {
    async onopen(response) {
        if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
            return; // everything's good
        } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
            // client-side errors are usually non-retriable:
            throw new FatalError();
        } else {
            throw new RetriableError();
        }
    },
    onmessage(msg) {
        // if the server emits an error message, throw an exception
        // so it gets handled by the onerror callback below:
        if (msg.event === 'FatalError') {
            throw new FatalError(msg.data);
        }
    },
    onclose() {
        // if the server closes the connection unexpectedly, retry:
        throw new RetriableError();
    },
    onerror(err) {
        if (err instanceof FatalError) {
            throw err; // rethrow to stop the operation
        } else {
            // do nothing to automatically retry. You can also
            // return a specific retry interval here.
        }
    }
});

Compatibility

This library is written in typescript and targets ES2017 features supported by all evergreen browsers (Chrome, Firefox, Safari, Edge.) You might need to polyfill TextDecoder for old Edge (versions < 79), though:

require('fast-text-encoding');

Contributing

This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.

When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact [email protected] with any additional questions or comments.

fetch-event-source's People

Contributors

dependabot[bot] avatar firedog1024 avatar microsoft-github-operations[bot] avatar microsoftopensource avatar roxus avatar vishwam avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fetch-event-source's Issues

Possibility for Node.JS support?

Amazing package!
Just wanted to check if there could be a way to also support this in non-browser node.js environments.
I was able to get this working in node for my use case but it involved quite a bit of hacks.

Request triggered another tab

Sample

I have 2 chrome tab. I am getting request x tab. And changing x to y tab. I am coming back to x tab, getting o more request. How can I fix this error

requests aren't made when custom header is defined

instance below does not produce a request (unless the header definition is removed)

await new fetchEventSource( endpointUrl, {
    headers: {
         Accept: 'text/event-stream'
    },
    async onopen( response ) {
        console.log( 'open', response );
    },
    onerror ( err ) {
        console.log( 'error', err );
    },
    onmessage( ev ) {
        console.log( 'message', ev.data );
    },
    onclose ( ev ) {
        console.log( 'close', ev );
    }
});

Exponential reconnect backoff

When fetch-event-source loses it's connection to the server, it tries to reconnect rather aggressively. Given enough clients, this continious repeating traffic pattern may pose an unneccesary risk of overloading a router/ load balancer that might still be receiving the traffic.

grafik

This issue proposes to implement an exponential backoff behavior that increases the reconnect time with each unsuccessful reconnect iteration (see https://www.npmjs.com/package/exponential-backoff for an example implementation)

Request: Improve documentation for `openWhenHidden` property

The default for openWhenHidden is false. This means that the SSE will abort on the client side if the document is hidden, however the default SSE behaviour is that the SSE will continue even if the document is hidden.

Since this is different then normal SSE, I think there should be something in the readme that says something along the lines of "To continue SSE streaming when browser is hidden, set openWhenHidden flag to true". I'm happy to do so if this is approved.

Thank you!

update README to use 'new'

loading this script as a browser module and using the README call without new results in an error

fetchEventSource('/api/sse', { ... }); // TypeError: class constructors must be invoked with 'new'

如果接口:Failed to fetch错误,如何关闭连接?使用curRequestController.abort();未生效,请指教

const abortController = new AbortController();
const signal = abortController.signal;
const headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('x-client-token', token);
await fetchEventSource(Url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-client-token': ${token},
},
body: JSON.stringify({ chat_detail_id: chat_detail_id }),
signal: signal,
retry: false,
async onopen(response) {
console.log('Open:', response);
if (
response.ok &&
response.headers.get('content-type') === 'text/event-stream'
) {
console.log("everything's good");
} else if (
response.status >= 400 &&
response.status < 500 &&
response.status !== 429
) {
console.log('请求错误');
} else {
console.log('其他错误');
}
},
async onerror(error) {
console.log('Error:', error);
abortController.abort();
},
});

onmessage is not working on a Azure deployed site.

After doing send response data from api back to the post call, the onmessage function is not doing anything when we use it on hosted application on azure , can you please test it as its working on prem but not on Azure. Thank you so much.

connection close event is fired shortly after connection

Hello ,

I tried the same server side code and client side with this package and default event source . i see that server is getting close event as soon as connection is established . Just to make sure that my Server side code is not the issue I changed the client code to native event source and i cam seeing that it works as expected . i am interested to send header and other good things that this Package has . Can any one help me

My client side code (removed all other codes just to keep simple ) wrapped in useEffect reactHook

const fetchData = async () => {
      await fetchEventSource(`${serverBaseURL}`, {
        method: "POST",
        body: JSON.stringify({ userName, uniqueUserIdentifier }),
        headers: {
          Accept: "text/event-stream",
        },
        
        openWhenHidden: true,
        onclose() {
          console.log("Connection closed by the server");
        },
      });
    };
    fetchData();
  }, []);

Can not catch network connection error during streaming data

Hi all, I have an issue. I create request to the server it sends me response with code 200 after few times the server starts to send me data by chunks. If I switch off my internet connection nothing happens and I do not handle this error in my application.

Page Visibility based connection retry is not happening in Error scenario

As per the current implementation, whenever the page visibility changes the connection will be disconnected & then reconnect only after the page comes back to visibility (during the time of page hidden, no server call is triggered).
But in case of error scenario, even when page is hidden, we are triggering the reconnect request to server.

Sample code to replicate the issue:

fetchEventSource('https://sample.api-service.com/stream', {
    openWhenHidden: false,
    onopen (res) { throw new Error("Dummy Error"); },
    onerror (err) { return 1000; },
}

with the above code executed, the connection keeps failing and tries to reconnect for every 1 second.
So far, fine. But when we navigate away from the page (document visibility goes to hidden), even then the server calls are getting triggered in the background.

Need to handle this and make sure that the reconnect calls are not triggers even when page visibility is off.

Should abort trigger onclose?

Hello!

While I was working with server-sent events I've discovered similar shortcomings as you. Glad your library exists to fix them!

One issue I've discovered with your library is that calling ctrl.abort() won't result in either onclose or onerror calls. This makes it difficult to collocate code which is responsible of handling connection closing, be it by a server or manual.

Is there something I am missing? Would it make sense as a feature?

[Request]: Support the rest of the EventSource public APIs

Request:

  • As a Dev, I would like the ability to add event handlers for specific events dynamically after time of connection, so that I can have other events add event listeners based off a sse response.
  • As a Dev, I would like the ability to remove event handlers for specific events dynamically after time of connection, so that I can have other events remove event listeners based off a sse response.
  • As a Dev, I would like the ability to dispatch events for specific events dynamically, so that I can rebroadcast event after making changes to it a sse response.

Proposal:

Reasoning:

  • Allow for devs to dynamically attach and detach event handlers as needed.
  • Follow standards put in place in spec while expanding functionality.

"Event Stream" tab is empty in Chrome dev tools

Hi! Thank you very much for your work.

I was wondering if you have ever faced with this issue: "Event Stream" tab is empty in Chrome dev tools:

image

It seems that this problem is related to these issues:

At the moment, it looks like that the events will be shown only if the native EventSource used, but if you could share any workarounds or ideas about it, that would be amazing. Thanks!

CORS error while using open ai completions

fetchEventSource('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      Accept: 'text/event-stream',
      'Content-Type': 'application/json',
      Authorization: 'Bearer ' + process.env.NEXT_PUBLIC_OPENAI_KEY,
    },
    body: raw,
    signal: ctrl.signal,
    onopen(res) {
      console.log('onopen ', res);
    },

    onmessage(evt) {
      console.log('msg.data', evt.data);
      if (evt.data == '[DONE]') return;
    
    },

    onclose() {
      console.log('onclose - server error');
    },

    onerror(err) {
      console.log('onerror', err);
      throw err;
    },

Screenshot 2023-11-25 at 7 30 25 PM

onmessage called for comment lines

The onmessage callback is called with an empty message if the server sends \n:\n. The spec says that servers can choose to send : as a form of keep-alive message every 15 seconds or so.

The bug is here:

if (line.length === 0) {
// empty line denotes end of message. Trigger the callback and start a new message:
onMessage?.(message);
message = newMessage();
} else if (fieldLength > 0) { // exclude comments and lines with no values

The comment on the last line of that reads "exclude comments and lines with no values", but onMessage?.(message) is called unconditionally on a blank line. Instead, empty messages should not be emitted.

Luckily, for my use case, I control the server, so as a workaround, I can remove the extra trailing \n from the keep-alive message.

Warning due to incomplete sourcemaps

Description

The sourcemaps in the published package reference files that are not included in the published package.

When I use this package in my create-react-app project, I see the following warning:

WARNING in ./node_modules/@microsoft/fetch-event-source/lib/esm/fetch.js
Module Warning (from ./node_modules/source-map-loader/dist/cjs.js):
Failed to parse source map from '/[REDACTED]/node_modules/@microsoft/fetch-event-source/src/fetch.ts' file: Error: ENOENT: no such file or directory, open '/[REDACTED]/node_modules/@microsoft/fetch-event-source/src/fetch.ts'

fetch.js has //# sourceMappingURL=fetch.js.map. And fetch.js.map starts as follows:{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/fetch.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,OAA.

The "sources":["../../src/fetch.ts"] points to a file that doesn't exist in the package, which causes the warning to be emitted.

Proposed solution

Either (a) include the src directory in the published package, or (b) update tsconfig to inline the ts file content using the inlineSources option

Steps to reproduce

  1. Scaffold new app using create-react-app npx create-react-app my-app
  2. npm install --save @microsoft/fetch-event-source
  3. Add import {fetchEventSource} from '@microsoft/fetch-event-source to top of index.js
  4. npm run start, warnings should show

onerror not called

I do not see onerror called when the event source server is stopped . resulting in my client browser application to reload

Following is my implementation at client side

useEffect(() => {
    try{
        const controller = new AbortController();
        const fetchData = async () => {
          await fetchEventSource(`${serverBaseURL}/sse`, {
            method: 'POST',
            body: JSON.stringify({ userid: 'ABC123' }),
            headers: {
              Accept: 'text/event-stream'
            },
            signal: controller.signal,
            onopen(res) {
              if (res.ok && res.status === 200) {
                console.log('Connection made ', res);
              } else if (res.status >= 400 && res.status < 500 && res.status !== 429) {
                console.log('Client side error ', res);
              }
            },
            onmessage(event) {
              console.log(event.data);
              const parsedData = JSON.parse(event.data);
              setData(data => [parsedData]);
              if (event.event === 'FatalError') {
                throw new FatalError(event.data);
            }
            },
            onclose() {
                console.log('Closed')
               // if the server closes the connection unexpectedly, retry:
            //throw new RetriableError();
            },
            onerror(err) {
                if (err instanceof FatalError) {
                    throw err; // rethrow to stop the operation
                } else {
                    // do nothing to automatically retry. You can also
                    // return a specific retry interval here.
                }
            },
            openWhenHidden:true
          });
        };
        fetchData();
    }catch(err){
        console.log('ERROR '.err)
    }
  }, []);

fetch响应body不是的ReadableStream:苹果端qq浏览器中

在苹果端qq浏览器中会报错,看了下是fetch请求在这个浏览器中返回的response不一样,感觉回来的不是一个ReadableStream,没有body,但是有_bodyBlob和_bodyInit字段,响应回来的type是default,数据是一个blob的,然后这个组件读不到body报错了,这个问题怎么处理下可以在苹果端的qq浏览器中返回和其他浏览器一样的响应体ReadableStream

TypeError: failed to execute 'getreader' on 'readablestream': readablestreamdefaultreader constructor can only accept readable streams that are not yet locked to a reader

When stream.getReader in parse.js is called, sometimes I get the following error.

TypeError: failed to execute 'getreader' on 'readablestream': readablestreamdefaultreader constructor can only accept readable streams that are not yet locked to a reader

I tried to isolate the issue, but it seems to happen randomly. Any ideas to fix this, please?

Cannot find name 'ReadableStreamDefaultReadResult'

let result: ReadableStreamDefaultReadResult<Uint8Array>;

https://github.com/Azure/fetch-event-source/blob/1589ec1f49d96450f3bae9adb20ab4a5b3deb204/src/parse.ts#L24C61-L24C61

> @microsoft/[email protected] build
> tsc && tsc -p tsconfig.esm.json

src/parse.ts:24:17 - error TS2552: Cannot find name 'ReadableStreamDefaultReadResult'. Did you mean 'ReadableStreamDefaultReader'?

24     let result: ReadableStreamDefaultReadResult<Uint8Array>;
                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  node_modules/typescript/lib/lib.dom.d.ts:11665:13
    11665 declare var ReadableStreamDefaultReader: {
                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~
    'ReadableStreamDefaultReader' is declared here.


Found 1 error in src/parse.ts:24

connect disconnected after change Chrome Tabs

image
As u can see, when I CHANGE the tab page to another page ,the SSE will be CANCELED , and when I active the page, the SSE will reconnect .
I dont want SSE be canceled when change tab pages ,how can I to?

Avoid stale headers by supplying them as a function

One of the most common usages for headers is authentication. However, if one provides a token as part of a header to this library, its value will remain the same forever, even through retries and whatnot. If at a given moment the token expires and needs to be refreshed, there will be no hope in reconnecting, so the retry mechanism becomes quite pointless.

I propose to allow passing "headers" in as a function, in which case it will be called every time fetch() is, in order to recompute the potential dynamic values in them.

后端500报错,如何捕获报错信息?

class RetriableError extends Error { }
class FatalError extends Error { }

fetchEventSource('/api/sse', {
async onopen(response) {
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
return; // everything's good
} else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
// client-side errors are usually non-retriable:
throw new FatalError();
} else {
throw new RetriableError();
}
},
onmessage(msg) {
// if the server emits an error message, throw an exception
// so it gets handled by the onerror callback below:
if (msg.event === 'FatalError') {
throw new FatalError(msg.data);
}
},
onclose() {
// if the server closes the connection unexpectedly, retry:
throw new RetriableError();
},
onerror(err) {
if (err instanceof FatalError) {
throw err; // rethrow to stop the operation
} else {
// do nothing to automatically retry. You can also
// return a specific retry interval here.
}
}
});
看了官网的实例,似乎没办法获取到后端500报错的body中的detail,打印看过onopen中的response,似乎也没有参数能直接获取到
而且我发现onopen不抛异常的话onerror是捕获不了请求500的错误

Support `Headers` in init

Basically this comment says it:

* The request headers. FetchEventSource only supports the Record<string,string> format.

It would be nice to support Headers as well here, as failing requests because of this problem can be quite hard to debug.

handle basic request errors or update README with a demonstration

an error is seen in the browser tools using the below call TypeError: Error in body stream

await new fetchEventSource( `${endpointUrl}/path-does-not-exist`, {
    onerror ( err ) {
        console.log( 'error', err );
    },
    onclose ( ev ) {
        console.log( 'close', ev );
    }
});

How does one get error details, such as a response code 404, 401 or 400? Thank you :)

Error when charset is in response content-type header

My spring-boot application adds the charset automatically, on an event I get a closed connection and the following error:

Error: Expected content-type to be text/event-stream, Actual: text/event-stream;charset=UTF-8

The error you provided does not contain a stack trace.

This is my code, For openai API.

const abortController = new AbortController();
      await fetchEventSource(process.env.OPENAI_ENDPOINT + '/v1/chat/completions', {
        signal: abortController.signal,
        method: 'POST',
        headers: {
          Authorization: 'Bearer ' + process.env.OPENAI_KEY,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          model: model,
          messages: messagesAI,
          stream: true,
          temperature: 0,
        }),
        async onopen(response) {
          if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
            return; // everything's good
          } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
            // client-side errors are usually non-retriable:
            throw new FatalError();
          } else {
            throw new RetriableError();
          }
        },
        onmessage(msg) {
          if (msg.data === '[DONE]') {
            abortController.abort();
            return;
          }
        },
        onerror(err) {
          if (err instanceof FatalError) {
            console.error('[Request] FatalError', err);
          } else if (err instanceof RetriableError) {
            console.error('[Request] RetriableError', err);
          } else {
            console.error('[Request] error', err);
            throw err;
          }

          return;
        },
        openWhenHidden: true,
});

Each request will print an error message on the console:

image

Resurrect repo

@RoXuS @firedog1024 @vishwam @microsoftopensource is there anyone around to help ensure that this repository can accept some pull requests?

I think that this solution remains widely used, and it would be great if this repo could have some degree of maintenance to make sure that improvements such as #67 can reach the users.

Does React Native app supported?

Hello everyone,

I'd like to inquire about the compatibility of React Native with this project. I've reviewed the documentation and discussions but couldn't find any explicit mentions regarding React Native support. The only reference I came across is this closed PR: link to closed PR.

I've attempted to integrate this project into a React Native app, specifically for iOS on Apple TV. However, it seems that I'm encountering an issue where I cannot establish a connection.

Here's the error message I've encountered:

ReferenceError: document is not defined

You can view a screenshot of the error here.

I also experimented with adding openWhenHidden: true, but that only resulted in more errors.

Interestingly, when I tried the same integration with a web app, everything appeared to work correctly.

I appreciate any insights or guidance on this matter. Thanks!

503 RECEIVED

Request Headers sent
Accept: application/json, text/event-stream
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjJaUXBKM1VwYmpBWVhZR2FYRUpsOGxWMFRPSSIsImtpZCI6IjJaUXBKM1VwYmpBWVhZR2FYRUpsOGxWMFRPSSJ9.eyJhdWQiOiJhcGk6Ly82MjE3YzRjYy01Mjg2LTRjMTktYjUyMi01MTEyYTRhNTlmMWMiLCJp

@slf4j
@validated
@RestController
@crossorigin(origins = "http://localhost:3000")
@requiredargsconstructor

public class ResourceController {

@GetMapping(value = "/event/resources/usage", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Usage> getResourceUsage() {

    Random random = new Random();
    log.debug("getResourceUsage ");

    return Flux.interval(Duration.ofSeconds(1))
            .map(it -> new Usage(
                    random.nextInt(101),
                    random.nextInt(101),
                    new Date()));

}

}

use import paths that include extension

eg, change this

export { fetchEventSource, EventStreamContentType } from './fetch';

to this

export { fetchEventSource, EventStreamContentType } from './fetch.js';

this change would make the script sources usable as-is from a cdn

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.