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,440 @@
1
+ /**
2
+ * @module ws/connection
3
+ * @description Full-featured WebSocket connection wrapper over a raw TCP socket.
4
+ * Implements RFC 6455 framing for text, binary, ping, pong, and close.
5
+ */
6
+
7
+ /** Auto-incrementing connection ID counter. */
8
+ let _wsIdCounter = 0;
9
+
10
+ /**
11
+ * WebSocket ready-state constants (mirrors the browser WebSocket API).
12
+ * @enum {number}
13
+ */
14
+ const WS_READY_STATE = {
15
+ CONNECTING: 0,
16
+ OPEN: 1,
17
+ CLOSING: 2,
18
+ CLOSED: 3,
19
+ };
20
+
21
+ /**
22
+ * Full-featured WebSocket connection wrapper over a raw TCP socket.
23
+ * Implements RFC 6455 framing for text, binary, ping, pong, and close.
24
+ *
25
+ * @class
26
+ *
27
+ * Properties:
28
+ * - `id` {string} Unique connection identifier.
29
+ * - `readyState` {number} Current state (0-3, see WS_READY_STATE).
30
+ * - `protocol` {string} Negotiated sub-protocol (or '').
31
+ * - `extensions` {string} Requested extensions header (or '').
32
+ * - `headers` {object} Request headers from the upgrade.
33
+ * - `ip` {string|null} Remote IP address.
34
+ * - `query` {object} Parsed query-string params from the upgrade URL.
35
+ * - `url` {string} Full upgrade request URL.
36
+ * - `bufferedAmount`{number} Bytes waiting to be flushed to the network.
37
+ * - `maxPayload` {number} Maximum accepted incoming payload (bytes).
38
+ *
39
+ * Events (via `.on()`):
40
+ * - `'message'` (data: string|Buffer) — Text or binary message received.
41
+ * - `'close'` (code?: number, reason?: string) — Connection closed.
42
+ * - `'error'` (err: Error) — Socket error.
43
+ * - `'pong'` (payload: Buffer) — Pong frame received.
44
+ * - `'ping'` (payload: Buffer) — Ping frame received (auto-ponged).
45
+ * - `'drain'` — Socket write buffer drained.
46
+ */
47
+ class WebSocketConnection
48
+ {
49
+ /**
50
+ * @param {import('net').Socket} socket - The upgraded TCP socket.
51
+ * @param {object} [meta]
52
+ * @param {number} [meta.maxPayload=1048576]
53
+ * @param {number} [meta.pingInterval=30000]
54
+ * @param {string} [meta.protocol]
55
+ * @param {string} [meta.extensions]
56
+ * @param {object} [meta.headers]
57
+ * @param {string} [meta.ip]
58
+ * @param {object} [meta.query]
59
+ * @param {string} [meta.url]
60
+ * @param {boolean} [meta.secure=false]
61
+ */
62
+ constructor(socket, meta = {})
63
+ {
64
+ this._socket = socket;
65
+ this._buffer = Buffer.alloc(0);
66
+
67
+ /** Unique connection identifier. */
68
+ this.id = 'ws_' + (++_wsIdCounter) + '_' + Date.now().toString(36);
69
+
70
+ /** Current ready state. */
71
+ this.readyState = WS_READY_STATE.OPEN;
72
+
73
+ /** Negotiated sub-protocol. */
74
+ this.protocol = meta.protocol || '';
75
+
76
+ /** Requested extensions. */
77
+ this.extensions = meta.extensions || '';
78
+
79
+ /** Request headers from the upgrade. */
80
+ this.headers = meta.headers || {};
81
+
82
+ /** Remote IP address. */
83
+ this.ip = meta.ip || (socket.remoteAddress || null);
84
+
85
+ /** Parsed query params from the upgrade URL. */
86
+ this.query = meta.query || {};
87
+
88
+ /** Full upgrade URL. */
89
+ this.url = meta.url || '';
90
+
91
+ /** `true` when the underlying connection is over TLS (WSS). */
92
+ this.secure = !!meta.secure;
93
+
94
+ /** Maximum incoming frame payload in bytes (default 1 MB). */
95
+ this.maxPayload = meta.maxPayload || 1048576;
96
+
97
+ /** Timestamp (ms) when the connection was established. */
98
+ this.connectedAt = Date.now();
99
+
100
+ /** Arbitrary user-data store. Attach anything you need. */
101
+ this.data = {};
102
+
103
+ /** @type {Object<string, Function[]>} */
104
+ this._listeners = {};
105
+
106
+ /** @private */
107
+ this._pingTimer = null;
108
+
109
+ // Set up auto-ping keep-alive
110
+ const pingInterval = meta.pingInterval !== undefined ? meta.pingInterval : 30000;
111
+ if (pingInterval > 0)
112
+ {
113
+ this._pingTimer = setInterval(() => this.ping(), pingInterval);
114
+ if (this._pingTimer.unref) this._pingTimer.unref();
115
+ }
116
+
117
+ socket.on('data', (chunk) => this._onData(chunk));
118
+ socket.on('close', () =>
119
+ {
120
+ if (this.readyState !== WS_READY_STATE.CLOSED)
121
+ {
122
+ this.readyState = WS_READY_STATE.CLOSED;
123
+ this._clearPing();
124
+ this._emit('close', 1006, '');
125
+ }
126
+ });
127
+ socket.on('error', (err) => this._emit('error', err));
128
+ socket.on('drain', () => this._emit('drain'));
129
+ }
130
+
131
+ // -- Event Emitter ---------------------------------
132
+
133
+ /**
134
+ * Register an event listener.
135
+ * @param {'message'|'close'|'error'|'pong'|'ping'|'drain'} event
136
+ * @param {Function} fn
137
+ * @returns {WebSocketConnection} this
138
+ */
139
+ on(event, fn)
140
+ {
141
+ if (!this._listeners[event]) this._listeners[event] = [];
142
+ this._listeners[event].push(fn);
143
+ return this;
144
+ }
145
+
146
+ /**
147
+ * Register a one-time event listener.
148
+ * @param {'message'|'close'|'error'|'pong'|'ping'|'drain'} event
149
+ * @param {Function} fn
150
+ * @returns {WebSocketConnection} this
151
+ */
152
+ once(event, fn)
153
+ {
154
+ const wrapper = (...args) => { this.off(event, wrapper); fn(...args); };
155
+ wrapper._original = fn;
156
+ return this.on(event, wrapper);
157
+ }
158
+
159
+ /**
160
+ * Remove a specific event listener.
161
+ * @param {string} event
162
+ * @param {Function} fn
163
+ * @returns {WebSocketConnection} this
164
+ */
165
+ off(event, fn)
166
+ {
167
+ const list = this._listeners[event];
168
+ if (!list) return this;
169
+ this._listeners[event] = list.filter(f => f !== fn && f._original !== fn);
170
+ return this;
171
+ }
172
+
173
+ /**
174
+ * Remove all listeners for an event, or all events if none specified.
175
+ * @param {string} [event]
176
+ * @returns {WebSocketConnection} this
177
+ */
178
+ removeAllListeners(event)
179
+ {
180
+ if (event) delete this._listeners[event];
181
+ else this._listeners = {};
182
+ return this;
183
+ }
184
+
185
+ /**
186
+ * Count listeners for a given event.
187
+ * @param {string} event
188
+ * @returns {number}
189
+ */
190
+ listenerCount(event)
191
+ {
192
+ return (this._listeners[event] || []).length;
193
+ }
194
+
195
+ /** @private */
196
+ _emit(event, ...args)
197
+ {
198
+ const fns = this._listeners[event];
199
+ if (fns) fns.slice().forEach(fn => { try { fn(...args); } catch (e) { } });
200
+ }
201
+
202
+ // -- Sending ---------------------------------------
203
+
204
+ /**
205
+ * Send a text or binary message.
206
+ * @param {string|Buffer} data - Payload.
207
+ * @param {object} [opts]
208
+ * @param {boolean} [opts.binary] - Force binary frame (opcode 0x02).
209
+ * @param {Function} [opts.callback] - Called after the data is flushed.
210
+ * @returns {boolean} `false` if the socket buffer is full (backpressure).
211
+ */
212
+ send(data, opts)
213
+ {
214
+ if (this.readyState !== WS_READY_STATE.OPEN) return false;
215
+ const cb = opts && opts.callback;
216
+ const forceBinary = opts && opts.binary;
217
+ const isBinary = forceBinary || Buffer.isBuffer(data);
218
+ const opcode = isBinary ? 0x02 : 0x01;
219
+ const payload = isBinary ? (Buffer.isBuffer(data) ? data : Buffer.from(data)) : Buffer.from(String(data), 'utf8');
220
+ const frame = this._buildFrame(opcode, payload);
221
+ try { return this._socket.write(frame, cb); } catch (e) { return false; }
222
+ }
223
+
224
+ /**
225
+ * Send a JSON-serialised message (sets text frame).
226
+ * @param {*} obj - Value to serialise.
227
+ * @param {Function} [cb] - Called after the data is flushed.
228
+ * @returns {boolean}
229
+ */
230
+ sendJSON(obj, cb)
231
+ {
232
+ return this.send(JSON.stringify(obj), { callback: cb });
233
+ }
234
+
235
+ /**
236
+ * Send a ping frame.
237
+ * @param {string|Buffer} [payload] - Optional payload (max 125 bytes).
238
+ * @param {Function} [cb] - Called after the frame is flushed.
239
+ * @returns {boolean}
240
+ */
241
+ ping(payload, cb)
242
+ {
243
+ if (this.readyState !== WS_READY_STATE.OPEN) return false;
244
+ const data = payload ? (Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload))) : Buffer.alloc(0);
245
+ try { return this._socket.write(this._buildFrame(0x09, data), cb); } catch (e) { return false; }
246
+ }
247
+
248
+ /**
249
+ * Send a pong frame.
250
+ * @param {string|Buffer} [payload] - Optional payload.
251
+ * @param {Function} [cb] - Called after the frame is flushed.
252
+ * @returns {boolean}
253
+ */
254
+ pong(payload, cb)
255
+ {
256
+ if (this.readyState !== WS_READY_STATE.OPEN) return false;
257
+ const data = payload ? (Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload))) : Buffer.alloc(0);
258
+ try { return this._socket.write(this._buildFrame(0x0A, data), cb); } catch (e) { return false; }
259
+ }
260
+
261
+ /**
262
+ * Close the WebSocket connection.
263
+ * @param {number} [code=1000] - Close status code.
264
+ * @param {string} [reason] - Close reason string.
265
+ */
266
+ close(code, reason)
267
+ {
268
+ if (this.readyState === WS_READY_STATE.CLOSED || this.readyState === WS_READY_STATE.CLOSING) return;
269
+ this.readyState = WS_READY_STATE.CLOSING;
270
+ this._clearPing();
271
+ const statusCode = code || 1000;
272
+ const reasonBuf = reason ? Buffer.from(String(reason), 'utf8') : Buffer.alloc(0);
273
+ const payload = Buffer.alloc(2 + reasonBuf.length);
274
+ payload.writeUInt16BE(statusCode, 0);
275
+ reasonBuf.copy(payload, 2);
276
+ try
277
+ {
278
+ this._socket.write(this._buildFrame(0x08, payload));
279
+ this._socket.end();
280
+ }
281
+ catch (e) { }
282
+ }
283
+
284
+ /**
285
+ * Forcefully destroy the underlying socket without a close frame.
286
+ */
287
+ terminate()
288
+ {
289
+ this.readyState = WS_READY_STATE.CLOSED;
290
+ this._clearPing();
291
+ try { this._socket.destroy(); } catch (e) { }
292
+ }
293
+
294
+ // -- Computed Properties ---------------------------
295
+
296
+ /**
297
+ * Bytes waiting in the send buffer.
298
+ * @type {number}
299
+ */
300
+ get bufferedAmount()
301
+ {
302
+ return this._socket ? (this._socket.writableLength || 0) : 0;
303
+ }
304
+
305
+ /**
306
+ * How long this connection has been alive (ms).
307
+ * @type {number}
308
+ */
309
+ get uptime()
310
+ {
311
+ return Date.now() - this.connectedAt;
312
+ }
313
+
314
+ // -- Internals -------------------------------------
315
+
316
+ /** @private */
317
+ _clearPing()
318
+ {
319
+ if (this._pingTimer) { clearInterval(this._pingTimer); this._pingTimer = null; }
320
+ }
321
+
322
+ /** @private Build a WebSocket frame. */
323
+ _buildFrame(opcode, payload)
324
+ {
325
+ const len = payload.length;
326
+ let header;
327
+ if (len < 126)
328
+ {
329
+ header = Buffer.alloc(2);
330
+ header[0] = 0x80 | opcode; // FIN + opcode
331
+ header[1] = len;
332
+ }
333
+ else if (len < 65536)
334
+ {
335
+ header = Buffer.alloc(4);
336
+ header[0] = 0x80 | opcode;
337
+ header[1] = 126;
338
+ header.writeUInt16BE(len, 2);
339
+ }
340
+ else
341
+ {
342
+ header = Buffer.alloc(10);
343
+ header[0] = 0x80 | opcode;
344
+ header[1] = 127;
345
+ header.writeUInt32BE(0, 2);
346
+ header.writeUInt32BE(len, 6);
347
+ }
348
+ return Buffer.concat([header, payload]);
349
+ }
350
+
351
+ /** @private Parse incoming WebSocket frames. */
352
+ _onData(chunk)
353
+ {
354
+ this._buffer = Buffer.concat([this._buffer, chunk]);
355
+
356
+ while (this._buffer.length >= 2)
357
+ {
358
+ const firstByte = this._buffer[0];
359
+ const secondByte = this._buffer[1];
360
+ const opcode = firstByte & 0x0F;
361
+ const masked = (secondByte & 0x80) !== 0;
362
+ let payloadLen = secondByte & 0x7F;
363
+ let offset = 2;
364
+
365
+ if (payloadLen === 126)
366
+ {
367
+ if (this._buffer.length < 4) return;
368
+ payloadLen = this._buffer.readUInt16BE(2);
369
+ offset = 4;
370
+ }
371
+ else if (payloadLen === 127)
372
+ {
373
+ if (this._buffer.length < 10) return;
374
+ payloadLen = this._buffer.readUInt32BE(6);
375
+ offset = 10;
376
+ }
377
+
378
+ // Enforce max payload
379
+ if (payloadLen > this.maxPayload)
380
+ {
381
+ this.close(1009, 'Message too big');
382
+ this._buffer = Buffer.alloc(0);
383
+ return;
384
+ }
385
+
386
+ const maskSize = masked ? 4 : 0;
387
+ const totalLen = offset + maskSize + payloadLen;
388
+ if (this._buffer.length < totalLen) return;
389
+
390
+ let payload = this._buffer.slice(offset + maskSize, totalLen);
391
+ if (masked)
392
+ {
393
+ const mask = this._buffer.slice(offset, offset + 4);
394
+ payload = Buffer.alloc(payloadLen);
395
+ for (let i = 0; i < payloadLen; i++)
396
+ {
397
+ payload[i] = this._buffer[offset + maskSize + i] ^ mask[i & 3];
398
+ }
399
+ }
400
+
401
+ this._buffer = this._buffer.slice(totalLen);
402
+
403
+ switch (opcode)
404
+ {
405
+ case 0x01: // text
406
+ this._emit('message', payload.toString('utf8'));
407
+ break;
408
+ case 0x02: // binary
409
+ this._emit('message', payload);
410
+ break;
411
+ case 0x08: // close
412
+ {
413
+ const closeCode = payload.length >= 2 ? payload.readUInt16BE(0) : 1005;
414
+ const closeReason = payload.length > 2 ? payload.slice(2).toString('utf8') : '';
415
+ this.readyState = WS_READY_STATE.CLOSED;
416
+ this._clearPing();
417
+ try { this._socket.write(this._buildFrame(0x08, payload)); } catch (e) { }
418
+ this._socket.end();
419
+ this._emit('close', closeCode, closeReason);
420
+ return;
421
+ }
422
+ case 0x09: // ping
423
+ this._emit('ping', payload);
424
+ try { this._socket.write(this._buildFrame(0x0A, payload)); } catch (e) { }
425
+ break;
426
+ case 0x0A: // pong
427
+ this._emit('pong', payload);
428
+ break;
429
+ }
430
+ }
431
+ }
432
+ }
433
+
434
+ /** Ready-state constants exposed on the class for convenience. */
435
+ WebSocketConnection.CONNECTING = WS_READY_STATE.CONNECTING;
436
+ WebSocketConnection.OPEN = WS_READY_STATE.OPEN;
437
+ WebSocketConnection.CLOSING = WS_READY_STATE.CLOSING;
438
+ WebSocketConnection.CLOSED = WS_READY_STATE.CLOSED;
439
+
440
+ module.exports = WebSocketConnection;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @module ws/handshake
3
+ * @description WebSocket upgrade handshake logic (RFC 6455).
4
+ * Handles the HTTP → WS upgrade, verifyClient, sub-protocol
5
+ * negotiation, query parsing, and instantiation of a
6
+ * {@link WebSocketConnection}.
7
+ */
8
+ const crypto = require('crypto');
9
+ const WebSocketConnection = require('./connection');
10
+
11
+ /** RFC 6455 magic GUID used in the Sec-WebSocket-Accept hash. */
12
+ const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
13
+
14
+ /**
15
+ * Handle an HTTP upgrade request and turn it into a WebSocket connection.
16
+ *
17
+ * @param {import('http').IncomingMessage} req - The upgrade request.
18
+ * @param {import('net').Socket} socket - The underlying TCP socket.
19
+ * @param {Buffer} head - First packet of the upgraded stream.
20
+ * @param {Map<string, { handler: Function, opts: object }>} wsHandlers - Registered WS path→handler map.
21
+ */
22
+ function handleUpgrade(req, socket, head, wsHandlers)
23
+ {
24
+ // Guard against socket errors (e.g. ECONNRESET from restricted webviews)
25
+ socket.on('error', () => {});
26
+
27
+ const urlPath = req.url.split('?')[0];
28
+ const entry = wsHandlers.get(urlPath);
29
+
30
+ if (!entry)
31
+ {
32
+ socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
33
+ socket.destroy();
34
+ return;
35
+ }
36
+
37
+ const { handler, opts } = entry;
38
+
39
+ // -- Optional client verification ------------------
40
+ if (typeof opts.verifyClient === 'function')
41
+ {
42
+ try
43
+ {
44
+ if (!opts.verifyClient(req))
45
+ {
46
+ socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
47
+ socket.destroy();
48
+ return;
49
+ }
50
+ }
51
+ catch (e)
52
+ {
53
+ socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
54
+ socket.destroy();
55
+ return;
56
+ }
57
+ }
58
+
59
+ // -- Validate key ----------------------------------
60
+ const key = req.headers['sec-websocket-key'];
61
+ if (!key)
62
+ {
63
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
64
+ socket.destroy();
65
+ return;
66
+ }
67
+
68
+ // -- Negotiate sub-protocol ------------------------
69
+ const clientProtocols = req.headers['sec-websocket-protocol'];
70
+ const extensions = req.headers['sec-websocket-extensions'] || '';
71
+
72
+ // -- Perform the WebSocket handshake (RFC 6455) ----
73
+ const accept = crypto.createHash('sha1').update(key + WS_MAGIC).digest('base64');
74
+
75
+ let handshake =
76
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
77
+ 'Upgrade: websocket\r\n' +
78
+ 'Connection: Upgrade\r\n' +
79
+ 'Sec-WebSocket-Accept: ' + accept + '\r\n';
80
+
81
+ // Echo first requested protocol if any
82
+ const protocol = clientProtocols ? clientProtocols.split(',')[0].trim() : '';
83
+ if (protocol) handshake += 'Sec-WebSocket-Protocol: ' + protocol + '\r\n';
84
+
85
+ handshake += '\r\n';
86
+ socket.write(handshake);
87
+
88
+ // -- Parse query string ----------------------------
89
+ const qIdx = req.url.indexOf('?');
90
+ const query = {};
91
+ if (qIdx !== -1)
92
+ {
93
+ for (const [k, v] of new URLSearchParams(req.url.slice(qIdx + 1)))
94
+ {
95
+ query[k] = v;
96
+ }
97
+ }
98
+
99
+ // -- Create the connection wrapper -----------------
100
+ const ws = new WebSocketConnection(socket, {
101
+ maxPayload: opts.maxPayload,
102
+ pingInterval: opts.pingInterval,
103
+ protocol,
104
+ extensions,
105
+ headers: req.headers,
106
+ ip: req.socket ? req.socket.remoteAddress : null,
107
+ query,
108
+ url: req.url,
109
+ secure: !!(req.socket && req.socket.encrypted),
110
+ });
111
+
112
+ try
113
+ {
114
+ handler(ws, req);
115
+ }
116
+ catch (err)
117
+ {
118
+ ws.close(1011, 'Internal error');
119
+ }
120
+ }
121
+
122
+ module.exports = handleUpgrade;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @module ws
3
+ * @description WebSocket support for zero-http.
4
+ * Exports the connection class and upgrade handler.
5
+ */
6
+ const WebSocketConnection = require('./connection');
7
+ const handleUpgrade = require('./handshake');
8
+
9
+ module.exports = {
10
+ WebSocketConnection,
11
+ handleUpgrade,
12
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-http",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Zero-dependency, minimal Express-like HTTP server and tiny fetch replacement",
5
5
  "main": "index.js",
6
6
  "files": [
package/lib/router.js DELETED
@@ -1,87 +0,0 @@
1
- /**
2
- * @module router
3
- * @description Simple pattern-matching router with named parameters,
4
- * wildcard catch-alls, and sequential handler chains.
5
- */
6
-
7
- /**
8
- * Convert a route path pattern into a RegExp and extract named parameter keys.
9
- * Supports `:param` segments and trailing `*` wildcards.
10
- *
11
- * @param {string} path - Route pattern (e.g. '/users/:id', '/api/*').
12
- * @returns {{ regex: RegExp, keys: string[] }} Compiled regex and ordered parameter names.
13
- */
14
- function pathToRegex(path)
15
- {
16
- // Wildcard catch-all: /api/*
17
- if (path.endsWith('*'))
18
- {
19
- const prefix = path.slice(0, -1); // e.g. "/api/"
20
- const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
21
- return { regex: new RegExp('^' + escaped + '(.*)$'), keys: ['0'] };
22
- }
23
-
24
- const parts = path.split('/').filter(Boolean);
25
- const keys = [];
26
- const pattern = parts.map(p =>
27
- {
28
- if (p.startsWith(':')) { keys.push(p.slice(1)); return '([^/]+)'; }
29
- return p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
30
- }).join('/');
31
- return { regex: new RegExp('^/' + pattern + '/?$'), keys };
32
- }
33
-
34
- class Router
35
- {
36
- /** Create a new Router with an empty route table. */
37
- constructor() { this.routes = []; }
38
-
39
- /**
40
- * Register a route.
41
- *
42
- * @param {string} method - HTTP method (e.g. 'GET') or 'ALL' to match any.
43
- * @param {string} path - Route pattern.
44
- * @param {Function[]} handlers - One or more handler functions `(req, res, next) => void`.
45
- */
46
- add(method, path, handlers)
47
- {
48
- const { regex, keys } = pathToRegex(path);
49
- this.routes.push({ method: method.toUpperCase(), path, regex, keys, handlers });
50
- }
51
-
52
- /**
53
- * Match an incoming request against the route table and execute the first
54
- * matching handler chain. Sends a 404 JSON response when no route matches.
55
- *
56
- * @param {import('./request')} req - Wrapped request.
57
- * @param {import('./response')} res - Wrapped response.
58
- */
59
- handle(req, res)
60
- {
61
- const method = req.method.toUpperCase();
62
- const url = req.url.split('?')[0];
63
- for (const r of this.routes)
64
- {
65
- // ALL matches any method
66
- if (r.method !== 'ALL' && r.method !== method) continue;
67
- const m = url.match(r.regex);
68
- if (!m) continue;
69
- req.params = {};
70
- r.keys.forEach((k, i) => req.params[k] = decodeURIComponent(m[i + 1] || ''));
71
- // run handlers sequentially
72
- let idx = 0;
73
- const next = () =>
74
- {
75
- if (idx < r.handlers.length)
76
- {
77
- const h = r.handlers[idx++];
78
- return h(req, res, next);
79
- }
80
- };
81
- return next();
82
- }
83
- res.status(404).json({ error: 'Not Found' });
84
- }
85
- }
86
-
87
- module.exports = Router;
File without changes
File without changes
File without changes
File without changes