better-sse 0.8.0 → 0.9.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/README.md CHANGED
@@ -9,15 +9,15 @@
9
9
 
10
10
  A dead simple, dependency-less, spec-compliant server-side events implementation for Node, written in TypeScript.
11
11
 
12
- This package aims to be the easiest to use, most compliant and most streamlined solution to server-side 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-side events with Node 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
 
16
16
  ## Why use Server-sent Events?
17
17
 
18
- Server-sent events (SSE) is a standardised protocol that allows web-servers to push data to clients without the need for alternative mechanisms such as pinging or long-polling.
18
+ Server-sent events (SSE) is a standardised protocol that allows web-servers to push data to clients without the need for alternative mechanisms such as pinging, long-polling or [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API).
19
19
 
20
- Using SSE can allow for significant savings in bandwidth and battery life on portable devices, and will work with your existing infrastructure as it operates directly over the HTTP protocol without the need for the connection upgrade that [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) require.
20
+ Using SSE can allow for significant savings in bandwidth and battery life on portable devices and will work with your existing infrastructure as it operates directly over the HTTP protocol without the need for the connection upgrade that WebSockets require.
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
 
@@ -32,12 +32,12 @@ Compared to WebSockets it has comparable performance and bandwidth usage, especi
32
32
  * [Thoroughly tested](./src/Session.test.ts) (+ 100% code coverage!).
33
33
  * [Comprehensively documented](./docs) with guides and API documentation.
34
34
  * [Channels](./docs/channels.md) allow you to broadcast events to many clients at once.
35
- * Configurable reconnection time, message serialization and data sanitization (but with good defaults).
35
+ * Configurable reconnection time, message serialization and data sanitization (with good defaults).
36
36
  * Trust or ignore the client-given last event ID.
37
37
  * Automatically send keep-alive pings to keep connections open.
38
38
  * Add or override the response status code and headers.
39
- * Fine-grained control by either sending [individual fields](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#fields) of events or sending full events with simple helpers.
40
- * 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 stream of events.
39
+ * Fine-grained control by either sending [individual fields](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#fields) of events or by sending full events with simple helpers.
40
+ * 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
41
  * Support for popular EventStream polyfills [`event-source-polyfill`](https://www.npmjs.com/package/event-source-polyfill) and [`eventsource-polyfill`](https://www.npmjs.com/package/eventsource-polyfill).
42
42
 
43
43
  [See a comparison with other Node SSE libraries in the documentation.](./docs/comparison.md)
@@ -55,11 +55,11 @@ yarn add better-sse
55
55
  pnpm add better-sse
56
56
  ```
57
57
 
58
- _Better SSE ships with types built in. No need to install from `@types` for TypeScript users!_
58
+ _Better SSE ships with types built in. No need to install from DefinitelyTyped for TypeScript users!_
59
59
 
60
60
  # Usage
61
61
 
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)).
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).
63
63
 
64
64
  See the [Recipes](./docs/recipes.md) section of the documentation for use with other frameworks and libraries.
65
65
 
@@ -69,7 +69,7 @@ Use [sessions](./docs/api.md#session) to push events to clients:
69
69
 
70
70
  ```typescript
71
71
  // Server
72
- import {createSession} from "better-sse";
72
+ import { createSession } from "better-sse";
73
73
 
74
74
  app.get("/sse", async (req, res) => {
75
75
  const session = await createSession(req, res);
@@ -82,7 +82,7 @@ app.get("/sse", async (req, res) => {
82
82
  // Client
83
83
  const sse = new EventSource("/sse");
84
84
 
85
- sse.addEventListener("message", ({data}) => {
85
+ sse.addEventListener("message", ({ data }) => {
86
86
  console.log(data);
87
87
  });
88
88
  ```
@@ -92,7 +92,7 @@ sse.addEventListener("message", ({data}) => {
92
92
  Use [channels](./docs/channels.md) to send events to many clients at once:
93
93
 
94
94
  ```typescript
95
- import {createSession, createChannel} from "better-sse";
95
+ import { createSession, createChannel } from "better-sse";
96
96
 
97
97
  const channel = createChannel();
98
98
 
@@ -174,4 +174,4 @@ pnpm t
174
174
 
175
175
  # License
176
176
 
177
- This project is licensed under the MIT license.
177
+ This project is licensed under the [MIT license](https://opensource.org/license/mit/).
@@ -1,27 +1,33 @@
1
1
  import { TypedEmitter, EventMap } from "./lib/TypedEmitter";
2
- import { Session } from "./Session";
3
- interface BroadcastOptions {
2
+ import { Session, DefaultSessionState } from "./Session";
3
+ interface BroadcastOptions<SessionState extends Record<string, unknown> = DefaultSessionState> {
4
4
  /**
5
5
  * Filter sessions that should receive the event.
6
6
  *
7
7
  * Called with each session and should return `true` to allow the event to be sent and otherwise return `false` to prevent the session from receiving the event.
8
8
  */
9
- filter?: (session: Session) => boolean;
9
+ filter?: (session: Session<SessionState>) => boolean;
10
10
  }
11
- interface ChannelEvents extends EventMap {
12
- "session-registered": (session: Session) => void;
13
- "session-deregistered": (session: Session) => void;
14
- "session-disconnected": (session: Session) => void;
11
+ interface ChannelEvents<SessionState extends Record<string, unknown> = DefaultSessionState> extends EventMap {
12
+ "session-registered": (session: Session<SessionState>) => void;
13
+ "session-deregistered": (session: Session<SessionState>) => void;
14
+ "session-disconnected": (session: Session<SessionState>) => void;
15
15
  broadcast: (data: unknown, eventName: string, eventId: string) => void;
16
16
  }
17
+ interface DefaultChannelState {
18
+ [key: string]: unknown;
19
+ }
17
20
  /**
18
- * A Channel is used to broadcast events to many sessions at once.
21
+ * A `Channel` is used to broadcast events to many sessions at once.
19
22
  *
20
23
  * It extends from the {@link https://nodejs.org/api/events.html#events_class_eventemitter | EventEmitter} class.
24
+ *
25
+ * You may use the second generic argument `SessionState` to enforce that only sessions with the same state type may be registered with this channel.
21
26
  */
22
- declare class Channel<State extends Record<string, unknown> = Record<string, unknown>> extends TypedEmitter<ChannelEvents> {
27
+ declare class Channel<State extends Record<string, unknown> = DefaultChannelState, SessionState extends Record<string, unknown> = DefaultSessionState> extends TypedEmitter<ChannelEvents<SessionState>> {
23
28
  /**
24
29
  * Custom state for this channel.
30
+ *
25
31
  * Use this object to safely store information related to the channel.
26
32
  */
27
33
  state: State;
@@ -30,7 +36,7 @@ declare class Channel<State extends Record<string, unknown> = Record<string, unk
30
36
  /**
31
37
  * List of the currently active sessions subscribed to this channel.
32
38
  */
33
- get activeSessions(): ReadonlyArray<Session>;
39
+ get activeSessions(): ReadonlyArray<Session<SessionState>>;
34
40
  /**
35
41
  * Number of sessions subscribed to this channel.
36
42
  */
@@ -38,11 +44,11 @@ declare class Channel<State extends Record<string, unknown> = Record<string, unk
38
44
  /**
39
45
  * Register a session so that it can start receiving events from this channel.
40
46
  *
41
- * If the session is already registered this method does nothing.
47
+ * If the session was already registered to begin with this method does nothing.
42
48
  *
43
49
  * @param session - Session to register.
44
50
  */
45
- register(session: Session): this;
51
+ register(session: Session<SessionState>): this;
46
52
  /**
47
53
  * Deregister a session so that it no longer receives events from this channel.
48
54
  *
@@ -50,13 +56,13 @@ declare class Channel<State extends Record<string, unknown> = Record<string, unk
50
56
  *
51
57
  * @param session - Session to deregister.
52
58
  */
53
- deregister(session: Session): this;
59
+ deregister(session: Session<SessionState>): this;
54
60
  /**
55
- * Push an event to every active session on this channel.
61
+ * Broadcast an event with the given data and name to every active session registered with this channel.
56
62
  *
57
- * Takes the same arguments as the `Session#push` method.
63
+ * Note that the broadcasted event will have the same ID across all receiving sessions instead of generating a unique ID for each.
58
64
  */
59
- broadcast: (data: unknown, eventName?: string, options?: BroadcastOptions) => this;
65
+ broadcast: (data: unknown, eventName?: string, options?: BroadcastOptions<SessionState>) => this;
60
66
  }
61
- export type { BroadcastOptions, ChannelEvents };
67
+ export type { BroadcastOptions, ChannelEvents, DefaultChannelState };
62
68
  export { Channel };
@@ -22,7 +22,7 @@ interface SessionOptions {
22
22
  */
23
23
  sanitizer?: SanitizerFunction;
24
24
  /**
25
- * Whether to trust the last event ID given by the client in the `Last-Event-ID` request header.
25
+ * Whether to trust or ignore the last event ID given by the client in the `Last-Event-ID` request header.
26
26
  *
27
27
  * When set to `false`, the `lastId` property will always be initialized to an empty string.
28
28
  *
@@ -55,7 +55,6 @@ interface SessionOptions {
55
55
  * Status code to be sent to the client.
56
56
  *
57
57
  * Event stream requests can be redirected using HTTP 301 and 307 status codes.
58
- *
59
58
  * Make sure to set `Location` header when using these status codes using the `headers` property.
60
59
  *
61
60
  * A client can be asked to stop reconnecting by using 204 status code.
@@ -84,7 +83,7 @@ interface IterateOptions {
84
83
  */
85
84
  eventName?: string;
86
85
  }
87
- interface SessionState {
86
+ interface DefaultSessionState {
88
87
  [key: string]: unknown;
89
88
  }
90
89
  interface SessionEvents extends EventMap {
@@ -93,28 +92,37 @@ interface SessionEvents extends EventMap {
93
92
  push: (data: unknown, eventName: string, eventId: string) => void;
94
93
  }
95
94
  /**
96
- * A Session represents an open connection between the server and a client.
95
+ * A `Session` represents an open connection between the server and a client.
97
96
  *
98
97
  * It extends from the {@link https://nodejs.org/api/events.html#events_class_eventemitter | EventEmitter} class.
99
98
  *
100
- * It emits the `connected` event after it has connected and flushed all headers to the client, and the
101
- * `disconnected` event after client connection has been closed.
99
+ * It emits the `connected` event after it has connected and sent all headers to the client, and the
100
+ * `disconnected` event after the connection has been closed.
101
+ *
102
+ * Note that creating a new session will immediately send the initial status code and headers to the client.
103
+ * Attempting to write additional headers after you have created a new session will result in an error.
104
+ *
105
+ * As a performance optimisation, all events and data are first written to an internal buffer
106
+ * where it is stored until it is flushed to the client by calling the `flush` method. This is
107
+ * done for you when using the `push` helper method.
102
108
  *
103
109
  * @param req - The Node HTTP {@link https://nodejs.org/api/http.html#http_class_http_incomingmessage | ServerResponse} object.
104
110
  * @param res - The Node HTTP {@link https://nodejs.org/api/http.html#http_class_http_serverresponse | IncomingMessage} object.
105
111
  * @param options - Options given to the session instance.
106
112
  */
107
- declare class Session<State extends Record<string, unknown> = SessionState> extends TypedEmitter<SessionEvents> {
113
+ declare class Session<State extends Record<string, unknown> = DefaultSessionState> extends TypedEmitter<SessionEvents> {
108
114
  /**
109
- * The last ID sent to the client.
115
+ * The last event ID sent to the client.
110
116
  *
111
117
  * This is initialized to the last event ID given by the user, and otherwise is equal to the last number given to the `.id` method.
112
118
  *
119
+ * For security reasons, keep in mind that the client can provide *any* initial ID here. Use the `trustClientEventId` to ignore the client-given initial ID.
120
+ *
113
121
  * @readonly
114
122
  */
115
123
  lastId: string;
116
124
  /**
117
- * Indicates whether the session and connection is open or not.
125
+ * Indicates whether the session and underlying connection is open or not.
118
126
  *
119
127
  * @readonly
120
128
  */
@@ -123,6 +131,9 @@ declare class Session<State extends Record<string, unknown> = SessionState> exte
123
131
  * Custom state for this session.
124
132
  *
125
133
  * Use this object to safely store information related to the session and user.
134
+ *
135
+ * Use [module augmentation and declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)
136
+ * to safely add new properties to the `DefaultSessionState` interface.
126
137
  */
127
138
  state: State;
128
139
  /**
@@ -157,7 +168,7 @@ declare class Session<State extends Record<string, unknown> = SessionState> exte
157
168
  private writeField;
158
169
  private keepAlive;
159
170
  /**
160
- * Set the event to the given name (also referred to as "type" in the specification).
171
+ * Set the event to the given name (also referred to as the event "type" in the specification).
161
172
  *
162
173
  * @param type - Event name/type.
163
174
  */
@@ -201,7 +212,7 @@ declare class Session<State extends Record<string, unknown> = SessionState> exte
201
212
  */
202
213
  flush: () => this;
203
214
  /**
204
- * Create, dispatch and flush an event with the given data all at once.
215
+ * Create, write, dispatch and flush an event with the given data to the client all at once.
205
216
  *
206
217
  * This is equivalent to calling the methods `event`, `id`, `data`, `dispatch` and `flush` in that order.
207
218
  *
@@ -219,10 +230,11 @@ declare class Session<State extends Record<string, unknown> = SessionState> exte
219
230
  /**
220
231
  * Pipe readable stream data to the client.
221
232
  *
222
- * Each data emission by the stream emits a new event that is dispatched to the client.
233
+ * Each data emission by the stream pushes a new event to the client.
234
+ *
223
235
  * This uses the `push` method under the hood.
224
236
  *
225
- * If no event name is given in the options object, the event name (type) is set to `"stream"`.
237
+ * If no event name is given in the options object, the event name is set to `"stream"`.
226
238
  *
227
239
  * @param stream - Readable stream to consume from.
228
240
  * @param options - Options to alter how the stream is flushed to the client.
@@ -231,12 +243,13 @@ declare class Session<State extends Record<string, unknown> = SessionState> exte
231
243
  */
232
244
  stream: (stream: Readable, options?: StreamOptions) => Promise<boolean>;
233
245
  /**
234
- * Iterate over an iterable and send yielded values as data to the client.
246
+ * Iterate over an iterable and send yielded values to the client.
247
+ *
248
+ * Each yield pushes a new event to the client.
235
249
  *
236
- * Each yield emits a new event that is dispatched to the client.
237
250
  * This uses the `push` method under the hood.
238
251
  *
239
- * If no event name is given in the options object, the event name (type) is set to `"iteration"`.
252
+ * If no event name is given in the options object, the event name is set to `"iteration"`.
240
253
  *
241
254
  * @param iterable - Iterable to consume data from.
242
255
  *
@@ -244,5 +257,5 @@ declare class Session<State extends Record<string, unknown> = SessionState> exte
244
257
  */
245
258
  iterate: <DataType = unknown>(iterable: Iterable<DataType> | AsyncIterable<DataType>, options?: IterateOptions) => Promise<void>;
246
259
  }
247
- export type { SessionOptions, StreamOptions, IterateOptions, SessionState, SessionEvents, };
260
+ export type { SessionOptions, StreamOptions, IterateOptions, SessionEvents, DefaultSessionState, };
248
261
  export { Session };
@@ -1,3 +1,3 @@
1
1
  import { Channel } from "./Channel";
2
- declare const createChannel: <State extends Record<string, unknown>>() => Channel<State>;
2
+ declare const createChannel: <State extends Record<string, unknown>, SessionState extends Record<string, unknown>>() => Channel<State, SessionState>;
3
3
  export { createChannel };
@@ -1,6 +1,6 @@
1
- import { Session, SessionState } from "./Session";
1
+ import { Session } from "./Session";
2
2
  /**
3
3
  * Create a new session and return the session instance once it has connected.
4
4
  */
5
- declare const createSession: <State extends Record<string, unknown> = SessionState>(req: import("http").IncomingMessage | import("http2").Http2ServerRequest, res: import("http").ServerResponse | import("http2").Http2ServerResponse, options?: import("./Session").SessionOptions | undefined) => Promise<Session<State>>;
5
+ declare const createSession: <State extends Record<string, unknown>>(req: import("http").IncomingMessage | import("http2").Http2ServerRequest, res: import("http").ServerResponse<import("http").IncomingMessage> | import("http2").Http2ServerResponse, options?: import("./Session").SessionOptions | undefined) => Promise<Session<State>>;
6
6
  export { createSession };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "better-sse",
3
3
  "description": "Dead simple, dependency-less, spec-compliant server-side events implementation for Node, written in TypeScript.",
4
- "version": "0.8.0",
4
+ "version": "0.9.0",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
7
7
  "license": "MIT",
@@ -26,30 +26,27 @@
26
26
  "pnpm": ">=6"
27
27
  },
28
28
  "devDependencies": {
29
- "@jest/types": "^28.1.0",
30
- "@types/eventsource": "^1.1.8",
31
- "@types/express": "^4.17.13",
32
- "@types/jest": "^26.0.24",
33
- "@types/node": "^17.0.36",
34
- "@typescript-eslint/eslint-plugin": "^5.26.0",
35
- "@typescript-eslint/parser": "^5.26.0",
36
- "eslint": "^8.16.0",
37
- "eslint-plugin-tsdoc": "^0.2.16",
38
- "eventsource": "^1.1.1",
39
- "jest": "^26.6.3",
29
+ "@types/eventsource": "^1.1.11",
30
+ "@types/express": "^4.17.16",
31
+ "@types/node": "^18.11.18",
32
+ "@typescript-eslint/eslint-plugin": "^5.49.0",
33
+ "@typescript-eslint/parser": "^5.49.0",
34
+ "eslint": "^8.32.0",
35
+ "eslint-plugin-tsdoc": "^0.2.17",
36
+ "eventsource": "^2.0.2",
40
37
  "npm-run-all": "^4.1.5",
41
- "prettier": "^2.6.2",
42
- "rimraf": "^3.0.2",
43
- "ts-jest": "^26.5.6",
44
- "ts-loader": "^9.3.0",
45
- "ts-node": "^10.8.0",
46
- "typescript": "^4.7.2",
47
- "webpack": "^5.72.1",
48
- "webpack-cli": "^4.9.2"
38
+ "prettier": "^2.8.3",
39
+ "rimraf": "^4.1.2",
40
+ "ts-loader": "^9.4.2",
41
+ "ts-node": "^10.9.1",
42
+ "typescript": "^4.9.4",
43
+ "vitest": "^0.28.2",
44
+ "webpack": "^5.75.0",
45
+ "webpack-cli": "^5.0.1"
49
46
  },
50
47
  "scripts": {
51
48
  "build": "webpack --env production",
52
- "test": "jest",
49
+ "test": "vitest",
53
50
  "clean": "rimraf ./build",
54
51
  "format": "prettier --write ./src/**/*.ts",
55
52
  "lint": "eslint \"./src/**/*.ts\""