@zero-server/sdk 0.9.1 → 0.9.3

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.
Files changed (128) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +460 -443
  3. package/index.js +414 -412
  4. package/lib/app.js +1172 -1172
  5. package/lib/auth/authorize.js +399 -399
  6. package/lib/auth/enrollment.js +367 -367
  7. package/lib/auth/index.js +57 -57
  8. package/lib/auth/jwt.js +731 -731
  9. package/lib/auth/oauth.js +362 -362
  10. package/lib/auth/session.js +588 -588
  11. package/lib/auth/trustedDevice.js +409 -409
  12. package/lib/auth/twoFactor.js +1150 -1150
  13. package/lib/auth/webauthn.js +946 -946
  14. package/lib/body/index.js +14 -14
  15. package/lib/body/json.js +109 -109
  16. package/lib/body/multipart.js +440 -440
  17. package/lib/body/raw.js +71 -71
  18. package/lib/body/rawBuffer.js +160 -160
  19. package/lib/body/sendError.js +25 -25
  20. package/lib/body/text.js +75 -75
  21. package/lib/body/typeMatch.js +41 -41
  22. package/lib/body/urlencoded.js +235 -235
  23. package/lib/cli.js +845 -845
  24. package/lib/cluster.js +666 -666
  25. package/lib/debug.js +372 -372
  26. package/lib/env/index.js +465 -465
  27. package/lib/errors.js +683 -683
  28. package/lib/fetch/index.js +256 -256
  29. package/lib/grpc/balancer.js +378 -378
  30. package/lib/grpc/call.js +708 -708
  31. package/lib/grpc/client.js +764 -764
  32. package/lib/grpc/codec.js +1221 -1221
  33. package/lib/grpc/credentials.js +398 -398
  34. package/lib/grpc/frame.js +262 -262
  35. package/lib/grpc/health.js +287 -287
  36. package/lib/grpc/index.js +121 -121
  37. package/lib/grpc/metadata.js +461 -461
  38. package/lib/grpc/proto.js +821 -821
  39. package/lib/grpc/reflection.js +590 -590
  40. package/lib/grpc/server.js +445 -445
  41. package/lib/grpc/status.js +118 -118
  42. package/lib/grpc/watch.js +173 -173
  43. package/lib/http/index.js +10 -10
  44. package/lib/http/request.js +727 -727
  45. package/lib/http/response.js +799 -799
  46. package/lib/lifecycle.js +557 -557
  47. package/lib/middleware/compress.js +230 -230
  48. package/lib/middleware/cookieParser.js +237 -237
  49. package/lib/middleware/cors.js +93 -93
  50. package/lib/middleware/csrf.js +137 -137
  51. package/lib/middleware/errorHandler.js +101 -101
  52. package/lib/middleware/helmet.js +175 -175
  53. package/lib/middleware/index.js +19 -17
  54. package/lib/middleware/logger.js +74 -74
  55. package/lib/middleware/rateLimit.js +88 -88
  56. package/lib/middleware/requestId.js +53 -53
  57. package/lib/middleware/static.js +326 -326
  58. package/lib/middleware/timeout.js +71 -71
  59. package/lib/middleware/validator.js +255 -255
  60. package/lib/observe/health.js +326 -326
  61. package/lib/observe/index.js +50 -50
  62. package/lib/observe/logger.js +359 -359
  63. package/lib/observe/metrics.js +805 -805
  64. package/lib/observe/tracing.js +592 -592
  65. package/lib/orm/adapters/json.js +290 -290
  66. package/lib/orm/adapters/memory.js +764 -764
  67. package/lib/orm/adapters/mongo.js +764 -764
  68. package/lib/orm/adapters/mysql.js +933 -933
  69. package/lib/orm/adapters/postgres.js +1144 -1144
  70. package/lib/orm/adapters/redis.js +1534 -1534
  71. package/lib/orm/adapters/sql-base.js +212 -212
  72. package/lib/orm/adapters/sqlite.js +858 -858
  73. package/lib/orm/audit.js +649 -649
  74. package/lib/orm/cache.js +394 -394
  75. package/lib/orm/geo.js +387 -387
  76. package/lib/orm/index.js +784 -784
  77. package/lib/orm/migrate.js +432 -432
  78. package/lib/orm/model.js +1706 -1706
  79. package/lib/orm/plugin.js +375 -375
  80. package/lib/orm/procedures.js +836 -836
  81. package/lib/orm/profiler.js +233 -233
  82. package/lib/orm/query.js +1772 -1772
  83. package/lib/orm/replicas.js +241 -241
  84. package/lib/orm/schema.js +307 -307
  85. package/lib/orm/search.js +380 -380
  86. package/lib/orm/seed/data/commerce.js +136 -136
  87. package/lib/orm/seed/data/internet.js +111 -111
  88. package/lib/orm/seed/data/locations.js +204 -204
  89. package/lib/orm/seed/data/names.js +338 -338
  90. package/lib/orm/seed/data/person.js +128 -128
  91. package/lib/orm/seed/data/phone.js +211 -211
  92. package/lib/orm/seed/data/words.js +134 -134
  93. package/lib/orm/seed/factory.js +178 -178
  94. package/lib/orm/seed/fake.js +1186 -1186
  95. package/lib/orm/seed/index.js +18 -18
  96. package/lib/orm/seed/rng.js +70 -70
  97. package/lib/orm/seed/seeder.js +124 -124
  98. package/lib/orm/seed/unique.js +68 -68
  99. package/lib/orm/snapshot.js +366 -366
  100. package/lib/orm/tenancy.js +605 -605
  101. package/lib/orm/views.js +350 -350
  102. package/lib/router/index.js +436 -436
  103. package/lib/sse/index.js +8 -8
  104. package/lib/sse/stream.js +349 -349
  105. package/lib/ws/connection.js +451 -451
  106. package/lib/ws/handshake.js +125 -125
  107. package/lib/ws/index.js +14 -14
  108. package/lib/ws/room.js +223 -223
  109. package/package.json +73 -73
  110. package/types/app.d.ts +223 -223
  111. package/types/auth.d.ts +520 -520
  112. package/types/body.d.ts +14 -0
  113. package/types/cli.d.ts +2 -0
  114. package/types/cluster.d.ts +75 -75
  115. package/types/env.d.ts +80 -80
  116. package/types/errors.d.ts +316 -316
  117. package/types/fetch.d.ts +43 -43
  118. package/types/grpc.d.ts +432 -432
  119. package/types/index.d.ts +384 -384
  120. package/types/lifecycle.d.ts +60 -60
  121. package/types/middleware.d.ts +320 -320
  122. package/types/observe.d.ts +304 -304
  123. package/types/orm.d.ts +1887 -1887
  124. package/types/request.d.ts +109 -109
  125. package/types/response.d.ts +157 -157
  126. package/types/router.d.ts +78 -78
  127. package/types/sse.d.ts +78 -78
  128. package/types/websocket.d.ts +126 -126
package/lib/sse/stream.js CHANGED
@@ -1,349 +1,349 @@
1
- /**
2
- * @module sse/stream
3
- * @description SSE (Server-Sent Events) stream controller.
4
- * Wraps a raw HTTP response and provides the full SSE text protocol.
5
- * Tracks connection state, event counts, and bytes sent.
6
- * Emits `'close'` when the client disconnects and `'error'` on write failures.
7
- *
8
- * @example
9
- * app.get('/events', (req, res) => {
10
- * const stream = res.sse(); // opens SSE connection
11
- *
12
- * stream.send({ hello: 'world' }); // unnamed event
13
- * stream.event('update', { id: 1 }); // named event
14
- * stream.retry(3000); // set client reconnect delay
15
- * stream.keepAlive(15000); // auto-ping every 15s
16
- *
17
- * stream.on('close', () => {
18
- * console.log('client disconnected after', stream.uptime, 'ms');
19
- * });
20
- * });
21
- */
22
- class SSEStream
23
- {
24
- /**
25
- * @constructor
26
- * @param {import('http').ServerResponse} raw - Raw HTTP response stream.
27
- * @param {object} [opts] - Configuration options.
28
- * @param {boolean} [opts.secure] - Whether the connection is over TLS.
29
- * @param {boolean} [opts.autoId] - Auto-increment event IDs.
30
- * @param {number} [opts.startId] - Starting value for auto-ID (default 1).
31
- * @param {string} [opts.lastEventId] - Last-Event-ID from the client reconnection header.
32
- * @param {number} [opts.keepAlive] - Interval (ms) for automatic keep-alive pings. 0 to disable.
33
- * @param {string} [opts.keepAliveComment] - Comment text for keep-alive pings (default `'ping'`).
34
- */
35
- constructor(raw, opts = {})
36
- {
37
- this._raw = raw;
38
- this._closed = false;
39
- this._log = require('../debug')('zero:sse');
40
-
41
- /** `true` when the underlying connection is over TLS (HTTPS). */
42
- this.secure = !!opts.secure;
43
-
44
- /** Auto-increment counter for event IDs. */
45
- this._autoId = opts.autoId || false;
46
- this._nextId = opts.startId || 1;
47
-
48
- /** The Last-Event-ID sent by the client on reconnection. */
49
- this.lastEventId = opts.lastEventId || null;
50
-
51
- /** Total number of events pushed. */
52
- this.eventCount = 0;
53
-
54
- /** Total bytes written to the stream. */
55
- this.bytesSent = 0;
56
-
57
- /** Timestamp when the stream was opened. */
58
- this.connectedAt = Date.now();
59
-
60
- /** Arbitrary user-data store. */
61
- this.data = {};
62
-
63
- /** @type {Object<string, Function[]>} */
64
- this._listeners = {};
65
-
66
- /** @private */
67
- this._keepAliveTimer = null;
68
-
69
- // Auto keep-alive
70
- if (opts.keepAlive && opts.keepAlive > 0)
71
- {
72
- const commentText = opts.keepAliveComment || 'ping';
73
- this._keepAliveTimer = setInterval(() => this.comment(commentText), opts.keepAlive);
74
- if (this._keepAliveTimer.unref) this._keepAliveTimer.unref();
75
- }
76
-
77
- raw.on('close', () =>
78
- {
79
- this._closed = true;
80
- this._clearKeepAlive();
81
- this._log.debug('stream closed, %d events sent', this.eventCount);
82
- this._emit('close');
83
- });
84
-
85
- raw.on('error', (err) => { this._log.error('stream error: %s', err.message); this._emit('error', err); });
86
- }
87
-
88
- // -- Event Emitter ---------------------------------
89
-
90
- /**
91
- * Register an event listener.
92
- * @param {'close'|'error'} event - Event name.
93
- * @param {Function} fn - Callback function.
94
- * @returns {SSEStream} this
95
- */
96
- on(event, fn)
97
- {
98
- if (!this._listeners[event]) this._listeners[event] = [];
99
- this._listeners[event].push(fn);
100
- return this;
101
- }
102
-
103
- /**
104
- * Register a one-time listener.
105
- * @param {'close'|'error'} event - Event name.
106
- * @param {Function} fn - Callback function.
107
- * @returns {SSEStream} this
108
- */
109
- once(event, fn)
110
- {
111
- const wrapper = (...args) => { this.off(event, wrapper); fn(...args); };
112
- wrapper._original = fn;
113
- return this.on(event, wrapper);
114
- }
115
-
116
- /**
117
- * Remove a listener.
118
- * @param {string} event - Event name.
119
- * @param {Function} fn - Callback function.
120
- * @returns {SSEStream} this
121
- */
122
- off(event, fn)
123
- {
124
- const list = this._listeners[event];
125
- if (!list) return this;
126
- this._listeners[event] = list.filter(f => f !== fn && f._original !== fn);
127
- return this;
128
- }
129
-
130
- /**
131
- * Remove all listeners for an event (or all events).
132
- * @param {string} [event] - Event name.
133
- * @returns {SSEStream} this
134
- */
135
- removeAllListeners(event)
136
- {
137
- if (event) delete this._listeners[event];
138
- else this._listeners = {};
139
- return this;
140
- }
141
-
142
- /**
143
- * Count listeners for an event.
144
- * @param {string} event - Event name.
145
- * @returns {number} Number of registered listeners.
146
- */
147
- listenerCount(event)
148
- {
149
- return (this._listeners[event] || []).length;
150
- }
151
-
152
- /** @private */
153
- _emit(event, ...args)
154
- {
155
- const fns = this._listeners[event];
156
- if (fns) fns.slice().forEach(fn => { try { fn(...args); } catch (e) { } });
157
- }
158
-
159
- // -- Writing Helpers -------------------------------
160
-
161
- /**
162
- * Write a raw string to the underlying response.
163
- * @private
164
- * @param {string} str - String to write.
165
- */
166
- _write(str)
167
- {
168
- if (this._closed) return;
169
- try
170
- {
171
- this._raw.write(str);
172
- this.bytesSent += Buffer.byteLength(str, 'utf8');
173
- }
174
- catch (e) { }
175
- }
176
-
177
- /**
178
- * Format a payload into `data:` lines per the SSE spec.
179
- * Objects are JSON-serialised automatically.
180
- * @private
181
- * @param {string|object} data - Record data object.
182
- * @returns {string} Formatted string.
183
- */
184
- _formatData(data)
185
- {
186
- let payload;
187
- if (typeof data === 'object')
188
- {
189
- try { payload = JSON.stringify(data); }
190
- catch (e) { payload = '[Serialization Error]'; }
191
- }
192
- else { payload = String(data); }
193
- return payload.split('\n').map(line => `data: ${line}\n`).join('');
194
- }
195
-
196
- // -- Public API ------------------------------------
197
-
198
- /**
199
- * Send an unnamed data event.
200
- * Objects are automatically JSON-serialised.
201
- *
202
- * @param {string|object} data - Payload to send.
203
- * @param {string|number} [id] - Optional event ID (overrides auto-ID).
204
- * @returns {SSEStream} this
205
- */
206
- send(data, id)
207
- {
208
- if (this._closed) return this;
209
- let msg = '';
210
- const eventId = id !== undefined ? id : (this._autoId ? this._nextId++ : undefined);
211
- if (eventId !== undefined) msg += `id: ${eventId}\n`;
212
- msg += this._formatData(data);
213
- msg += '\n';
214
- this._write(msg);
215
- this.eventCount++;
216
- return this;
217
- }
218
-
219
- /**
220
- * Convenience: send an object as JSON data (same as `.send(obj)`).
221
- * @param {*} obj - Data object to send.
222
- * @param {string|number} [id] - Unique identifier.
223
- * @returns {SSEStream} this
224
- */
225
- sendJSON(obj, id)
226
- {
227
- return this.send(obj, id);
228
- }
229
-
230
- /**
231
- * Send a named event with data.
232
- *
233
- * @param {string} eventName - Event type (appears as `event:` field).
234
- * @param {string|object} data - Payload.
235
- * @param {string|number} [id] - Optional event ID (overrides auto-ID).
236
- * @returns {SSEStream} this
237
- */
238
- event(eventName, data, id)
239
- {
240
- if (this._closed) return this;
241
- let msg = `event: ${eventName}\n`;
242
- const eventId = id !== undefined ? id : (this._autoId ? this._nextId++ : undefined);
243
- if (eventId !== undefined) msg += `id: ${eventId}\n`;
244
- msg += this._formatData(data);
245
- msg += '\n';
246
- this._write(msg);
247
- this.eventCount++;
248
- return this;
249
- }
250
-
251
- /**
252
- * Send a comment line. Comments are ignored by EventSource clients
253
- * but useful as a keep-alive mechanism.
254
- *
255
- * @param {string} text - Comment text.
256
- * @returns {SSEStream} this
257
- */
258
- comment(text)
259
- {
260
- if (this._closed) return this;
261
- // Escape newlines to prevent SSE frame injection
262
- const safe = String(text).split('\n').join('\n: ');
263
- this._write(`: ${safe}\n\n`);
264
- return this;
265
- }
266
-
267
- /**
268
- * Send (or update) the retry interval hint.
269
- * The client's EventSource will use this value for reconnection delay.
270
- *
271
- * @param {number} ms - Retry interval in milliseconds.
272
- * @returns {SSEStream} this
273
- */
274
- retry(ms)
275
- {
276
- if (this._closed) return this;
277
- this._write(`retry: ${ms}\n\n`);
278
- return this;
279
- }
280
-
281
- /**
282
- * Start or restart an automatic keep-alive timer that sends comment
283
- * pings at the given interval.
284
- *
285
- * @param {number} intervalMs - Interval in ms. Pass `0` to stop.
286
- * @param {string} [comment='ping'] - Comment text to send.
287
- * @returns {SSEStream} this
288
- */
289
- keepAlive(intervalMs, comment)
290
- {
291
- this._clearKeepAlive();
292
- if (intervalMs && intervalMs > 0)
293
- {
294
- const text = comment || 'ping';
295
- this._keepAliveTimer = setInterval(() => this.comment(text), intervalMs);
296
- if (this._keepAliveTimer.unref) this._keepAliveTimer.unref();
297
- }
298
- return this;
299
- }
300
-
301
- /**
302
- * Flush the response (hint to Node to push buffered data to the network).
303
- * Useful when piping through reverse proxies that buffer.
304
- *
305
- * @returns {SSEStream} this
306
- */
307
- flush()
308
- {
309
- if (this._closed) return this;
310
- try
311
- {
312
- if (typeof this._raw.flushHeaders === 'function') this._raw.flushHeaders();
313
- }
314
- catch (e) { }
315
- return this;
316
- }
317
-
318
- /**
319
- * Close the SSE connection from the server side.
320
- * @returns {void}
321
- */
322
- close()
323
- {
324
- if (this._closed) return;
325
- this._closed = true;
326
- this._clearKeepAlive();
327
- try { this._raw.end(); } catch (e) { }
328
- }
329
-
330
- /**
331
- * Whether the connection is still open.
332
- * @returns {boolean} `true` if the stream has not been closed.
333
- */
334
- get connected() { return !this._closed; }
335
-
336
- /**
337
- * How long this stream has been open (ms).
338
- * @returns {number} Milliseconds since the stream was opened.
339
- */
340
- get uptime() { return Date.now() - this.connectedAt; }
341
-
342
- /** @private */
343
- _clearKeepAlive()
344
- {
345
- if (this._keepAliveTimer) { clearInterval(this._keepAliveTimer); this._keepAliveTimer = null; }
346
- }
347
- }
348
-
349
- module.exports = SSEStream;
1
+ /**
2
+ * @module sse/stream
3
+ * @description SSE (Server-Sent Events) stream controller.
4
+ * Wraps a raw HTTP response and provides the full SSE text protocol.
5
+ * Tracks connection state, event counts, and bytes sent.
6
+ * Emits `'close'` when the client disconnects and `'error'` on write failures.
7
+ *
8
+ * @example
9
+ * app.get('/events', (req, res) => {
10
+ * const stream = res.sse(); // opens SSE connection
11
+ *
12
+ * stream.send({ hello: 'world' }); // unnamed event
13
+ * stream.event('update', { id: 1 }); // named event
14
+ * stream.retry(3000); // set client reconnect delay
15
+ * stream.keepAlive(15000); // auto-ping every 15s
16
+ *
17
+ * stream.on('close', () => {
18
+ * console.log('client disconnected after', stream.uptime, 'ms');
19
+ * });
20
+ * });
21
+ */
22
+ class SSEStream
23
+ {
24
+ /**
25
+ * @constructor
26
+ * @param {import('http').ServerResponse} raw - Raw HTTP response stream.
27
+ * @param {object} [opts] - Configuration options.
28
+ * @param {boolean} [opts.secure] - Whether the connection is over TLS.
29
+ * @param {boolean} [opts.autoId] - Auto-increment event IDs.
30
+ * @param {number} [opts.startId] - Starting value for auto-ID (default 1).
31
+ * @param {string} [opts.lastEventId] - Last-Event-ID from the client reconnection header.
32
+ * @param {number} [opts.keepAlive] - Interval (ms) for automatic keep-alive pings. 0 to disable.
33
+ * @param {string} [opts.keepAliveComment] - Comment text for keep-alive pings (default `'ping'`).
34
+ */
35
+ constructor(raw, opts = {})
36
+ {
37
+ this._raw = raw;
38
+ this._closed = false;
39
+ this._log = require('../debug')('zero:sse');
40
+
41
+ /** `true` when the underlying connection is over TLS (HTTPS). */
42
+ this.secure = !!opts.secure;
43
+
44
+ /** Auto-increment counter for event IDs. */
45
+ this._autoId = opts.autoId || false;
46
+ this._nextId = opts.startId || 1;
47
+
48
+ /** The Last-Event-ID sent by the client on reconnection. */
49
+ this.lastEventId = opts.lastEventId || null;
50
+
51
+ /** Total number of events pushed. */
52
+ this.eventCount = 0;
53
+
54
+ /** Total bytes written to the stream. */
55
+ this.bytesSent = 0;
56
+
57
+ /** Timestamp when the stream was opened. */
58
+ this.connectedAt = Date.now();
59
+
60
+ /** Arbitrary user-data store. */
61
+ this.data = {};
62
+
63
+ /** @type {Object<string, Function[]>} */
64
+ this._listeners = {};
65
+
66
+ /** @private */
67
+ this._keepAliveTimer = null;
68
+
69
+ // Auto keep-alive
70
+ if (opts.keepAlive && opts.keepAlive > 0)
71
+ {
72
+ const commentText = opts.keepAliveComment || 'ping';
73
+ this._keepAliveTimer = setInterval(() => this.comment(commentText), opts.keepAlive);
74
+ if (this._keepAliveTimer.unref) this._keepAliveTimer.unref();
75
+ }
76
+
77
+ raw.on('close', () =>
78
+ {
79
+ this._closed = true;
80
+ this._clearKeepAlive();
81
+ this._log.debug('stream closed, %d events sent', this.eventCount);
82
+ this._emit('close');
83
+ });
84
+
85
+ raw.on('error', (err) => { this._log.error('stream error: %s', err.message); this._emit('error', err); });
86
+ }
87
+
88
+ // -- Event Emitter ---------------------------------
89
+
90
+ /**
91
+ * Register an event listener.
92
+ * @param {'close'|'error'} event - Event name.
93
+ * @param {Function} fn - Callback function.
94
+ * @returns {SSEStream} this
95
+ */
96
+ on(event, fn)
97
+ {
98
+ if (!this._listeners[event]) this._listeners[event] = [];
99
+ this._listeners[event].push(fn);
100
+ return this;
101
+ }
102
+
103
+ /**
104
+ * Register a one-time listener.
105
+ * @param {'close'|'error'} event - Event name.
106
+ * @param {Function} fn - Callback function.
107
+ * @returns {SSEStream} this
108
+ */
109
+ once(event, fn)
110
+ {
111
+ const wrapper = (...args) => { this.off(event, wrapper); fn(...args); };
112
+ wrapper._original = fn;
113
+ return this.on(event, wrapper);
114
+ }
115
+
116
+ /**
117
+ * Remove a listener.
118
+ * @param {string} event - Event name.
119
+ * @param {Function} fn - Callback function.
120
+ * @returns {SSEStream} this
121
+ */
122
+ off(event, fn)
123
+ {
124
+ const list = this._listeners[event];
125
+ if (!list) return this;
126
+ this._listeners[event] = list.filter(f => f !== fn && f._original !== fn);
127
+ return this;
128
+ }
129
+
130
+ /**
131
+ * Remove all listeners for an event (or all events).
132
+ * @param {string} [event] - Event name.
133
+ * @returns {SSEStream} this
134
+ */
135
+ removeAllListeners(event)
136
+ {
137
+ if (event) delete this._listeners[event];
138
+ else this._listeners = {};
139
+ return this;
140
+ }
141
+
142
+ /**
143
+ * Count listeners for an event.
144
+ * @param {string} event - Event name.
145
+ * @returns {number} Number of registered listeners.
146
+ */
147
+ listenerCount(event)
148
+ {
149
+ return (this._listeners[event] || []).length;
150
+ }
151
+
152
+ /** @private */
153
+ _emit(event, ...args)
154
+ {
155
+ const fns = this._listeners[event];
156
+ if (fns) fns.slice().forEach(fn => { try { fn(...args); } catch (e) { } });
157
+ }
158
+
159
+ // -- Writing Helpers -------------------------------
160
+
161
+ /**
162
+ * Write a raw string to the underlying response.
163
+ * @private
164
+ * @param {string} str - String to write.
165
+ */
166
+ _write(str)
167
+ {
168
+ if (this._closed) return;
169
+ try
170
+ {
171
+ this._raw.write(str);
172
+ this.bytesSent += Buffer.byteLength(str, 'utf8');
173
+ }
174
+ catch (e) { }
175
+ }
176
+
177
+ /**
178
+ * Format a payload into `data:` lines per the SSE spec.
179
+ * Objects are JSON-serialised automatically.
180
+ * @private
181
+ * @param {string|object} data - Record data object.
182
+ * @returns {string} Formatted string.
183
+ */
184
+ _formatData(data)
185
+ {
186
+ let payload;
187
+ if (typeof data === 'object')
188
+ {
189
+ try { payload = JSON.stringify(data); }
190
+ catch (e) { payload = '[Serialization Error]'; }
191
+ }
192
+ else { payload = String(data); }
193
+ return payload.split('\n').map(line => `data: ${line}\n`).join('');
194
+ }
195
+
196
+ // -- Public API ------------------------------------
197
+
198
+ /**
199
+ * Send an unnamed data event.
200
+ * Objects are automatically JSON-serialised.
201
+ *
202
+ * @param {string|object} data - Payload to send.
203
+ * @param {string|number} [id] - Optional event ID (overrides auto-ID).
204
+ * @returns {SSEStream} this
205
+ */
206
+ send(data, id)
207
+ {
208
+ if (this._closed) return this;
209
+ let msg = '';
210
+ const eventId = id !== undefined ? id : (this._autoId ? this._nextId++ : undefined);
211
+ if (eventId !== undefined) msg += `id: ${eventId}\n`;
212
+ msg += this._formatData(data);
213
+ msg += '\n';
214
+ this._write(msg);
215
+ this.eventCount++;
216
+ return this;
217
+ }
218
+
219
+ /**
220
+ * Convenience: send an object as JSON data (same as `.send(obj)`).
221
+ * @param {*} obj - Data object to send.
222
+ * @param {string|number} [id] - Unique identifier.
223
+ * @returns {SSEStream} this
224
+ */
225
+ sendJSON(obj, id)
226
+ {
227
+ return this.send(obj, id);
228
+ }
229
+
230
+ /**
231
+ * Send a named event with data.
232
+ *
233
+ * @param {string} eventName - Event type (appears as `event:` field).
234
+ * @param {string|object} data - Payload.
235
+ * @param {string|number} [id] - Optional event ID (overrides auto-ID).
236
+ * @returns {SSEStream} this
237
+ */
238
+ event(eventName, data, id)
239
+ {
240
+ if (this._closed) return this;
241
+ let msg = `event: ${eventName}\n`;
242
+ const eventId = id !== undefined ? id : (this._autoId ? this._nextId++ : undefined);
243
+ if (eventId !== undefined) msg += `id: ${eventId}\n`;
244
+ msg += this._formatData(data);
245
+ msg += '\n';
246
+ this._write(msg);
247
+ this.eventCount++;
248
+ return this;
249
+ }
250
+
251
+ /**
252
+ * Send a comment line. Comments are ignored by EventSource clients
253
+ * but useful as a keep-alive mechanism.
254
+ *
255
+ * @param {string} text - Comment text.
256
+ * @returns {SSEStream} this
257
+ */
258
+ comment(text)
259
+ {
260
+ if (this._closed) return this;
261
+ // Escape newlines to prevent SSE frame injection
262
+ const safe = String(text).split('\n').join('\n: ');
263
+ this._write(`: ${safe}\n\n`);
264
+ return this;
265
+ }
266
+
267
+ /**
268
+ * Send (or update) the retry interval hint.
269
+ * The client's EventSource will use this value for reconnection delay.
270
+ *
271
+ * @param {number} ms - Retry interval in milliseconds.
272
+ * @returns {SSEStream} this
273
+ */
274
+ retry(ms)
275
+ {
276
+ if (this._closed) return this;
277
+ this._write(`retry: ${ms}\n\n`);
278
+ return this;
279
+ }
280
+
281
+ /**
282
+ * Start or restart an automatic keep-alive timer that sends comment
283
+ * pings at the given interval.
284
+ *
285
+ * @param {number} intervalMs - Interval in ms. Pass `0` to stop.
286
+ * @param {string} [comment='ping'] - Comment text to send.
287
+ * @returns {SSEStream} this
288
+ */
289
+ keepAlive(intervalMs, comment)
290
+ {
291
+ this._clearKeepAlive();
292
+ if (intervalMs && intervalMs > 0)
293
+ {
294
+ const text = comment || 'ping';
295
+ this._keepAliveTimer = setInterval(() => this.comment(text), intervalMs);
296
+ if (this._keepAliveTimer.unref) this._keepAliveTimer.unref();
297
+ }
298
+ return this;
299
+ }
300
+
301
+ /**
302
+ * Flush the response (hint to Node to push buffered data to the network).
303
+ * Useful when piping through reverse proxies that buffer.
304
+ *
305
+ * @returns {SSEStream} this
306
+ */
307
+ flush()
308
+ {
309
+ if (this._closed) return this;
310
+ try
311
+ {
312
+ if (typeof this._raw.flushHeaders === 'function') this._raw.flushHeaders();
313
+ }
314
+ catch (e) { }
315
+ return this;
316
+ }
317
+
318
+ /**
319
+ * Close the SSE connection from the server side.
320
+ * @returns {void}
321
+ */
322
+ close()
323
+ {
324
+ if (this._closed) return;
325
+ this._closed = true;
326
+ this._clearKeepAlive();
327
+ try { this._raw.end(); } catch (e) { }
328
+ }
329
+
330
+ /**
331
+ * Whether the connection is still open.
332
+ * @returns {boolean} `true` if the stream has not been closed.
333
+ */
334
+ get connected() { return !this._closed; }
335
+
336
+ /**
337
+ * How long this stream has been open (ms).
338
+ * @returns {number} Milliseconds since the stream was opened.
339
+ */
340
+ get uptime() { return Date.now() - this.connectedAt; }
341
+
342
+ /** @private */
343
+ _clearKeepAlive()
344
+ {
345
+ if (this._keepAliveTimer) { clearInterval(this._keepAliveTimer); this._keepAliveTimer = null; }
346
+ }
347
+ }
348
+
349
+ module.exports = SSEStream;