UNPKG

@elefunc/fetcheventsource

Version:

FetchEventSource - combines the full power of fetch() with EventSource streaming. Supports ALL HTTP methods, request bodies, headers, and fetch options.

763 lines (541 loc) 28.3 kB
# FetchEventSource The **`FetchEventSource`** interface represents a connection to server-sent events that supports the [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) API options. Unlike [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource), `FetchEventSource` accepts all `fetch()` options including request bodies and custom HTTP methods. Like [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource), `FetchEventSource` automatically reconnects after network errors with a default delay of 5 seconds (5000ms). When the server responds with HTTP status 204 (No Content) or any other non-ok HTTP status, the connection is closed and no reconnection attempt is made. When called without `method`, `headers`, or `body` parameters, the constructor returns a standard [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) instance for backward compatibility. > [!NOTE] > > [`ReadableStream.prototype.events()`<br>`Response.prototype.events()` ↑](//js.rt.ht/Response/events) > > If you don't need strict `EventSource` API compatibility, consider using `Response.prototype.events()` instead. It's a simpler method that directly parses SSE streams from fetch responses: > ```js > const response = await fetch('https://api.example.com/events'); > for await (const { type, data } of response.body.events()) { > console.log(type, data); > } > ``` > Use `FetchEventSource` when you need the full EventSource API (readyState, automatic reconnection, onerror/onopen handlers, etc.). ## API Compatibility Commitment `FetchEventSource` preserves **100% API compatibility** with both `fetch()` and `EventSource`. This is a core design principle: - **No convenience methods**: The library will never add helper functions or shortcuts that don't exist in the standard APIs - **No proprietary extensions**: All options and behaviors strictly follow the `fetch()` and `EventSource` specifications - **Pure compatibility layer**: It simply combines the existing APIs without introducing new concepts or patterns - **Exhaustive testing**: The package includes a comprehensive test suite that ensures it behaves exactly like `EventSource` under all conditions This commitment ensures that `FetchEventSource` can serve as a drop-in replacement for `EventSource` while adding `fetch()` capabilities, without introducing any non-standard behaviors or API surface. > [!WARNING] > When **not used over HTTP/2**, SSE suffers from a limitation to the maximum number of open connections, which can be specially painful when opening various tabs as the limit is _per browser_ and set to a very low number (6). The issue has been marked as "Won't fix" in [Chrome](https://crbug.com/275955) and [Firefox](https://bugzil.la/906896). This limit is per browser + domain, so that means that you can open 6 SSE connections across all of the tabs to `www.example1.com` and another 6 SSE connections to `www.example2.com`. When using HTTP/2, the maximum number of simultaneous _HTTP streams_ is negotiated between the server and the client (defaults to 100). ## Constructor - [FetchEventSource()](#fetcheventsource-fetcheventsource-constructor) - : Creates a new `FetchEventSource` object. ## Static properties - [FetchEventSource.CONNECTING](#static-properties) (Read only) - : A numeric value of `0`, indicating that the connection has not yet been established. - [FetchEventSource.OPEN](#static-properties) (Read only) - : A numeric value of `1`, indicating that the connection is open and ready to receive events. - [FetchEventSource.CLOSED](#static-properties) (Read only) - : A numeric value of `2`, indicating that the connection is closed. ## Instance properties - [FetchEventSource.readyState](#fetcheventsource-readystate-property) (Read only) - : A number representing the state of the connection. Possible values are `0` (CONNECTING), `1` (OPEN), or `2` (CLOSED). - [FetchEventSource.url](#fetcheventsource-url-property) (Read only) - : A string representing the URL of the source. - [FetchEventSource.withCredentials](#fetcheventsource-withcredentials-property) (Read only) - : A boolean value indicating whether the `FetchEventSource` object was instantiated with cross-origin ([CORS](/en-US/docs/Web/HTTP/CORS)) credentials set (`true`), or not (`false`, the default). ## Instance methods - [FetchEventSource.close()](#fetcheventsource-close-method) - : Closes the connection. ## Event handlers - [FetchEventSource.onopen](#fetcheventsource-onopen-property) - : An event handler for the [open](#fetcheventsource-open-event) event, which is fired when the connection is opened. - [FetchEventSource.onmessage](#fetcheventsource-onmessage-property) - : An event handler for the [message](#fetcheventsource-message-event) event, which is fired when a message without an `event` field is received from the source. - [FetchEventSource.onerror](#fetcheventsource-onerror-property) - : An event handler for the [error](#fetcheventsource-error-event) event, which is fired when the source fails to open or when a connection error occurs. ## Events - [error](#fetcheventsource-error-event) - : Fired when a connection to an event source fails to open. - [message](#fetcheventsource-message-event) - : Fired when data is received from an event source. - [open](#fetcheventsource-open-event) - : Fired when a connection to an event source is opened. Additionally, the server can send custom event types with an event field, which will create named events. ## Server-Sent Events Protocol `FetchEventSource` implements the full SSE protocol, including: ### Field Processing - **Comments**: Lines starting with `:` are ignored - **Fields**: Lines are split on the first `:` character, with optional space after colon removed - **Blank lines**: Trigger message dispatch ### Special Fields - **`event:`** Sets the event type (defaults to "message") - **`data:`** Accumulates message data (multiple lines joined with `\n`) - **`id:`** Sets the last event ID (rejected if contains null, newline, or carriage return) - **`retry:`** Updates reconnection delay in milliseconds (must contain only ASCII digits) ### Data Handling - Multiple `data:` lines are concatenated with newline characters - Messages exceeding 50MB are skipped with an error event - Empty messages (no data) are not dispatched ## Example ```js const eventSource = new FetchEventSource('https://api.example.com/events', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer token' }, body: JSON.stringify({ channel: 'updates' }) }); eventSource.onopen = (event) => { console.log('Connection opened'); }; eventSource.onmessage = (event) => { console.log('Message:', event.data); }; eventSource.onerror = (event) => { console.log('Error occurred'); }; // Close the connection after 30 seconds setTimeout(() => { eventSource.close(); }, 30000); ``` ## Specifications `FetchEventSource` is not part of any specification. It combines the [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) API with [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) request options. The implementation strictly adheres to: - [HTML Living Standard - Server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html) for EventSource behavior - [Fetch Living Standard](https://fetch.spec.whatwg.org/) for fetch() options and request handling All behaviors, including reconnection logic, event parsing, and error handling, match the standards exactly. ## Browser compatibility Requires support for: - EventSource API - Fetch API - ReadableStream - TextDecoderStream - EventTarget - Private class fields The exhaustive test suite verifies compatibility across all major browsers and ensures that `FetchEventSource` behaves identically to native `EventSource` in every scenario, including edge cases and error conditions. ## See also - [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) - [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) - [Using server-sent events](/en-US/docs/Web/API/Server-sent_events/Using_server_sent_events) - [Fetch API](/en-US/docs/Web/API/Fetch_API) ## Implementation Notes ### Request Body Handling `FetchEventSource` preserves the original request body and attempts to clone it for each reconnection attempt. This works with cloneable body types like `Blob`, `Response.body`, and `FormData`. Non-cloneable bodies may not work correctly across reconnections. ### AbortSignal Handling When an external `signal` is provided in options, `FetchEventSource` uses `AbortSignal.any()` to combine it with an internal abort controller. This allows both external cancellation and internal connection management. ### Content-Type Validation The response Content-Type header must be exactly `text/event-stream` (case-insensitive, before any semicolon). Other content types will close the connection without retry. ### URL.parse Polyfill The implementation includes a polyfill for `URL.parse()` to support environments where it's not available (pre-Baseline September 2024). --- # FetchEventSource: FetchEventSource() constructor The **`FetchEventSource()`** constructor creates a new [FetchEventSource](#fetcheventsource) object to establish a connection to a server to receive server-sent events. ## Syntax ```js-nolint new FetchEventSource(url) new FetchEventSource(url, options) ``` ### Parameters - `url` - : A string that represents the location of the remote resource serving the events/messages. - `options` (optional) - : An object containing any custom settings you want to apply to the request. Accepts all [RequestInit](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit) options from the Fetch API. Notable options include: - `method` (optional) - : The request method, e.g., `"GET"`, `"POST"`, `"PUT"`, `"DELETE"`. - `headers` (optional) - : Any headers you want to add to your request, contained within a [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object or object literal. - `body` (optional) - : Any body you want to add to your request: this can be a `Blob`, an [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), a [TypedArray](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray), a [DataView](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView), a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData), a [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams), string object or literal, or a [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) object. **Important**: When specifying a body, you must also specify a method (e.g., POST, PUT) as GET/HEAD requests cannot have a body. - `mode` (optional) - : The mode you want to use for the request, e.g., `cors`, `no-cors`, or `same-origin`. - `credentials` (optional) - : Controls what browsers do with credentials (cookies, HTTP authentication entries, and TLS client certificates). - `signal` (optional) - : An [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) object that allows you to communicate with a fetch request and abort it if required via an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). - `withCredentials` (optional) - : A boolean value indicating if CORS should be set to include credentials. This option is only used when `body` is not provided, for backward compatibility with standard [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource). ### Return value A [FetchEventSource](#fetcheventsource) object configured with the provided options. If `method`, `headers`, and `body` are all `undefined`, returns a standard [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) instance instead. > [!NOTE] > When `method`, `headers`, and `body` are all `undefined`, the constructor returns a standard [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) for backward compatibility. An empty string (`""`) is considered a valid body and will create a `FetchEventSource` instance. The class includes protection against malicious servers that send extremely large messages - any message exceeding 50MB will be skipped and an error event will be dispatched. > > **Body Requires Method**: When providing a `body` parameter, you **must** also specify a `method` (typically POST or PUT). Attempting to send a body with the default GET method will result in a TypeError, as GET/HEAD requests cannot have a body according to the HTTP specification. > > **Automatic Headers**: FetchEventSource automatically sets the following headers: > - `Cache-Control: no-store` (to prevent caching) > - `Accept: text/event-stream` (to request SSE format) > - `Last-Event-ID: <id>` (if reconnecting after receiving an event ID) > > **Credentials Handling**: The `withCredentials` option is mapped to the fetch `credentials` option: > - `withCredentials: true` → `credentials: 'include'` > - `withCredentials: false` → `credentials: 'same-origin'` (default) ## Examples ### Basic usage ```js // Returns a standard EventSource (no method, headers, or body) const eventSource = new FetchEventSource('https://api.example.com/events'); eventSource.onmessage = (event) => { console.log(event.data); }; ``` ### POST request with JSON body ```js const eventSource = new FetchEventSource('https://api.example.com/events', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ channel: 'updates', userId: '12345' }) }); eventSource.onopen = () => { console.log('Connection established'); }; eventSource.onmessage = (event) => { const data = JSON.parse(event.data); console.log('Received:', data); }; ``` ### GET request with custom headers ```js // Returns FetchEventSource instance because headers are provided const eventSource = new FetchEventSource('https://api.example.com/events', { headers: { 'Authorization': 'Bearer token123', 'X-Custom-Header': 'value' } }); eventSource.onmessage = (event) => { console.log('Authenticated message:', event.data); }; ``` ### Using AbortController ```js const controller = new AbortController(); const eventSource = new FetchEventSource('https://api.example.com/events', { method: 'POST', body: 'subscribe=true', signal: controller.signal }); // Abort the connection after 5 seconds setTimeout(() => { controller.abort(); }, 5000); ``` ### Common Mistake: Body Without Method ```js // ❌ WRONG - This will throw a TypeError const eventSource = new FetchEventSource('https://api.example.com/events', { body: 'subscribe=true' // Missing method! }); // Error: "Request with GET/HEAD method cannot have body" // ✅ CORRECT - Always specify method when using body const eventSource = new FetchEventSource('https://api.example.com/events', { method: 'POST', body: 'subscribe=true' }); ``` --- # FetchEventSource: readyState property The **`readyState`** read-only property of the [FetchEventSource](#fetcheventsource) interface returns a number representing the state of the connection. ## Value A number which can be one of three values: - `0` — connecting - `1` — open - `2` — closed ## Examples ```js const eventSource = new FetchEventSource('https://api.example.com/events', { method: 'POST', body: 'subscribe=true' }); console.log(eventSource.readyState); // 0 (connecting) eventSource.onopen = () => { console.log(eventSource.readyState); // 1 (open) }; eventSource.onerror = () => { if (eventSource.readyState === FetchEventSource.CONNECTING) { console.log('Reconnecting...'); } else if (eventSource.readyState === FetchEventSource.CLOSED) { console.log('Connection closed'); } }; ``` --- # FetchEventSource: url property The **`url`** read-only property of the [FetchEventSource](#fetcheventsource) interface returns a string containing the URL of the source. ## Value A string. ## Examples ```js const eventSource = new FetchEventSource('https://api.example.com/events', { method: 'POST', body: 'subscribe=true' }); console.log(eventSource.url); // "https://api.example.com/events" ``` --- # FetchEventSource: withCredentials property The **`withCredentials`** read-only property of the [FetchEventSource](#fetcheventsource) interface returns a boolean value indicating whether the `FetchEventSource` object was instantiated with CORS credentials set. ## Value A boolean value indicating whether the `FetchEventSource` object was instantiated with CORS credentials set (`true`), or not (`false`, the default). ## Examples ```js const eventSource = new FetchEventSource('https://api.example.com/events', { credentials: 'include' }); console.log(eventSource.withCredentials); // true const eventSource2 = new FetchEventSource('https://api.example.com/events'); console.log(eventSource2.withCredentials); // false ``` --- # FetchEventSource: close() method The **`close()`** method of the [FetchEventSource](#fetcheventsource) interface closes the connection, if one is made, and sets the [FetchEventSource.readyState](#fetcheventsource-readystate-property) attribute to `2` (closed). If the connection is already closed, the method does nothing. When closing, the method: 1. Sets the internal "explicitly closed" flag to prevent reconnection 2. Immediately sets `readyState` to `CLOSED` 3. Clears any pending reconnection timeout 4. Cancels the active stream reader (if any) 5. Aborts the fetch request via the internal AbortController ## Syntax ```js-nolint close() ``` ### Parameters None. ### Return value None ([undefined](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined)). ## Examples ```js const eventSource = new FetchEventSource('https://api.example.com/events', { method: 'POST', body: 'subscribe=true' }); eventSource.onopen = () => { console.log('Connected'); }; // Close the connection after 10 seconds setTimeout(() => { eventSource.close(); console.log('Connection closed'); }, 10000); ``` --- # FetchEventSource: error event The **`error`** event of the [FetchEventSource](#fetcheventsource) interface is fired when a connection to an event source fails to be opened or when an error occurs during communication. Like [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource), `FetchEventSource` automatically attempts to reconnect after network errors, with the connection state changing to `CONNECTING` during reconnection attempts. However, no reconnection is attempted for HTTP error responses (including 204 No Content) or Content-Type mismatches. ## Syntax Use the event name in methods like [addEventListener()](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener), or set an event handler property. ```js addEventListener("error", (event) => {}); onerror = (event) => {}; ``` ### Reconnection Behavior When a network error occurs: 1. The `readyState` changes to `CONNECTING` (0) 2. An error event is dispatched 3. A reconnection attempt is scheduled after the retry delay 4. The retry delay starts at 5 seconds and can be updated by the server No reconnection occurs if: - The connection was explicitly closed via `close()` - The external AbortSignal was aborted - The server returned a non-200 status or wrong Content-Type ## Event type An [ErrorEvent](https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent) with the following properties: - `message`: A string describing the error - `error`: The actual Error object (when available) ## Examples ```js const eventSource = new FetchEventSource('https://api.example.com/events', { method: 'POST', body: 'subscribe=true' }); // Using addEventListener eventSource.addEventListener('error', (event) => { if (eventSource.readyState === FetchEventSource.CONNECTING) { console.log('Connection lost. Attempting to reconnect...'); } else if (eventSource.readyState === FetchEventSource.CLOSED) { console.log('Connection closed'); } }); // Using the onerror property eventSource.onerror = (event) => { console.error('An error occurred'); }; ``` --- # FetchEventSource: message event The **`message`** event of the [FetchEventSource](#fetcheventsource) interface is fired when data is received from the event source. A `message` event is fired when the event source sends an event that either has no event field or has the event field set to `message`. ## Syntax Use the event name in methods like [addEventListener()](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener), or set an event handler property. ```js addEventListener("message", (event) => {}); onmessage = (event) => {}; ``` ## Event type A [MessageEvent](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent). Inherits from [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event). ## Event properties _This interface also inherits properties from its parent, [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event)._ - [MessageEvent.data](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data) (Read only) - : The data sent by the message emitter. - [MessageEvent.origin](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/origin) (Read only) - : The origin of the response URL for server-sent events. - [MessageEvent.lastEventId](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/lastEventId) (Read only) - : A string representing a unique ID for the event. - [MessageEvent.source](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/source) (Read only) - : `null` for server-sent events. - [MessageEvent.ports](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/ports) (Read only) - : An empty array for server-sent events. ## Examples ```js const eventSource = new FetchEventSource('https://api.example.com/events', { method: 'POST', body: 'subscribe=true' }); // Using addEventListener eventSource.addEventListener('message', (event) => { console.log('Received data:', event.data); console.log('Event ID:', event.lastEventId); }); // Using the onmessage property eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); console.log('Parsed data:', data); } catch (e) { console.log('Raw data:', event.data); } }; ``` --- # FetchEventSource: open event The **`open`** event of the [FetchEventSource](#fetcheventsource) interface is fired when a connection to an event source is opened. ## Syntax Use the event name in methods like [addEventListener()](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener), or set an event handler property. ```js addEventListener("open", (event) => {}); onopen = (event) => {}; ``` ## Event type A generic [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event). ## Examples ```js const eventSource = new FetchEventSource('https://api.example.com/events', { method: 'POST', body: 'subscribe=true' }); // Using addEventListener eventSource.addEventListener('open', (event) => { console.log('Connection opened'); console.log('Ready state:', eventSource.readyState); // 1 }); // Using the onopen property eventSource.onopen = (event) => { console.log('Successfully connected to event stream'); }; ``` --- # FetchEventSource: onopen property The **`onopen`** property of the [FetchEventSource](#fetcheventsource) interface is an event handler for the [open](#fetcheventsource-open-event) event; this event is fired when a connection to the server is opened. ## Syntax ```js-nolint eventSource.onopen = function; ``` ## Examples ```js const eventSource = new FetchEventSource('https://api.example.com/events', { method: 'POST', body: 'subscribe=true' }); eventSource.onopen = (event) => { console.log('Connection opened'); console.log('Ready state:', eventSource.readyState); // 1 (OPEN) }; ``` --- # FetchEventSource: onmessage property The **`onmessage`** property of the [FetchEventSource](#fetcheventsource) interface is an event handler for the [message](#fetcheventsource-message-event) event; this event is fired when data is received from the server with no event field or with the event field set to `message`. ## Syntax ```js-nolint eventSource.onmessage = function; ``` ## Examples ```js const eventSource = new FetchEventSource('https://api.example.com/events', { method: 'POST', body: 'subscribe=true' }); eventSource.onmessage = (event) => { console.log('Message received:', event.data); console.log('Last event ID:', event.lastEventId); try { const data = JSON.parse(event.data); console.log('Parsed data:', data); } catch (e) { console.log('Non-JSON data:', event.data); } }; ``` --- # FetchEventSource: onerror property The **`onerror`** property of the [FetchEventSource](#fetcheventsource) interface is an event handler for the [error](#fetcheventsource-error-event) event; this event is fired when an error occurs. When a network error occurs, `FetchEventSource` automatically attempts to reconnect. During reconnection, the `readyState` changes to `CONNECTING` (0). ## Syntax ```js-nolint eventSource.onerror = function; ``` ## Examples ```js const eventSource = new FetchEventSource('https://api.example.com/events', { method: 'POST', body: 'subscribe=true' }); eventSource.onerror = (event) => { if (eventSource.readyState === FetchEventSource.CONNECTING) { console.log('Connection lost. Attempting to reconnect...'); } else if (eventSource.readyState === FetchEventSource.CLOSED) { console.log('Connection closed permanently'); } }; ``` --- # FetchEventSource: Custom events The [FetchEventSource](#fetcheventsource) interface supports custom event types sent by the server. When the server sends an event with a custom event field, `FetchEventSource` dispatches a named event of that type. ## Syntax ```js addEventListener("eventname", (event) => {}); ``` ## Event type A [MessageEvent](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent) with the same properties as the [message](#fetcheventsource-message-event) event. ## Examples ```js const eventSource = new FetchEventSource('https://api.example.com/events', { method: 'POST', body: 'subscribe=true' }); // Listen for custom 'update' events eventSource.addEventListener('update', (event) => { console.log('Update event:', event.data); }); // Listen for custom 'notification' events eventSource.addEventListener('notification', (event) => { const notification = JSON.parse(event.data); console.log('Notification:', notification); }); // Listen for custom 'ping' events eventSource.addEventListener('ping', (event) => { console.log('Ping received at:', new Date()); }); ``` ### Server-side example The server sends custom events by including an event field: ``` event: update data: {"status": "processing"} event: notification data: {"type": "info", "message": "Task completed"} event: ping data: {"timestamp": 1234567890} ```