better-sse 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +84 -47
- package/build/index.d.mts +70 -61
- package/build/index.d.ts +70 -61
- package/build/index.js +356 -159
- package/build/index.mjs +353 -156
- package/package.json +7 -11
package/build/index.mjs
CHANGED
|
@@ -1,23 +1,51 @@
|
|
|
1
1
|
// src/Session.ts
|
|
2
2
|
import {
|
|
3
|
+
IncomingMessage as Http1ServerRequest,
|
|
3
4
|
ServerResponse as Http1ServerResponse
|
|
4
|
-
} from "http";
|
|
5
|
+
} from "node:http";
|
|
6
|
+
import { Http2ServerRequest, Http2ServerResponse } from "node:http2";
|
|
7
|
+
import { setImmediate } from "node:timers";
|
|
5
8
|
|
|
6
|
-
// src/lib/
|
|
7
|
-
var
|
|
9
|
+
// src/lib/createPushFromIterable.ts
|
|
10
|
+
var createPushFromIterable = (push) => async (iterable, options = {}) => {
|
|
11
|
+
const { eventName = "iteration" } = options;
|
|
12
|
+
for await (const data of iterable) {
|
|
13
|
+
push(data, eventName);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
8
16
|
|
|
9
|
-
// src/lib/
|
|
10
|
-
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
// src/lib/createPushFromStream.ts
|
|
18
|
+
import { Readable as NodeReadableStream } from "node:stream";
|
|
19
|
+
var createPushFromStream = (push) => async (stream, options = {}) => {
|
|
20
|
+
const { eventName = "stream" } = options;
|
|
21
|
+
if (stream instanceof NodeReadableStream) {
|
|
22
|
+
return await new Promise((resolve, reject) => {
|
|
23
|
+
stream.on("data", (chunk) => {
|
|
24
|
+
let data;
|
|
25
|
+
if (Buffer.isBuffer(chunk)) {
|
|
26
|
+
data = chunk.toString();
|
|
27
|
+
} else {
|
|
28
|
+
data = chunk;
|
|
29
|
+
}
|
|
30
|
+
push(data, eventName);
|
|
31
|
+
});
|
|
32
|
+
stream.once("end", () => resolve(true));
|
|
33
|
+
stream.once("close", () => resolve(true));
|
|
34
|
+
stream.once("error", (err) => reject(err));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
for await (const chunk of stream) {
|
|
38
|
+
if (Buffer.isBuffer(chunk)) {
|
|
39
|
+
push(chunk.toString(), eventName);
|
|
40
|
+
} else {
|
|
41
|
+
push(chunk, eventName);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
17
45
|
};
|
|
18
46
|
|
|
19
47
|
// src/lib/generateId.ts
|
|
20
|
-
import {
|
|
48
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
21
49
|
var generateId;
|
|
22
50
|
if (randomUUID) {
|
|
23
51
|
generateId = () => randomUUID();
|
|
@@ -25,32 +53,18 @@ if (randomUUID) {
|
|
|
25
53
|
generateId = () => randomBytes(4).toString("hex");
|
|
26
54
|
}
|
|
27
55
|
|
|
28
|
-
// src/lib/
|
|
29
|
-
var
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} else {
|
|
37
|
-
data = chunk;
|
|
38
|
-
}
|
|
39
|
-
push(data, eventName);
|
|
40
|
-
});
|
|
41
|
-
stream.once("end", () => resolve(true));
|
|
42
|
-
stream.once("close", () => resolve(true));
|
|
43
|
-
stream.once("error", (err) => reject(err));
|
|
44
|
-
});
|
|
56
|
+
// src/lib/sanitize.ts
|
|
57
|
+
var newlineVariantsRegex = /(\r\n|\r|\n)/g;
|
|
58
|
+
var newlineTrailingRegex = /\n+$/g;
|
|
59
|
+
var sanitize = (text) => {
|
|
60
|
+
let sanitized = text;
|
|
61
|
+
sanitized = sanitized.replace(newlineVariantsRegex, "\n");
|
|
62
|
+
sanitized = sanitized.replace(newlineTrailingRegex, "");
|
|
63
|
+
return sanitized;
|
|
45
64
|
};
|
|
46
65
|
|
|
47
|
-
// src/lib/
|
|
48
|
-
var
|
|
49
|
-
const { eventName = "iteration" } = options;
|
|
50
|
-
for await (const data of iterable) {
|
|
51
|
-
push(data, eventName);
|
|
52
|
-
}
|
|
53
|
-
};
|
|
66
|
+
// src/lib/serialize.ts
|
|
67
|
+
var serialize = (data) => JSON.stringify(data);
|
|
54
68
|
|
|
55
69
|
// src/EventBuffer.ts
|
|
56
70
|
var EventBuffer = class {
|
|
@@ -184,8 +198,180 @@ var EventBuffer = class {
|
|
|
184
198
|
read = () => this.buffer;
|
|
185
199
|
};
|
|
186
200
|
|
|
201
|
+
// src/lib/applyHeaders.ts
|
|
202
|
+
var applyHeaders = (from, to) => {
|
|
203
|
+
const fromMap = from instanceof Headers ? Object.fromEntries(from) : from;
|
|
204
|
+
for (const [key, value] of Object.entries(fromMap)) {
|
|
205
|
+
if (Array.isArray(value)) {
|
|
206
|
+
to.delete(key);
|
|
207
|
+
for (const item of value) {
|
|
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
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// src/lib/constants.ts
|
|
219
|
+
var DEFAULT_REQUEST_HOST = "localhost";
|
|
220
|
+
var DEFAULT_REQUEST_METHOD = "GET";
|
|
221
|
+
var DEFAULT_RESPONSE_CODE = 200;
|
|
222
|
+
var DEFAULT_RESPONSE_HEADERS = {
|
|
223
|
+
"Content-Type": "text/event-stream",
|
|
224
|
+
"Cache-Control": "private, no-cache, no-store, no-transform, must-revalidate, max-age=0",
|
|
225
|
+
Connection: "keep-alive",
|
|
226
|
+
Pragma: "no-cache",
|
|
227
|
+
"X-Accel-Buffering": "no"
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// src/adapters/FetchConnection.ts
|
|
231
|
+
var FetchConnection = class _FetchConnection {
|
|
232
|
+
static encoder = new TextEncoder();
|
|
233
|
+
writer;
|
|
234
|
+
url;
|
|
235
|
+
request;
|
|
236
|
+
response;
|
|
237
|
+
constructor(request, response, options = {}) {
|
|
238
|
+
this.url = new URL(request.url);
|
|
239
|
+
this.request = request;
|
|
240
|
+
const { readable, writable } = new TransformStream();
|
|
241
|
+
this.writer = writable.getWriter();
|
|
242
|
+
this.response = new Response(readable, {
|
|
243
|
+
status: options.statusCode ?? response?.status ?? DEFAULT_RESPONSE_CODE,
|
|
244
|
+
headers: DEFAULT_RESPONSE_HEADERS
|
|
245
|
+
});
|
|
246
|
+
if (response) {
|
|
247
|
+
applyHeaders(response.headers, this.response.headers);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
sendHead = () => {
|
|
251
|
+
};
|
|
252
|
+
sendChunk = (chunk) => {
|
|
253
|
+
const encoded = _FetchConnection.encoder.encode(chunk);
|
|
254
|
+
this.writer.write(encoded);
|
|
255
|
+
};
|
|
256
|
+
cleanup = () => {
|
|
257
|
+
this.writer.close();
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// src/adapters/NodeHttp1Connection.ts
|
|
262
|
+
var NodeHttp1Connection = class {
|
|
263
|
+
constructor(req, res, options = {}) {
|
|
264
|
+
this.req = req;
|
|
265
|
+
this.res = res;
|
|
266
|
+
this.url = new URL(
|
|
267
|
+
`http://${req.headers.host ?? DEFAULT_REQUEST_HOST}${req.url}`
|
|
268
|
+
);
|
|
269
|
+
this.controller = new AbortController();
|
|
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
|
+
};
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// src/adapters/NodeHttp2CompatConnection.ts
|
|
311
|
+
var NodeHttp2CompatConnection = class {
|
|
312
|
+
constructor(req, res, options = {}) {
|
|
313
|
+
this.req = req;
|
|
314
|
+
this.res = res;
|
|
315
|
+
this.url = new URL(
|
|
316
|
+
`http://${req.headers.host ?? DEFAULT_REQUEST_HOST}${req.url}`
|
|
317
|
+
);
|
|
318
|
+
this.controller = new AbortController();
|
|
319
|
+
req.once("close", this.onClose);
|
|
320
|
+
res.once("close", this.onClose);
|
|
321
|
+
this.request = new Request(this.url, {
|
|
322
|
+
method: req.method ?? DEFAULT_REQUEST_METHOD,
|
|
323
|
+
signal: this.controller.signal
|
|
324
|
+
});
|
|
325
|
+
const allowedHeaders = { ...req.headers };
|
|
326
|
+
for (const header of Object.keys(allowedHeaders)) {
|
|
327
|
+
if (header.startsWith(":")) {
|
|
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
|
+
};
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// src/lib/SseError.ts
|
|
366
|
+
var SseError = class extends Error {
|
|
367
|
+
constructor(message) {
|
|
368
|
+
super(message);
|
|
369
|
+
this.message = `better-sse: ${message}`;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
187
373
|
// src/lib/TypedEmitter.ts
|
|
188
|
-
import { EventEmitter } from "events";
|
|
374
|
+
import { EventEmitter } from "node:events";
|
|
189
375
|
var TypedEmitter = class extends EventEmitter {
|
|
190
376
|
addListener(event, listener) {
|
|
191
377
|
return super.addListener(event, listener);
|
|
@@ -213,14 +399,6 @@ var TypedEmitter = class extends EventEmitter {
|
|
|
213
399
|
}
|
|
214
400
|
};
|
|
215
401
|
|
|
216
|
-
// src/lib/SseError.ts
|
|
217
|
-
var SseError = class extends Error {
|
|
218
|
-
constructor(message) {
|
|
219
|
-
super(message);
|
|
220
|
-
this.message = `better-sse: ${message}`;
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
|
|
224
402
|
// src/Session.ts
|
|
225
403
|
var Session = class extends TypedEmitter {
|
|
226
404
|
/**
|
|
@@ -249,70 +427,78 @@ var Session = class extends TypedEmitter {
|
|
|
249
427
|
*/
|
|
250
428
|
state;
|
|
251
429
|
buffer;
|
|
252
|
-
|
|
253
|
-
* Raw HTTP request.
|
|
254
|
-
*/
|
|
255
|
-
req;
|
|
256
|
-
/**
|
|
257
|
-
* Raw HTTP response that is the minimal interface needed and forms the
|
|
258
|
-
* intersection between the HTTP/1.1 and HTTP/2 server response interfaces.
|
|
259
|
-
*/
|
|
260
|
-
res;
|
|
261
|
-
serialize;
|
|
430
|
+
connection;
|
|
262
431
|
sanitize;
|
|
263
|
-
|
|
432
|
+
serialize;
|
|
264
433
|
initialRetry;
|
|
265
434
|
keepAliveInterval;
|
|
266
435
|
keepAliveTimer;
|
|
267
|
-
|
|
268
|
-
headers;
|
|
269
|
-
constructor(req, res, options = {}) {
|
|
436
|
+
constructor(req, res, options) {
|
|
270
437
|
super();
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
438
|
+
let givenOptions = options ?? {};
|
|
439
|
+
if (req instanceof Request) {
|
|
440
|
+
let givenRes = null;
|
|
441
|
+
if (res) {
|
|
442
|
+
if (res instanceof Response) {
|
|
443
|
+
givenRes = res;
|
|
444
|
+
} else {
|
|
445
|
+
if (options) {
|
|
446
|
+
throw new SseError(
|
|
447
|
+
"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."
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
givenOptions = res;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
this.connection = new FetchConnection(req, givenRes, givenOptions);
|
|
454
|
+
} else if (req instanceof Http1ServerRequest) {
|
|
455
|
+
if (res instanceof Http1ServerResponse) {
|
|
456
|
+
this.connection = new NodeHttp1Connection(req, res, givenOptions);
|
|
457
|
+
} else {
|
|
458
|
+
throw new SseError(
|
|
459
|
+
"When providing a Node IncomingMessage object, a corresponding ServerResponse object must also be provided."
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
} else if (req instanceof Http2ServerRequest) {
|
|
463
|
+
if (res instanceof Http2ServerResponse) {
|
|
464
|
+
this.connection = new NodeHttp2CompatConnection(req, res, givenOptions);
|
|
465
|
+
} else {
|
|
466
|
+
throw new SseError(
|
|
467
|
+
"When providing a Node HTTP2ServerRequest object, a corresponding HTTP2ServerResponse object must also be provided."
|
|
468
|
+
);
|
|
469
|
+
}
|
|
302
470
|
} else {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
471
|
+
throw new SseError(
|
|
472
|
+
"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."
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
if (givenOptions.headers) {
|
|
476
|
+
applyHeaders(givenOptions.headers, this.connection.response.headers);
|
|
307
477
|
}
|
|
308
|
-
|
|
309
|
-
|
|
478
|
+
if (givenOptions.trustClientEventId !== false) {
|
|
479
|
+
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") ?? "";
|
|
310
480
|
}
|
|
311
|
-
this.
|
|
312
|
-
|
|
481
|
+
this.state = givenOptions.state ?? {};
|
|
482
|
+
this.initialRetry = givenOptions.retry === null ? null : givenOptions.retry ?? 2e3;
|
|
483
|
+
this.keepAliveInterval = givenOptions.keepAlive === null ? null : givenOptions.keepAlive ?? 1e4;
|
|
484
|
+
this.serialize = givenOptions.serializer ?? serialize;
|
|
485
|
+
this.sanitize = givenOptions.sanitizer ?? sanitize;
|
|
486
|
+
this.buffer = new EventBuffer({
|
|
487
|
+
serializer: this.serialize,
|
|
488
|
+
sanitizer: this.sanitize
|
|
489
|
+
});
|
|
490
|
+
this.connection.request.signal.addEventListener(
|
|
491
|
+
"abort",
|
|
492
|
+
this.onDisconnected
|
|
493
|
+
);
|
|
494
|
+
setImmediate(this.initialize);
|
|
495
|
+
}
|
|
496
|
+
initialize = () => {
|
|
497
|
+
this.connection.sendHead();
|
|
498
|
+
if (this.connection.url.searchParams.has("padding")) {
|
|
313
499
|
this.buffer.comment(" ".repeat(2049)).dispatch();
|
|
314
500
|
}
|
|
315
|
-
if (
|
|
501
|
+
if (this.connection.url.searchParams.has("evs_preamble")) {
|
|
316
502
|
this.buffer.comment(" ".repeat(2056)).dispatch();
|
|
317
503
|
}
|
|
318
504
|
if (this.initialRetry !== null) {
|
|
@@ -320,80 +506,57 @@ var Session = class extends TypedEmitter {
|
|
|
320
506
|
}
|
|
321
507
|
this.flush();
|
|
322
508
|
if (this.keepAliveInterval !== null) {
|
|
323
|
-
this.keepAliveTimer = setInterval(
|
|
324
|
-
this.keepAlive,
|
|
325
|
-
this.keepAliveInterval
|
|
326
|
-
);
|
|
509
|
+
this.keepAliveTimer = setInterval(this.keepAlive, this.keepAliveInterval);
|
|
327
510
|
}
|
|
328
511
|
this.isConnected = true;
|
|
329
512
|
this.emit("connected");
|
|
330
513
|
};
|
|
331
514
|
onDisconnected = () => {
|
|
332
|
-
this.
|
|
333
|
-
|
|
515
|
+
this.connection.request.signal.removeEventListener(
|
|
516
|
+
"abort",
|
|
517
|
+
this.onDisconnected
|
|
518
|
+
);
|
|
519
|
+
this.connection.cleanup();
|
|
334
520
|
if (this.keepAliveTimer) {
|
|
335
521
|
clearInterval(this.keepAliveTimer);
|
|
336
522
|
}
|
|
337
523
|
this.isConnected = false;
|
|
338
524
|
this.emit("disconnected");
|
|
339
525
|
};
|
|
526
|
+
/**
|
|
527
|
+
* Write an empty comment and flush it to the client.
|
|
528
|
+
*/
|
|
340
529
|
keepAlive = () => {
|
|
341
530
|
this.buffer.comment().dispatch();
|
|
342
531
|
this.flush();
|
|
343
532
|
};
|
|
344
533
|
/**
|
|
345
|
-
*
|
|
346
|
-
*/
|
|
347
|
-
event(type) {
|
|
348
|
-
this.buffer.event(type);
|
|
349
|
-
return this;
|
|
350
|
-
}
|
|
351
|
-
/**
|
|
352
|
-
* @deprecated see https://github.com/MatthewWid/better-sse/issues/52
|
|
353
|
-
*/
|
|
354
|
-
data = (data) => {
|
|
355
|
-
this.buffer.data(data);
|
|
356
|
-
return this;
|
|
357
|
-
};
|
|
358
|
-
/**
|
|
359
|
-
* @deprecated see https://github.com/MatthewWid/better-sse/issues/52
|
|
360
|
-
*/
|
|
361
|
-
id = (id = "") => {
|
|
362
|
-
this.buffer.id(id);
|
|
363
|
-
this.lastId = id;
|
|
364
|
-
return this;
|
|
365
|
-
};
|
|
366
|
-
/**
|
|
367
|
-
* @deprecated see https://github.com/MatthewWid/better-sse/issues/52
|
|
368
|
-
*/
|
|
369
|
-
retry = (time) => {
|
|
370
|
-
this.buffer.retry(time);
|
|
371
|
-
return this;
|
|
372
|
-
};
|
|
373
|
-
/**
|
|
374
|
-
* @deprecated see https://github.com/MatthewWid/better-sse/issues/52
|
|
534
|
+
* Flush the contents of the internal buffer to the client and clear the buffer.
|
|
375
535
|
*/
|
|
376
|
-
|
|
377
|
-
this.buffer.
|
|
378
|
-
|
|
536
|
+
flush = () => {
|
|
537
|
+
const contents = this.buffer.read();
|
|
538
|
+
this.buffer.clear();
|
|
539
|
+
this.connection.sendChunk(contents);
|
|
379
540
|
};
|
|
380
541
|
/**
|
|
381
|
-
*
|
|
542
|
+
* Get a Request object representing the request of the underlying connection this session manages.
|
|
543
|
+
*
|
|
544
|
+
* When using the Fetch API, this will be the original Request object passed to the session constructor.
|
|
545
|
+
*
|
|
546
|
+
* When using the Node HTTP APIs, this will be a new Request object with status code and headers copied from the original request.
|
|
547
|
+
* When the originally given request or response is closed, the abort signal attached to this Request will be triggered.
|
|
382
548
|
*/
|
|
383
|
-
|
|
384
|
-
this.buffer.dispatch();
|
|
385
|
-
return this;
|
|
386
|
-
};
|
|
549
|
+
getRequest = () => this.connection.request;
|
|
387
550
|
/**
|
|
388
|
-
*
|
|
551
|
+
* Get a Response object representing the response of the underlying connection this session manages.
|
|
389
552
|
*
|
|
390
|
-
*
|
|
553
|
+
* When using the Fetch API, this will be a new Response object with status code and headers copied from the original response if given.
|
|
554
|
+
* Its body will be a ReadableStream that should begin being consumed for the session to consider itself connected.
|
|
555
|
+
*
|
|
556
|
+
* When using the Node HTTP APIs, this will be a new Response object with status code and headers copied from the original response.
|
|
557
|
+
* Its body will be `null`, as data is instead written to the stream of the originally given response object.
|
|
391
558
|
*/
|
|
392
|
-
|
|
393
|
-
this.res.write(this.buffer.read());
|
|
394
|
-
this.buffer.clear();
|
|
395
|
-
return this;
|
|
396
|
-
};
|
|
559
|
+
getResponse = () => this.connection.response;
|
|
397
560
|
/**
|
|
398
561
|
* Push an event to the client.
|
|
399
562
|
*
|
|
@@ -401,6 +564,8 @@ var Session = class extends TypedEmitter {
|
|
|
401
564
|
*
|
|
402
565
|
* If no event ID is given, the event ID (and thus the `lastId` property) is set to a unique string generated using a cryptographic pseudorandom number generator.
|
|
403
566
|
*
|
|
567
|
+
* If the session has disconnected, an `SseError` will be thrown.
|
|
568
|
+
*
|
|
404
569
|
* Emits the `push` event with the given data, event name and event ID in that order.
|
|
405
570
|
*
|
|
406
571
|
* @param data - Data to write.
|
|
@@ -409,7 +574,9 @@ var Session = class extends TypedEmitter {
|
|
|
409
574
|
*/
|
|
410
575
|
push = (data, eventName = "message", eventId = generateId()) => {
|
|
411
576
|
if (!this.isConnected) {
|
|
412
|
-
throw new SseError(
|
|
577
|
+
throw new SseError(
|
|
578
|
+
"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."
|
|
579
|
+
);
|
|
413
580
|
}
|
|
414
581
|
this.buffer.push(data, eventName, eventId);
|
|
415
582
|
this.flush();
|
|
@@ -458,25 +625,54 @@ var Session = class extends TypedEmitter {
|
|
|
458
625
|
*/
|
|
459
626
|
batch = async (batcher) => {
|
|
460
627
|
if (batcher instanceof EventBuffer) {
|
|
461
|
-
this.
|
|
628
|
+
this.connection.sendChunk(batcher.read());
|
|
462
629
|
} else {
|
|
463
630
|
const buffer = new EventBuffer({
|
|
464
631
|
serializer: this.serialize,
|
|
465
632
|
sanitizer: this.sanitize
|
|
466
633
|
});
|
|
467
634
|
await batcher(buffer);
|
|
468
|
-
this.
|
|
635
|
+
this.connection.sendChunk(buffer.read());
|
|
469
636
|
}
|
|
470
637
|
};
|
|
471
638
|
};
|
|
472
639
|
|
|
473
640
|
// src/createSession.ts
|
|
474
|
-
|
|
641
|
+
function createSession(req, res, options) {
|
|
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
|
|
655
|
+
function createResponse(request, response, options, callback) {
|
|
656
|
+
const args = [request, response, options, callback];
|
|
657
|
+
let givenCallback;
|
|
658
|
+
for (let index = args.length - 1; index >= 0; --index) {
|
|
659
|
+
const arg = args.pop();
|
|
660
|
+
if (arg) {
|
|
661
|
+
givenCallback = arg;
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (typeof givenCallback !== "function") {
|
|
666
|
+
throw new SseError(
|
|
667
|
+
"Last argument given to createResponse must be a callback function."
|
|
668
|
+
);
|
|
669
|
+
}
|
|
475
670
|
const session = new Session(...args);
|
|
476
671
|
session.once("connected", () => {
|
|
477
|
-
|
|
672
|
+
givenCallback(session);
|
|
478
673
|
});
|
|
479
|
-
|
|
674
|
+
return session.getResponse();
|
|
675
|
+
}
|
|
480
676
|
|
|
481
677
|
// src/Channel.ts
|
|
482
678
|
var Channel = class extends TypedEmitter {
|
|
@@ -575,5 +771,6 @@ export {
|
|
|
575
771
|
SseError,
|
|
576
772
|
createChannel,
|
|
577
773
|
createEventBuffer,
|
|
774
|
+
createResponse,
|
|
578
775
|
createSession
|
|
579
776
|
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-sse",
|
|
3
|
-
"description": "Dead simple, dependency-less, spec-compliant server-sent events implementation
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "Dead simple, dependency-less, spec-compliant server-sent events implementation written in TypeScript.",
|
|
4
|
+
"version": "0.15.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Matthew W. <matthew.widdi@gmail.com>",
|
|
7
7
|
"repository": "github:MatthewWid/better-sse",
|
|
8
|
-
"homepage": "https://github.
|
|
8
|
+
"homepage": "https://matthewwid.github.io/better-sse",
|
|
9
9
|
"bugs": "https://github.com/MatthewWid/better-sse/issues",
|
|
10
10
|
"keywords": [
|
|
11
11
|
"server-sent-events",
|
|
@@ -34,18 +34,13 @@
|
|
|
34
34
|
"pnpm": ">=9"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
+
"@biomejs/biome": "1.9.4",
|
|
37
38
|
"@tsconfig/node20": "^20.1.4",
|
|
38
39
|
"@types/eventsource": "^1.1.15",
|
|
39
40
|
"@types/express": "^5.0.0",
|
|
40
41
|
"@types/node": "^22.7.6",
|
|
41
|
-
"@typescript-eslint/eslint-plugin": "^8.9.0",
|
|
42
|
-
"@typescript-eslint/parser": "^8.9.0",
|
|
43
|
-
"eslint": "^8.57.1",
|
|
44
|
-
"eslint-plugin-tsdoc": "^0.3.0",
|
|
45
42
|
"eventsource": "^2.0.2",
|
|
46
43
|
"npm-run-all": "^4.1.5",
|
|
47
|
-
"prettier": "^3.3.3",
|
|
48
|
-
"ts-node": "^10.9.2",
|
|
49
44
|
"tsup": "^8.3.0",
|
|
50
45
|
"typescript": "^5.6.3",
|
|
51
46
|
"vitest": "^2.1.3"
|
|
@@ -53,7 +48,8 @@
|
|
|
53
48
|
"scripts": {
|
|
54
49
|
"build": "tsup",
|
|
55
50
|
"test": "vitest",
|
|
56
|
-
"format": "
|
|
57
|
-
"lint": "
|
|
51
|
+
"format": "biome check --linter-enabled=false --write",
|
|
52
|
+
"lint": "biome lint --write",
|
|
53
|
+
"ci": "biome ci"
|
|
58
54
|
}
|
|
59
55
|
}
|