better-sse 0.15.1 → 0.16.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 +26 -3
- package/build/index.cjs +771 -0
- package/build/index.d.cts +662 -0
- package/build/index.d.mts +580 -415
- package/build/index.mjs +728 -744
- package/package.json +15 -16
- package/build/index.d.ts +0 -497
- package/build/index.js +0 -807
package/build/index.mjs
CHANGED
|
@@ -1,776 +1,760 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
ServerResponse as Http1ServerResponse
|
|
5
|
-
} from "node:http";
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
6
4
|
import { Http2ServerRequest, Http2ServerResponse } from "node:http2";
|
|
7
5
|
import { setImmediate } from "node:timers";
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
//#region src/adapters/Connection.ts
|
|
8
|
+
const connectionConstants = Object.freeze({
|
|
9
|
+
REQUEST_METHOD: "GET",
|
|
10
|
+
REQUEST_HOST: "localhost",
|
|
11
|
+
RESPONSE_CODE: 200,
|
|
12
|
+
RESPONSE_HEADERS: Object.freeze({
|
|
13
|
+
"Content-Type": "text/event-stream",
|
|
14
|
+
"Cache-Control": "private, no-cache, no-store, no-transform, must-revalidate, max-age=0",
|
|
15
|
+
Connection: "keep-alive",
|
|
16
|
+
Pragma: "no-cache",
|
|
17
|
+
"X-Accel-Buffering": "no"
|
|
18
|
+
})
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Represents the full request and response of an underlying network connection,
|
|
22
|
+
* abstracting away the differences between the Node HTTP/1, HTTP/2, Fetch and
|
|
23
|
+
* any other APIs.
|
|
24
|
+
*
|
|
25
|
+
* You can implement your own custom `Connection` subclass to make Better SSE
|
|
26
|
+
* compatible with any framework.
|
|
27
|
+
*/
|
|
28
|
+
var Connection = class {
|
|
29
|
+
/**
|
|
30
|
+
* Useful constants that you may use when implementing your own custom connection.
|
|
31
|
+
*/
|
|
32
|
+
static constants = connectionConstants;
|
|
33
|
+
/**
|
|
34
|
+
* Utility method to consistently merge headers from an object into a `Headers` object.
|
|
35
|
+
*
|
|
36
|
+
* For each entry in `from`:
|
|
37
|
+
* - If the value is a `string`, it will replace the target header.
|
|
38
|
+
* - If the value is an `Array`, it will replace the target header and then append each item.
|
|
39
|
+
* - If the value is `undefined`, it will delete the target header.
|
|
40
|
+
*/
|
|
41
|
+
static applyHeaders(from, to) {
|
|
42
|
+
const fromMap = from instanceof Headers ? Object.fromEntries(from) : from;
|
|
43
|
+
for (const [key, value] of Object.entries(fromMap)) if (Array.isArray(value)) {
|
|
44
|
+
to.delete(key);
|
|
45
|
+
for (const item of value) to.append(key, item);
|
|
46
|
+
} else if (value === void 0) to.delete(key);
|
|
47
|
+
else to.set(key, value);
|
|
48
|
+
}
|
|
15
49
|
};
|
|
16
50
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
var
|
|
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
|
-
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/adapters/FetchConnection.ts
|
|
53
|
+
var FetchConnection = class FetchConnection extends Connection {
|
|
54
|
+
static encoder = new TextEncoder();
|
|
55
|
+
writer;
|
|
56
|
+
url;
|
|
57
|
+
request;
|
|
58
|
+
response;
|
|
59
|
+
constructor(request, response, options = {}) {
|
|
60
|
+
super();
|
|
61
|
+
this.url = new URL(request.url);
|
|
62
|
+
this.request = request;
|
|
63
|
+
const { readable, writable } = new TransformStream();
|
|
64
|
+
this.writer = writable.getWriter();
|
|
65
|
+
this.response = new Response(readable, {
|
|
66
|
+
status: options.statusCode ?? response?.status ?? Connection.constants.RESPONSE_CODE,
|
|
67
|
+
headers: Connection.constants.RESPONSE_HEADERS
|
|
68
|
+
});
|
|
69
|
+
if (response) Connection.applyHeaders(response.headers, this.response.headers);
|
|
70
|
+
if (options.headers) Connection.applyHeaders(options.headers, this.response.headers);
|
|
71
|
+
}
|
|
72
|
+
sendHead = () => {};
|
|
73
|
+
sendChunk = (chunk) => {
|
|
74
|
+
const encoded = FetchConnection.encoder.encode(chunk);
|
|
75
|
+
this.writer.write(encoded);
|
|
76
|
+
};
|
|
77
|
+
cleanup = () => {
|
|
78
|
+
this.writer.close();
|
|
79
|
+
};
|
|
45
80
|
};
|
|
46
81
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
var
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/adapters/NodeHttp1Connection.ts
|
|
84
|
+
var NodeHttp1Connection = class extends Connection {
|
|
85
|
+
controller;
|
|
86
|
+
url;
|
|
87
|
+
request;
|
|
88
|
+
response;
|
|
89
|
+
constructor(req, res, options = {}) {
|
|
90
|
+
super();
|
|
91
|
+
this.req = req;
|
|
92
|
+
this.res = res;
|
|
93
|
+
req.socket.setNoDelay(true);
|
|
94
|
+
res.socket?.setNoDelay(true);
|
|
95
|
+
this.url = new URL(req.url ?? "/", `http://${req.headers.host ?? Connection.constants.REQUEST_HOST}`);
|
|
96
|
+
this.controller = new AbortController();
|
|
97
|
+
req.once("close", this.onClose);
|
|
98
|
+
res.once("close", this.onClose);
|
|
99
|
+
this.request = new Request(this.url, {
|
|
100
|
+
method: req.method ?? Connection.constants.REQUEST_METHOD,
|
|
101
|
+
signal: this.controller.signal
|
|
102
|
+
});
|
|
103
|
+
Connection.applyHeaders(req.headers, this.request.headers);
|
|
104
|
+
this.response = new Response(null, {
|
|
105
|
+
status: options.statusCode ?? res.statusCode ?? Connection.constants.RESPONSE_CODE,
|
|
106
|
+
headers: Connection.constants.RESPONSE_HEADERS
|
|
107
|
+
});
|
|
108
|
+
if (res) Connection.applyHeaders(res.getHeaders(), this.response.headers);
|
|
109
|
+
if (options.headers) Connection.applyHeaders(options.headers, this.response.headers);
|
|
110
|
+
}
|
|
111
|
+
onClose = () => {
|
|
112
|
+
this.controller.abort();
|
|
113
|
+
};
|
|
114
|
+
sendHead = () => {
|
|
115
|
+
this.res.writeHead(this.response.status, Object.fromEntries(this.response.headers));
|
|
116
|
+
};
|
|
117
|
+
sendChunk = (chunk) => {
|
|
118
|
+
this.res.write(chunk);
|
|
119
|
+
};
|
|
120
|
+
cleanup = () => {
|
|
121
|
+
this.req.removeListener("close", this.onClose);
|
|
122
|
+
this.res.removeListener("close", this.onClose);
|
|
123
|
+
};
|
|
64
124
|
};
|
|
65
125
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
* Defaults to an empty string if no argument is given.
|
|
111
|
-
*
|
|
112
|
-
* @param id - Identification string to write.
|
|
113
|
-
*/
|
|
114
|
-
id = (id = "") => {
|
|
115
|
-
this.writeField("id", id);
|
|
116
|
-
return this;
|
|
117
|
-
};
|
|
118
|
-
/**
|
|
119
|
-
* Write a retry field that suggests a reconnection time with the given milliseconds.
|
|
120
|
-
*
|
|
121
|
-
* @param time - Time in milliseconds to retry.
|
|
122
|
-
*/
|
|
123
|
-
retry = (time) => {
|
|
124
|
-
const stringifed = time.toString();
|
|
125
|
-
this.writeField("retry", stringifed);
|
|
126
|
-
return this;
|
|
127
|
-
};
|
|
128
|
-
/**
|
|
129
|
-
* Write a comment (an ignored field).
|
|
130
|
-
*
|
|
131
|
-
* This will not fire an event but is often used to keep the connection alive.
|
|
132
|
-
*
|
|
133
|
-
* @param text - Text of the comment. Otherwise writes an empty field value.
|
|
134
|
-
*/
|
|
135
|
-
comment = (text = "") => {
|
|
136
|
-
this.writeField("", text);
|
|
137
|
-
return this;
|
|
138
|
-
};
|
|
139
|
-
/**
|
|
140
|
-
* Indicate that the event has finished being created by writing an additional newline character.
|
|
141
|
-
*/
|
|
142
|
-
dispatch = () => {
|
|
143
|
-
this.buffer += "\n";
|
|
144
|
-
return this;
|
|
145
|
-
};
|
|
146
|
-
/**
|
|
147
|
-
* Create, write and dispatch an event with the given data all at once.
|
|
148
|
-
*
|
|
149
|
-
* This is equivalent to calling the methods `event`, `id`, `data` and `dispatch` in that order.
|
|
150
|
-
*
|
|
151
|
-
* If no event name is given, the event name is set to `"message"`.
|
|
152
|
-
*
|
|
153
|
-
* If no event ID is given, the event ID is set to a unique string generated using a cryptographic pseudorandom number generator.
|
|
154
|
-
*
|
|
155
|
-
* @param data - Data to write.
|
|
156
|
-
* @param eventName - Event name to write.
|
|
157
|
-
* @param eventId - Event ID to write.
|
|
158
|
-
*/
|
|
159
|
-
push = (data, eventName = "message", eventId = generateId()) => {
|
|
160
|
-
this.event(eventName).id(eventId).data(data).dispatch();
|
|
161
|
-
return this;
|
|
162
|
-
};
|
|
163
|
-
/**
|
|
164
|
-
* Pipe readable stream data as a series of events into the buffer.
|
|
165
|
-
*
|
|
166
|
-
* This uses the `push` method under the hood.
|
|
167
|
-
*
|
|
168
|
-
* If no event name is given in the `options` object, the event name is set to `"stream"`.
|
|
169
|
-
*
|
|
170
|
-
* @param stream - Readable stream to consume data from.
|
|
171
|
-
* @param options - Event name to use for each event created.
|
|
172
|
-
*
|
|
173
|
-
* @returns A promise that resolves with `true` or rejects based on the success of the stream write finishing.
|
|
174
|
-
*/
|
|
175
|
-
stream = createPushFromStream(this.push);
|
|
176
|
-
/**
|
|
177
|
-
* Iterate over an iterable and write yielded values as events into the buffer.
|
|
178
|
-
*
|
|
179
|
-
* This uses the `push` method under the hood.
|
|
180
|
-
*
|
|
181
|
-
* If no event name is given in the `options` object, the event name is set to `"iteration"`.
|
|
182
|
-
*
|
|
183
|
-
* @param iterable - Iterable to consume data from.
|
|
184
|
-
*
|
|
185
|
-
* @returns A promise that resolves once all data has been successfully yielded from the iterable.
|
|
186
|
-
*/
|
|
187
|
-
iterate = createPushFromIterable(this.push);
|
|
188
|
-
/**
|
|
189
|
-
* Clear the contents of the buffer.
|
|
190
|
-
*/
|
|
191
|
-
clear = () => {
|
|
192
|
-
this.buffer = "";
|
|
193
|
-
return this;
|
|
194
|
-
};
|
|
195
|
-
/**
|
|
196
|
-
* Get a copy of the buffer contents.
|
|
197
|
-
*/
|
|
198
|
-
read = () => this.buffer;
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/adapters/NodeHttp2CompatConnection.ts
|
|
128
|
+
var NodeHttp2CompatConnection = class extends Connection {
|
|
129
|
+
controller;
|
|
130
|
+
url;
|
|
131
|
+
request;
|
|
132
|
+
response;
|
|
133
|
+
constructor(req, res, options = {}) {
|
|
134
|
+
super();
|
|
135
|
+
this.req = req;
|
|
136
|
+
this.res = res;
|
|
137
|
+
req.socket.setNoDelay(true);
|
|
138
|
+
res.socket?.setNoDelay(true);
|
|
139
|
+
this.url = new URL(req.url ?? "/", `http://${req.headers.host ?? Connection.constants.REQUEST_HOST}`);
|
|
140
|
+
this.controller = new AbortController();
|
|
141
|
+
req.once("close", this.onClose);
|
|
142
|
+
res.once("close", this.onClose);
|
|
143
|
+
this.request = new Request(this.url, {
|
|
144
|
+
method: req.method ?? Connection.constants.REQUEST_METHOD,
|
|
145
|
+
signal: this.controller.signal
|
|
146
|
+
});
|
|
147
|
+
const allowedHeaders = { ...req.headers };
|
|
148
|
+
for (const header of Object.keys(allowedHeaders)) if (header.startsWith(":")) delete allowedHeaders[header];
|
|
149
|
+
Connection.applyHeaders(allowedHeaders, this.request.headers);
|
|
150
|
+
this.response = new Response(null, {
|
|
151
|
+
status: options.statusCode ?? res.statusCode ?? Connection.constants.RESPONSE_CODE,
|
|
152
|
+
headers: Connection.constants.RESPONSE_HEADERS
|
|
153
|
+
});
|
|
154
|
+
if (res) Connection.applyHeaders(res.getHeaders(), this.response.headers);
|
|
155
|
+
if (options.headers) Connection.applyHeaders(options.headers, this.response.headers);
|
|
156
|
+
}
|
|
157
|
+
onClose = () => {
|
|
158
|
+
this.controller.abort();
|
|
159
|
+
};
|
|
160
|
+
sendHead = () => {
|
|
161
|
+
this.res.writeHead(this.response.status, Object.fromEntries(this.response.headers));
|
|
162
|
+
};
|
|
163
|
+
sendChunk = (chunk) => {
|
|
164
|
+
this.res.write(chunk);
|
|
165
|
+
};
|
|
166
|
+
cleanup = () => {
|
|
167
|
+
this.req.removeListener("close", this.onClose);
|
|
168
|
+
this.res.removeListener("close", this.onClose);
|
|
169
|
+
};
|
|
199
170
|
};
|
|
200
171
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
to.append(key, item);
|
|
209
|
-
}
|
|
210
|
-
} else if (value === void 0) {
|
|
211
|
-
to.delete(key);
|
|
212
|
-
} else {
|
|
213
|
-
to.set(key, value);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/utils/SseError.ts
|
|
174
|
+
var SseError = class extends Error {
|
|
175
|
+
constructor(message) {
|
|
176
|
+
super(message);
|
|
177
|
+
this.message = `better-sse: ${message}`;
|
|
178
|
+
}
|
|
216
179
|
};
|
|
217
180
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/utils/TypedEmitter.ts
|
|
183
|
+
/**
|
|
184
|
+
* Wraps the EventEmitter class to add types that map event names
|
|
185
|
+
* to types of arguments in the event handler callback.
|
|
186
|
+
*/
|
|
187
|
+
var TypedEmitter = class extends EventEmitter {
|
|
188
|
+
constructor(...args) {
|
|
189
|
+
super(...args);
|
|
190
|
+
this.setMaxListeners(Number.POSITIVE_INFINITY);
|
|
191
|
+
}
|
|
192
|
+
addListener(event, listener) {
|
|
193
|
+
return super.addListener(event, listener);
|
|
194
|
+
}
|
|
195
|
+
prependListener(event, listener) {
|
|
196
|
+
return super.prependListener(event, listener);
|
|
197
|
+
}
|
|
198
|
+
prependOnceListener(event, listener) {
|
|
199
|
+
return super.prependOnceListener(event, listener);
|
|
200
|
+
}
|
|
201
|
+
on(event, listener) {
|
|
202
|
+
return super.on(event, listener);
|
|
203
|
+
}
|
|
204
|
+
once(event, listener) {
|
|
205
|
+
return super.once(event, listener);
|
|
206
|
+
}
|
|
207
|
+
emit(event, ...args) {
|
|
208
|
+
return super.emit(event, ...args);
|
|
209
|
+
}
|
|
210
|
+
off(event, listener) {
|
|
211
|
+
return super.off(event, listener);
|
|
212
|
+
}
|
|
213
|
+
removeListener(event, listener) {
|
|
214
|
+
return super.removeListener(event, listener);
|
|
215
|
+
}
|
|
228
216
|
};
|
|
229
217
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/Channel.ts
|
|
220
|
+
/**
|
|
221
|
+
* A `Channel` is used to broadcast events to many sessions at once.
|
|
222
|
+
*
|
|
223
|
+
* It extends from the {@link https://nodejs.org/api/events.html#events_class_eventemitter | EventEmitter} class.
|
|
224
|
+
*
|
|
225
|
+
* You may use the second generic argument `SessionState` to enforce that only sessions with the same state type may be registered with this channel.
|
|
226
|
+
*/
|
|
227
|
+
var Channel = class extends TypedEmitter {
|
|
228
|
+
/**
|
|
229
|
+
* Custom state for this channel.
|
|
230
|
+
*
|
|
231
|
+
* Use this object to safely store information related to the channel.
|
|
232
|
+
*/
|
|
233
|
+
state;
|
|
234
|
+
sessions = /* @__PURE__ */ new Set();
|
|
235
|
+
constructor(options = {}) {
|
|
236
|
+
super();
|
|
237
|
+
this.state = options.state ?? {};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* List of the currently active sessions registered with this channel.
|
|
241
|
+
*/
|
|
242
|
+
get activeSessions() {
|
|
243
|
+
return Array.from(this.sessions);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Number of sessions registered with this channel.
|
|
247
|
+
*/
|
|
248
|
+
get sessionCount() {
|
|
249
|
+
return this.sessions.size;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Register a session so that it can start receiving events from this channel.
|
|
253
|
+
*
|
|
254
|
+
* If the session was already registered to begin with this method does nothing.
|
|
255
|
+
*
|
|
256
|
+
* @param session - Session to register.
|
|
257
|
+
*/
|
|
258
|
+
register(session) {
|
|
259
|
+
if (this.sessions.has(session)) return this;
|
|
260
|
+
if (!session.isConnected) throw new SseError("Cannot register a non-active session.");
|
|
261
|
+
session.once("disconnected", () => {
|
|
262
|
+
this.emit("session-disconnected", session);
|
|
263
|
+
this.deregister(session);
|
|
264
|
+
});
|
|
265
|
+
this.sessions.add(session);
|
|
266
|
+
this.emit("session-registered", session);
|
|
267
|
+
return this;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Deregister a session so that it no longer receives events from this channel.
|
|
271
|
+
*
|
|
272
|
+
* If the session was not registered to begin with this method does nothing.
|
|
273
|
+
*
|
|
274
|
+
* @param session - Session to deregister.
|
|
275
|
+
*/
|
|
276
|
+
deregister(session) {
|
|
277
|
+
if (!this.sessions.has(session)) return this;
|
|
278
|
+
this.sessions.delete(session);
|
|
279
|
+
this.emit("session-deregistered", session);
|
|
280
|
+
return this;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Broadcast an event to every active session registered with this channel.
|
|
284
|
+
*
|
|
285
|
+
* Under the hood this calls the `push` method on every active session.
|
|
286
|
+
*
|
|
287
|
+
* If no event name is given, the event name is set to `"message"`.
|
|
288
|
+
*
|
|
289
|
+
* Note that the broadcasted event will have the same ID across all receiving sessions instead of generating a unique ID for each.
|
|
290
|
+
*
|
|
291
|
+
* @param data - Data to write.
|
|
292
|
+
* @param eventName - Event name to write.
|
|
293
|
+
*/
|
|
294
|
+
broadcast = (data, eventName = "message", options = {}) => {
|
|
295
|
+
const eventId = options.eventId ?? crypto.randomUUID();
|
|
296
|
+
const sessions = options.filter ? this.activeSessions.filter(options.filter) : this.sessions;
|
|
297
|
+
for (const session of sessions) session.push(data, eventName, eventId);
|
|
298
|
+
this.emit("broadcast", data, eventName, eventId);
|
|
299
|
+
return this;
|
|
300
|
+
};
|
|
259
301
|
};
|
|
260
302
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
req.once("close", this.onClose);
|
|
271
|
-
res.once("close", this.onClose);
|
|
272
|
-
this.request = new Request(this.url, {
|
|
273
|
-
method: req.method ?? DEFAULT_REQUEST_METHOD,
|
|
274
|
-
signal: this.controller.signal
|
|
275
|
-
});
|
|
276
|
-
applyHeaders(req.headers, this.request.headers);
|
|
277
|
-
this.response = new Response(null, {
|
|
278
|
-
status: options.statusCode ?? res.statusCode ?? DEFAULT_RESPONSE_CODE,
|
|
279
|
-
headers: DEFAULT_RESPONSE_HEADERS
|
|
280
|
-
});
|
|
281
|
-
if (res) {
|
|
282
|
-
applyHeaders(
|
|
283
|
-
res.getHeaders(),
|
|
284
|
-
this.response.headers
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
controller;
|
|
289
|
-
url;
|
|
290
|
-
request;
|
|
291
|
-
response;
|
|
292
|
-
onClose = () => {
|
|
293
|
-
this.controller.abort();
|
|
294
|
-
};
|
|
295
|
-
sendHead = () => {
|
|
296
|
-
this.res.writeHead(
|
|
297
|
-
this.response.status,
|
|
298
|
-
Object.fromEntries(this.response.headers)
|
|
299
|
-
);
|
|
300
|
-
};
|
|
301
|
-
sendChunk = (chunk) => {
|
|
302
|
-
this.res.write(chunk);
|
|
303
|
-
};
|
|
304
|
-
cleanup = () => {
|
|
305
|
-
this.req.removeListener("close", this.onClose);
|
|
306
|
-
this.res.removeListener("close", this.onClose);
|
|
307
|
-
};
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/createChannel.ts
|
|
305
|
+
const createChannel = (...args) => new Channel(...args);
|
|
306
|
+
|
|
307
|
+
//#endregion
|
|
308
|
+
//#region src/utils/createPushFromIterable.ts
|
|
309
|
+
const createPushFromIterable = (push) => async (iterable, options = {}) => {
|
|
310
|
+
const { eventName = "iteration" } = options;
|
|
311
|
+
for await (const data of iterable) push(data, eventName);
|
|
308
312
|
};
|
|
309
313
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
delete allowedHeaders[header];
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
applyHeaders(allowedHeaders, this.request.headers);
|
|
332
|
-
this.response = new Response(null, {
|
|
333
|
-
status: options.statusCode ?? res.statusCode ?? DEFAULT_RESPONSE_CODE,
|
|
334
|
-
headers: DEFAULT_RESPONSE_HEADERS
|
|
335
|
-
});
|
|
336
|
-
if (res) {
|
|
337
|
-
applyHeaders(
|
|
338
|
-
res.getHeaders(),
|
|
339
|
-
this.response.headers
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
controller;
|
|
344
|
-
url;
|
|
345
|
-
request;
|
|
346
|
-
response;
|
|
347
|
-
onClose = () => {
|
|
348
|
-
this.controller.abort();
|
|
349
|
-
};
|
|
350
|
-
sendHead = () => {
|
|
351
|
-
this.res.writeHead(
|
|
352
|
-
this.response.status,
|
|
353
|
-
Object.fromEntries(this.response.headers)
|
|
354
|
-
);
|
|
355
|
-
};
|
|
356
|
-
sendChunk = (chunk) => {
|
|
357
|
-
this.res.write(chunk);
|
|
358
|
-
};
|
|
359
|
-
cleanup = () => {
|
|
360
|
-
this.req.removeListener("close", this.onClose);
|
|
361
|
-
this.res.removeListener("close", this.onClose);
|
|
362
|
-
};
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region src/utils/createPushFromStream.ts
|
|
316
|
+
const createPushFromStream = (push) => async (stream, options = {}) => {
|
|
317
|
+
const { eventName = "stream" } = options;
|
|
318
|
+
if (stream instanceof Readable) return await new Promise((resolve, reject) => {
|
|
319
|
+
stream.on("data", (chunk) => {
|
|
320
|
+
let data;
|
|
321
|
+
if (Buffer.isBuffer(chunk)) data = chunk.toString();
|
|
322
|
+
else data = chunk;
|
|
323
|
+
push(data, eventName);
|
|
324
|
+
});
|
|
325
|
+
stream.once("end", () => resolve(true));
|
|
326
|
+
stream.once("close", () => resolve(true));
|
|
327
|
+
stream.once("error", (err) => reject(err));
|
|
328
|
+
});
|
|
329
|
+
for await (const chunk of stream) if (Buffer.isBuffer(chunk)) push(chunk.toString(), eventName);
|
|
330
|
+
else push(chunk, eventName);
|
|
331
|
+
return true;
|
|
363
332
|
};
|
|
364
333
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/utils/sanitize.ts
|
|
336
|
+
const newlineVariantsRegex = /(\r\n|\r|\n)/g;
|
|
337
|
+
const newlineTrailingRegex = /\n+$/g;
|
|
338
|
+
const sanitize = (text) => {
|
|
339
|
+
let sanitized = text;
|
|
340
|
+
sanitized = sanitized.replace(newlineVariantsRegex, "\n");
|
|
341
|
+
sanitized = sanitized.replace(newlineTrailingRegex, "");
|
|
342
|
+
return sanitized;
|
|
371
343
|
};
|
|
372
344
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
345
|
+
//#endregion
|
|
346
|
+
//#region src/utils/serialize.ts
|
|
347
|
+
const serialize = (data) => JSON.stringify(data);
|
|
348
|
+
|
|
349
|
+
//#endregion
|
|
350
|
+
//#region src/EventBuffer.ts
|
|
351
|
+
/**
|
|
352
|
+
* An `EventBuffer` allows you to write raw spec-compliant SSE fields into a text buffer that can be sent directly over the wire.
|
|
353
|
+
*/
|
|
354
|
+
var EventBuffer = class {
|
|
355
|
+
buffer = "";
|
|
356
|
+
serialize;
|
|
357
|
+
sanitize;
|
|
358
|
+
constructor(options = {}) {
|
|
359
|
+
this.serialize = options.serializer ?? serialize;
|
|
360
|
+
this.sanitize = options.sanitizer ?? sanitize;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Write a line with a field key and value appended with a newline character.
|
|
364
|
+
*/
|
|
365
|
+
writeField = (name, value) => {
|
|
366
|
+
const sanitized = this.sanitize(value);
|
|
367
|
+
this.buffer += name + ":" + sanitized + "\n";
|
|
368
|
+
return this;
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* Write an event name field (also referred to as the event "type" in the specification).
|
|
372
|
+
*
|
|
373
|
+
* @param type - Event name/type.
|
|
374
|
+
*/
|
|
375
|
+
event(type) {
|
|
376
|
+
this.writeField("event", type);
|
|
377
|
+
return this;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Write arbitrary data into a data field.
|
|
381
|
+
*
|
|
382
|
+
* Data is serialized to a string using the given `serializer` function option or JSON stringification by default.
|
|
383
|
+
*
|
|
384
|
+
* @param data - Data to serialize and write.
|
|
385
|
+
*/
|
|
386
|
+
data = (data) => {
|
|
387
|
+
const serialized = this.serialize(data);
|
|
388
|
+
this.writeField("data", serialized);
|
|
389
|
+
return this;
|
|
390
|
+
};
|
|
391
|
+
/**
|
|
392
|
+
* Write an event ID field.
|
|
393
|
+
*
|
|
394
|
+
* Defaults to an empty string if no argument is given.
|
|
395
|
+
*
|
|
396
|
+
* @param id - Identification string to write.
|
|
397
|
+
*/
|
|
398
|
+
id = (id = "") => {
|
|
399
|
+
this.writeField("id", id);
|
|
400
|
+
return this;
|
|
401
|
+
};
|
|
402
|
+
/**
|
|
403
|
+
* Write a retry field that suggests a reconnection time with the given milliseconds.
|
|
404
|
+
*
|
|
405
|
+
* @param time - Time in milliseconds to retry.
|
|
406
|
+
*/
|
|
407
|
+
retry = (time) => {
|
|
408
|
+
const stringifed = time.toString();
|
|
409
|
+
this.writeField("retry", stringifed);
|
|
410
|
+
return this;
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* Write a comment (an ignored field).
|
|
414
|
+
*
|
|
415
|
+
* This will not fire an event but is often used to keep the connection alive.
|
|
416
|
+
*
|
|
417
|
+
* @param text - Text of the comment. Otherwise writes an empty field value.
|
|
418
|
+
*/
|
|
419
|
+
comment = (text = "") => {
|
|
420
|
+
this.writeField("", text);
|
|
421
|
+
return this;
|
|
422
|
+
};
|
|
423
|
+
/**
|
|
424
|
+
* Indicate that the event has finished being created by writing an additional newline character.
|
|
425
|
+
*/
|
|
426
|
+
dispatch = () => {
|
|
427
|
+
this.buffer += "\n";
|
|
428
|
+
return this;
|
|
429
|
+
};
|
|
430
|
+
/**
|
|
431
|
+
* Create, write and dispatch an event with the given data all at once.
|
|
432
|
+
*
|
|
433
|
+
* This is equivalent to calling the methods `event`, `id`, `data` and `dispatch` in that order.
|
|
434
|
+
*
|
|
435
|
+
* If no event name is given, the event name is set to `"message"`.
|
|
436
|
+
*
|
|
437
|
+
* If no event ID is given, the event ID is set to a randomly generated UUIDv4.
|
|
438
|
+
*
|
|
439
|
+
* @param data - Data to write.
|
|
440
|
+
* @param eventName - Event name to write.
|
|
441
|
+
* @param eventId - Event ID to write.
|
|
442
|
+
*/
|
|
443
|
+
push = (data, eventName = "message", eventId = crypto.randomUUID()) => {
|
|
444
|
+
this.event(eventName).id(eventId).data(data).dispatch();
|
|
445
|
+
return this;
|
|
446
|
+
};
|
|
447
|
+
/**
|
|
448
|
+
* Pipe readable stream data as a series of events into the buffer.
|
|
449
|
+
*
|
|
450
|
+
* This uses the `push` method under the hood.
|
|
451
|
+
*
|
|
452
|
+
* If no event name is given in the `options` object, the event name is set to `"stream"`.
|
|
453
|
+
*
|
|
454
|
+
* @param stream - Readable stream to consume data from.
|
|
455
|
+
* @param options - Event name to use for each event created.
|
|
456
|
+
*
|
|
457
|
+
* @returns A promise that resolves with `true` or rejects based on the success of the stream write finishing.
|
|
458
|
+
*/
|
|
459
|
+
stream = createPushFromStream(this.push);
|
|
460
|
+
/**
|
|
461
|
+
* Iterate over an iterable and write yielded values as events into the buffer.
|
|
462
|
+
*
|
|
463
|
+
* This uses the `push` method under the hood.
|
|
464
|
+
*
|
|
465
|
+
* If no event name is given in the `options` object, the event name is set to `"iteration"`.
|
|
466
|
+
*
|
|
467
|
+
* @param iterable - Iterable to consume data from.
|
|
468
|
+
*
|
|
469
|
+
* @returns A promise that resolves once all data has been successfully yielded from the iterable.
|
|
470
|
+
*/
|
|
471
|
+
iterate = createPushFromIterable(this.push);
|
|
472
|
+
/**
|
|
473
|
+
* Clear the contents of the buffer.
|
|
474
|
+
*/
|
|
475
|
+
clear = () => {
|
|
476
|
+
this.buffer = "";
|
|
477
|
+
return this;
|
|
478
|
+
};
|
|
479
|
+
/**
|
|
480
|
+
* Get a copy of the buffer contents.
|
|
481
|
+
*/
|
|
482
|
+
read = () => this.buffer;
|
|
400
483
|
};
|
|
401
484
|
|
|
402
|
-
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/createEventBuffer.ts
|
|
487
|
+
const createEventBuffer = (...args) => new EventBuffer(...args);
|
|
488
|
+
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/Session.ts
|
|
491
|
+
/**
|
|
492
|
+
* A `Session` represents an open connection between the server and a client.
|
|
493
|
+
*
|
|
494
|
+
* It extends from the {@link https://nodejs.org/api/events.html#events_class_eventemitter | EventEmitter} class.
|
|
495
|
+
*
|
|
496
|
+
* It emits the `connected` event after it has connected and sent the response head to the client.
|
|
497
|
+
* It emits the `disconnected` event after the connection has been closed.
|
|
498
|
+
*
|
|
499
|
+
* When using the Fetch API, the session is considered connected only once the `ReadableStream` contained in the body
|
|
500
|
+
* of the `Response` returned by `getResponse` has began being consumed.
|
|
501
|
+
*
|
|
502
|
+
* When using the Node HTTP APIs, the session will send the response with status code, headers and other preamble data ahead of time,
|
|
503
|
+
* allowing the session to connect and start pushing events immediately. As such, keep in mind that attempting
|
|
504
|
+
* to write additional headers after the session has been created will result in an error being thrown.
|
|
505
|
+
*
|
|
506
|
+
* @param req - The Node HTTP/1 {@link https://nodejs.org/api/http.html#http_class_http_incomingmessage | ServerResponse}, HTTP/2 {@link https://nodejs.org/api/http2.html#class-http2http2serverrequest | Http2ServerRequest} or the Fetch API {@link https://developer.mozilla.org/en-US/docs/Web/API/Request | Request} object.
|
|
507
|
+
* @param res - The Node HTTP {@link https://nodejs.org/api/http.html#http_class_http_serverresponse | IncomingMessage}, HTTP/2 {@link https://nodejs.org/api/http2.html#class-http2http2serverresponse | Http2ServerResponse} or the Fetch API {@link https://developer.mozilla.org/en-US/docs/Web/API/Response | Response} object. Optional if using the Fetch API.
|
|
508
|
+
* @param options - Optional additional configuration for the session.
|
|
509
|
+
*/
|
|
403
510
|
var Session = class extends TypedEmitter {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
*
|
|
594
|
-
* @param stream - Readable stream to consume data from.
|
|
595
|
-
* @param options - Options to alter how the stream is flushed to the client.
|
|
596
|
-
*
|
|
597
|
-
* @returns A promise that resolves with `true` or rejects based on the success of the stream write finishing.
|
|
598
|
-
*/
|
|
599
|
-
stream = createPushFromStream(this.push);
|
|
600
|
-
/**
|
|
601
|
-
* Iterate over an iterable and send yielded values as events to the client.
|
|
602
|
-
*
|
|
603
|
-
* This uses the `push` method under the hood.
|
|
604
|
-
*
|
|
605
|
-
* If no event name is given in the `options` object, the event name is set to `"iteration"`.
|
|
606
|
-
*
|
|
607
|
-
* @param iterable - Iterable to consume data from.
|
|
608
|
-
*
|
|
609
|
-
* @returns A promise that resolves once all data has been successfully yielded from the iterable.
|
|
610
|
-
*/
|
|
611
|
-
iterate = createPushFromIterable(this.push);
|
|
612
|
-
/**
|
|
613
|
-
* Batch and send multiple events at once.
|
|
614
|
-
*
|
|
615
|
-
* If given an `EventBuffer` instance, its contents will be sent to the client.
|
|
616
|
-
*
|
|
617
|
-
* If given a callback, it will be passed an instance of `EventBuffer` which uses the same serializer and sanitizer as the session.
|
|
618
|
-
* Once its execution completes - or once it resolves if it returns a promise - the contents of the passed `EventBuffer` will be sent to the client.
|
|
619
|
-
*
|
|
620
|
-
* @param batcher - Event buffer to get contents from, or callback that takes an event buffer to write to.
|
|
621
|
-
*
|
|
622
|
-
* @returns A promise that resolves once all data from the event buffer has been successfully sent to the client.
|
|
623
|
-
*
|
|
624
|
-
* @see EventBuffer
|
|
625
|
-
*/
|
|
626
|
-
batch = async (batcher) => {
|
|
627
|
-
if (batcher instanceof EventBuffer) {
|
|
628
|
-
this.connection.sendChunk(batcher.read());
|
|
629
|
-
} else {
|
|
630
|
-
const buffer = new EventBuffer({
|
|
631
|
-
serializer: this.serialize,
|
|
632
|
-
sanitizer: this.sanitize
|
|
633
|
-
});
|
|
634
|
-
await batcher(buffer);
|
|
635
|
-
this.connection.sendChunk(buffer.read());
|
|
636
|
-
}
|
|
637
|
-
};
|
|
511
|
+
/**
|
|
512
|
+
* The last event ID sent to the client.
|
|
513
|
+
*
|
|
514
|
+
* 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.
|
|
515
|
+
*
|
|
516
|
+
* For security reasons, keep in mind that the client can provide *any* initial ID here. Use the `trustClientEventId` constructor option to ignore the client-given initial ID.
|
|
517
|
+
*
|
|
518
|
+
* @readonly
|
|
519
|
+
*/
|
|
520
|
+
lastId = "";
|
|
521
|
+
/**
|
|
522
|
+
* Indicates whether the session and underlying connection is open or not.
|
|
523
|
+
*
|
|
524
|
+
* @readonly
|
|
525
|
+
*/
|
|
526
|
+
isConnected = false;
|
|
527
|
+
/**
|
|
528
|
+
* Custom state for this session.
|
|
529
|
+
*
|
|
530
|
+
* Use this object to safely store information related to the session and user.
|
|
531
|
+
*
|
|
532
|
+
* Use [module augmentation and declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)
|
|
533
|
+
* to safely add new properties to the `DefaultSessionState` interface.
|
|
534
|
+
*/
|
|
535
|
+
state;
|
|
536
|
+
buffer;
|
|
537
|
+
connection;
|
|
538
|
+
sanitize;
|
|
539
|
+
serialize;
|
|
540
|
+
initialRetry;
|
|
541
|
+
keepAliveInterval;
|
|
542
|
+
keepAliveTimer;
|
|
543
|
+
constructor(req, res, options) {
|
|
544
|
+
super();
|
|
545
|
+
let givenOptions = options ?? {};
|
|
546
|
+
if (req instanceof Request) {
|
|
547
|
+
let givenRes = null;
|
|
548
|
+
if (res) if (res instanceof Response) givenRes = res;
|
|
549
|
+
else {
|
|
550
|
+
if (options) throw new SseError("When providing a Fetch Request object but no Response object, you may pass options as the second OR third argument to the session constructor, but not to both.");
|
|
551
|
+
givenOptions = res ?? {};
|
|
552
|
+
}
|
|
553
|
+
this.connection = new FetchConnection(req, givenRes, givenOptions);
|
|
554
|
+
} else if (req instanceof IncomingMessage) if (res instanceof ServerResponse) this.connection = new NodeHttp1Connection(req, res, givenOptions);
|
|
555
|
+
else throw new SseError("When providing a Node IncomingMessage object, a corresponding ServerResponse object must also be provided.");
|
|
556
|
+
else if (req instanceof Http2ServerRequest) if (res instanceof Http2ServerResponse) this.connection = new NodeHttp2CompatConnection(req, res, givenOptions);
|
|
557
|
+
else throw new SseError("When providing a Node HTTP2ServerRequest object, a corresponding HTTP2ServerResponse object must also be provided.");
|
|
558
|
+
else if (req instanceof Connection) {
|
|
559
|
+
this.connection = req;
|
|
560
|
+
if (options) throw new SseError("When providing a Connection object, you may pass options as the second argument, but not to the third argument.");
|
|
561
|
+
givenOptions = res ?? {};
|
|
562
|
+
} else throw new SseError("Malformed request or response objects given to session constructor. Must be one of IncomingMessage/ServerResponse from the Node HTTP/1 API, HTTP2ServerRequest/HTTP2ServerResponse from the Node HTTP/2 Compatibility API, or Request/Response from the Fetch API.");
|
|
563
|
+
if (givenOptions.trustClientEventId !== false) this.lastId = this.connection.request.headers.get("last-event-id") ?? this.connection.url.searchParams.get("lastEventId") ?? this.connection.url.searchParams.get("evs_last_event_id") ?? "";
|
|
564
|
+
this.state = givenOptions.state ?? {};
|
|
565
|
+
this.initialRetry = givenOptions.retry === null ? null : givenOptions.retry ?? 2e3;
|
|
566
|
+
this.keepAliveInterval = givenOptions.keepAlive === null ? null : givenOptions.keepAlive ?? 1e4;
|
|
567
|
+
this.serialize = givenOptions.serializer ?? serialize;
|
|
568
|
+
this.sanitize = givenOptions.sanitizer ?? sanitize;
|
|
569
|
+
this.buffer = new EventBuffer({
|
|
570
|
+
serializer: this.serialize,
|
|
571
|
+
sanitizer: this.sanitize
|
|
572
|
+
});
|
|
573
|
+
this.connection.request.signal.addEventListener("abort", this.onDisconnected);
|
|
574
|
+
setImmediate(this.initialize);
|
|
575
|
+
}
|
|
576
|
+
initialize = () => {
|
|
577
|
+
this.connection.sendHead();
|
|
578
|
+
if (this.connection.url.searchParams.has("padding")) this.buffer.comment(" ".repeat(2049)).dispatch();
|
|
579
|
+
if (this.connection.url.searchParams.has("evs_preamble")) this.buffer.comment(" ".repeat(2056)).dispatch();
|
|
580
|
+
if (this.initialRetry !== null) this.buffer.retry(this.initialRetry).dispatch();
|
|
581
|
+
this.flush();
|
|
582
|
+
if (this.keepAliveInterval !== null) this.keepAliveTimer = setInterval(this.keepAlive, this.keepAliveInterval);
|
|
583
|
+
this.isConnected = true;
|
|
584
|
+
this.emit("connected");
|
|
585
|
+
};
|
|
586
|
+
onDisconnected = () => {
|
|
587
|
+
this.connection.request.signal.removeEventListener("abort", this.onDisconnected);
|
|
588
|
+
this.connection.cleanup();
|
|
589
|
+
if (this.keepAliveTimer) clearInterval(this.keepAliveTimer);
|
|
590
|
+
this.isConnected = false;
|
|
591
|
+
this.emit("disconnected");
|
|
592
|
+
};
|
|
593
|
+
/**
|
|
594
|
+
* Write an empty comment and flush it to the client.
|
|
595
|
+
*/
|
|
596
|
+
keepAlive = () => {
|
|
597
|
+
this.buffer.comment().dispatch();
|
|
598
|
+
this.flush();
|
|
599
|
+
};
|
|
600
|
+
/**
|
|
601
|
+
* Flush the contents of the internal buffer to the client and clear the buffer.
|
|
602
|
+
*/
|
|
603
|
+
flush = () => {
|
|
604
|
+
const contents = this.buffer.read();
|
|
605
|
+
this.buffer.clear();
|
|
606
|
+
this.connection.sendChunk(contents);
|
|
607
|
+
};
|
|
608
|
+
/**
|
|
609
|
+
* Get a Request object representing the request of the underlying connection this session manages.
|
|
610
|
+
*
|
|
611
|
+
* When using the Fetch API, this will be the original Request object passed to the session constructor.
|
|
612
|
+
*
|
|
613
|
+
* When using the Node HTTP APIs, this will be a new Request object with status code and headers copied from the original request.
|
|
614
|
+
* When the originally given request or response is closed, the abort signal attached to this Request will be triggered.
|
|
615
|
+
*/
|
|
616
|
+
getRequest = () => this.connection.request;
|
|
617
|
+
/**
|
|
618
|
+
* Get a Response object representing the response of the underlying connection this session manages.
|
|
619
|
+
*
|
|
620
|
+
* When using the Fetch API, this will be a new Response object with status code and headers copied from the original response if given.
|
|
621
|
+
* Its body will be a ReadableStream that should begin being consumed for the session to consider itself connected.
|
|
622
|
+
*
|
|
623
|
+
* When using the Node HTTP APIs, this will be a new Response object with status code and headers copied from the original response.
|
|
624
|
+
* Its body will be `null`, as data is instead written to the stream of the originally given response object.
|
|
625
|
+
*/
|
|
626
|
+
getResponse = () => this.connection.response;
|
|
627
|
+
/**
|
|
628
|
+
* Push an event to the client.
|
|
629
|
+
*
|
|
630
|
+
* If no event name is given, the event name is set to `"message"`.
|
|
631
|
+
*
|
|
632
|
+
* If no event ID is given, the event ID (and thus the `lastId` property) is set to a randomly generated UUIDv4.
|
|
633
|
+
*
|
|
634
|
+
* If the session has disconnected, an `SseError` will be thrown.
|
|
635
|
+
*
|
|
636
|
+
* Emits the `push` event with the given data, event name and event ID in that order.
|
|
637
|
+
*
|
|
638
|
+
* @param data - Data to write.
|
|
639
|
+
* @param eventName - Event name to write.
|
|
640
|
+
* @param eventId - Event ID to write.
|
|
641
|
+
*/
|
|
642
|
+
push = (data, eventName = "message", eventId = crypto.randomUUID()) => {
|
|
643
|
+
if (!this.isConnected) throw new SseError("Cannot push data to a non-active session. Ensure the session is connected before attempting to push events. If using the Fetch API, the response stream must begin being consumed before the session is considered connected.");
|
|
644
|
+
this.buffer.push(data, eventName, eventId);
|
|
645
|
+
this.flush();
|
|
646
|
+
this.lastId = eventId;
|
|
647
|
+
this.emit("push", data, eventName, eventId);
|
|
648
|
+
return this;
|
|
649
|
+
};
|
|
650
|
+
/**
|
|
651
|
+
* Pipe readable stream data as a series of events to the client.
|
|
652
|
+
*
|
|
653
|
+
* This uses the `push` method under the hood.
|
|
654
|
+
*
|
|
655
|
+
* If no event name is given in the `options` object, the event name is set to `"stream"`.
|
|
656
|
+
*
|
|
657
|
+
* @param stream - Readable stream to consume data from.
|
|
658
|
+
* @param options - Options to alter how the stream is flushed to the client.
|
|
659
|
+
*
|
|
660
|
+
* @returns A promise that resolves with `true` or rejects based on the success of the stream write finishing.
|
|
661
|
+
*/
|
|
662
|
+
stream = createPushFromStream(this.push);
|
|
663
|
+
/**
|
|
664
|
+
* Iterate over an iterable and send yielded values as events to the client.
|
|
665
|
+
*
|
|
666
|
+
* This uses the `push` method under the hood.
|
|
667
|
+
*
|
|
668
|
+
* If no event name is given in the `options` object, the event name is set to `"iteration"`.
|
|
669
|
+
*
|
|
670
|
+
* @param iterable - Iterable to consume data from.
|
|
671
|
+
*
|
|
672
|
+
* @returns A promise that resolves once all data has been successfully yielded from the iterable.
|
|
673
|
+
*/
|
|
674
|
+
iterate = createPushFromIterable(this.push);
|
|
675
|
+
/**
|
|
676
|
+
* Batch and send multiple events at once.
|
|
677
|
+
*
|
|
678
|
+
* If given an `EventBuffer` instance, its contents will be sent to the client.
|
|
679
|
+
*
|
|
680
|
+
* If given a callback, it will be passed an instance of `EventBuffer` which uses the same serializer and sanitizer as the session.
|
|
681
|
+
* Once its execution completes - or once it resolves if it returns a promise - the contents of the passed `EventBuffer` will be sent to the client.
|
|
682
|
+
*
|
|
683
|
+
* @param batcher - Event buffer to get contents from, or callback that takes an event buffer to write to.
|
|
684
|
+
*
|
|
685
|
+
* @returns A promise that resolves once all data from the event buffer has been successfully sent to the client.
|
|
686
|
+
*
|
|
687
|
+
* @see EventBuffer
|
|
688
|
+
*/
|
|
689
|
+
batch = async (batcher) => {
|
|
690
|
+
if (batcher instanceof EventBuffer) this.connection.sendChunk(batcher.read());
|
|
691
|
+
else {
|
|
692
|
+
const buffer = new EventBuffer({
|
|
693
|
+
serializer: this.serialize,
|
|
694
|
+
sanitizer: this.sanitize
|
|
695
|
+
});
|
|
696
|
+
await batcher(buffer);
|
|
697
|
+
this.connection.sendChunk(buffer.read());
|
|
698
|
+
}
|
|
699
|
+
};
|
|
638
700
|
};
|
|
639
701
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
return new Promise((resolve) => {
|
|
643
|
-
const session = new Session(req, res, options);
|
|
644
|
-
if (req instanceof Request) {
|
|
645
|
-
resolve(session);
|
|
646
|
-
} else {
|
|
647
|
-
session.once("connected", () => {
|
|
648
|
-
resolve(session);
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// src/createResponse.ts
|
|
702
|
+
//#endregion
|
|
703
|
+
//#region src/createResponse.ts
|
|
655
704
|
function createResponse(request, response, options, callback) {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
705
|
+
const args = [
|
|
706
|
+
request,
|
|
707
|
+
response,
|
|
708
|
+
options,
|
|
709
|
+
callback
|
|
710
|
+
];
|
|
711
|
+
let givenCallback;
|
|
712
|
+
for (let index = args.length - 1; index >= 0; --index) {
|
|
713
|
+
const arg = args.pop();
|
|
714
|
+
if (arg) {
|
|
715
|
+
givenCallback = arg;
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (typeof givenCallback !== "function") throw new SseError("Last argument given to createResponse must be a callback function.");
|
|
720
|
+
/**
|
|
721
|
+
* TypeScript compares every type in the union with every type in every overload,
|
|
722
|
+
* guaranteeing an incompatibility even if each of the passed combinations of arguments
|
|
723
|
+
* actually does have at least one matching counterpart.
|
|
724
|
+
*
|
|
725
|
+
* As such, we must decide between this small ignore-line or having the
|
|
726
|
+
* `createResponse` and `Session#constructor` functions not be overloaded at all.
|
|
727
|
+
*
|
|
728
|
+
* @see https://github.com/microsoft/TypeScript/issues/14107
|
|
729
|
+
*/
|
|
730
|
+
const session = new Session(...args);
|
|
731
|
+
session.once("connected", () => {
|
|
732
|
+
givenCallback(session);
|
|
733
|
+
});
|
|
734
|
+
return session.getResponse();
|
|
675
735
|
}
|
|
676
736
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
*/
|
|
699
|
-
get sessionCount() {
|
|
700
|
-
return this.sessions.size;
|
|
701
|
-
}
|
|
702
|
-
/**
|
|
703
|
-
* Register a session so that it can start receiving events from this channel.
|
|
704
|
-
*
|
|
705
|
-
* If the session was already registered to begin with this method does nothing.
|
|
706
|
-
*
|
|
707
|
-
* @param session - Session to register.
|
|
708
|
-
*/
|
|
709
|
-
register(session) {
|
|
710
|
-
if (this.sessions.has(session)) {
|
|
711
|
-
return this;
|
|
712
|
-
}
|
|
713
|
-
if (!session.isConnected) {
|
|
714
|
-
throw new SseError("Cannot register a non-active session.");
|
|
715
|
-
}
|
|
716
|
-
session.once("disconnected", () => {
|
|
717
|
-
this.emit("session-disconnected", session);
|
|
718
|
-
this.deregister(session);
|
|
719
|
-
});
|
|
720
|
-
this.sessions.add(session);
|
|
721
|
-
this.emit("session-registered", session);
|
|
722
|
-
return this;
|
|
723
|
-
}
|
|
724
|
-
/**
|
|
725
|
-
* Deregister a session so that it no longer receives events from this channel.
|
|
726
|
-
*
|
|
727
|
-
* If the session was not registered to begin with this method does nothing.
|
|
728
|
-
*
|
|
729
|
-
* @param session - Session to deregister.
|
|
730
|
-
*/
|
|
731
|
-
deregister(session) {
|
|
732
|
-
if (!this.sessions.has(session)) {
|
|
733
|
-
return this;
|
|
734
|
-
}
|
|
735
|
-
this.sessions.delete(session);
|
|
736
|
-
this.emit("session-deregistered", session);
|
|
737
|
-
return this;
|
|
738
|
-
}
|
|
739
|
-
/**
|
|
740
|
-
* Broadcast an event to every active session registered with this channel.
|
|
741
|
-
*
|
|
742
|
-
* Under the hood this calls the `push` method on every active session.
|
|
743
|
-
*
|
|
744
|
-
* If no event name is given, the event name is set to `"message"`.
|
|
745
|
-
*
|
|
746
|
-
* Note that the broadcasted event will have the same ID across all receiving sessions instead of generating a unique ID for each.
|
|
747
|
-
*
|
|
748
|
-
* @param data - Data to write.
|
|
749
|
-
* @param eventName - Event name to write.
|
|
750
|
-
*/
|
|
751
|
-
broadcast = (data, eventName = "message", options = {}) => {
|
|
752
|
-
const eventId = options.eventId ?? generateId();
|
|
753
|
-
const sessions = options.filter ? this.activeSessions.filter(options.filter) : this.sessions;
|
|
754
|
-
for (const session of sessions) {
|
|
755
|
-
session.push(data, eventName, eventId);
|
|
756
|
-
}
|
|
757
|
-
this.emit("broadcast", data, eventName, eventId);
|
|
758
|
-
return this;
|
|
759
|
-
};
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
// src/createChannel.ts
|
|
763
|
-
var createChannel = (...args) => new Channel(...args);
|
|
737
|
+
//#endregion
|
|
738
|
+
//#region src/createSession.ts
|
|
739
|
+
function createSession(req, res, options) {
|
|
740
|
+
return new Promise((resolve) => {
|
|
741
|
+
/**
|
|
742
|
+
* TypeScript compares every type in the union with every type in every overload,
|
|
743
|
+
* guaranteeing an incompatibility even if each of the passed combinations of arguments
|
|
744
|
+
* actually does have at least one matching counterpart.
|
|
745
|
+
*
|
|
746
|
+
* As such, we must decide between this small ignore-line or having the
|
|
747
|
+
* `createSession` and `Session#constructor` functions not be overloaded at all.
|
|
748
|
+
*
|
|
749
|
+
* @see https://github.com/microsoft/TypeScript/issues/14107
|
|
750
|
+
*/
|
|
751
|
+
const session = new Session(req, res, options);
|
|
752
|
+
if (req instanceof Request) resolve(session);
|
|
753
|
+
else session.once("connected", () => {
|
|
754
|
+
resolve(session);
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
}
|
|
764
758
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
export {
|
|
768
|
-
Channel,
|
|
769
|
-
EventBuffer,
|
|
770
|
-
Session,
|
|
771
|
-
SseError,
|
|
772
|
-
createChannel,
|
|
773
|
-
createEventBuffer,
|
|
774
|
-
createResponse,
|
|
775
|
-
createSession
|
|
776
|
-
};
|
|
759
|
+
//#endregion
|
|
760
|
+
export { Channel, Connection, EventBuffer, FetchConnection, NodeHttp1Connection, NodeHttp2CompatConnection, Session, SseError, createChannel, createEventBuffer, createResponse, createSession };
|