better-sse 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,7 +36,7 @@ Compared to WebSockets it has comparable performance and bandwidth usage, especi
36
36
  * Trust or ignore the client-given last event ID.
37
37
  * Automatically send keep-alive pings to keep connections open.
38
38
  * Add or override the response status code and headers.
39
- * Fine-grained control by either sending [individual fields](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#fields) of events or by sending full events with simple helpers.
39
+ * Fine-grained control by either sending [individual fields](./docs/api.md#eventbuffer) of events or by sending [full events with simple helpers](./docs/api.md#sessionpush-data-unknown-eventname-string-eventid-string--this).
40
40
  * Pipe [streams](https://nodejs.org/api/stream.html#stream_readable_streams) and [iterables](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators) directly from the server to the client as a series of events.
41
41
  * Support for popular EventStream polyfills [`event-source-polyfill`](https://www.npmjs.com/package/event-source-polyfill) and [`eventsource-polyfill`](https://www.npmjs.com/package/eventsource-polyfill).
42
42
 
@@ -139,11 +139,11 @@ API documentation, getting started guides and usage with other frameworks is [av
139
139
 
140
140
  # Contributing
141
141
 
142
- This library is always open to contributions, whether it be code, bug reports, documentation or anything else.
142
+ This library is always open to contributions whether it be code, bug reports, documentation or anything else.
143
143
 
144
144
  Please submit suggestions, bugs and issues to the [GitHub issues page](https://github.com/MatthewWid/better-sse/issues).
145
145
 
146
- For code or documentation changes, [submit a pull request on GitHub](https://github.com/MatthewWid/better-sse/pulls).
146
+ For code or documentation changes [submit a pull request on GitHub](https://github.com/MatthewWid/better-sse/pulls).
147
147
 
148
148
  ## Local Development
149
149
 
@@ -58,9 +58,16 @@ declare class Channel<State extends Record<string, unknown> = DefaultChannelStat
58
58
  */
59
59
  deregister(session: Session<SessionState>): this;
60
60
  /**
61
- * Broadcast an event with the given data and name to every active session registered with this channel.
61
+ * Broadcast an event to every active session registered with this channel.
62
+ *
63
+ * Under the hood this calls the `push` method on every active session.
64
+ *
65
+ * If no event name is given, the event name is set to `"message"`.
62
66
  *
63
67
  * Note that the broadcasted event will have the same ID across all receiving sessions instead of generating a unique ID for each.
68
+ *
69
+ * @param data - Data to write.
70
+ * @param eventName - Event name to write.
64
71
  */
65
72
  broadcast: (data: unknown, eventName?: string, options?: BroadcastOptions<SessionState>) => this;
66
73
  }
@@ -0,0 +1,118 @@
1
+ import { SerializerFunction } from "./lib/serialize";
2
+ import { SanitizerFunction } from "./lib/sanitize";
3
+ interface EventBufferOptions {
4
+ /**
5
+ * Serialize data to a string that can be written.
6
+ *
7
+ * Defaults to `JSON.stringify`.
8
+ */
9
+ serializer?: SerializerFunction;
10
+ /**
11
+ * Sanitize values so as to not prematurely dispatch events when writing fields whose text inadvertently contains newlines.
12
+ *
13
+ * By default, CR, LF and CRLF characters are replaced with a single LF character (`\n`) and then any trailing LF characters are stripped so as to prevent a blank line being written and accidentally dispatching the event before `.dispatch()` is called.
14
+ */
15
+ sanitizer?: SanitizerFunction;
16
+ }
17
+ /**
18
+ * An `EventBuffer` allows you to write raw spec-compliant SSE fields into a text buffer that can be sent directly over the wire.
19
+ */
20
+ declare class EventBuffer {
21
+ private buffer;
22
+ private serialize;
23
+ private sanitize;
24
+ constructor(options?: EventBufferOptions);
25
+ /**
26
+ * Write a line with a field key and value appended with a newline character.
27
+ */
28
+ private writeField;
29
+ /**
30
+ * Write an event name field (also referred to as the event "type" in the specification).
31
+ *
32
+ * @param type - Event name/type.
33
+ */
34
+ event(type: string): this;
35
+ /**
36
+ * Write arbitrary data into a data field.
37
+ *
38
+ * Data is serialized to a string using the given `serializer` function option or JSON stringification by default.
39
+ *
40
+ * @param data - Data to serialize and write.
41
+ */
42
+ data: (data: unknown) => this;
43
+ /**
44
+ * Write an event ID field.
45
+ *
46
+ * Defaults to an empty string if no argument is given.
47
+ *
48
+ * @param id - Identification string to write.
49
+ */
50
+ id: (id?: string) => this;
51
+ /**
52
+ * Write a retry field that suggests a reconnection time with the given milliseconds.
53
+ *
54
+ * @param time - Time in milliseconds to retry.
55
+ */
56
+ retry: (time: number) => this;
57
+ /**
58
+ * Write a comment (an ignored field).
59
+ *
60
+ * This will not fire an event but is often used to keep the connection alive.
61
+ *
62
+ * @param text - Text of the comment. Otherwise writes an empty field value.
63
+ */
64
+ comment: (text?: string) => this;
65
+ /**
66
+ * Indicate that the event has finished being created by writing an additional newline character.
67
+ */
68
+ dispatch: () => this;
69
+ /**
70
+ * Create, write and dispatch an event with the given data all at once.
71
+ *
72
+ * This is equivalent to calling the methods `event`, `id`, `data` and `dispatch` in that order.
73
+ *
74
+ * If no event name is given, the event name is set to `"message"`.
75
+ *
76
+ * If no event ID is given, the event ID is set to a unique string generated using a cryptographic pseudorandom number generator.
77
+ *
78
+ * @param data - Data to write.
79
+ * @param eventName - Event name to write.
80
+ * @param eventId - Event ID to write.
81
+ */
82
+ push: (data: unknown, eventName?: string, eventId?: string) => this;
83
+ /**
84
+ * Pipe readable stream data as a series of events into the buffer.
85
+ *
86
+ * This uses the `push` method under the hood.
87
+ *
88
+ * If no event name is given in the `options` object, the event name is set to `"stream"`.
89
+ *
90
+ * @param stream - Readable stream to consume data from.
91
+ * @param options - Event name to use for each event created.
92
+ *
93
+ * @returns A promise that resolves or rejects based on the success of the stream write finishing.
94
+ */
95
+ stream: (stream: import("stream").Readable, options?: import("./lib/createPushFromStream").StreamOptions) => Promise<boolean>;
96
+ /**
97
+ * Iterate over an iterable and write yielded values as events into the buffer.
98
+ *
99
+ * This uses the `push` method under the hood.
100
+ *
101
+ * If no event name is given in the `options` object, the event name is set to `"iteration"`.
102
+ *
103
+ * @param iterable - Iterable to consume data from.
104
+ *
105
+ * @returns A promise that resolves once all data has been successfully yielded from the iterable.
106
+ */
107
+ iterate: <DataType = unknown>(iterable: Iterable<DataType> | AsyncIterable<DataType>, options?: import("./lib/createPushFromIterable").IterateOptions) => Promise<void>;
108
+ /**
109
+ * Clear the contents of the buffer.
110
+ */
111
+ clear: () => this;
112
+ /**
113
+ * Get a copy of the buffer contents.
114
+ */
115
+ read: () => string;
116
+ }
117
+ export type { EventBufferOptions };
118
+ export { EventBuffer };
@@ -1,26 +1,10 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
- import { Readable } from "stream";
4
3
  import { IncomingMessage as Http1ServerRequest, ServerResponse as Http1ServerResponse, OutgoingHttpHeaders } from "http";
5
4
  import { Http2ServerRequest, Http2ServerResponse } from "http2";
5
+ import { EventBuffer, EventBufferOptions } from "./EventBuffer";
6
6
  import { TypedEmitter, EventMap } from "./lib/TypedEmitter";
7
- import { SerializerFunction } from "./lib/serialize";
8
- import { SanitizerFunction } from "./lib/sanitize";
9
- interface SessionOptions {
10
- /**
11
- * Serialize data to a string that can be written.
12
- *
13
- * Note that only values written with `.data()` or `.push()` are serialized, as everything else is assumed to already be a string.
14
- *
15
- * Defaults to `JSON.stringify`.
16
- */
17
- serializer?: SerializerFunction;
18
- /**
19
- * Sanitize values so as to not prematurely dispatch events when writing fields whose text inadvertently contains newlines.
20
- *
21
- * By default, CR, LF and CRLF characters are replaced with a single LF character (`\n`) and then any trailing LF characters are stripped so as to prevent a blank line being written and accidentally dispatching the event before `.dispatch()` is called.
22
- */
23
- sanitizer?: SanitizerFunction;
7
+ interface SessionOptions extends Pick<EventBufferOptions, "serializer" | "sanitizer"> {
24
8
  /**
25
9
  * Whether to trust or ignore the last event ID given by the client in the `Last-Event-ID` request header.
26
10
  *
@@ -67,22 +51,6 @@ interface SessionOptions {
67
51
  */
68
52
  headers?: OutgoingHttpHeaders;
69
53
  }
70
- interface StreamOptions {
71
- /**
72
- * Event name/type to be emitted when stream data is sent to the client.
73
- *
74
- * Defaults to `"stream"`.
75
- */
76
- eventName?: string;
77
- }
78
- interface IterateOptions {
79
- /**
80
- * Event name/type to be emitted when iterable data is sent to the client.
81
- *
82
- * Defaults to `"iteration"`.
83
- */
84
- eventName?: string;
85
- }
86
54
  interface DefaultSessionState {
87
55
  [key: string]: unknown;
88
56
  }
@@ -102,10 +70,6 @@ interface SessionEvents extends EventMap {
102
70
  * Note that creating a new session will immediately send the initial status code and headers to the client.
103
71
  * Attempting to write additional headers after you have created a new session will result in an error.
104
72
  *
105
- * As a performance optimisation, all events and data are first written to an internal buffer
106
- * where it is stored until it is flushed to the client by calling the `flush` method. This is
107
- * done for you when using the `push` helper method.
108
- *
109
73
  * @param req - The Node HTTP {@link https://nodejs.org/api/http.html#http_class_http_incomingmessage | ServerResponse} object.
110
74
  * @param res - The Node HTTP {@link https://nodejs.org/api/http.html#http_class_http_serverresponse | IncomingMessage} object.
111
75
  * @param options - Options given to the session instance.
@@ -116,7 +80,7 @@ declare class Session<State extends Record<string, unknown> = DefaultSessionStat
116
80
  *
117
81
  * 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.
118
82
  *
119
- * For security reasons, keep in mind that the client can provide *any* initial ID here. Use the `trustClientEventId` to ignore the client-given initial ID.
83
+ * 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.
120
84
  *
121
85
  * @readonly
122
86
  */
@@ -136,11 +100,6 @@ declare class Session<State extends Record<string, unknown> = DefaultSessionStat
136
100
  * to safely add new properties to the `DefaultSessionState` interface.
137
101
  */
138
102
  state: State;
139
- /**
140
- * Internal buffer used to store raw data from written fields.
141
- *
142
- * When Session#dispatch is called its buffer data will be flushed.
143
- */
144
103
  private buffer;
145
104
  /**
146
105
  * Raw HTTP request.
@@ -162,63 +121,43 @@ declare class Session<State extends Record<string, unknown> = DefaultSessionStat
162
121
  constructor(req: Http1ServerRequest | Http2ServerRequest, res: Http1ServerResponse | Http2ServerResponse, options?: SessionOptions);
163
122
  private initialize;
164
123
  private onDisconnected;
165
- /**
166
- * Write a line with a field key and value appended with a newline character.
167
- */
168
- private writeField;
169
124
  private keepAlive;
170
125
  /**
171
- * Set the event to the given name (also referred to as the event "type" in the specification).
172
- *
173
- * @param type - Event name/type.
126
+ * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
174
127
  */
175
128
  event(type: string): this;
176
129
  /**
177
- * Write an arbitrary data field that is automatically serialized to a string using the given `serializer` function option or JSON stringification by default.
178
- *
179
- * @param data - Data to serialize and write.
130
+ * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
180
131
  */
181
132
  data: (data: unknown) => this;
182
133
  /**
183
- * Set the event ID to the given string.
184
- *
185
- * Defaults to an empty string if no argument is given.
186
- *
187
- * @param id - Identification string to write.
134
+ * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
188
135
  */
189
136
  id: (id?: string) => this;
190
137
  /**
191
- * Set the suggested reconnection time to the given milliseconds.
192
- *
193
- * @param time - Time in milliseconds to retry.
138
+ * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
194
139
  */
195
140
  retry: (time: number) => this;
196
141
  /**
197
- * Write a comment (an ignored field).
198
- *
199
- * This will not fire an event, but is often used to keep the connection alive.
200
- *
201
- * @param text - Text of the comment. Otherwise writes an empty field value.
142
+ * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
202
143
  */
203
144
  comment: (text?: string) => this;
204
145
  /**
205
- * Indicate that the event has finished being created by writing an additional newline character.
206
- *
207
- * Note that this does **not** send the written data to the client. Use `flush` to flush the internal buffer.
146
+ * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
208
147
  */
209
148
  dispatch: () => this;
210
149
  /**
211
- * Flush the buffered data to the client and clear the buffer.
150
+ * Flush the contents of the internal buffer to the client and clear the buffer.
151
+ *
152
+ * @deprecated see https://github.com/MatthewWid/better-sse/issues/52
212
153
  */
213
154
  flush: () => this;
214
155
  /**
215
- * Create, write, dispatch and flush an event with the given data to the client all at once.
216
- *
217
- * This is equivalent to calling the methods `event`, `id`, `data`, `dispatch` and `flush` in that order.
156
+ * Push an event to the client.
218
157
  *
219
158
  * If no event name is given, the event name is set to `"message"`.
220
159
  *
221
- * 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.
160
+ * 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.
222
161
  *
223
162
  * Emits the `push` event with the given data, event name and event ID in that order.
224
163
  *
@@ -228,34 +167,45 @@ declare class Session<State extends Record<string, unknown> = DefaultSessionStat
228
167
  */
229
168
  push: (data: unknown, eventName?: string, eventId?: string) => this;
230
169
  /**
231
- * Pipe readable stream data to the client.
232
- *
233
- * Each data emission by the stream pushes a new event to the client.
170
+ * Pipe readable stream data as a series of events to the client.
234
171
  *
235
172
  * This uses the `push` method under the hood.
236
173
  *
237
- * If no event name is given in the options object, the event name is set to `"stream"`.
174
+ * If no event name is given in the `options` object, the event name is set to `"stream"`.
238
175
  *
239
- * @param stream - Readable stream to consume from.
176
+ * @param stream - Readable stream to consume data from.
240
177
  * @param options - Options to alter how the stream is flushed to the client.
241
178
  *
242
179
  * @returns A promise that resolves or rejects based on the success of the stream write finishing.
243
180
  */
244
- stream: (stream: Readable, options?: StreamOptions) => Promise<boolean>;
181
+ stream: (stream: import("stream").Readable, options?: import("./lib/createPushFromStream").StreamOptions) => Promise<boolean>;
245
182
  /**
246
- * Iterate over an iterable and send yielded values to the client.
247
- *
248
- * Each yield pushes a new event to the client.
183
+ * Iterate over an iterable and send yielded values as events to the client.
249
184
  *
250
185
  * This uses the `push` method under the hood.
251
186
  *
252
- * If no event name is given in the options object, the event name is set to `"iteration"`.
187
+ * If no event name is given in the `options` object, the event name is set to `"iteration"`.
253
188
  *
254
189
  * @param iterable - Iterable to consume data from.
255
190
  *
256
- * @returns A promise that resolves once all the data has been yielded from the iterable.
191
+ * @returns A promise that resolves once all data has been successfully yielded from the iterable.
192
+ */
193
+ iterate: <DataType = unknown>(iterable: Iterable<DataType> | AsyncIterable<DataType>, options?: import("./lib/createPushFromIterable").IterateOptions) => Promise<void>;
194
+ /**
195
+ * Batch and send multiple events at once.
196
+ *
197
+ * If given an `EventBuffer` instance, its contents will be sent to the client.
198
+ *
199
+ * If given a callback, it will be passed an instance of `EventBuffer` which uses the same serializer and sanitizer as the session.
200
+ * 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.
201
+ *
202
+ * @param batcher - Event buffer to get contents from, or callback that takes an event buffer to write to.
203
+ *
204
+ * @returns A promise that resolves once all data from the event buffer has been successfully sent to the client.
205
+ *
206
+ * @see EventBuffer
257
207
  */
258
- iterate: <DataType = unknown>(iterable: Iterable<DataType> | AsyncIterable<DataType>, options?: IterateOptions) => Promise<void>;
208
+ batch: (batcher: EventBuffer | ((buffer: EventBuffer) => void | Promise<void>)) => Promise<void>;
259
209
  }
260
- export type { SessionOptions, StreamOptions, IterateOptions, SessionEvents, DefaultSessionState, };
210
+ export type { SessionOptions, SessionEvents, DefaultSessionState };
261
211
  export { Session };
@@ -0,0 +1,3 @@
1
+ import { EventBuffer } from "./EventBuffer";
2
+ declare const createEventBuffer: (options?: import("./EventBuffer").EventBufferOptions | undefined) => EventBuffer;
3
+ export { createEventBuffer };
package/build/index.d.ts CHANGED
@@ -2,3 +2,7 @@ export * from "./Session";
2
2
  export * from "./createSession";
3
3
  export * from "./Channel";
4
4
  export * from "./createChannel";
5
+ export * from "./EventBuffer";
6
+ export * from "./createEventBuffer";
7
+ export type { StreamOptions } from "./lib/createPushFromStream";
8
+ export type { IterateOptions } from "./lib/createPushFromIterable";
package/build/index.js CHANGED
@@ -1,2 +1,2 @@
1
- !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var s=t();for(var i in s)("object"==typeof exports?exports:e)[i]=s[i]}}(global,(()=>(()=>{"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:()=>f,Session:()=>u,createChannel:()=>v,createSession:()=>p});const s=require("http"),i=require("events");var r=e.n(i);class n extends(r()){addListener(e,t){return super.addListener(e,t)}prependListener(e,t){return super.prependListener(e,t)}prependOnceListener(e,t){return super.prependOnceListener(e,t)}on(e,t){return super.on(e,t)}once(e,t){return super.once(e,t)}emit(e,...t){return super.emit(e,...t)}off(e,t){return super.off(e,t)}removeListener(e,t){return super.removeListener(e,t)}}const o=e=>JSON.stringify(e),a=/(\r\n|\r|\n)/g,h=/\n+$/g,l=e=>{let t=e;return t=t.replace(a,"\n"),t=t.replace(h,""),t},d=require("crypto");let c;c=d.randomUUID?()=>(0,d.randomUUID)():()=>(0,d.randomBytes)(4).toString("hex");class u extends n{constructor(e,t,i={}){var r,n,a,h,d,u,p;super(),this.lastId="",this.isConnected=!1,this.state={},this.buffer="",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.comment(" ".repeat(2049)).dispatch(),n.has("evs_preamble")&&this.comment(" ".repeat(2056)).dispatch(),null!==this.initialRetry&&this.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.keepAliveTimer&&clearInterval(this.keepAliveTimer),this.isConnected=!1,this.emit("disconnected")},this.writeField=(e,t)=>{const s=this.sanitize(t);return this.buffer+=e+":"+s+"\n",this},this.keepAlive=()=>{this.comment().dispatch().flush()},this.data=e=>{const t=this.serialize(e);return this.writeField("data",t),this},this.id=(e="")=>(this.writeField("id",e),this.lastId=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.flush=()=>(this.res.write(this.buffer),this.buffer="",this),this.push=(e,t="message",s=c())=>(this.event(t).id(s).data(e).dispatch().flush(),this.emit("push",e,t,s),this),this.stream=async(e,t={})=>{const{eventName:s="stream"}=t;return new Promise(((t,i)=>{e.on("data",(e=>{let t;t=Buffer.isBuffer(e)?e.toString():e,this.push(t,s)})),e.once("end",(()=>t(!0))),e.once("close",(()=>t(!0))),e.once("error",(e=>i(e)))}))},this.iterate=async(e,t={})=>{const{eventName:s="iteration"}=t;for await(const t of e)this.push(t,s)},this.req=e,this.res=t,this.serialize=null!==(r=i.serializer)&&void 0!==r?r:o,this.sanitize=null!==(n=i.sanitizer)&&void 0!==n?n:l,this.trustClientEventId=null===(a=i.trustClientEventId)||void 0===a||a,this.initialRetry=null===i.retry?null:null!==(h=i.retry)&&void 0!==h?h:2e3,this.keepAliveInterval=null===i.keepAlive?null:null!==(d=i.keepAlive)&&void 0!==d?d:1e4,this.statusCode=null!==(u=i.statusCode)&&void 0!==u?u:200,this.headers=null!==(p=i.headers)&&void 0!==p?p:{},this.req.once("close",this.onDisconnected),setImmediate(this.initialize)}event(e){return this.writeField("event",e),this}}const p=(...e)=>new Promise((t=>{const s=new u(...e);s.once("connected",(()=>{t(s)}))}));class f extends n{constructor(){super(),this.state={},this.sessions=new Set,this.broadcast=(e,t="message",s={})=>{const i=c();let r;r=s.filter?Array.from(this.sessions).filter(s.filter):this.sessions;for(const s of r)s.push(e,t,i);return this.emit("broadcast",e,t,i),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 Error("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 v=(...e)=>new f(...e);return t})()));
1
+ !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var s=t();for(var i in s)("object"==typeof exports?exports:e)[i]=s[i]}}(global,(()=>(()=>{"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:()=>b,EventBuffer:()=>d,Session:()=>v,createChannel:()=>g,createEventBuffer:()=>y,createSession:()=>m});const s=require("http"),i=e=>JSON.stringify(e),r=/(\r\n|\r|\n)/g,n=/\n+$/g,h=e=>{let t=e;return t=t.replace(r,"\n"),t=t.replace(n,""),t},o=require("crypto");let a;a=o.randomUUID?()=>(0,o.randomUUID)():()=>(0,o.randomBytes)(4).toString("hex");const l=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)))}))},c=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=l(this.push),this.iterate=c(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:h}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 p{constructor(e,t,r={}){var n,o,u,f,p,v,m;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 h={};this.res instanceof s.ServerResponse?(h["Content-Type"]="text/event-stream",h["Cache-Control"]="private, no-cache, no-store, no-transform, must-revalidate, max-age=0",h.Connection="keep-alive",h.Pragma="no-cache",h["X-Accel-Buffering"]="no"):(h["content-type"]="text/event-stream",h["cache-control"]="private, no-cache, no-store, no-transform, must-revalidate, max-age=0",h.pragma="no-cache",h["x-accel-buffering"]="no");for(const[e,t]of Object.entries(this.headers))h[e]=null!=t?t:"";this.res.writeHead(this.statusCode,h),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.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())=>(this.buffer.push(e,t,s),this.flush(),this.lastId=s,this.emit("push",e,t,s),this),this.stream=l(this.push),this.iterate=c(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 b=null!==(n=r.serializer)&&void 0!==n?n:i,g=null!==(o=r.sanitizer)&&void 0!==o?o:h;this.serialize=b,this.sanitize=g,this.buffer=new d({serializer:b,sanitizer:g}),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!==(v=r.statusCode)&&void 0!==v?v:200,this.headers=null!==(m=r.headers)&&void 0!==m?m:{},this.req.once("close",this.onDisconnected),setImmediate(this.initialize)}event(e){return this.buffer.event(e),this}}const m=(...e)=>new Promise((t=>{const s=new v(...e);s.once("connected",(()=>{t(s)}))}));class b extends p{constructor(){super(),this.state={},this.sessions=new Set,this.broadcast=(e,t="message",s={})=>{const i=a(),r=s.filter?this.activeSessions.filter(s.filter):this.sessions;for(const s of r)s.push(e,t,i);return this.emit("broadcast",e,t,i),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 Error("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 g=(...e)=>new b(...e),y=(...e)=>new d(...e);return t})()));
2
2
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ interface IterateOptions {
2
+ /**
3
+ * Event name/type to be emitted when iterable data is sent to the client.
4
+ *
5
+ * Defaults to `"iteration"`.
6
+ */
7
+ eventName?: string;
8
+ }
9
+ declare const createPushFromIterable: (push: (data: unknown, eventName: string) => void) => <DataType = unknown>(iterable: Iterable<DataType> | AsyncIterable<DataType>, options?: IterateOptions) => Promise<void>;
10
+ export type { IterateOptions };
11
+ export { createPushFromIterable };
@@ -0,0 +1,12 @@
1
+ import { Readable } from "stream";
2
+ interface StreamOptions {
3
+ /**
4
+ * Event name/type to be emitted when stream data is sent to the client.
5
+ *
6
+ * Defaults to `"stream"`.
7
+ */
8
+ eventName?: string;
9
+ }
10
+ declare const createPushFromStream: (push: (data: unknown, eventName: string) => void) => (stream: Readable, options?: StreamOptions) => Promise<boolean>;
11
+ export type { StreamOptions };
12
+ export { createPushFromStream };
@@ -2,9 +2,11 @@ import http from "http";
2
2
  import http2 from "http2";
3
3
  import net from "net";
4
4
  import { Session } from "../Session";
5
+ import { EventBuffer } from "../EventBuffer";
5
6
  declare const createHttpServer: () => Promise<http.Server>;
6
7
  declare const createHttp2Server: () => Promise<http2.Http2Server>;
7
8
  declare const closeServer: (server: net.Server) => Promise<void>;
8
9
  declare const getUrl: (server: net.Server) => string;
9
10
  declare const waitForConnect: (session: Session) => Promise<void>;
10
- export { createHttpServer, createHttp2Server, closeServer, getUrl, waitForConnect, };
11
+ declare const getBuffer: (session: Session) => EventBuffer;
12
+ export { createHttpServer, createHttp2Server, closeServer, getUrl, waitForConnect, getBuffer, };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "better-sse",
3
3
  "description": "Dead simple, dependency-less, spec-compliant server-side events implementation for Node, written in TypeScript.",
4
- "version": "0.9.0",
4
+ "version": "0.10.0",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
7
7
  "license": "MIT",