better-sse 0.7.1 → 0.8.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,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2020 Matthew
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Matthew
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,177 +1,177 @@
1
- # Better SSE
2
-
3
- <p>
4
- <img src="https://img.shields.io/npm/v/better-sse?color=blue&style=flat-square" />
5
- <img src="https://img.shields.io/npm/l/better-sse?color=green&style=flat-square" />
6
- <img src="https://img.shields.io/npm/dt/better-sse?color=grey&style=flat-square" />
7
- <a href="https://github.com/MatthewWid/better-sse"><img src="https://img.shields.io/github/stars/MatthewWid/better-sse?style=social" /></a>
8
- </p>
9
-
10
- A dead simple, dependency-less, spec-compliant server-side events implementation for Node, written in TypeScript.
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.
13
-
14
- Please consider starring the project [on GitHub ⭐](https://github.com/MatthewWid/better-sse).
15
-
16
- ## Why use Server-sent Events?
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.
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.
21
-
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
-
24
- * [Comparison: Server-sent Events vs WebSockets vs Polling](https://medium.com/dailyjs/a-comparison-between-websockets-server-sent-events-and-polling-7a27c98cb1e3)
25
- * [WHATWG standards section for server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html)
26
- * [MDN guide to server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
27
-
28
- ## Highlights
29
-
30
- * Compatible with all popular Node HTTP frameworks ([http](https://nodejs.org/api/http.html), [Express](https://nodejs.org/api/http.html), [Koa](https://www.npmjs.com/package/koa), [Fastify](https://www.npmjs.com/package/fastify), etc.)
31
- * Fully written in TypeScript (+ ships with types directly).
32
- * [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.
35
- * Configurable reconnection time, message serialization and data sanitization (but with good defaults).
36
- * Trust or ignore the client-given last event ID.
37
- * Automatically send keep-alive pings to keep connections open.
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.
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
-
43
- [See a comparison with other Node SSE libraries in the documentation.](./docs/comparison.md)
44
-
45
- # Installation
46
-
47
- ```bash
48
- # npm
49
- npm install better-sse
50
-
51
- # Yarn
52
- yarn add better-sse
53
-
54
- # pnpm
55
- pnpm add better-sse
56
- ```
57
-
58
- _Better SSE ships with types built in. No need to install from `@types` for TypeScript users!_
59
-
60
- # Usage
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)).
63
-
64
- See the [Recipes](./docs/recipes.md) section of the documentation for use with other frameworks and libraries.
65
-
66
- ---
67
-
68
- Use [sessions](./docs/api.md#session) to push events to clients:
69
-
70
- ```typescript
71
- // Server
72
- import {createSession} from "better-sse";
73
-
74
- app.get("/sse", async (req, res) => {
75
- const session = await createSession(req, res);
76
-
77
- session.push("Hello world!");
78
- });
79
- ```
80
-
81
- ```typescript
82
- // Client
83
- const sse = new EventSource("/sse");
84
-
85
- sse.addEventListener("message", ({data}) => {
86
- console.log(data);
87
- });
88
- ```
89
-
90
- ---
91
-
92
- Use [channels](./docs/channels.md) to send events to many clients at once:
93
-
94
- ```typescript
95
- import {createSession, createChannel} from "better-sse";
96
-
97
- const channel = createChannel();
98
-
99
- app.get("/sse", async (req, res) => {
100
- const session = await createSession(req, res);
101
-
102
- channel.register(session);
103
-
104
- channel.broadcast("A user has joined.", "join-notification");
105
- });
106
- ```
107
-
108
- ---
109
-
110
- Loop over sync and async [iterables](./docs/api.md#sessioniterate-iterable-iterable--asynciterable-options-object--promisevoid) and send each value as an event:
111
-
112
- ```typescript
113
- const session = await createSession(req, res);
114
-
115
- const list = [1, 2, 3];
116
-
117
- await session.iterate(list);
118
- ```
119
-
120
- ---
121
-
122
- Pipe [readable stream](#sessionstream-stream-readable-options-object--promiseboolean) data to the client as a stream of events:
123
-
124
- ```typescript
125
- const session = await createSession(req, res);
126
-
127
- const stream = Readable.from([1, 2, 3]);
128
-
129
- await session.stream(stream);
130
- ```
131
-
132
- ---
133
-
134
- Check the [API documentation](./docs/api.md) and [live examples](https://github.com/MatthewWid/better-sse/tree/master/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!
135
-
136
- # Documentation
137
-
138
- API documentation, getting started guides and usage with other frameworks is [available on GitHub](https://github.com/MatthewWid/better-sse/tree/master/docs).
139
-
140
- # Contributing
141
-
142
- This library is always open to contributions, whether it be code, bug reports, documentation or anything else.
143
-
144
- Please submit suggestions, bugs and issues to the [GitHub issues page](https://github.com/MatthewWid/better-sse/issues).
145
-
146
- For code or documentation changes, [submit a pull request on GitHub](https://github.com/MatthewWid/better-sse/pulls).
147
-
148
- ## Local Development
149
-
150
- Install Node:
151
-
152
- ```bash
153
- curl -L https://git.io/n-install | bash
154
- n auto
155
- ```
156
-
157
- Install pnpm:
158
-
159
- ```bash
160
- npm i -g pnpm
161
- ```
162
-
163
- Install dependencies:
164
-
165
- ```bash
166
- pnpm i
167
- ```
168
-
169
- Run tests:
170
-
171
- ```bash
172
- pnpm t
173
- ```
174
-
175
- # License
176
-
177
- This project is licensed under the MIT license.
1
+ # Better SSE
2
+
3
+ <p>
4
+ <img src="https://img.shields.io/npm/v/better-sse?color=blue&style=flat-square" />
5
+ <img src="https://img.shields.io/npm/l/better-sse?color=green&style=flat-square" />
6
+ <img src="https://img.shields.io/npm/dt/better-sse?color=grey&style=flat-square" />
7
+ <a href="https://github.com/MatthewWid/better-sse"><img src="https://img.shields.io/github/stars/MatthewWid/better-sse?style=social" /></a>
8
+ </p>
9
+
10
+ A dead simple, dependency-less, spec-compliant server-side events implementation for Node, written in TypeScript.
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.
13
+
14
+ Please consider starring the project [on GitHub ⭐](https://github.com/MatthewWid/better-sse).
15
+
16
+ ## Why use Server-sent Events?
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.
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.
21
+
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
+
24
+ * [Comparison: Server-sent Events vs WebSockets vs Polling](https://medium.com/dailyjs/a-comparison-between-websockets-server-sent-events-and-polling-7a27c98cb1e3)
25
+ * [WHATWG standards section for server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html)
26
+ * [MDN guide to server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
27
+
28
+ ## Highlights
29
+
30
+ * Compatible with all popular Node HTTP frameworks ([http](https://nodejs.org/api/http.html), [Express](https://nodejs.org/api/http.html), [Koa](https://www.npmjs.com/package/koa), [Fastify](https://www.npmjs.com/package/fastify), etc.)
31
+ * Fully written in TypeScript (+ ships with types directly).
32
+ * [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.
35
+ * Configurable reconnection time, message serialization and data sanitization (but with good defaults).
36
+ * Trust or ignore the client-given last event ID.
37
+ * Automatically send keep-alive pings to keep connections open.
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.
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
+
43
+ [See a comparison with other Node SSE libraries in the documentation.](./docs/comparison.md)
44
+
45
+ # Installation
46
+
47
+ ```bash
48
+ # npm
49
+ npm install better-sse
50
+
51
+ # Yarn
52
+ yarn add better-sse
53
+
54
+ # pnpm
55
+ pnpm add better-sse
56
+ ```
57
+
58
+ _Better SSE ships with types built in. No need to install from `@types` for TypeScript users!_
59
+
60
+ # Usage
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)).
63
+
64
+ See the [Recipes](./docs/recipes.md) section of the documentation for use with other frameworks and libraries.
65
+
66
+ ---
67
+
68
+ Use [sessions](./docs/api.md#session) to push events to clients:
69
+
70
+ ```typescript
71
+ // Server
72
+ import {createSession} from "better-sse";
73
+
74
+ app.get("/sse", async (req, res) => {
75
+ const session = await createSession(req, res);
76
+
77
+ session.push("Hello world!");
78
+ });
79
+ ```
80
+
81
+ ```typescript
82
+ // Client
83
+ const sse = new EventSource("/sse");
84
+
85
+ sse.addEventListener("message", ({data}) => {
86
+ console.log(data);
87
+ });
88
+ ```
89
+
90
+ ---
91
+
92
+ Use [channels](./docs/channels.md) to send events to many clients at once:
93
+
94
+ ```typescript
95
+ import {createSession, createChannel} from "better-sse";
96
+
97
+ const channel = createChannel();
98
+
99
+ app.get("/sse", async (req, res) => {
100
+ const session = await createSession(req, res);
101
+
102
+ channel.register(session);
103
+
104
+ channel.broadcast("A user has joined.", "join-notification");
105
+ });
106
+ ```
107
+
108
+ ---
109
+
110
+ Loop over sync and async [iterables](./docs/api.md#sessioniterate-iterable-iterable--asynciterable-options-object--promisevoid) and send each value as an event:
111
+
112
+ ```typescript
113
+ const session = await createSession(req, res);
114
+
115
+ const list = [1, 2, 3];
116
+
117
+ await session.iterate(list);
118
+ ```
119
+
120
+ ---
121
+
122
+ Pipe [readable stream](#sessionstream-stream-readable-options-object--promiseboolean) data to the client as a stream of events:
123
+
124
+ ```typescript
125
+ const session = await createSession(req, res);
126
+
127
+ const stream = Readable.from([1, 2, 3]);
128
+
129
+ await session.stream(stream);
130
+ ```
131
+
132
+ ---
133
+
134
+ 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!
135
+
136
+ # Documentation
137
+
138
+ API documentation, getting started guides and usage with other frameworks is [available on GitHub](./docs).
139
+
140
+ # Contributing
141
+
142
+ This library is always open to contributions, whether it be code, bug reports, documentation or anything else.
143
+
144
+ Please submit suggestions, bugs and issues to the [GitHub issues page](https://github.com/MatthewWid/better-sse/issues).
145
+
146
+ For code or documentation changes, [submit a pull request on GitHub](https://github.com/MatthewWid/better-sse/pulls).
147
+
148
+ ## Local Development
149
+
150
+ Install Node:
151
+
152
+ ```bash
153
+ curl -L https://git.io/n-install | bash
154
+ n auto
155
+ ```
156
+
157
+ Install pnpm:
158
+
159
+ ```bash
160
+ npm i -g pnpm
161
+ ```
162
+
163
+ Install dependencies:
164
+
165
+ ```bash
166
+ pnpm i
167
+ ```
168
+
169
+ Run tests:
170
+
171
+ ```bash
172
+ pnpm t
173
+ ```
174
+
175
+ # License
176
+
177
+ This project is licensed under the MIT license.
@@ -12,7 +12,7 @@ interface ChannelEvents extends EventMap {
12
12
  "session-registered": (session: Session) => void;
13
13
  "session-deregistered": (session: Session) => void;
14
14
  "session-disconnected": (session: Session) => void;
15
- broadcast: (data: unknown, eventName: string) => void;
15
+ broadcast: (data: unknown, eventName: string, eventId: string) => void;
16
16
  }
17
17
  /**
18
18
  * A Channel is used to broadcast events to many sessions at once.
@@ -38,12 +38,16 @@ declare class Channel<State extends Record<string, unknown> = Record<string, unk
38
38
  /**
39
39
  * Register a session so that it can start receiving events from this channel.
40
40
  *
41
+ * If the session is already registered this method does nothing.
42
+ *
41
43
  * @param session - Session to register.
42
44
  */
43
45
  register(session: Session): this;
44
46
  /**
45
47
  * Deregister a session so that it no longer receives events from this channel.
46
48
  *
49
+ * If the session was not registered to begin with this method does nothing.
50
+ *
47
51
  * @param session - Session to deregister.
48
52
  */
49
53
  deregister(session: Session): this;
@@ -52,7 +56,7 @@ declare class Channel<State extends Record<string, unknown> = Record<string, unk
52
56
  *
53
57
  * Takes the same arguments as the `Session#push` method.
54
58
  */
55
- broadcast: (data: unknown, eventName?: string | undefined, options?: BroadcastOptions) => this;
59
+ broadcast: (data: unknown, eventName?: string, options?: BroadcastOptions) => this;
56
60
  }
57
61
  export type { BroadcastOptions, ChannelEvents };
58
62
  export { Channel };
@@ -1,6 +1,8 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  import { Readable } from "stream";
3
- import { IncomingMessage, ServerResponse, OutgoingHttpHeaders } from "http";
4
+ import { IncomingMessage as Http1ServerRequest, ServerResponse as Http1ServerResponse, OutgoingHttpHeaders } from "http";
5
+ import { Http2ServerRequest, Http2ServerResponse } from "http2";
4
6
  import { TypedEmitter, EventMap } from "./lib/TypedEmitter";
5
7
  import { SerializerFunction } from "./lib/serialize";
6
8
  import { SanitizerFunction } from "./lib/sanitize";
@@ -28,9 +30,11 @@ interface SessionOptions {
28
30
  */
29
31
  trustClientEventId?: boolean;
30
32
  /**
31
- * Time in milliseconds for the client to wait before attempting to reconnect if the connection is closed. This is a request to the client browser, and does not guarantee that the client will actually respect the given time.
33
+ * Time in milliseconds for the client to wait before attempting to reconnect if the connection is closed.
34
+ *
35
+ * This is a request to the client browser, and does not guarantee that the client will actually respect the given time.
32
36
  *
33
- * This is equivalent to immediately calling `.retry().dispatch()` after a connection is made.
37
+ * Equivalent to immediately calling `.retry().dispatch().flush()` after a connection is made.
34
38
  *
35
39
  * Give as `null` to avoid sending an explicit reconnection time and allow the client browser to decide itself.
36
40
  *
@@ -103,6 +107,7 @@ interface SessionEvents extends EventMap {
103
107
  declare class Session<State extends Record<string, unknown> = SessionState> extends TypedEmitter<SessionEvents> {
104
108
  /**
105
109
  * The last ID sent to the client.
110
+ *
106
111
  * 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.
107
112
  *
108
113
  * @readonly
@@ -116,10 +121,24 @@ declare class Session<State extends Record<string, unknown> = SessionState> exte
116
121
  isConnected: boolean;
117
122
  /**
118
123
  * Custom state for this session.
124
+ *
119
125
  * Use this object to safely store information related to the session and user.
120
126
  */
121
127
  state: State;
128
+ /**
129
+ * Internal buffer used to store raw data from written fields.
130
+ *
131
+ * When Session#dispatch is called its buffer data will be flushed.
132
+ */
133
+ private buffer;
134
+ /**
135
+ * Raw HTTP request.
136
+ */
122
137
  private req;
138
+ /**
139
+ * Raw HTTP response that is the minimal interface needed and forms the
140
+ * intersection between the HTTP/1.1 and HTTP/2 server response interfaces.
141
+ */
123
142
  private res;
124
143
  private serialize;
125
144
  private sanitize;
@@ -129,18 +148,14 @@ declare class Session<State extends Record<string, unknown> = SessionState> exte
129
148
  private keepAliveTimer?;
130
149
  private statusCode;
131
150
  private headers;
132
- constructor(req: IncomingMessage, res: ServerResponse, options?: SessionOptions);
133
- private onConnected;
151
+ constructor(req: Http1ServerRequest | Http2ServerRequest, res: Http1ServerResponse | Http2ServerResponse, options?: SessionOptions);
152
+ private initialize;
134
153
  private onDisconnected;
135
154
  /**
136
155
  * Write a line with a field key and value appended with a newline character.
137
156
  */
138
157
  private writeField;
139
158
  private keepAlive;
140
- /**
141
- * Flush the buffered data to the client by writing an additional newline.
142
- */
143
- dispatch: () => this;
144
159
  /**
145
160
  * Set the event to the given name (also referred to as "type" in the specification).
146
161
  *
@@ -148,7 +163,7 @@ declare class Session<State extends Record<string, unknown> = SessionState> exte
148
163
  */
149
164
  event(type: string): this;
150
165
  /**
151
- * Write arbitrary data onto the wire that is automatically serialized to a string using the given `serializer` function option or JSON stringification by default.
166
+ * Write an arbitrary data field that is automatically serialized to a string using the given `serializer` function option or JSON stringification by default.
152
167
  *
153
168
  * @param data - Data to serialize and write.
154
169
  */
@@ -156,11 +171,11 @@ declare class Session<State extends Record<string, unknown> = SessionState> exte
156
171
  /**
157
172
  * Set the event ID to the given string.
158
173
  *
159
- * Passing `null` will set the event ID to an empty string value.
174
+ * Defaults to an empty string if no argument is given.
160
175
  *
161
176
  * @param id - Identification string to write.
162
177
  */
163
- id: (id: string | null) => this;
178
+ id: (id?: string) => this;
164
179
  /**
165
180
  * Set the suggested reconnection time to the given milliseconds.
166
181
  *
@@ -174,19 +189,33 @@ declare class Session<State extends Record<string, unknown> = SessionState> exte
174
189
  *
175
190
  * @param text - Text of the comment. Otherwise writes an empty field value.
176
191
  */
177
- comment: (text?: string | undefined) => this;
192
+ comment: (text?: string) => this;
178
193
  /**
179
- * Create and dispatch an event with the given data all at once.
180
- * This is equivalent to calling `.event()`, `.id()`, `.data()` and `.dispatch()` in that order.
194
+ * Indicate that the event has finished being created by writing an additional newline character.
195
+ *
196
+ * Note that this does **not** send the written data to the client. Use `flush` to flush the internal buffer.
197
+ */
198
+ dispatch: () => this;
199
+ /**
200
+ * Flush the buffered data to the client and clear the buffer.
201
+ */
202
+ flush: () => this;
203
+ /**
204
+ * Create, dispatch and flush an event with the given data all at once.
205
+ *
206
+ * This is equivalent to calling the methods `event`, `id`, `data`, `dispatch` and `flush` in that order.
207
+ *
208
+ * If no event name is given, the event name is set to `"message"`.
181
209
  *
182
- * If no event name is given, the event name (type) is set to `"message"`.
210
+ * 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.
183
211
  *
184
- * Note that this sets the event ID (and thus the `lastId` property) to a string of eight random characters (`a-z0-9`).
212
+ * Emits the `push` event with the given data, event name and event ID in that order.
185
213
  *
186
214
  * @param data - Data to write.
187
215
  * @param eventName - Event name to write.
216
+ * @param eventId - Event ID to write.
188
217
  */
189
- push: (data: unknown, eventName?: string | undefined, eventId?: string | undefined) => this;
218
+ push: (data: unknown, eventName?: string, eventId?: string) => this;
190
219
  /**
191
220
  * Pipe readable stream data to the client.
192
221
  *
@@ -2,5 +2,5 @@ import { Session, SessionState } 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, res: import("http").ServerResponse, options?: import("./Session").SessionOptions | undefined) => Promise<Session<State>>;
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>>;
6
6
  export { createSession };
package/build/index.js CHANGED
@@ -1,2 +1,2 @@
1
- !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var s=t();for(var i in s)("object"==typeof exports?exports:e)[i]=s[i]}}(global,(function(){return(()=>{"use strict";var e={n:t=>{var s=t&&t.__esModule?()=>t.default:()=>t;return e.d(s,{a:s}),s},d:(t,s)=>{for(var i in s)e.o(s,i)&&!e.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:s[i]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{Channel:()=>d,Session:()=>a,createChannel:()=>c,createSession:()=>l});const s=require("crypto"),i=require("events");var n=e.n(i);class r extends(n()){addListener(e,t){return super.addListener(e,t)}prependListener(e,t){return super.prependListener(e,t)}prependOnceListener(e,t){return super.prependOnceListener(e,t)}on(e,t){return super.on(e,t)}once(e,t){return super.once(e,t)}emit(e,...t){return super.emit(e,...t)}off(e,t){return super.off(e,t)}removeListener(e,t){return super.removeListener(e,t)}}const o=e=>JSON.stringify(e),h=e=>{let t=e;return t=t.replace(/(\r\n|\r|\n)/g,"\n"),t=t.replace(/\n+$/g,""),t};class a extends r{constructor(e,t,i={}){var n,r,a,l,d,c,u;super(),this.lastId="",this.isConnected=!1,this.state={},this.onConnected=()=>{var e,t,s;const i=`http://${this.req.headers.host}${this.req.url}`,n=new URL(i).searchParams;if(this.trustClientEventId){const i=null!==(s=null!==(t=null!==(e=this.req.headers["last-event-id"])&&void 0!==e?e:n.get("lastEventId"))&&void 0!==t?t:n.get("evs_last_event_id"))&&void 0!==s?s:"";this.lastId=i}Object.entries(this.headers).forEach((([e,t])=>{this.res.setHeader(e,null!=t?t:"")})),this.res.statusCode=this.statusCode,this.res.setHeader("Content-Type","text/event-stream"),this.res.setHeader("Cache-Control","no-cache, no-transform"),this.res.setHeader("Connection","keep-alive"),this.res.flushHeaders(),n.has("padding")&&this.comment(" ".repeat(2049)).dispatch(),n.has("evs_preamble")&&this.comment(" ".repeat(2056)).dispatch(),null!==this.initialRetry&&this.retry(this.initialRetry).dispatch(),null!==this.keepAliveInterval&&(this.keepAliveTimer=setInterval(this.keepAlive,this.keepAliveInterval)),this.isConnected=!0,this.emit("connected")},this.onDisconnected=()=>{this.keepAliveTimer&&clearInterval(this.keepAliveTimer),this.isConnected=!1,this.emit("disconnected")},this.writeField=(e,t)=>{const s=`${e}:${this.sanitize(t)}\n`;return this.res.write(s),this},this.keepAlive=()=>{this.comment().dispatch()},this.dispatch=()=>(this.res.write("\n"),this),this.data=e=>{const t=this.serialize(e);return this.writeField("data",t),this},this.id=e=>{const t=e||"";return this.writeField("id",t),this.lastId=t,this},this.retry=e=>{const t=e.toString();return this.writeField("retry",t),this},this.comment=e=>(this.writeField("",null!=e?e:""),this),this.push=(e,t,i)=>(t||(t="message"),i||(i=(0,s.randomBytes)(4).toString("hex")),this.event(t).id(i).data(e).dispatch(),this.emit("push",e,t,i),this),this.stream=async(e,t={})=>{const{eventName:s="stream"}=t;return new Promise(((t,i)=>{e.on("data",(e=>{let t;t=Buffer.isBuffer(e)?e.toString():e,this.push(t,s)})),e.once("end",(()=>t(!0))),e.once("close",(()=>t(!0))),e.once("error",(e=>i(e)))}))},this.iterate=async(e,t={})=>{const{eventName:s="iteration"}=t;for await(const t of e)this.push(t,s)},this.req=e,this.res=t,this.serialize=null!==(n=i.serializer)&&void 0!==n?n:o,this.sanitize=null!==(r=i.sanitizer)&&void 0!==r?r:h,this.trustClientEventId=null===(a=i.trustClientEventId)||void 0===a||a,this.initialRetry=null===i.retry?null:null!==(l=i.retry)&&void 0!==l?l:2e3,this.keepAliveInterval=null===i.keepAlive?null:null!==(d=i.keepAlive)&&void 0!==d?d:1e4,this.statusCode=null!==(c=i.statusCode)&&void 0!==c?c:200,this.headers=null!==(u=i.headers)&&void 0!==u?u:{},this.req.on("close",this.onDisconnected),setImmediate(this.onConnected)}event(e){return this.writeField("event",e),this}}const l=(...e)=>new Promise((t=>{const s=new a(...e);s.once("connected",(()=>{t(s)}))}));class d extends r{constructor(){super(),this.state={},this.sessions=[],this.broadcast=(e,t,s={})=>{t||(t="message");const i=s.filter?this.sessions.filter(s.filter):this.sessions;for(const s of i)s.push(e,t);return this.emit("broadcast",e,t),this}}get activeSessions(){return this.sessions}get sessionCount(){return this.sessions.length}register(e){if(!e.isConnected)throw new Error("Cannot register a non-active session.");return e.once("disconnected",(()=>{this.deregister(e),this.emit("session-disconnected",e)})),this.sessions.push(e),this.emit("session-registered",e),this}deregister(e){return this.sessions=this.sessions.filter((t=>t!==e)),this.emit("session-deregistered",e),this}}const c=(...e)=>new d(...e);return t})()}));
1
+ !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var s=t();for(var i in s)("object"==typeof exports?exports:e)[i]=s[i]}}(global,(()=>(()=>{"use strict";var e={n:t=>{var s=t&&t.__esModule?()=>t.default:()=>t;return e.d(s,{a:s}),s},d:(t,s)=>{for(var i in s)e.o(s,i)&&!e.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:s[i]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{Channel:()=>f,Session:()=>u,createChannel:()=>v,createSession:()=>p});const s=require("http"),i=require("events");var r=e.n(i);class n extends(r()){addListener(e,t){return super.addListener(e,t)}prependListener(e,t){return super.prependListener(e,t)}prependOnceListener(e,t){return super.prependOnceListener(e,t)}on(e,t){return super.on(e,t)}once(e,t){return super.once(e,t)}emit(e,...t){return super.emit(e,...t)}off(e,t){return super.off(e,t)}removeListener(e,t){return super.removeListener(e,t)}}const o=e=>JSON.stringify(e),a=/(\r\n|\r|\n)/g,h=/\n+$/g,l=e=>{let t=e;return t=t.replace(a,"\n"),t=t.replace(h,""),t},d=require("crypto");let c;c=d.randomUUID?()=>(0,d.randomUUID)():()=>(0,d.randomBytes)(4).toString("hex");class u extends n{constructor(e,t,i={}){var r,n,a,h,d,u,p;super(),this.lastId="",this.isConnected=!1,this.state={},this.buffer="",this.initialize=()=>{var e,t,i;const r=`http://${this.req.headers.host}${this.req.url}`,n=new URL(r).searchParams;if(this.trustClientEventId){const s=null!==(i=null!==(t=null!==(e=this.req.headers["last-event-id"])&&void 0!==e?e:n.get("lastEventId"))&&void 0!==t?t:n.get("evs_last_event_id"))&&void 0!==i?i:"";this.lastId=s}const o={};this.res instanceof s.ServerResponse?(o["Content-Type"]="text/event-stream",o["Cache-Control"]="private, no-cache, no-store, no-transform, must-revalidate, max-age=0",o.Connection="keep-alive",o.Pragma="no-cache",o["X-Accel-Buffering"]="no"):(o["content-type"]="text/event-stream",o["cache-control"]="private, no-cache, no-store, no-transform, must-revalidate, max-age=0",o.pragma="no-cache",o["x-accel-buffering"]="no");for(const[e,t]of Object.entries(this.headers))o[e]=null!=t?t:"";this.res.writeHead(this.statusCode,o),n.has("padding")&&this.comment(" ".repeat(2049)).dispatch(),n.has("evs_preamble")&&this.comment(" ".repeat(2056)).dispatch(),null!==this.initialRetry&&this.retry(this.initialRetry).dispatch(),this.flush(),null!==this.keepAliveInterval&&(this.keepAliveTimer=setInterval(this.keepAlive,this.keepAliveInterval)),this.isConnected=!0,this.emit("connected")},this.onDisconnected=()=>{this.keepAliveTimer&&clearInterval(this.keepAliveTimer),this.isConnected=!1,this.emit("disconnected")},this.writeField=(e,t)=>{const s=this.sanitize(t);return this.buffer+=e+":"+s+"\n",this},this.keepAlive=()=>{this.comment().dispatch().flush()},this.data=e=>{const t=this.serialize(e);return this.writeField("data",t),this},this.id=(e="")=>(this.writeField("id",e),this.lastId=e,this),this.retry=e=>{const t=e.toString();return this.writeField("retry",t),this},this.comment=(e="")=>(this.writeField("",e),this),this.dispatch=()=>(this.buffer+="\n",this),this.flush=()=>(this.res.write(this.buffer),this.buffer="",this),this.push=(e,t="message",s=c())=>(this.event(t).id(s).data(e).dispatch().flush(),this.emit("push",e,t,s),this),this.stream=async(e,t={})=>{const{eventName:s="stream"}=t;return new Promise(((t,i)=>{e.on("data",(e=>{let t;t=Buffer.isBuffer(e)?e.toString():e,this.push(t,s)})),e.once("end",(()=>t(!0))),e.once("close",(()=>t(!0))),e.once("error",(e=>i(e)))}))},this.iterate=async(e,t={})=>{const{eventName:s="iteration"}=t;for await(const t of e)this.push(t,s)},this.req=e,this.res=t,this.serialize=null!==(r=i.serializer)&&void 0!==r?r:o,this.sanitize=null!==(n=i.sanitizer)&&void 0!==n?n:l,this.trustClientEventId=null===(a=i.trustClientEventId)||void 0===a||a,this.initialRetry=null===i.retry?null:null!==(h=i.retry)&&void 0!==h?h:2e3,this.keepAliveInterval=null===i.keepAlive?null:null!==(d=i.keepAlive)&&void 0!==d?d:1e4,this.statusCode=null!==(u=i.statusCode)&&void 0!==u?u:200,this.headers=null!==(p=i.headers)&&void 0!==p?p:{},this.req.once("close",this.onDisconnected),setImmediate(this.initialize)}event(e){return this.writeField("event",e),this}}const p=(...e)=>new Promise((t=>{const s=new u(...e);s.once("connected",(()=>{t(s)}))}));class f extends n{constructor(){super(),this.state={},this.sessions=new Set,this.broadcast=(e,t="message",s={})=>{const i=c();let r;r=s.filter?Array.from(this.sessions).filter(s.filter):this.sessions;for(const s of r)s.push(e,t,i);return this.emit("broadcast",e,t,i),this}}get activeSessions(){return Array.from(this.sessions)}get sessionCount(){return this.sessions.size}register(e){if(this.sessions.has(e))return this;if(!e.isConnected)throw new Error("Cannot register a non-active session.");return e.once("disconnected",(()=>{this.emit("session-disconnected",e),this.deregister(e)})),this.sessions.add(e),this.emit("session-registered",e),this}deregister(e){return this.sessions.has(e)?(this.sessions.delete(e),this.emit("session-deregistered",e),this):this}}const v=(...e)=>new f(...e);return t})()));
2
2
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,2 @@
1
+ declare let generateId: () => string;
2
+ export { generateId };
@@ -1,7 +1,10 @@
1
1
  import http from "http";
2
+ import http2 from "http2";
3
+ import net from "net";
2
4
  import { Session } from "../Session";
3
- declare const createServer: () => Promise<http.Server>;
4
- declare const closeServer: (server: http.Server) => Promise<void>;
5
- declare const getUrl: (server: http.Server) => string;
5
+ declare const createHttpServer: () => Promise<http.Server>;
6
+ declare const createHttp2Server: () => Promise<http2.Http2Server>;
7
+ declare const closeServer: (server: net.Server) => Promise<void>;
8
+ declare const getUrl: (server: net.Server) => string;
6
9
  declare const waitForConnect: (session: Session) => Promise<void>;
7
- export { createServer, closeServer, getUrl, waitForConnect };
10
+ export { createHttpServer, createHttp2Server, closeServer, getUrl, waitForConnect, };
package/package.json CHANGED
@@ -1,13 +1,13 @@
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.7.1",
4
+ "version": "0.8.0",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
7
7
  "license": "MIT",
8
8
  "author": "Matthew W. <matthew.widdi@gmail.com>",
9
9
  "repository": "github:MatthewWid/better-sse",
10
- "homepage": "https://github.com/MatthewWid/better-sse/blob/master/README.md",
10
+ "homepage": "https://github.com/MatthewWid/better-sse",
11
11
  "bugs": "https://github.com/MatthewWid/better-sse/issues",
12
12
  "keywords": [
13
13
  "server-sent-events",
@@ -26,25 +26,26 @@
26
26
  "pnpm": ">=6"
27
27
  },
28
28
  "devDependencies": {
29
+ "@jest/types": "^28.1.0",
29
30
  "@types/eventsource": "^1.1.8",
30
31
  "@types/express": "^4.17.13",
31
- "@types/jest": "^26.0.14",
32
- "@types/node": "^17.0.8",
33
- "@typescript-eslint/eslint-plugin": "^5.9.0",
34
- "@typescript-eslint/parser": "^5.9.0",
35
- "eslint": "^8.6.0",
36
- "eslint-plugin-tsdoc": "^0.2.14",
37
- "eventsource": "^1.1.0",
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",
38
39
  "jest": "^26.6.3",
39
40
  "npm-run-all": "^4.1.5",
40
- "prettier": "^2.5.1",
41
+ "prettier": "^2.6.2",
41
42
  "rimraf": "^3.0.2",
42
43
  "ts-jest": "^26.5.6",
43
- "ts-loader": "^9.2.6",
44
- "ts-node": "^10.4.0",
45
- "typescript": "^4.5.4",
46
- "webpack": "^5.65.0",
47
- "webpack-cli": "^4.9.1"
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"
48
49
  },
49
50
  "scripts": {
50
51
  "build": "webpack --env production",
@@ -52,6 +53,5 @@
52
53
  "clean": "rimraf ./build",
53
54
  "format": "prettier --write ./src/**/*.ts",
54
55
  "lint": "eslint \"./src/**/*.ts\""
55
- },
56
- "readme": "# Better SSE\n\n<p>\n\t<img src=\"https://img.shields.io/npm/v/better-sse?color=blue&style=flat-square\" />\n\t<img src=\"https://img.shields.io/npm/l/better-sse?color=green&style=flat-square\" />\n\t<img src=\"https://img.shields.io/npm/dt/better-sse?color=grey&style=flat-square\" />\n\t<a href=\"https://github.com/MatthewWid/better-sse\"><img src=\"https://img.shields.io/github/stars/MatthewWid/better-sse?style=social\" /></a>\n</p>\n\nA dead simple, dependency-less, spec-compliant server-side events implementation for Node, written in TypeScript.\n\nThis 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.\n\nPlease consider starring the project [on GitHub ⭐](https://github.com/MatthewWid/better-sse).\n\n## Why use Server-sent Events?\n\nServer-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.\n\nUsing 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.\n\nCompared 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.\n\n* [Comparison: Server-sent Events vs WebSockets vs Polling](https://medium.com/dailyjs/a-comparison-between-websockets-server-sent-events-and-polling-7a27c98cb1e3)\n* [WHATWG standards section for server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html)\n* [MDN guide to server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)\n\n## Highlights\n\n* Compatible with all popular Node HTTP frameworks ([http](https://nodejs.org/api/http.html), [Express](https://nodejs.org/api/http.html), [Koa](https://www.npmjs.com/package/koa), [Fastify](https://www.npmjs.com/package/fastify), etc.)\n* Fully written in TypeScript (+ ships with types directly).\n* [Thoroughly tested](./src/Session.test.ts) (+ 100% code coverage!).\n* [Comprehensively documented](./docs) with guides and API documentation.\n* [Channels](./docs/channels.md) allow you to broadcast events to many clients at once.\n* Configurable reconnection time, message serialization and data sanitization (but with good defaults).\n* Trust or ignore the client-given last event ID.\n* Automatically send keep-alive pings to keep connections open.\n* Add or override the response status code and headers.\n* 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.\n* 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.\n* 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).\n\n[See a comparison with other Node SSE libraries in the documentation.](./docs/comparison.md)\n\n# Installation\n\n```bash\n# npm\nnpm install better-sse\n\n# Yarn\nyarn add better-sse\n\n# pnpm\npnpm add better-sse\n```\n\n_Better SSE ships with types built in. No need to install from `@types` for TypeScript users!_\n\n# Usage\n\nThe 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)).\n\nSee the [Recipes](./docs/recipes.md) section of the documentation for use with other frameworks and libraries.\n\n---\n\nUse [sessions](./docs/api.md#session) to push events to clients:\n\n```typescript\n// Server\nimport {createSession} from \"better-sse\";\n\napp.get(\"/sse\", async (req, res) => {\n\tconst session = await createSession(req, res);\n\n\tsession.push(\"Hello world!\");\n});\n```\n\n```typescript\n// Client\nconst sse = new EventSource(\"/sse\");\n\nsse.addEventListener(\"message\", ({data}) => {\n\tconsole.log(data);\n});\n```\n\n---\n\nUse [channels](./docs/channels.md) to send events to many clients at once:\n\n```typescript\nimport {createSession, createChannel} from \"better-sse\";\n\nconst channel = createChannel();\n\napp.get(\"/sse\", async (req, res) => {\n\tconst session = await createSession(req, res);\n\n\tchannel.register(session);\n\n\tchannel.broadcast(\"A user has joined.\", \"join-notification\");\n});\n```\n\n---\n\nLoop over sync and async [iterables](./docs/api.md#sessioniterate-iterable-iterable--asynciterable-options-object--promisevoid) and send each value as an event:\n\n```typescript\nconst session = await createSession(req, res);\n\nconst list = [1, 2, 3];\n\nawait session.iterate(list);\n```\n\n---\n\nPipe [readable stream](#sessionstream-stream-readable-options-object--promiseboolean) data to the client as a stream of events:\n\n```typescript\nconst session = await createSession(req, res);\n\nconst stream = Readable.from([1, 2, 3]);\n\nawait session.stream(stream);\n```\n\n---\n\nCheck the [API documentation](./docs/api.md) and [live examples](https://github.com/MatthewWid/better-sse/tree/master/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!\n\n# Documentation\n\nAPI documentation, getting started guides and usage with other frameworks is [available on GitHub](https://github.com/MatthewWid/better-sse/tree/master/docs).\n\n# Contributing\n\nThis library is always open to contributions, whether it be code, bug reports, documentation or anything else.\n\nPlease submit suggestions, bugs and issues to the [GitHub issues page](https://github.com/MatthewWid/better-sse/issues).\n\nFor code or documentation changes, [submit a pull request on GitHub](https://github.com/MatthewWid/better-sse/pulls).\n\n## Local Development\n\nInstall Node:\n\n```bash\ncurl -L https://git.io/n-install | bash\nn auto\n```\n\nInstall pnpm:\n\n```bash\nnpm i -g pnpm\n```\n\nInstall dependencies:\n\n```bash\npnpm i\n```\n\nRun tests:\n\n```bash\npnpm t\n```\n\n# License\n\nThis project is licensed under the MIT license.\n"
56
+ }
57
57
  }