better-sse 0.12.1 → 0.14.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/build/index.js CHANGED
@@ -1,2 +1,610 @@
1
- !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var s=t();for(var i in s)("object"==typeof exports?exports:e)[i]=s[i]}}(global,(()=>(()=>{"use strict";var e={n:t=>{var s=t&&t.__esModule?()=>t.default:()=>t;return e.d(s,{a:s}),s},d:(t,s)=>{for(var i in s)e.o(s,i)&&!e.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:s[i]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{Channel:()=>g,EventBuffer:()=>d,Session:()=>m,SseError:()=>v,createChannel:()=>y,createEventBuffer:()=>w,createSession:()=>b});const s=require("http"),i=e=>JSON.stringify(e),r=/(\r\n|\r|\n)/g,n=/\n+$/g,o=e=>{let t=e;return t=t.replace(r,"\n"),t=t.replace(n,""),t},h=require("crypto");let a;a=h.randomUUID?()=>(0,h.randomUUID)():()=>(0,h.randomBytes)(4).toString("hex");const c=e=>async(t,s={})=>{const{eventName:i="stream"}=s;return await new Promise(((s,r)=>{t.on("data",(t=>{let s;s=Buffer.isBuffer(t)?t.toString():t,e(s,i)})),t.once("end",(()=>s(!0))),t.once("close",(()=>s(!0))),t.once("error",(e=>r(e)))}))},l=e=>async(t,s={})=>{const{eventName:i="iteration"}=s;for await(const s of t)e(s,i)};class d{constructor(e={}){var t,s;this.buffer="",this.writeField=(e,t)=>{const s=this.sanitize(t);return this.buffer+=e+":"+s+"\n",this},this.data=e=>{const t=this.serialize(e);return this.writeField("data",t),this},this.id=(e="")=>(this.writeField("id",e),this),this.retry=e=>{const t=e.toString();return this.writeField("retry",t),this},this.comment=(e="")=>(this.writeField("",e),this),this.dispatch=()=>(this.buffer+="\n",this),this.push=(e,t="message",s=a())=>(this.event(t).id(s).data(e).dispatch(),this),this.stream=c(this.push),this.iterate=l(this.push),this.clear=()=>(this.buffer="",this),this.read=()=>this.buffer,this.serialize=null!==(t=e.serializer)&&void 0!==t?t:i,this.sanitize=null!==(s=e.sanitizer)&&void 0!==s?s:o}event(e){return this.writeField("event",e),this}}const u=require("events");var f=e.n(u);class p extends(f()){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)}}class v extends Error{constructor(e){super(e),this.message=`better-sse: ${e}`}}class m extends p{constructor(e,t,r={}){var n,h,u,f,p,m,b;super(),this.lastId="",this.isConnected=!1,this.state={},this.initialize=()=>{var e,t,i;const r=`http://${this.req.headers.host}${this.req.url}`,n=new URL(r).searchParams;if(this.trustClientEventId){const s=null!==(i=null!==(t=null!==(e=this.req.headers["last-event-id"])&&void 0!==e?e:n.get("lastEventId"))&&void 0!==t?t:n.get("evs_last_event_id"))&&void 0!==i?i:"";this.lastId=s}const o={};this.res instanceof s.ServerResponse?(o["Content-Type"]="text/event-stream",o["Cache-Control"]="private, no-cache, no-store, no-transform, must-revalidate, max-age=0",o.Connection="keep-alive",o.Pragma="no-cache",o["X-Accel-Buffering"]="no"):(o["content-type"]="text/event-stream",o["cache-control"]="private, no-cache, no-store, no-transform, must-revalidate, max-age=0",o.pragma="no-cache",o["x-accel-buffering"]="no");for(const[e,t]of Object.entries(this.headers))o[e]=null!=t?t:"";this.res.writeHead(this.statusCode,o),n.has("padding")&&this.buffer.comment(" ".repeat(2049)).dispatch(),n.has("evs_preamble")&&this.buffer.comment(" ".repeat(2056)).dispatch(),null!==this.initialRetry&&this.buffer.retry(this.initialRetry).dispatch(),this.flush(),null!==this.keepAliveInterval&&(this.keepAliveTimer=setInterval(this.keepAlive,this.keepAliveInterval)),this.isConnected=!0,this.emit("connected")},this.onDisconnected=()=>{this.req.removeListener("close",this.onDisconnected),this.res.removeListener("close",this.onDisconnected),this.keepAliveTimer&&clearInterval(this.keepAliveTimer),this.isConnected=!1,this.emit("disconnected")},this.keepAlive=()=>{this.buffer.comment().dispatch(),this.flush()},this.data=e=>(this.buffer.data(e),this),this.id=(e="")=>(this.buffer.id(e),this.lastId=e,this),this.retry=e=>(this.buffer.retry(e),this),this.comment=e=>(this.buffer.comment(e),this),this.dispatch=()=>(this.buffer.dispatch(),this),this.flush=()=>(this.res.write(this.buffer.read()),this.buffer.clear(),this),this.push=(e,t="message",s=a())=>{if(!this.isConnected)throw new v("Cannot push data to a non-active session.");return this.buffer.push(e,t,s),this.flush(),this.lastId=s,this.emit("push",e,t,s),this},this.stream=c(this.push),this.iterate=l(this.push),this.batch=async e=>{if(e instanceof d)this.res.write(e.read());else{const t=new d({serializer:this.serialize,sanitizer:this.sanitize});await e(t),this.res.write(t.read())}},this.req=e,this.res=t;const g=null!==(n=r.serializer)&&void 0!==n?n:i,y=null!==(h=r.sanitizer)&&void 0!==h?h:o;this.serialize=g,this.sanitize=y,this.buffer=new d({serializer:g,sanitizer:y}),this.trustClientEventId=null===(u=r.trustClientEventId)||void 0===u||u,this.initialRetry=null===r.retry?null:null!==(f=r.retry)&&void 0!==f?f:2e3,this.keepAliveInterval=null===r.keepAlive?null:null!==(p=r.keepAlive)&&void 0!==p?p:1e4,this.statusCode=null!==(m=r.statusCode)&&void 0!==m?m:200,this.headers=null!==(b=r.headers)&&void 0!==b?b:{},this.req.once("close",this.onDisconnected),this.res.once("close",this.onDisconnected),setImmediate(this.initialize)}event(e){return this.buffer.event(e),this}}const b=(...e)=>new Promise((t=>{const s=new m(...e);s.once("connected",(()=>{t(s)}))}));class g extends p{constructor(){super(),this.state={},this.sessions=new Set,this.broadcast=(e,t="message",s={})=>{var i;const r=null!==(i=s.eventId)&&void 0!==i?i:a(),n=s.filter?this.activeSessions.filter(s.filter):this.sessions;for(const s of n)s.push(e,t,r);return this.emit("broadcast",e,t,r),this}}get activeSessions(){return Array.from(this.sessions)}get sessionCount(){return this.sessions.size}register(e){if(this.sessions.has(e))return this;if(!e.isConnected)throw new v("Cannot register a non-active session.");return e.once("disconnected",(()=>{this.emit("session-disconnected",e),this.deregister(e)})),this.sessions.add(e),this.emit("session-registered",e),this}deregister(e){return this.sessions.has(e)?(this.sessions.delete(e),this.emit("session-deregistered",e),this):this}}const y=(...e)=>new g(...e),w=(...e)=>new d(...e);return t})()));
2
- //# sourceMappingURL=index.js.map
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ Channel: () => Channel,
24
+ EventBuffer: () => EventBuffer,
25
+ Session: () => Session,
26
+ SseError: () => SseError,
27
+ createChannel: () => createChannel,
28
+ createEventBuffer: () => createEventBuffer,
29
+ createSession: () => createSession
30
+ });
31
+ module.exports = __toCommonJS(src_exports);
32
+
33
+ // src/Session.ts
34
+ var import_http = require("http");
35
+
36
+ // src/lib/serialize.ts
37
+ var serialize = (data) => JSON.stringify(data);
38
+
39
+ // src/lib/sanitize.ts
40
+ var newlineVariantsRegex = /(\r\n|\r|\n)/g;
41
+ var newlineTrailingRegex = /\n+$/g;
42
+ var sanitize = (text) => {
43
+ let sanitized = text;
44
+ sanitized = sanitized.replace(newlineVariantsRegex, "\n");
45
+ sanitized = sanitized.replace(newlineTrailingRegex, "");
46
+ return sanitized;
47
+ };
48
+
49
+ // src/lib/generateId.ts
50
+ var import_crypto = require("crypto");
51
+ var generateId;
52
+ if (import_crypto.randomUUID) {
53
+ generateId = () => (0, import_crypto.randomUUID)();
54
+ } else {
55
+ generateId = () => (0, import_crypto.randomBytes)(4).toString("hex");
56
+ }
57
+
58
+ // src/lib/createPushFromStream.ts
59
+ var createPushFromStream = (push) => async (stream, options = {}) => {
60
+ const { eventName = "stream" } = options;
61
+ return await new Promise((resolve, reject) => {
62
+ stream.on("data", (chunk) => {
63
+ let data;
64
+ if (Buffer.isBuffer(chunk)) {
65
+ data = chunk.toString();
66
+ } else {
67
+ data = chunk;
68
+ }
69
+ push(data, eventName);
70
+ });
71
+ stream.once("end", () => resolve(true));
72
+ stream.once("close", () => resolve(true));
73
+ stream.once("error", (err) => reject(err));
74
+ });
75
+ };
76
+
77
+ // src/lib/createPushFromIterable.ts
78
+ var createPushFromIterable = (push) => async (iterable, options = {}) => {
79
+ const { eventName = "iteration" } = options;
80
+ for await (const data of iterable) {
81
+ push(data, eventName);
82
+ }
83
+ };
84
+
85
+ // src/EventBuffer.ts
86
+ var EventBuffer = class {
87
+ buffer = "";
88
+ serialize;
89
+ sanitize;
90
+ constructor(options = {}) {
91
+ this.serialize = options.serializer ?? serialize;
92
+ this.sanitize = options.sanitizer ?? sanitize;
93
+ }
94
+ /**
95
+ * Write a line with a field key and value appended with a newline character.
96
+ */
97
+ writeField = (name, value) => {
98
+ const sanitized = this.sanitize(value);
99
+ this.buffer += name + ":" + sanitized + "\n";
100
+ return this;
101
+ };
102
+ /**
103
+ * Write an event name field (also referred to as the event "type" in the specification).
104
+ *
105
+ * @param type - Event name/type.
106
+ */
107
+ event(type) {
108
+ this.writeField("event", type);
109
+ return this;
110
+ }
111
+ /**
112
+ * Write arbitrary data into a data field.
113
+ *
114
+ * Data is serialized to a string using the given `serializer` function option or JSON stringification by default.
115
+ *
116
+ * @param data - Data to serialize and write.
117
+ */
118
+ data = (data) => {
119
+ const serialized = this.serialize(data);
120
+ this.writeField("data", serialized);
121
+ return this;
122
+ };
123
+ /**
124
+ * Write an event ID field.
125
+ *
126
+ * Defaults to an empty string if no argument is given.
127
+ *
128
+ * @param id - Identification string to write.
129
+ */
130
+ id = (id = "") => {
131
+ this.writeField("id", id);
132
+ return this;
133
+ };
134
+ /**
135
+ * Write a retry field that suggests a reconnection time with the given milliseconds.
136
+ *
137
+ * @param time - Time in milliseconds to retry.
138
+ */
139
+ retry = (time) => {
140
+ const stringifed = time.toString();
141
+ this.writeField("retry", stringifed);
142
+ return this;
143
+ };
144
+ /**
145
+ * Write a comment (an ignored field).
146
+ *
147
+ * This will not fire an event but is often used to keep the connection alive.
148
+ *
149
+ * @param text - Text of the comment. Otherwise writes an empty field value.
150
+ */
151
+ comment = (text = "") => {
152
+ this.writeField("", text);
153
+ return this;
154
+ };
155
+ /**
156
+ * Indicate that the event has finished being created by writing an additional newline character.
157
+ */
158
+ dispatch = () => {
159
+ this.buffer += "\n";
160
+ return this;
161
+ };
162
+ /**
163
+ * Create, write and dispatch an event with the given data all at once.
164
+ *
165
+ * This is equivalent to calling the methods `event`, `id`, `data` and `dispatch` in that order.
166
+ *
167
+ * If no event name is given, the event name is set to `"message"`.
168
+ *
169
+ * If no event ID is given, the event ID is set to a unique string generated using a cryptographic pseudorandom number generator.
170
+ *
171
+ * @param data - Data to write.
172
+ * @param eventName - Event name to write.
173
+ * @param eventId - Event ID to write.
174
+ */
175
+ push = (data, eventName = "message", eventId = generateId()) => {
176
+ this.event(eventName).id(eventId).data(data).dispatch();
177
+ return this;
178
+ };
179
+ /**
180
+ * Pipe readable stream data as a series of events into the buffer.
181
+ *
182
+ * This uses the `push` method under the hood.
183
+ *
184
+ * If no event name is given in the `options` object, the event name is set to `"stream"`.
185
+ *
186
+ * @param stream - Readable stream to consume data from.
187
+ * @param options - Event name to use for each event created.
188
+ *
189
+ * @returns A promise that resolves or rejects based on the success of the stream write finishing.
190
+ */
191
+ stream = createPushFromStream(this.push);
192
+ /**
193
+ * Iterate over an iterable and write yielded values as events into the buffer.
194
+ *
195
+ * This uses the `push` method under the hood.
196
+ *
197
+ * If no event name is given in the `options` object, the event name is set to `"iteration"`.
198
+ *
199
+ * @param iterable - Iterable to consume data from.
200
+ *
201
+ * @returns A promise that resolves once all data has been successfully yielded from the iterable.
202
+ */
203
+ iterate = createPushFromIterable(this.push);
204
+ /**
205
+ * Clear the contents of the buffer.
206
+ */
207
+ clear = () => {
208
+ this.buffer = "";
209
+ return this;
210
+ };
211
+ /**
212
+ * Get a copy of the buffer contents.
213
+ */
214
+ read = () => this.buffer;
215
+ };
216
+
217
+ // src/lib/TypedEmitter.ts
218
+ var import_events = require("events");
219
+ var TypedEmitter = class extends import_events.EventEmitter {
220
+ addListener(event, listener) {
221
+ return super.addListener(event, listener);
222
+ }
223
+ prependListener(event, listener) {
224
+ return super.prependListener(event, listener);
225
+ }
226
+ prependOnceListener(event, listener) {
227
+ return super.prependOnceListener(event, listener);
228
+ }
229
+ on(event, listener) {
230
+ return super.on(event, listener);
231
+ }
232
+ once(event, listener) {
233
+ return super.once(event, listener);
234
+ }
235
+ emit(event, ...args) {
236
+ return super.emit(event, ...args);
237
+ }
238
+ off(event, listener) {
239
+ return super.off(event, listener);
240
+ }
241
+ removeListener(event, listener) {
242
+ return super.removeListener(event, listener);
243
+ }
244
+ };
245
+
246
+ // src/lib/SseError.ts
247
+ var SseError = class extends Error {
248
+ constructor(message) {
249
+ super(message);
250
+ this.message = `better-sse: ${message}`;
251
+ }
252
+ };
253
+
254
+ // src/Session.ts
255
+ var Session = class extends TypedEmitter {
256
+ /**
257
+ * The last event ID sent to the client.
258
+ *
259
+ * 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.
260
+ *
261
+ * 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.
262
+ *
263
+ * @readonly
264
+ */
265
+ lastId = "";
266
+ /**
267
+ * Indicates whether the session and underlying connection is open or not.
268
+ *
269
+ * @readonly
270
+ */
271
+ isConnected = false;
272
+ /**
273
+ * Custom state for this session.
274
+ *
275
+ * Use this object to safely store information related to the session and user.
276
+ *
277
+ * Use [module augmentation and declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)
278
+ * to safely add new properties to the `DefaultSessionState` interface.
279
+ */
280
+ state;
281
+ buffer;
282
+ /**
283
+ * Raw HTTP request.
284
+ */
285
+ req;
286
+ /**
287
+ * Raw HTTP response that is the minimal interface needed and forms the
288
+ * intersection between the HTTP/1.1 and HTTP/2 server response interfaces.
289
+ */
290
+ res;
291
+ serialize;
292
+ sanitize;
293
+ trustClientEventId;
294
+ initialRetry;
295
+ keepAliveInterval;
296
+ keepAliveTimer;
297
+ statusCode;
298
+ headers;
299
+ constructor(req, res, options = {}) {
300
+ super();
301
+ this.req = req;
302
+ this.res = res;
303
+ const serializer = options.serializer ?? serialize;
304
+ const sanitizer = options.sanitizer ?? sanitize;
305
+ this.serialize = serializer;
306
+ this.sanitize = sanitizer;
307
+ this.buffer = new EventBuffer({ serializer, sanitizer });
308
+ this.trustClientEventId = options.trustClientEventId ?? true;
309
+ this.initialRetry = options.retry === null ? null : options.retry ?? 2e3;
310
+ this.keepAliveInterval = options.keepAlive === null ? null : options.keepAlive ?? 1e4;
311
+ this.statusCode = options.statusCode ?? 200;
312
+ this.headers = options.headers ?? {};
313
+ this.state = options.state ?? {};
314
+ this.req.once("close", this.onDisconnected);
315
+ this.res.once("close", this.onDisconnected);
316
+ setImmediate(this.initialize);
317
+ }
318
+ initialize = () => {
319
+ const url = `http://${this.req.headers.host}${this.req.url}`;
320
+ const params = new URL(url).searchParams;
321
+ if (this.trustClientEventId) {
322
+ const givenLastEventId = this.req.headers["last-event-id"] ?? params.get("lastEventId") ?? params.get("evs_last_event_id") ?? "";
323
+ this.lastId = givenLastEventId;
324
+ }
325
+ const headers = {};
326
+ if (this.res instanceof import_http.ServerResponse) {
327
+ headers["Content-Type"] = "text/event-stream";
328
+ headers["Cache-Control"] = "private, no-cache, no-store, no-transform, must-revalidate, max-age=0";
329
+ headers["Connection"] = "keep-alive";
330
+ headers["Pragma"] = "no-cache";
331
+ headers["X-Accel-Buffering"] = "no";
332
+ } else {
333
+ headers["content-type"] = "text/event-stream";
334
+ headers["cache-control"] = "private, no-cache, no-store, no-transform, must-revalidate, max-age=0";
335
+ headers["pragma"] = "no-cache";
336
+ headers["x-accel-buffering"] = "no";
337
+ }
338
+ for (const [name, value] of Object.entries(this.headers)) {
339
+ headers[name] = value ?? "";
340
+ }
341
+ this.res.writeHead(this.statusCode, headers);
342
+ if (params.has("padding")) {
343
+ this.buffer.comment(" ".repeat(2049)).dispatch();
344
+ }
345
+ if (params.has("evs_preamble")) {
346
+ this.buffer.comment(" ".repeat(2056)).dispatch();
347
+ }
348
+ if (this.initialRetry !== null) {
349
+ this.buffer.retry(this.initialRetry).dispatch();
350
+ }
351
+ this.flush();
352
+ if (this.keepAliveInterval !== null) {
353
+ this.keepAliveTimer = setInterval(
354
+ this.keepAlive,
355
+ this.keepAliveInterval
356
+ );
357
+ }
358
+ this.isConnected = true;
359
+ this.emit("connected");
360
+ };
361
+ onDisconnected = () => {
362
+ this.req.removeListener("close", this.onDisconnected);
363
+ this.res.removeListener("close", this.onDisconnected);
364
+ if (this.keepAliveTimer) {
365
+ clearInterval(this.keepAliveTimer);
366
+ }
367
+ this.isConnected = false;
368
+ this.emit("disconnected");
369
+ };
370
+ keepAlive = () => {
371
+ this.buffer.comment().dispatch();
372
+ this.flush();
373
+ };
374
+ /**
375
+ * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
376
+ */
377
+ event(type) {
378
+ this.buffer.event(type);
379
+ return this;
380
+ }
381
+ /**
382
+ * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
383
+ */
384
+ data = (data) => {
385
+ this.buffer.data(data);
386
+ return this;
387
+ };
388
+ /**
389
+ * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
390
+ */
391
+ id = (id = "") => {
392
+ this.buffer.id(id);
393
+ this.lastId = id;
394
+ return this;
395
+ };
396
+ /**
397
+ * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
398
+ */
399
+ retry = (time) => {
400
+ this.buffer.retry(time);
401
+ return this;
402
+ };
403
+ /**
404
+ * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
405
+ */
406
+ comment = (text) => {
407
+ this.buffer.comment(text);
408
+ return this;
409
+ };
410
+ /**
411
+ * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
412
+ */
413
+ dispatch = () => {
414
+ this.buffer.dispatch();
415
+ return this;
416
+ };
417
+ /**
418
+ * Flush the contents of the internal buffer to the client and clear the buffer.
419
+ *
420
+ * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
421
+ */
422
+ flush = () => {
423
+ this.res.write(this.buffer.read());
424
+ this.buffer.clear();
425
+ return this;
426
+ };
427
+ /**
428
+ * Push an event to the client.
429
+ *
430
+ * If no event name is given, the event name is set to `"message"`.
431
+ *
432
+ * 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.
433
+ *
434
+ * Emits the `push` event with the given data, event name and event ID in that order.
435
+ *
436
+ * @param data - Data to write.
437
+ * @param eventName - Event name to write.
438
+ * @param eventId - Event ID to write.
439
+ */
440
+ push = (data, eventName = "message", eventId = generateId()) => {
441
+ if (!this.isConnected) {
442
+ throw new SseError("Cannot push data to a non-active session.");
443
+ }
444
+ this.buffer.push(data, eventName, eventId);
445
+ this.flush();
446
+ this.lastId = eventId;
447
+ this.emit("push", data, eventName, eventId);
448
+ return this;
449
+ };
450
+ /**
451
+ * Pipe readable stream data as a series of events to the client.
452
+ *
453
+ * This uses the `push` method under the hood.
454
+ *
455
+ * If no event name is given in the `options` object, the event name is set to `"stream"`.
456
+ *
457
+ * @param stream - Readable stream to consume data from.
458
+ * @param options - Options to alter how the stream is flushed to the client.
459
+ *
460
+ * @returns A promise that resolves or rejects based on the success of the stream write finishing.
461
+ */
462
+ stream = createPushFromStream(this.push);
463
+ /**
464
+ * Iterate over an iterable and send yielded values as events to the client.
465
+ *
466
+ * This uses the `push` method under the hood.
467
+ *
468
+ * If no event name is given in the `options` object, the event name is set to `"iteration"`.
469
+ *
470
+ * @param iterable - Iterable to consume data from.
471
+ *
472
+ * @returns A promise that resolves once all data has been successfully yielded from the iterable.
473
+ */
474
+ iterate = createPushFromIterable(this.push);
475
+ /**
476
+ * Batch and send multiple events at once.
477
+ *
478
+ * If given an `EventBuffer` instance, its contents will be sent to the client.
479
+ *
480
+ * If given a callback, it will be passed an instance of `EventBuffer` which uses the same serializer and sanitizer as the session.
481
+ * 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.
482
+ *
483
+ * @param batcher - Event buffer to get contents from, or callback that takes an event buffer to write to.
484
+ *
485
+ * @returns A promise that resolves once all data from the event buffer has been successfully sent to the client.
486
+ *
487
+ * @see EventBuffer
488
+ */
489
+ batch = async (batcher) => {
490
+ if (batcher instanceof EventBuffer) {
491
+ this.res.write(batcher.read());
492
+ } else {
493
+ const buffer = new EventBuffer({
494
+ serializer: this.serialize,
495
+ sanitizer: this.sanitize
496
+ });
497
+ await batcher(buffer);
498
+ this.res.write(buffer.read());
499
+ }
500
+ };
501
+ };
502
+
503
+ // src/createSession.ts
504
+ var createSession = (...args) => new Promise((resolve) => {
505
+ const session = new Session(...args);
506
+ session.once("connected", () => {
507
+ resolve(session);
508
+ });
509
+ });
510
+
511
+ // src/Channel.ts
512
+ var Channel = class extends TypedEmitter {
513
+ /**
514
+ * Custom state for this channel.
515
+ *
516
+ * Use this object to safely store information related to the channel.
517
+ */
518
+ state;
519
+ sessions = /* @__PURE__ */ new Set();
520
+ constructor(options = {}) {
521
+ super();
522
+ this.state = options.state ?? {};
523
+ }
524
+ /**
525
+ * List of the currently active sessions subscribed to this channel.
526
+ */
527
+ get activeSessions() {
528
+ return Array.from(this.sessions);
529
+ }
530
+ /**
531
+ * Number of sessions subscribed to this channel.
532
+ */
533
+ get sessionCount() {
534
+ return this.sessions.size;
535
+ }
536
+ /**
537
+ * Register a session so that it can start receiving events from this channel.
538
+ *
539
+ * If the session was already registered to begin with this method does nothing.
540
+ *
541
+ * @param session - Session to register.
542
+ */
543
+ register(session) {
544
+ if (this.sessions.has(session)) {
545
+ return this;
546
+ }
547
+ if (!session.isConnected) {
548
+ throw new SseError("Cannot register a non-active session.");
549
+ }
550
+ session.once("disconnected", () => {
551
+ this.emit("session-disconnected", session);
552
+ this.deregister(session);
553
+ });
554
+ this.sessions.add(session);
555
+ this.emit("session-registered", session);
556
+ return this;
557
+ }
558
+ /**
559
+ * Deregister a session so that it no longer receives events from this channel.
560
+ *
561
+ * If the session was not registered to begin with this method does nothing.
562
+ *
563
+ * @param session - Session to deregister.
564
+ */
565
+ deregister(session) {
566
+ if (!this.sessions.has(session)) {
567
+ return this;
568
+ }
569
+ this.sessions.delete(session);
570
+ this.emit("session-deregistered", session);
571
+ return this;
572
+ }
573
+ /**
574
+ * Broadcast an event to every active session registered with this channel.
575
+ *
576
+ * Under the hood this calls the `push` method on every active session.
577
+ *
578
+ * If no event name is given, the event name is set to `"message"`.
579
+ *
580
+ * Note that the broadcasted event will have the same ID across all receiving sessions instead of generating a unique ID for each.
581
+ *
582
+ * @param data - Data to write.
583
+ * @param eventName - Event name to write.
584
+ */
585
+ broadcast = (data, eventName = "message", options = {}) => {
586
+ const eventId = options.eventId ?? generateId();
587
+ const sessions = options.filter ? this.activeSessions.filter(options.filter) : this.sessions;
588
+ for (const session of sessions) {
589
+ session.push(data, eventName, eventId);
590
+ }
591
+ this.emit("broadcast", data, eventName, eventId);
592
+ return this;
593
+ };
594
+ };
595
+
596
+ // src/createChannel.ts
597
+ var createChannel = (...args) => new Channel(...args);
598
+
599
+ // src/createEventBuffer.ts
600
+ var createEventBuffer = (...args) => new EventBuffer(...args);
601
+ // Annotate the CommonJS export names for ESM import in node:
602
+ 0 && (module.exports = {
603
+ Channel,
604
+ EventBuffer,
605
+ Session,
606
+ SseError,
607
+ createChannel,
608
+ createEventBuffer,
609
+ createSession
610
+ });