zero-http 0.2.0 → 0.2.1

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 (42) hide show
  1. package/README.md +314 -115
  2. package/documentation/full-server.js +82 -1
  3. package/documentation/public/data/api.json +154 -33
  4. package/documentation/public/data/examples.json +35 -11
  5. package/documentation/public/data/options.json +14 -8
  6. package/documentation/public/index.html +109 -17
  7. package/documentation/public/scripts/data-sections.js +4 -4
  8. package/documentation/public/scripts/helpers.js +4 -4
  9. package/documentation/public/scripts/playground.js +201 -0
  10. package/documentation/public/scripts/ui.js +6 -6
  11. package/documentation/public/scripts/uploads.js +9 -9
  12. package/documentation/public/styles.css +12 -12
  13. package/documentation/public/vendor/icons/compress.svg +27 -0
  14. package/documentation/public/vendor/icons/https.svg +23 -0
  15. package/documentation/public/vendor/icons/router.svg +29 -0
  16. package/documentation/public/vendor/icons/sse.svg +25 -0
  17. package/documentation/public/vendor/icons/websocket.svg +21 -0
  18. package/index.js +21 -4
  19. package/lib/app.js +145 -15
  20. package/lib/body/json.js +3 -0
  21. package/lib/body/multipart.js +2 -0
  22. package/lib/body/raw.js +3 -0
  23. package/lib/body/text.js +3 -0
  24. package/lib/body/urlencoded.js +3 -0
  25. package/lib/{fetch.js → fetch/index.js} +30 -1
  26. package/lib/http/index.js +9 -0
  27. package/lib/{request.js → http/request.js} +7 -1
  28. package/lib/{response.js → http/response.js} +70 -1
  29. package/lib/middleware/compress.js +194 -0
  30. package/lib/middleware/index.js +12 -0
  31. package/lib/router/index.js +278 -0
  32. package/lib/sse/index.js +8 -0
  33. package/lib/sse/stream.js +322 -0
  34. package/lib/ws/connection.js +440 -0
  35. package/lib/ws/handshake.js +122 -0
  36. package/lib/ws/index.js +12 -0
  37. package/package.json +1 -1
  38. package/lib/router.js +0 -87
  39. /package/lib/{cors.js → middleware/cors.js} +0 -0
  40. /package/lib/{logger.js → middleware/logger.js} +0 -0
  41. /package/lib/{rateLimit.js → middleware/rateLimit.js} +0 -0
  42. /package/lib/{static.js → middleware/static.js} +0 -0
@@ -0,0 +1,322 @@
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
+ *
6
+ * Properties:
7
+ * - `connected` {boolean} Whether the stream is still open.
8
+ * - `lastEventId` {string|null} The `Last-Event-ID` header from the client reconnection.
9
+ * - `eventCount` {number} Total events sent on this stream.
10
+ * - `bytesSent` {number} Total bytes written to the stream.
11
+ * - `connectedAt` {number} Timestamp (ms) when the stream was opened.
12
+ * - `uptime` {number} Milliseconds since the stream was opened (computed).
13
+ * - `data` {object} Arbitrary user-data store.
14
+ *
15
+ * Events (via `.on()`):
16
+ * - `'close'` — Client disconnected or `.close()` was called.
17
+ * - `'error'` (err: Error) — Write error on the underlying response.
18
+ */
19
+ class SSEStream
20
+ {
21
+ /**
22
+ * @param {import('http').ServerResponse} raw
23
+ * @param {object} opts
24
+ */
25
+ constructor(raw, opts = {})
26
+ {
27
+ this._raw = raw;
28
+ this._closed = false;
29
+
30
+ /** `true` when the underlying connection is over TLS (HTTPS). */
31
+ this.secure = !!opts.secure;
32
+
33
+ /** Auto-increment counter for event IDs. */
34
+ this._autoId = opts.autoId || false;
35
+ this._nextId = opts.startId || 1;
36
+
37
+ /** The Last-Event-ID sent by the client on reconnection. */
38
+ this.lastEventId = opts.lastEventId || null;
39
+
40
+ /** Total number of events pushed. */
41
+ this.eventCount = 0;
42
+
43
+ /** Total bytes written to the stream. */
44
+ this.bytesSent = 0;
45
+
46
+ /** Timestamp when the stream was opened. */
47
+ this.connectedAt = Date.now();
48
+
49
+ /** Arbitrary user-data store. */
50
+ this.data = {};
51
+
52
+ /** @type {Object<string, Function[]>} */
53
+ this._listeners = {};
54
+
55
+ /** @private */
56
+ this._keepAliveTimer = null;
57
+
58
+ // Auto keep-alive
59
+ if (opts.keepAlive && opts.keepAlive > 0)
60
+ {
61
+ const commentText = opts.keepAliveComment || 'ping';
62
+ this._keepAliveTimer = setInterval(() => this.comment(commentText), opts.keepAlive);
63
+ if (this._keepAliveTimer.unref) this._keepAliveTimer.unref();
64
+ }
65
+
66
+ raw.on('close', () =>
67
+ {
68
+ this._closed = true;
69
+ this._clearKeepAlive();
70
+ this._emit('close');
71
+ });
72
+
73
+ raw.on('error', (err) => this._emit('error', err));
74
+ }
75
+
76
+ // -- Event Emitter ---------------------------------
77
+
78
+ /**
79
+ * Register an event listener.
80
+ * @param {'close'|'error'} event
81
+ * @param {Function} fn
82
+ * @returns {SSEStream} this
83
+ */
84
+ on(event, fn)
85
+ {
86
+ if (!this._listeners[event]) this._listeners[event] = [];
87
+ this._listeners[event].push(fn);
88
+ return this;
89
+ }
90
+
91
+ /**
92
+ * Register a one-time listener.
93
+ * @param {'close'|'error'} event
94
+ * @param {Function} fn
95
+ * @returns {SSEStream} this
96
+ */
97
+ once(event, fn)
98
+ {
99
+ const wrapper = (...args) => { this.off(event, wrapper); fn(...args); };
100
+ wrapper._original = fn;
101
+ return this.on(event, wrapper);
102
+ }
103
+
104
+ /**
105
+ * Remove a listener.
106
+ * @param {string} event
107
+ * @param {Function} fn
108
+ * @returns {SSEStream} this
109
+ */
110
+ off(event, fn)
111
+ {
112
+ const list = this._listeners[event];
113
+ if (!list) return this;
114
+ this._listeners[event] = list.filter(f => f !== fn && f._original !== fn);
115
+ return this;
116
+ }
117
+
118
+ /**
119
+ * Remove all listeners for an event (or all events).
120
+ * @param {string} [event]
121
+ * @returns {SSEStream} this
122
+ */
123
+ removeAllListeners(event)
124
+ {
125
+ if (event) delete this._listeners[event];
126
+ else this._listeners = {};
127
+ return this;
128
+ }
129
+
130
+ /**
131
+ * Count listeners for an event.
132
+ * @param {string} event
133
+ * @returns {number}
134
+ */
135
+ listenerCount(event)
136
+ {
137
+ return (this._listeners[event] || []).length;
138
+ }
139
+
140
+ /** @private */
141
+ _emit(event, ...args)
142
+ {
143
+ const fns = this._listeners[event];
144
+ if (fns) fns.slice().forEach(fn => { try { fn(...args); } catch (e) { } });
145
+ }
146
+
147
+ // -- Writing Helpers -------------------------------
148
+
149
+ /**
150
+ * Write a raw string to the underlying response.
151
+ * @private
152
+ * @param {string} str
153
+ */
154
+ _write(str)
155
+ {
156
+ if (this._closed) return;
157
+ try
158
+ {
159
+ this._raw.write(str);
160
+ this.bytesSent += Buffer.byteLength(str, 'utf8');
161
+ }
162
+ catch (e) { }
163
+ }
164
+
165
+ /**
166
+ * Format a payload into `data:` lines per the SSE spec.
167
+ * Objects are JSON-serialised automatically.
168
+ * @private
169
+ * @param {string|object} data
170
+ * @returns {string}
171
+ */
172
+ _formatData(data)
173
+ {
174
+ const payload = typeof data === 'object' ? JSON.stringify(data) : String(data);
175
+ return payload.split('\n').map(line => `data: ${line}\n`).join('');
176
+ }
177
+
178
+ // -- Public API ------------------------------------
179
+
180
+ /**
181
+ * Send an unnamed data event.
182
+ * Objects are automatically JSON-serialised.
183
+ *
184
+ * @param {string|object} data - Payload to send.
185
+ * @param {string|number} [id] - Optional event ID (overrides auto-ID).
186
+ * @returns {SSEStream} this
187
+ */
188
+ send(data, id)
189
+ {
190
+ if (this._closed) return this;
191
+ let msg = '';
192
+ const eventId = id !== undefined ? id : (this._autoId ? this._nextId++ : undefined);
193
+ if (eventId !== undefined) msg += `id: ${eventId}\n`;
194
+ msg += this._formatData(data);
195
+ msg += '\n';
196
+ this._write(msg);
197
+ this.eventCount++;
198
+ return this;
199
+ }
200
+
201
+ /**
202
+ * Convenience: send an object as JSON data (same as `.send(obj)`).
203
+ * @param {*} obj
204
+ * @param {string|number} [id]
205
+ * @returns {SSEStream} this
206
+ */
207
+ sendJSON(obj, id)
208
+ {
209
+ return this.send(obj, id);
210
+ }
211
+
212
+ /**
213
+ * Send a named event with data.
214
+ *
215
+ * @param {string} eventName - Event type (appears as `event:` field).
216
+ * @param {string|object} data - Payload.
217
+ * @param {string|number} [id] - Optional event ID (overrides auto-ID).
218
+ * @returns {SSEStream} this
219
+ */
220
+ event(eventName, data, id)
221
+ {
222
+ if (this._closed) return this;
223
+ let msg = `event: ${eventName}\n`;
224
+ const eventId = id !== undefined ? id : (this._autoId ? this._nextId++ : undefined);
225
+ if (eventId !== undefined) msg += `id: ${eventId}\n`;
226
+ msg += this._formatData(data);
227
+ msg += '\n';
228
+ this._write(msg);
229
+ this.eventCount++;
230
+ return this;
231
+ }
232
+
233
+ /**
234
+ * Send a comment line. Comments are ignored by EventSource clients
235
+ * but useful as a keep-alive mechanism.
236
+ *
237
+ * @param {string} text - Comment text.
238
+ * @returns {SSEStream} this
239
+ */
240
+ comment(text)
241
+ {
242
+ if (this._closed) return this;
243
+ this._write(`: ${text}\n\n`);
244
+ return this;
245
+ }
246
+
247
+ /**
248
+ * Send (or update) the retry interval hint.
249
+ * The client's EventSource will use this value for reconnection delay.
250
+ *
251
+ * @param {number} ms - Retry interval in milliseconds.
252
+ * @returns {SSEStream} this
253
+ */
254
+ retry(ms)
255
+ {
256
+ if (this._closed) return this;
257
+ this._write(`retry: ${ms}\n\n`);
258
+ return this;
259
+ }
260
+
261
+ /**
262
+ * Start or restart an automatic keep-alive timer that sends comment
263
+ * pings at the given interval.
264
+ *
265
+ * @param {number} intervalMs - Interval in ms. Pass `0` to stop.
266
+ * @param {string} [comment='ping'] - Comment text to send.
267
+ * @returns {SSEStream} this
268
+ */
269
+ keepAlive(intervalMs, comment)
270
+ {
271
+ this._clearKeepAlive();
272
+ if (intervalMs && intervalMs > 0)
273
+ {
274
+ const text = comment || 'ping';
275
+ this._keepAliveTimer = setInterval(() => this.comment(text), intervalMs);
276
+ if (this._keepAliveTimer.unref) this._keepAliveTimer.unref();
277
+ }
278
+ return this;
279
+ }
280
+
281
+ /**
282
+ * Flush the response (hint to Node to push buffered data to the network).
283
+ * Useful when piping through reverse proxies that buffer.
284
+ *
285
+ * @returns {SSEStream} this
286
+ */
287
+ flush()
288
+ {
289
+ if (this._closed) return this;
290
+ try
291
+ {
292
+ if (typeof this._raw.flushHeaders === 'function') this._raw.flushHeaders();
293
+ }
294
+ catch (e) { }
295
+ return this;
296
+ }
297
+
298
+ /**
299
+ * Close the SSE connection from the server side.
300
+ */
301
+ close()
302
+ {
303
+ if (this._closed) return;
304
+ this._closed = true;
305
+ this._clearKeepAlive();
306
+ try { this._raw.end(); } catch (e) { }
307
+ }
308
+
309
+ /** Whether the connection is still open. */
310
+ get connected() { return !this._closed; }
311
+
312
+ /** How long this stream has been open (ms). */
313
+ get uptime() { return Date.now() - this.connectedAt; }
314
+
315
+ /** @private */
316
+ _clearKeepAlive()
317
+ {
318
+ if (this._keepAliveTimer) { clearInterval(this._keepAliveTimer); this._keepAliveTimer = null; }
319
+ }
320
+ }
321
+
322
+ module.exports = SSEStream;