better-sse 0.4.0 → 0.7.1
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 +74 -13
- package/build/Channel.d.ts +58 -0
- package/build/Session.d.ts +53 -11
- package/build/createChannel.d.ts +3 -0
- package/build/createSession.d.ts +3 -4
- package/build/index.d.ts +4 -2
- package/build/index.js +1 -1
- package/build/lib/TypedEmitter.d.ts +19 -0
- package/build/lib/sanitize.d.ts +3 -2
- package/build/lib/serialize.d.ts +3 -2
- package/build/lib/testUtils.d.ts +7 -0
- package/package.json +56 -57
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
```
|
|
81
|
+
```typescript
|
|
65
82
|
// Client
|
|
66
83
|
const sse = new EventSource("/sse");
|
|
67
84
|
|
|
68
|
-
sse.addEventListener("message", (
|
|
69
|
-
console.log(
|
|
85
|
+
sse.addEventListener("message", ({data}) => {
|
|
86
|
+
console.log(data);
|
|
70
87
|
});
|
|
71
88
|
```
|
|
72
89
|
|
|
73
|
-
|
|
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 };
|
package/build/Session.d.ts
CHANGED
|
@@ -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
|
-
|
|
7
|
+
interface SessionOptions {
|
|
8
8
|
/**
|
|
9
9
|
* Serialize data to a string that can be written.
|
|
10
10
|
*
|
|
@@ -64,13 +64,29 @@ export interface SessionOptions {
|
|
|
64
64
|
*/
|
|
65
65
|
headers?: OutgoingHttpHeaders;
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
interface StreamOptions {
|
|
68
68
|
/**
|
|
69
69
|
* Event name/type to be emitted when stream data is sent to the client.
|
|
70
70
|
*
|
|
71
71
|
* Defaults to `"stream"`.
|
|
72
72
|
*/
|
|
73
|
-
|
|
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;
|
|
74
90
|
}
|
|
75
91
|
/**
|
|
76
92
|
* A Session represents an open connection between the server and a client.
|
|
@@ -84,12 +100,25 @@ export interface StreamOptions {
|
|
|
84
100
|
* @param res - The Node HTTP {@link https://nodejs.org/api/http.html#http_class_http_serverresponse | IncomingMessage} object.
|
|
85
101
|
* @param options - Options given to the session instance.
|
|
86
102
|
*/
|
|
87
|
-
declare class Session extends
|
|
103
|
+
declare class Session<State extends Record<string, unknown> = SessionState> extends TypedEmitter<SessionEvents> {
|
|
88
104
|
/**
|
|
89
105
|
* The last ID sent to the client.
|
|
90
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
|
|
91
109
|
*/
|
|
92
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;
|
|
93
122
|
private req;
|
|
94
123
|
private res;
|
|
95
124
|
private serialize;
|
|
@@ -100,7 +129,6 @@ declare class Session extends EventEmitter {
|
|
|
100
129
|
private keepAliveTimer?;
|
|
101
130
|
private statusCode;
|
|
102
131
|
private headers;
|
|
103
|
-
isConnected: boolean;
|
|
104
132
|
constructor(req: IncomingMessage, res: ServerResponse, options?: SessionOptions);
|
|
105
133
|
private onConnected;
|
|
106
134
|
private onDisconnected;
|
|
@@ -155,17 +183,17 @@ declare class Session extends EventEmitter {
|
|
|
155
183
|
*
|
|
156
184
|
* Note that this sets the event ID (and thus the `lastId` property) to a string of eight random characters (`a-z0-9`).
|
|
157
185
|
*
|
|
158
|
-
* @param
|
|
159
|
-
* @param
|
|
186
|
+
* @param data - Data to write.
|
|
187
|
+
* @param eventName - Event name to write.
|
|
160
188
|
*/
|
|
161
|
-
push: (
|
|
189
|
+
push: (data: unknown, eventName?: string | undefined, eventId?: string | undefined) => this;
|
|
162
190
|
/**
|
|
163
191
|
* Pipe readable stream data to the client.
|
|
164
192
|
*
|
|
165
193
|
* Each data emission by the stream emits a new event that is dispatched to the client.
|
|
166
194
|
* This uses the `push` method under the hood.
|
|
167
195
|
*
|
|
168
|
-
* 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"`.
|
|
169
197
|
*
|
|
170
198
|
* @param stream - Readable stream to consume from.
|
|
171
199
|
* @param options - Options to alter how the stream is flushed to the client.
|
|
@@ -173,5 +201,19 @@ declare class Session extends EventEmitter {
|
|
|
173
201
|
* @returns A promise that resolves or rejects based on the success of the stream write finishing.
|
|
174
202
|
*/
|
|
175
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>;
|
|
176
217
|
}
|
|
177
|
-
export
|
|
218
|
+
export type { SessionOptions, StreamOptions, IterateOptions, SessionState, SessionEvents, };
|
|
219
|
+
export { Session };
|
package/build/createSession.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import Session from "./Session";
|
|
1
|
+
import { Session, SessionState } from "./Session";
|
|
3
2
|
/**
|
|
4
3
|
* Create a new session and return the session instance once it has connected.
|
|
5
4
|
*/
|
|
6
|
-
declare const createSession: (req: import("http").IncomingMessage, res: import("http").ServerResponse, options?: import("./Session").SessionOptions | undefined) => Promise<Session
|
|
7
|
-
export
|
|
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>>;
|
|
6
|
+
export { createSession };
|
package/build/index.d.ts
CHANGED
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
|
|
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
|
+
}
|
package/build/lib/sanitize.d.ts
CHANGED
package/build/lib/serialize.d.ts
CHANGED
|
@@ -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
|
-
|
|
4
|
+
interface SerializerFunction {
|
|
5
5
|
(data: unknown): string;
|
|
6
6
|
}
|
|
7
7
|
declare const serialize: SerializerFunction;
|
|
8
|
-
export
|
|
8
|
+
export type { SerializerFunction };
|
|
9
|
+
export { serialize };
|
|
@@ -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,58 +1,57 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
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.1",
|
|
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
|
+
"test": "jest",
|
|
52
|
+
"clean": "rimraf ./build",
|
|
53
|
+
"format": "prettier --write ./src/**/*.ts",
|
|
54
|
+
"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"
|
|
57
|
+
}
|