better-sse 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Matthew W.
3
+ Copyright (c) 2025 Matthew W.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -7,9 +7,9 @@
7
7
  <a href="https://github.com/MatthewWid/better-sse"><img src="https://img.shields.io/github/stars/MatthewWid/better-sse?style=social" /></a>
8
8
  </p>
9
9
 
10
- A dead simple, dependency-less, spec-compliant server-sent events implementation for Node, written in TypeScript.
10
+ A dead simple, dependency-less, spec-compliant server-sent events implementation written in TypeScript.
11
11
 
12
- This package aims to be the easiest to use, most compliant and most streamlined solution to server-sent events with Node that is framework-agnostic and feature-rich.
12
+ This package aims to be the easiest to use, most compliant and most streamlined solution to server-sent events that is framework-agnostic and feature-rich.
13
13
 
14
14
  Please consider starring the project [on GitHub ⭐](https://github.com/MatthewWid/better-sse).
15
15
 
@@ -21,115 +21,144 @@ Using SSE can allow for significant savings in bandwidth and battery life on por
21
21
 
22
22
  Compared to WebSockets it has comparable performance and bandwidth usage, especially over HTTP/2, and natively includes event ID generation and automatic reconnection when clients are disconnected.
23
23
 
24
+ Read the [Getting Started](https://matthewwid.github.io/better-sse/guides/getting-started/) guide for more.
25
+
24
26
  * [Comparison: Server-sent Events vs WebSockets vs Polling](https://medium.com/dailyjs/a-comparison-between-websockets-server-sent-events-and-polling-7a27c98cb1e3)
25
27
  * [WHATWG standards section for server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html)
26
28
  * [MDN guide to server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
29
+ * [Can I use... Server-sent events](https://caniuse.com/eventsource)
27
30
 
28
31
  ## Highlights
29
32
 
30
- * Compatible with all popular Node HTTP frameworks ([Express](https://nodejs.org/api/http.html), [Fastify](https://fastify.dev/), [Nest](https://nestjs.com/), [Next.js](https://nextjs.org/), etc.)
33
+ * Compatible with all popular HTTP frameworks ([Express](https://nodejs.org/api/http.html), [Hono](https://hono.dev/), [Fastify](https://fastify.dev/), [Nest](https://nestjs.com/), [Next.js](https://nextjs.org/), [Bun](https://bun.sh/docs/api/http), [Deno](https://docs.deno.com/runtime/fundamentals/http_server/), [etc.](https://matthewwid.github.io/better-sse/reference/recipes/))
31
34
  * Fully written in TypeScript (+ ships with types directly).
32
35
  * [Thoroughly tested](./src/Session.test.ts) (+ 100% code coverage!).
33
- * [Comprehensively documented](./docs) with guides and API documentation.
34
- * [Channels](./docs/channels.md) allow you to broadcast events to many clients at once.
36
+ * [Comprehensively documented](https://matthewwid.github.io/better-sse) with guides and API documentation.
37
+ * [Channels](https://matthewwid.github.io/better-sse/guides/channels) allow you to broadcast events to many clients at once.
38
+ * [Event buffers](https://matthewwid.github.io/better-sse/guides/batching/) allow you to batch events for increased performance and lower bandwidth usage.
35
39
  * Configurable reconnection time, message serialization and data sanitization (with good defaults).
36
40
  * Trust or ignore the client-given last event ID.
37
41
  * Automatically send keep-alive pings to keep connections open.
38
42
  * Add or override the response status code and headers.
39
- * Send [individual fields](./docs/api.md#eventbuffer) of events or send [full events with simple helpers](./docs/api.md#sessionpush-data-unknown-eventname-string-eventid-string--this).
40
43
  * Pipe [streams](https://nodejs.org/api/stream.html#stream_readable_streams) and [iterables](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators) directly from the server to the client as a series of events.
41
44
  * Support for popular EventSource polyfills [`event-source-polyfill`](https://www.npmjs.com/package/event-source-polyfill) and [`eventsource-polyfill`](https://www.npmjs.com/package/eventsource-polyfill).
42
45
 
43
- [See a comparison with other Node SSE libraries in the documentation.](./docs/comparison.md)
46
+ [See a comparison with other SSE libraries in the documentation.](https://matthewwid.github.io/better-sse/reference/comparison)
44
47
 
45
48
  # Installation
46
49
 
47
- ```bash
48
- # npm
50
+ Install with any package manager:
51
+
52
+ ```sh
49
53
  npm install better-sse
54
+ ```
50
55
 
51
- # Yarn
56
+ ```sh
52
57
  yarn add better-sse
58
+ ```
53
59
 
54
- # pnpm
60
+ ```sh
55
61
  pnpm add better-sse
56
62
  ```
57
63
 
64
+ ```sh
65
+ bun add better-sse
66
+ ```
67
+
68
+ ```sh
69
+ deno install npm:better-sse
70
+ ```
71
+
58
72
  _Better SSE ships with types built in. No need to install from DefinitelyTyped for TypeScript users!_
59
73
 
60
74
  # Usage
61
75
 
62
- The following example shows usage with [Express](http://expressjs.com/), but Better SSE works with any web-server framework that uses the underlying Node [HTTP module](https://nodejs.org/api/http.html).
76
+ The examples below show usage with [Express](http://expressjs.com/) and [Hono](https://hono.dev/), but Better SSE works with any web-server framework that uses the Node [HTTP module](https://nodejs.org/api/http.html) or the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
63
77
 
64
- See the [Recipes](./docs/recipes.md) section of the documentation for use with other frameworks and libraries.
78
+ See the [Recipes](https://matthewwid.github.io/better-sse/reference/recipes/) section of the documentation for use with other frameworks and libraries.
65
79
 
66
80
  ---
67
81
 
68
- Use [sessions](./docs/api.md#session) to push events to clients:
82
+ Use [sessions](https://matthewwid.github.io/better-sse/guides/getting-started/#create-a-session) to push events to clients:
69
83
 
70
84
  ```typescript
71
- // Server
72
- import { createSession } from "better-sse";
85
+ // Server - Express
86
+ import { createSession } from "better-sse"
73
87
 
74
88
  app.get("/sse", async (req, res) => {
75
- const session = await createSession(req, res);
89
+ const session = await createSession(req, res)
90
+ session.push("Hello world!", "message")
91
+ })
92
+ ```
76
93
 
77
- session.push("Hello world!");
78
- });
94
+ ```typescript
95
+ // Server - Hono
96
+ import { createResponse } from "better-sse"
97
+
98
+ app.get("/sse", (c) =>
99
+ createResponse(c.req.raw, (session) => {
100
+ session.push("Hello world!", "message")
101
+ })
102
+ )
79
103
  ```
80
104
 
81
105
  ```typescript
82
106
  // Client
83
- const sse = new EventSource("/sse");
107
+ const eventSource = new EventSource("/sse")
84
108
 
85
- sse.addEventListener("message", ({ data }) => {
86
- console.log(JSON.parse(data));
87
- });
109
+ eventSource.addEventListener("message", ({ data })) => {
110
+ const contents = JSON.parse(data)
111
+ console.log(contents) // Hello world!
112
+ })
88
113
  ```
89
114
 
90
- Use [channels](./docs/channels.md) to send events to many clients at once:
115
+ Use [channels](https://matthewwid.github.io/better-sse/guides/channels/#create-a-channel) to send events to many clients at once:
91
116
 
92
117
  ```typescript
93
- import { createSession, createChannel } from "better-sse";
118
+ import { createSession, createChannel } from "better-sse"
94
119
 
95
- const channel = createChannel();
120
+ const channel = createChannel()
96
121
 
97
122
  app.get("/sse", async (req, res) => {
98
- const session = await createSession(req, res);
123
+ const session = await createSession(req, res)
99
124
 
100
- channel.register(session);
125
+ channel.register(session)
101
126
 
102
- channel.broadcast("A user has joined.", "join-notification");
103
- });
127
+ channel.broadcast("A user has joined.", "join-notification")
128
+ })
104
129
  ```
105
130
 
106
- Loop over sync and async [iterables](./docs/api.md#sessioniterate-iterable-iterable--asynciterable-options-object--promisevoid) and send each value as an event:
131
+ Use [batching](https://matthewwid.github.io/better-sse/guides/batching/) to send multiple events at once for improved performance and lower bandwidth usage:
107
132
 
108
133
  ```typescript
109
- const session = await createSession(req, res);
134
+ await session.batch(async (buffer) => {
135
+ await buffer.iterate(["My", "huge", "event", "list"])
136
+ })
137
+ ```
110
138
 
111
- const list = [1, 2, 3];
139
+ Loop over sync and async [iterables](https://matthewwid.github.io/better-sse/reference/api/#sessioniterate-iterable-iterable--asynciterable-options-object--promisevoid) and send each value as an event:
112
140
 
113
- await session.iterate(list);
141
+ ```typescript
142
+ const iterable = [1, 2, 3]
143
+
144
+ await session.iterate(iterable)
114
145
  ```
115
146
 
116
- Pipe [readable stream](./docs/api.md#sessionstream-stream-readable-options-object--promiseboolean) data to the client as a stream of events:
147
+ Pipe [readable stream](https://matthewwid.github.io/better-sse/reference/api/#sessionstream-stream-readable-options-object--promiseboolean) data to the client as a stream of events:
117
148
 
118
149
  ```typescript
119
- const session = await createSession(req, res);
120
-
121
- const stream = Readable.from([1, 2, 3]);
150
+ const stream = Readable.from([1, 2, 3])
122
151
 
123
- await session.stream(stream);
152
+ await session.stream(stream)
124
153
  ```
125
154
 
126
155
  ---
127
156
 
128
- Check the [API documentation](./docs/api.md) and [live examples](./examples) for information on getting more fine-tuned control over your data such as managing event IDs, data serialization, event filtering, dispatch controls and more!
157
+ Check the [API documentation](https://matthewwid.github.io/better-sse/reference/api) and [live examples](./examples) for information on getting more fine-tuned control over your data such as managing event IDs, data serialization, event filtering, dispatch controls and more!
129
158
 
130
159
  # Documentation
131
160
 
132
- API documentation, getting started guides and usage with other frameworks is [available on GitHub](./docs).
161
+ API documentation, getting started guides and usage with other frameworks is [available on the documentation website](https://matthewwid.github.io/better-sse/).
133
162
 
134
163
  # Contributing
135
164
 
@@ -141,29 +170,37 @@ For code or documentation changes [submit a pull request on GitHub](https://gith
141
170
 
142
171
  ## Local Development
143
172
 
144
- Install Node:
173
+ Install [Node](https://nodejs.org/en) (with [n](https://github.com/tj/n)):
145
174
 
146
175
  ```bash
147
176
  curl -L https://git.io/n-install | bash
148
177
  n auto
149
178
  ```
150
179
 
151
- Install pnpm:
180
+ Install dependencies (with [pnpm](https://pnpm.io/)):
152
181
 
153
182
  ```bash
154
183
  npm i -g pnpm
184
+ pnpm i
155
185
  ```
156
186
 
157
- Install dependencies:
187
+ Run tests (with [Vitest](https://vitest.dev/)):
158
188
 
159
189
  ```bash
160
- pnpm i
190
+ pnpm t
161
191
  ```
162
192
 
163
- Run tests:
193
+ Lint and format (with [Biome](https://biomejs.dev/)):
164
194
 
165
195
  ```bash
166
- pnpm t
196
+ pnpm lint
197
+ pnpm format
198
+ ```
199
+
200
+ Bundle for distribution (with [tsup](https://tsup.egoist.dev/)):
201
+
202
+ ```bash
203
+ pnpm build
167
204
  ```
168
205
 
169
206
  # License
package/build/index.d.mts CHANGED
@@ -1,9 +1,7 @@
1
1
  import * as stream from 'stream';
2
- import * as http from 'http';
3
- import { OutgoingHttpHeaders, IncomingMessage, ServerResponse } from 'http';
4
- import * as http2 from 'http2';
5
- import { Http2ServerRequest, Http2ServerResponse } from 'http2';
6
- import { EventEmitter } from 'events';
2
+ import { IncomingMessage, ServerResponse } from 'node:http';
3
+ import { Http2ServerRequest, Http2ServerResponse } from 'node:http2';
4
+ import { EventEmitter } from 'node:events';
7
5
 
8
6
  interface IterateOptions {
9
7
  /**
@@ -23,16 +21,12 @@ interface StreamOptions {
23
21
  eventName?: string;
24
22
  }
25
23
 
24
+ type SanitizerFunction = (text: string) => string;
25
+
26
26
  /**
27
27
  * Serialize arbitrary data to a string that can be sent over the wire to the client.
28
28
  */
29
- interface SerializerFunction {
30
- (data: unknown): string;
31
- }
32
-
33
- interface SanitizerFunction {
34
- (text: string): string;
35
- }
29
+ type SerializerFunction = (data: unknown) => string;
36
30
 
37
31
  interface EventBufferOptions {
38
32
  /**
@@ -126,7 +120,7 @@ declare class EventBuffer {
126
120
  *
127
121
  * @returns A promise that resolves or rejects based on the success of the stream write finishing.
128
122
  */
129
- stream: (stream: stream.Readable, options?: StreamOptions) => Promise<boolean>;
123
+ stream: (stream: stream.Readable | ReadableStream<any>, options?: StreamOptions) => Promise<boolean>;
130
124
  /**
131
125
  * Iterate over an iterable and write yielded values as events into the buffer.
132
126
  *
@@ -218,7 +212,7 @@ interface SessionOptions<State = DefaultSessionState> extends Pick<EventBufferOp
218
212
  /**
219
213
  * Additional headers to be sent along with the response.
220
214
  */
221
- headers?: OutgoingHttpHeaders;
215
+ headers?: Record<string, string | string[] | undefined>;
222
216
  /**
223
217
  * Custom state for this session.
224
218
  *
@@ -239,15 +233,19 @@ interface SessionEvents extends EventMap {
239
233
  *
240
234
  * It extends from the {@link https://nodejs.org/api/events.html#events_class_eventemitter | EventEmitter} class.
241
235
  *
242
- * It emits the `connected` event after it has connected and sent all headers to the client, and the
243
- * `disconnected` event after the connection has been closed.
236
+ * It emits the `connected` event after it has connected and sent the response head to the client.
237
+ * It emits the `disconnected` event after the connection has been closed.
238
+ *
239
+ * When using the Fetch API, the session is considered connected only once the `ReadableStream` contained in the body
240
+ * of the `Response` returned by `getResponse` has began being consumed.
244
241
  *
245
- * Note that creating a new session will immediately send the initial status code and headers to the client.
246
- * Attempting to write additional headers after you have created a new session will result in an error.
242
+ * When using the Node HTTP APIs, the session will send the response with status code, headers and other preamble data ahead of time,
243
+ * allowing the session to connect and start pushing events immediately. As such, keep in mind that attempting
244
+ * to write additional headers after the session has been created will result in an error being thrown.
247
245
  *
248
- * @param req - The Node HTTP {@link https://nodejs.org/api/http.html#http_class_http_incomingmessage | ServerResponse} object.
249
- * @param res - The Node HTTP {@link https://nodejs.org/api/http.html#http_class_http_serverresponse | IncomingMessage} object.
250
- * @param options - Options given to the session instance.
246
+ * @param req - The Node HTTP/1 {@link https://nodejs.org/api/http.html#http_class_http_incomingmessage | ServerResponse}, HTTP/2 {@link https://nodejs.org/api/http2.html#class-http2http2serverrequest | Http2ServerRequest} or the Fetch API {@link https://developer.mozilla.org/en-US/docs/Web/API/Request | Request} object.
247
+ * @param res - The Node HTTP {@link https://nodejs.org/api/http.html#http_class_http_serverresponse | IncomingMessage}, HTTP/2 {@link https://nodejs.org/api/http2.html#class-http2http2serverresponse | Http2ServerResponse} or the Fetch API {@link https://developer.mozilla.org/en-US/docs/Web/API/Response | Response} object. Optional if using the Fetch API.
248
+ * @param options - Optional additional configuration for the session.
251
249
  */
252
250
  declare class Session<State = DefaultSessionState> extends TypedEmitter<SessionEvents> {
253
251
  /**
@@ -276,57 +274,45 @@ declare class Session<State = DefaultSessionState> extends TypedEmitter<SessionE
276
274
  */
277
275
  state: State;
278
276
  private buffer;
279
- /**
280
- * Raw HTTP request.
281
- */
282
- private req;
283
- /**
284
- * Raw HTTP response that is the minimal interface needed and forms the
285
- * intersection between the HTTP/1.1 and HTTP/2 server response interfaces.
286
- */
287
- private res;
288
- private serialize;
277
+ private connection;
289
278
  private sanitize;
290
- private trustClientEventId;
279
+ private serialize;
291
280
  private initialRetry;
292
281
  private keepAliveInterval;
293
282
  private keepAliveTimer?;
294
- private statusCode;
295
- private headers;
296
- constructor(req: IncomingMessage | Http2ServerRequest, res: ServerResponse | Http2ServerResponse, options?: SessionOptions<State>);
283
+ constructor(req: IncomingMessage, res: ServerResponse, options?: SessionOptions<State>);
284
+ constructor(req: Http2ServerRequest, res: Http2ServerResponse, options?: SessionOptions<State>);
285
+ constructor(req: Request, res?: Response, options?: SessionOptions<State>);
286
+ constructor(req: Request, options?: SessionOptions<State>);
297
287
  private initialize;
298
288
  private onDisconnected;
299
- private keepAlive;
300
- /**
301
- * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
302
- */
303
- event(type: string): this;
304
289
  /**
305
- * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
290
+ * Write an empty comment and flush it to the client.
306
291
  */
307
- data: (data: unknown) => this;
308
- /**
309
- * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
310
- */
311
- id: (id?: string) => this;
292
+ private keepAlive;
312
293
  /**
313
- * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
294
+ * Flush the contents of the internal buffer to the client and clear the buffer.
314
295
  */
315
- retry: (time: number) => this;
296
+ private flush;
316
297
  /**
317
- * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
318
- */
319
- comment: (text?: string) => this;
320
- /**
321
- * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
298
+ * Get a Request object representing the request of the underlying connection this session manages.
299
+ *
300
+ * When using the Fetch API, this will be the original Request object passed to the session constructor.
301
+ *
302
+ * When using the Node HTTP APIs, this will be a new Request object with status code and headers copied from the original request.
303
+ * When the originally given request or response is closed, the abort signal attached to this Request will be triggered.
322
304
  */
323
- dispatch: () => this;
305
+ getRequest: () => Request;
324
306
  /**
325
- * Flush the contents of the internal buffer to the client and clear the buffer.
307
+ * Get a Response object representing the response of the underlying connection this session manages.
308
+ *
309
+ * When using the Fetch API, this will be a new Response object with status code and headers copied from the original response if given.
310
+ * Its body will be a ReadableStream that should begin being consumed for the session to consider itself connected.
326
311
  *
327
- * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
312
+ * When using the Node HTTP APIs, this will be a new Response object with status code and headers copied from the original response.
313
+ * Its body will be `null`, as data is instead written to the stream of the originally given response object.
328
314
  */
329
- flush: () => this;
315
+ getResponse: () => Response;
330
316
  /**
331
317
  * Push an event to the client.
332
318
  *
@@ -334,6 +320,8 @@ declare class Session<State = DefaultSessionState> extends TypedEmitter<SessionE
334
320
  *
335
321
  * If no event ID is given, the event ID (and thus the `lastId` property) is set to a unique string generated using a cryptographic pseudorandom number generator.
336
322
  *
323
+ * If the session has disconnected, an `SseError` will be thrown.
324
+ *
337
325
  * Emits the `push` event with the given data, event name and event ID in that order.
338
326
  *
339
327
  * @param data - Data to write.
@@ -353,7 +341,7 @@ declare class Session<State = DefaultSessionState> extends TypedEmitter<SessionE
353
341
  *
354
342
  * @returns A promise that resolves or rejects based on the success of the stream write finishing.
355
343
  */
356
- stream: (stream: stream.Readable, options?: StreamOptions) => Promise<boolean>;
344
+ stream: (stream: stream.Readable | ReadableStream<any>, options?: StreamOptions) => Promise<boolean>;
357
345
  /**
358
346
  * Iterate over an iterable and send yielded values as events to the client.
359
347
  *
@@ -384,9 +372,30 @@ declare class Session<State = DefaultSessionState> extends TypedEmitter<SessionE
384
372
  }
385
373
 
386
374
  /**
387
- * Create a new session and return the session instance once it has connected.
375
+ * Create a new session.
376
+ *
377
+ * When using the Fetch API, resolves immediately with a session instance before it has connected.
378
+ * You can listen for the `connected` event on the session to know when it has connected, or
379
+ * otherwise use the shorthand `createResponse` function that does so for you instead.
380
+ *
381
+ * When using the Node HTTP APIs, waits for the session to connect before resolving with its instance.
382
+ */
383
+ declare function createSession<State = DefaultSessionState>(req: IncomingMessage, res: ServerResponse, options?: SessionOptions<State>): Promise<Session<State>>;
384
+ declare function createSession<State = DefaultSessionState>(req: Http2ServerRequest, res: Http2ServerResponse, options?: SessionOptions<State>): Promise<Session<State>>;
385
+ declare function createSession<State = DefaultSessionState>(req: Request, res?: Response, options?: SessionOptions<State>): Promise<Session<State>>;
386
+ declare function createSession<State = DefaultSessionState>(req: Request, options?: SessionOptions<State>): Promise<Session<State>>;
387
+
388
+ type CreateResponseCallback<State> = (session: Session<State>) => void;
389
+ /**
390
+ * Create a new session using the Fetch API and return its corresponding `Response` object.
391
+ *
392
+ * The last argument should be a callback function that will be invoked with
393
+ * the session instance once it has connected.
388
394
  */
389
- declare const createSession: <State = DefaultSessionState>(req: http.IncomingMessage | http2.Http2ServerRequest, res: http.ServerResponse<http.IncomingMessage> | http2.Http2ServerResponse<http2.Http2ServerRequest>, options?: SessionOptions<State> | undefined) => Promise<Session<State>>;
395
+ declare function createResponse<State = DefaultSessionState>(request: Request, callback: CreateResponseCallback<State>): Response;
396
+ declare function createResponse<State = DefaultSessionState>(request: Request, response: Response, callback: CreateResponseCallback<State>): Response;
397
+ declare function createResponse<State = DefaultSessionState>(request: Request, options: SessionOptions<State>, callback: CreateResponseCallback<State>): Response;
398
+ declare function createResponse<State = DefaultSessionState>(request: Request, response: Response, options: SessionOptions<State>, callback: CreateResponseCallback<State>): Response;
390
399
 
391
400
  interface ChannelOptions<State = DefaultChannelState> {
392
401
  /**
@@ -482,4 +491,4 @@ declare class SseError extends Error {
482
491
  constructor(message: string);
483
492
  }
484
493
 
485
- export { type BroadcastOptions, Channel, type ChannelEvents, type ChannelOptions, type DefaultChannelState, type DefaultSessionState, EventBuffer, type EventBufferOptions, type IterateOptions, Session, type SessionEvents, type SessionOptions, SseError, type StreamOptions, createChannel, createEventBuffer, createSession };
494
+ export { type BroadcastOptions, Channel, type ChannelEvents, type ChannelOptions, type CreateResponseCallback, type DefaultChannelState, type DefaultSessionState, EventBuffer, type EventBufferOptions, type IterateOptions, Session, type SessionEvents, type SessionOptions, SseError, type StreamOptions, createChannel, createEventBuffer, createResponse, createSession };