@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
Markdown
# 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}
```