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.
- package/README.md +314 -115
- package/documentation/full-server.js +82 -1
- package/documentation/public/data/api.json +154 -33
- package/documentation/public/data/examples.json +35 -11
- package/documentation/public/data/options.json +14 -8
- package/documentation/public/index.html +109 -17
- package/documentation/public/scripts/data-sections.js +4 -4
- package/documentation/public/scripts/helpers.js +4 -4
- package/documentation/public/scripts/playground.js +201 -0
- package/documentation/public/scripts/ui.js +6 -6
- package/documentation/public/scripts/uploads.js +9 -9
- package/documentation/public/styles.css +12 -12
- package/documentation/public/vendor/icons/compress.svg +27 -0
- package/documentation/public/vendor/icons/https.svg +23 -0
- package/documentation/public/vendor/icons/router.svg +29 -0
- package/documentation/public/vendor/icons/sse.svg +25 -0
- package/documentation/public/vendor/icons/websocket.svg +21 -0
- package/index.js +21 -4
- package/lib/app.js +145 -15
- package/lib/body/json.js +3 -0
- package/lib/body/multipart.js +2 -0
- package/lib/body/raw.js +3 -0
- package/lib/body/text.js +3 -0
- package/lib/body/urlencoded.js +3 -0
- package/lib/{fetch.js → fetch/index.js} +30 -1
- package/lib/http/index.js +9 -0
- package/lib/{request.js → http/request.js} +7 -1
- package/lib/{response.js → http/response.js} +70 -1
- package/lib/middleware/compress.js +194 -0
- package/lib/middleware/index.js +12 -0
- package/lib/router/index.js +278 -0
- package/lib/sse/index.js +8 -0
- package/lib/sse/stream.js +322 -0
- package/lib/ws/connection.js +440 -0
- package/lib/ws/handshake.js +122 -0
- package/lib/ws/index.js +12 -0
- package/package.json +1 -1
- package/lib/router.js +0 -87
- /package/lib/{cors.js → middleware/cors.js} +0 -0
- /package/lib/{logger.js → middleware/logger.js} +0 -0
- /package/lib/{rateLimit.js → middleware/rateLimit.js} +0 -0
- /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;
|