better-sse 0.3.0 → 0.7.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
@@ -11,11 +11,19 @@ A dead simple, dependency-less, spec-compliant server-side events implementation
11
11
 
12
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
- [See the WHATWG standards section for server-sent events.](https://html.spec.whatwg.org/multipage/server-sent-events.html)
14
+ Please consider starring the project [on GitHub ](https://github.com/MatthewWid/better-sse).
15
15
 
16
- [See the MDN guide to server-sent events.](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
16
+ ## Why use Server-sent Events?
17
17
 
18
- Please consider starring the project [on GitHub ⭐](https://github.com/MatthewWid/better-sse).
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)
19
27
 
20
28
  ## Highlights
21
29
 
@@ -23,12 +31,16 @@ Please consider starring the project [on GitHub ⭐](https://github.com/MatthewW
23
31
  * Fully written in TypeScript (+ ships with types directly).
24
32
  * [Thoroughly tested](./src/Session.test.ts) (+ 100% code coverage!).
25
33
  * [Comprehensively documented](./docs) with guides and API documentation.
26
- * Configurable reconnection time.
27
- * Configurable message serialization and data sanitization (but with good defaults).
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).
28
36
  * Trust or ignore the client-given last event ID.
37
+ * Automatically send keep-alive pings to keep connections open.
29
38
  * Add or override the response status code and headers.
30
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.
31
- * Pipe [streams](https://nodejs.org/api/stream.html#stream_readable_streams) directly from the server to the client as a stream of events.
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)
32
44
 
33
45
  # Installation
34
46
 
@@ -45,32 +57,81 @@ pnpm add better-sse
45
57
 
46
58
  _Better SSE ships with types built in. No need to install from `@types` for TypeScript users!_
47
59
 
48
- # Basic Usage
60
+ # Usage
49
61
 
50
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)).
51
63
 
52
- See the Recipes section of the documentation for use with other frameworks and libraries.
64
+ See the [Recipes](./docs/recipes.md) section of the documentation for use with other frameworks and libraries.
65
+
66
+ ---
53
67
 
54
- ```javascript
68
+ Use [sessions](./docs/api.md#session) to push events to clients:
69
+
70
+ ```typescript
55
71
  // Server
56
72
  import {createSession} from "better-sse";
57
73
 
58
74
  app.get("/sse", async (req, res) => {
59
75
  const session = await createSession(req, res);
76
+
60
77
  session.push("Hello world!");
61
78
  });
62
79
  ```
63
80
 
64
- ```javascript
81
+ ```typescript
65
82
  // Client
66
83
  const sse = new EventSource("/sse");
67
84
 
68
- sse.addEventListener("message", (event) => {
69
- console.log(event.data);
85
+ sse.addEventListener("message", ({data}) => {
86
+ console.log(data);
70
87
  });
71
88
  ```
72
89
 
73
- Check [the API documentation](https://github.com/MatthewWid/better-sse/blob/master/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, streams, dispatch controls and more!
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!
74
135
 
75
136
  # Documentation
76
137
 
@@ -0,0 +1,58 @@
1
+ import { TypedEmitter, EventMap } from "./lib/TypedEmitter";
2
+ import { Session } from "./Session";
3
+ interface BroadcastOptions {
4
+ /**
5
+ * Filter sessions that should receive the event.
6
+ *
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
+ */
9
+ filter?: (session: Session) => boolean;
10
+ }
11
+ interface ChannelEvents extends EventMap {
12
+ "session-registered": (session: Session) => void;
13
+ "session-deregistered": (session: Session) => void;
14
+ "session-disconnected": (session: Session) => void;
15
+ broadcast: (data: unknown, eventName: string) => void;
16
+ }
17
+ /**
18
+ * A Channel is used to broadcast events to many sessions at once.
19
+ *
20
+ * It extends from the {@link https://nodejs.org/api/events.html#events_class_eventemitter | EventEmitter} class.
21
+ */
22
+ declare class Channel<State extends Record<string, unknown> = Record<string, unknown>> extends TypedEmitter<ChannelEvents> {
23
+ /**
24
+ * Custom state for this channel.
25
+ * Use this object to safely store information related to the channel.
26
+ */
27
+ state: State;
28
+ private sessions;
29
+ constructor();
30
+ /**
31
+ * List of the currently active sessions subscribed to this channel.
32
+ */
33
+ get activeSessions(): ReadonlyArray<Session>;
34
+ /**
35
+ * Number of sessions subscribed to this channel.
36
+ */
37
+ get sessionCount(): number;
38
+ /**
39
+ * Register a session so that it can start receiving events from this channel.
40
+ *
41
+ * @param session - Session to register.
42
+ */
43
+ register(session: Session): this;
44
+ /**
45
+ * Deregister a session so that it no longer receives events from this channel.
46
+ *
47
+ * @param session - Session to deregister.
48
+ */
49
+ deregister(session: Session): this;
50
+ /**
51
+ * Push an event to every active session on this channel.
52
+ *
53
+ * Takes the same arguments as the `Session#push` method.
54
+ */
55
+ broadcast: (data: unknown, eventName?: string | undefined, options?: BroadcastOptions) => this;
56
+ }
57
+ export type { BroadcastOptions, ChannelEvents };
58
+ export { Channel };
@@ -0,0 +1,6 @@
1
+ declare module "./Session" {
2
+ interface SessionState {
3
+ isTrusted: boolean;
4
+ }
5
+ }
6
+ export {};
@@ -1,10 +1,10 @@
1
1
  /// <reference types="node" />
2
- import EventEmitter from "events";
3
2
  import { Readable } from "stream";
4
3
  import { IncomingMessage, ServerResponse, OutgoingHttpHeaders } from "http";
4
+ import { TypedEmitter, EventMap } from "./lib/TypedEmitter";
5
5
  import { SerializerFunction } from "./lib/serialize";
6
6
  import { SanitizerFunction } from "./lib/sanitize";
7
- export interface SessionOptions {
7
+ interface SessionOptions {
8
8
  /**
9
9
  * Serialize data to a string that can be written.
10
10
  *
@@ -39,6 +39,14 @@ export interface SessionOptions {
39
39
  * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#concept-event-stream-reconnection-time
40
40
  */
41
41
  retry?: number | null;
42
+ /**
43
+ * Time in milliseconds interval for the session to send a comment to keep the connection alive.
44
+ *
45
+ * Give as `null` to disable the connection keep-alive mechanism.
46
+ *
47
+ * Defaults to `10000` milliseconds (`10` seconds).
48
+ */
49
+ keepAlive?: number | null;
42
50
  /**
43
51
  * Status code to be sent to the client.
44
52
  *
@@ -56,13 +64,29 @@ export interface SessionOptions {
56
64
  */
57
65
  headers?: OutgoingHttpHeaders;
58
66
  }
59
- export interface StreamOptions {
67
+ interface StreamOptions {
60
68
  /**
61
69
  * Event name/type to be emitted when stream data is sent to the client.
62
70
  *
63
71
  * Defaults to `"stream"`.
64
72
  */
65
- event?: string;
73
+ eventName?: string;
74
+ }
75
+ interface IterateOptions {
76
+ /**
77
+ * Event name/type to be emitted when iterable data is sent to the client.
78
+ *
79
+ * Defaults to `"iteration"`.
80
+ */
81
+ eventName?: string;
82
+ }
83
+ interface SessionState {
84
+ [key: string]: unknown;
85
+ }
86
+ interface SessionEvents extends EventMap {
87
+ connected: () => void;
88
+ disconnected: () => void;
89
+ push: (data: unknown, eventName: string, eventId: string) => void;
66
90
  }
67
91
  /**
68
92
  * A Session represents an open connection between the server and a client.
@@ -76,18 +100,33 @@ export interface StreamOptions {
76
100
  * @param res - The Node HTTP {@link https://nodejs.org/api/http.html#http_class_http_serverresponse | IncomingMessage} object.
77
101
  * @param options - Options given to the session instance.
78
102
  */
79
- declare class Session extends EventEmitter {
103
+ declare class Session<State extends Record<string, unknown> = SessionState> extends TypedEmitter<SessionEvents> {
80
104
  /**
81
105
  * The last ID sent to the client.
82
106
  * 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
+ *
108
+ * @readonly
83
109
  */
84
110
  lastId: string;
111
+ /**
112
+ * Indicates whether the session and connection is open or not.
113
+ *
114
+ * @readonly
115
+ */
116
+ isConnected: boolean;
117
+ /**
118
+ * Custom state for this session.
119
+ * Use this object to safely store information related to the session and user.
120
+ */
121
+ state: State;
85
122
  private req;
86
123
  private res;
87
124
  private serialize;
88
125
  private sanitize;
89
126
  private trustClientEventId;
90
127
  private initialRetry;
128
+ private keepAliveInterval;
129
+ private keepAliveTimer?;
91
130
  private statusCode;
92
131
  private headers;
93
132
  constructor(req: IncomingMessage, res: ServerResponse, options?: SessionOptions);
@@ -97,6 +136,7 @@ declare class Session extends EventEmitter {
97
136
  * Write a line with a field key and value appended with a newline character.
98
137
  */
99
138
  private writeField;
139
+ private keepAlive;
100
140
  /**
101
141
  * Flush the buffered data to the client by writing an additional newline.
102
142
  */
@@ -132,9 +172,9 @@ declare class Session extends EventEmitter {
132
172
  *
133
173
  * This will not fire an event, but is often used to keep the connection alive.
134
174
  *
135
- * @param text - Field value of the comment.
175
+ * @param text - Text of the comment. Otherwise writes an empty field value.
136
176
  */
137
- comment: (text: string) => this;
177
+ comment: (text?: string | undefined) => this;
138
178
  /**
139
179
  * Create and dispatch an event with the given data all at once.
140
180
  * This is equivalent to calling `.event()`, `.id()`, `.data()` and `.dispatch()` in that order.
@@ -143,17 +183,17 @@ declare class Session extends EventEmitter {
143
183
  *
144
184
  * Note that this sets the event ID (and thus the `lastId` property) to a string of eight random characters (`a-z0-9`).
145
185
  *
146
- * @param eventOrData - Event name or data to write.
147
- * @param data - Data to write if `eventOrData` was an event name.
186
+ * @param data - Data to write.
187
+ * @param eventName - Event name to write.
148
188
  */
149
- push: (eventOrData: string | unknown, data?: unknown) => this;
189
+ push: (data: unknown, eventName?: string | undefined, eventId?: string | undefined) => this;
150
190
  /**
151
191
  * Pipe readable stream data to the client.
152
192
  *
153
193
  * Each data emission by the stream emits a new event that is dispatched to the client.
154
194
  * This uses the `push` method under the hood.
155
195
  *
156
- * If no event name is given in the options object, the event name (type) is to `"stream"`.
196
+ * If no event name is given in the options object, the event name (type) is set to `"stream"`.
157
197
  *
158
198
  * @param stream - Readable stream to consume from.
159
199
  * @param options - Options to alter how the stream is flushed to the client.
@@ -161,5 +201,19 @@ declare class Session extends EventEmitter {
161
201
  * @returns A promise that resolves or rejects based on the success of the stream write finishing.
162
202
  */
163
203
  stream: (stream: Readable, options?: StreamOptions) => Promise<boolean>;
204
+ /**
205
+ * Iterate over an iterable and send yielded values as data to the client.
206
+ *
207
+ * Each yield emits a new event that is dispatched to the client.
208
+ * This uses the `push` method under the hood.
209
+ *
210
+ * If no event name is given in the options object, the event name (type) is set to `"iteration"`.
211
+ *
212
+ * @param iterable - Iterable to consume data from.
213
+ *
214
+ * @returns A promise that resolves once all the data has been yielded from the iterable.
215
+ */
216
+ iterate: <DataType = unknown>(iterable: Iterable<DataType> | AsyncIterable<DataType>, options?: IterateOptions) => Promise<void>;
164
217
  }
165
- export default Session;
218
+ export type { SessionOptions, StreamOptions, IterateOptions, SessionState, SessionEvents, };
219
+ export { Session };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { Channel } from "./Channel";
2
+ declare const createChannel: <State extends Record<string, unknown>>() => Channel<State>;
3
+ export { createChannel };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,7 +1,7 @@
1
1
  /// <reference types="node" />
2
- import Session from "./Session";
2
+ import { Session, SessionState } from "./Session";
3
3
  /**
4
4
  * Create a new session and return the session instance once it has connected.
5
5
  */
6
- declare const createSession: (req: import("http").IncomingMessage, res: import("http").ServerResponse, options?: import("./Session").SessionOptions | undefined) => Promise<Session>;
7
- export default createSession;
6
+ 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>>;
7
+ export { createSession };
@@ -0,0 +1 @@
1
+ export {};
package/build/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
- export { default as Session } from "./Session";
2
- export { default as createSession } from "./createSession";
1
+ export * from "./Session";
2
+ export * from "./createSession";
3
+ export * from "./Channel";
4
+ export * from "./createChannel";
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,{Session:()=>h,createSession:()=>a});const s=require("events");var i=e.n(s);const r=require("crypto"),n=e=>JSON.stringify(e),o=e=>{let t=e;return t=t.replace(/(\r\n|\r|\n)/g,"\n"),t=t.replace(/\n+$/g,""),t};class d extends(i()){constructor(e,t,s={}){var i,d,h,a,l,c;super(),this.lastId="",this.onConnected=()=>{var e;if(this.trustClientEventId){const t=null!==(e=this.req.headers["last-event-id"])&&void 0!==e?e:"";this.lastId=t}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(),null!==this.initialRetry&&this.retry(this.initialRetry).dispatch(),this.emit("connected")},this.onDisconnected=()=>{this.emit("disconnected")},this.writeField=(e,t)=>{const s=`${e}:${this.sanitize(t)}\n`;return this.res.write(s),this},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("",e),this),this.push=(e,t)=>{let s,i;e&&void 0===t?(s="message",i=e):(s=e.toString(),i=t);const n=(0,r.randomBytes)(4).toString("hex");return this.event(s).id(n).data(i).dispatch(),this},this.stream=async(e,t={})=>{const{event:s="stream"}=t;return new Promise(((t,i)=>{e.on("data",(e=>{let t;t=Buffer.isBuffer(e)?e.toString():e,this.push(s,t)})),e.once("end",(()=>t(!0))),e.once("close",(()=>t(!0))),e.once("error",(e=>i(e)))}))},this.req=e,this.res=t,this.serialize=null!==(i=s.serializer)&&void 0!==i?i:n,this.sanitize=null!==(d=s.sanitizer)&&void 0!==d?d:o,this.trustClientEventId=null===(h=s.trustClientEventId)||void 0===h||h,this.initialRetry=null===s.retry?null:null!==(a=s.retry)&&void 0!==a?a:2e3,this.statusCode=null!==(l=s.statusCode)&&void 0!==l?l:200,this.headers=null!==(c=s.headers)&&void 0!==c?c:{},this.req.on("close",this.onDisconnected),setImmediate(this.onConnected)}event(e){return this.writeField("event",e),this}}const h=d,a=(...e)=>new Promise((t=>{const s=new h(...e);s.once("connected",(()=>{t(s)}))}));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,(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})()}));
2
2
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,19 @@
1
+ /// <reference types="node" />
2
+ import EventEmitter from "events";
3
+ export interface EventMap {
4
+ [name: string | symbol]: (...args: any[]) => void;
5
+ }
6
+ /**
7
+ * Wraps the EventEmitter class to add types that map event names
8
+ * to types of arguments in the event handler callback.
9
+ */
10
+ export declare class TypedEmitter<Events extends EventMap> extends EventEmitter {
11
+ addListener<EventName extends keyof Events>(event: EventName, listener: Events[EventName]): this;
12
+ prependListener<EventName extends keyof Events>(event: EventName, listener: Events[EventName]): this;
13
+ prependOnceListener<EventName extends keyof Events>(event: EventName, listener: Events[EventName]): this;
14
+ on<EventName extends keyof Events>(event: EventName, listener: Events[EventName]): this;
15
+ once<EventName extends keyof Events>(event: EventName, listener: Events[EventName]): this;
16
+ emit<EventName extends keyof Events>(event: EventName, ...args: Parameters<Events[EventName]>): boolean;
17
+ off<EventName extends keyof Events>(event: EventName, listener: Events[EventName]): this;
18
+ removeListener<EventName extends keyof Events>(event: EventName, listener: Events[EventName]): this;
19
+ }
@@ -1,5 +1,6 @@
1
- export interface SanitizerFunction {
1
+ interface SanitizerFunction {
2
2
  (text: string): string;
3
3
  }
4
4
  declare const sanitize: SanitizerFunction;
5
- export default sanitize;
5
+ export type { SanitizerFunction };
6
+ export { sanitize };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Serialize arbitrary data to a string that can be sent over the wire to the client.
3
3
  */
4
- export interface SerializerFunction {
4
+ interface SerializerFunction {
5
5
  (data: unknown): string;
6
6
  }
7
7
  declare const serialize: SerializerFunction;
8
- export default serialize;
8
+ export type { SerializerFunction };
9
+ export { serialize };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import http from "http";
2
+ 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;
6
+ declare const waitForConnect: (session: Session) => Promise<void>;
7
+ export { createServer, closeServer, getUrl, waitForConnect };
package/package.json CHANGED
@@ -1,60 +1,59 @@
1
1
  {
2
- "name": "better-sse",
3
- "description": "Dead simple, dependency-less, spec-compliant server-side events implementation for Node, written in TypeScript.",
4
- "version": "0.3.0",
5
- "main": "./build/index.js",
6
- "types": "./build/index.d.ts",
7
- "license": "MIT",
8
- "author": "Matthew W. <matthew.widdi@gmail.com>",
9
- "repository": "github:MatthewWid/better-sse",
10
- "homepage": "https://github.com/MatthewWid/better-sse/blob/master/README.md",
11
- "bugs": "https://github.com/MatthewWid/better-sse/issues",
12
- "keywords": [
13
- "server-sent-events",
14
- "sse",
15
- "realtime",
16
- "real-time",
17
- "tcp",
18
- "events"
19
- ],
20
- "scripts": {
21
- "preinstall": "npx only-allow pnpm",
22
- "build": "webpack --env production",
23
- "dev": "webpack --env development",
24
- "test": "jest",
25
- "clean": "rimraf ./build",
26
- "format": "prettier --write ./src/**/*.ts",
27
- "typedefs": "tsc --emitDeclarationOnly",
28
- "lint": "eslint \"./src/**/*.ts\"",
29
- "prepublishOnly": "npm-run-all clean format test build typedefs"
30
- },
31
- "files": [
32
- "build/",
33
- "!build/**/*.map"
34
- ],
35
- "engines": {
36
- "node": ">=12",
37
- "pnpm": ">=6"
38
- },
39
- "devDependencies": {
40
- "@types/eventsource": "^1.1.5",
41
- "@types/express": "^4.17.9",
42
- "@types/jest": "^26.0.19",
43
- "@types/node": "^14.14.20",
44
- "@typescript-eslint/eslint-plugin": "^4.11.0",
45
- "@typescript-eslint/parser": "^4.11.0",
46
- "eslint": "^7.16.0",
47
- "eslint-plugin-tsdoc": "^0.2.10",
48
- "eventsource": "^1.1.0",
49
- "jest": "^26.6.3",
50
- "npm-run-all": "^4.1.5",
51
- "prettier": "^2.2.1",
52
- "rimraf": "^3.0.2",
53
- "ts-jest": "^26.4.4",
54
- "ts-loader": "^9.2.3",
55
- "ts-node": "^10.0.0",
56
- "typescript": "^4.1.3",
57
- "webpack": "^5.38.1",
58
- "webpack-cli": "^4.7.0"
59
- }
60
- }
2
+ "name": "better-sse",
3
+ "description": "Dead simple, dependency-less, spec-compliant server-side events implementation for Node, written in TypeScript.",
4
+ "version": "0.7.0",
5
+ "main": "./build/index.js",
6
+ "types": "./build/index.d.ts",
7
+ "license": "MIT",
8
+ "author": "Matthew W. <matthew.widdi@gmail.com>",
9
+ "repository": "github:MatthewWid/better-sse",
10
+ "homepage": "https://github.com/MatthewWid/better-sse/blob/master/README.md",
11
+ "bugs": "https://github.com/MatthewWid/better-sse/issues",
12
+ "keywords": [
13
+ "server-sent-events",
14
+ "sse",
15
+ "realtime",
16
+ "real-time",
17
+ "tcp",
18
+ "events"
19
+ ],
20
+ "files": [
21
+ "build",
22
+ "!build/**/*.map"
23
+ ],
24
+ "engines": {
25
+ "node": ">=12",
26
+ "pnpm": ">=6"
27
+ },
28
+ "devDependencies": {
29
+ "@types/eventsource": "^1.1.8",
30
+ "@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",
38
+ "jest": "^26.6.3",
39
+ "npm-run-all": "^4.1.5",
40
+ "prettier": "^2.5.1",
41
+ "rimraf": "^3.0.2",
42
+ "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"
48
+ },
49
+ "scripts": {
50
+ "build": "webpack --env production",
51
+ "dev": "webpack --env development",
52
+ "test": "jest",
53
+ "clean": "rimraf ./build",
54
+ "format": "prettier --write ./src/**/*.ts",
55
+ "typedefs": "tsc --emitDeclarationOnly",
56
+ "lint": "eslint \"./src/**/*.ts\""
57
+ },
58
+ "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"
59
+ }