@ndiinginc/router 4.5.0 → 4.6.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
@@ -11,9 +11,9 @@ Router HTTP kecil, cepat, dan **zero-dependency** untuk Node.js. Dibangun langsu
11
11
  - Nested router dengan `use(path, router)`
12
12
  - Middleware global dan per-path, mendukung `async`
13
13
  - Error handler dengan signature `(err, req, res)`
14
- - Timeout per request (`req.timeout`) yang menghasilkan `504`
14
+ - Timeout per request yang menghasilkan `504`, dengan `req.signal` untuk pembatalan kooperatif
15
15
  - `req.path`, `req.query`, `req.params`, `req.body`
16
- - Helper `res.send()` dan `res.json()`
16
+ - Helper `res.status()`, `res.send()`, dan `res.json()`
17
17
  - Middleware bawaan: `json()` (dengan limit dan proteksi prototype pollution) dan `cors()`
18
18
  - Semua HTTP method, termasuk `QUERY`
19
19
 
@@ -43,7 +43,7 @@ app.get("/users/:id", (req, res) => {
43
43
  });
44
44
 
45
45
  app.post("/users", (req, res) => {
46
- res.json({ received: req.body });
46
+ res.status(201).json({ received: req.body });
47
47
  });
48
48
 
49
49
  app.listen(3000);
@@ -51,23 +51,19 @@ app.listen(3000);
51
51
 
52
52
  Yang diekspor adalah class `App`, dengan tiga properti tambahan:
53
53
 
54
- | Export | Keterangan |
55
- | --- | --- |
56
- | `App` | Router + HTTP server (`listen`) |
54
+ | Export | Keterangan |
55
+ | ------------ | ---------------------------------------- |
56
+ | `App` | Router + HTTP server (`listen`) |
57
57
  | `App.Router` | Router tanpa server, untuk nested router |
58
- | `App.json` | Middleware JSON body parser |
59
- | `App.cors` | Middleware CORS |
58
+ | `App.json` | Middleware JSON body parser |
59
+ | `App.cors` | Middleware CORS |
60
60
 
61
61
  ## Routing
62
62
 
63
63
  Semua method mengembalikan `this`, jadi bisa di-chain.
64
64
 
65
65
  ```js
66
- app.get("/users", handler)
67
- .post("/users", handler)
68
- .put("/users/:id", handler)
69
- .patch("/users/:id", handler)
70
- .delete("/users/:id", handler);
66
+ app.get("/users", handler).post("/users", handler).put("/users/:id", handler).patch("/users/:id", handler).delete("/users/:id", handler);
71
67
  ```
72
68
 
73
69
  Method yang tersedia: `get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `trace`, `connect`, `query`, dan `use` (semua method).
@@ -82,6 +78,8 @@ app.get("/profile", authenticate, loadUser, (req, res) => {
82
78
 
83
79
  ### Parameter dinamis
84
80
 
81
+ Nilai parameter otomatis di-decode dengan `decodeURIComponent`. Jika formatnya tidak valid (misalnya `%`), nilai mentah dipakai apa adanya.
82
+
85
83
  ```js
86
84
  app.get("/posts/:postId/comments/:commentId", (req, res) => {
87
85
  const { postId, commentId } = req.params;
@@ -118,8 +116,7 @@ app.use((req, res) => {
118
116
  // Scoped: jalan untuk /admin dan semua sub-path-nya
119
117
  app.use("/admin", async (req, res) => {
120
118
  if (req.headers.authorization !== "secret") {
121
- res.statusCode = 401;
122
- res.json({ error: "Unauthorized" });
119
+ res.status(401).json({ error: "Unauthorized" });
123
120
  }
124
121
  });
125
122
  ```
@@ -149,7 +146,7 @@ app.use("/admin", admin);
149
146
  // GET /admin/users/42 -> { id: "42" }
150
147
  ```
151
148
 
152
- > Tree sub-router digabungkan saat `use()` dipanggil. Daftarkan semua route ke sub-router **sebelum** di-mount; route yang ditambahkan setelahnya tidak ikut. Satu instance sub-router sebaiknya hanya di-mount satu kali.
149
+ > Isi sub-router disalin saat `use()` dipanggil. Daftarkan semua route ke sub-router **sebelum** di-mount; route yang ditambahkan setelahnya tidak ikut. Sub-router yang sama boleh di-mount di beberapa path.
153
150
 
154
151
  ## Error Handling
155
152
 
@@ -161,8 +158,7 @@ app.get("/boom", () => {
161
158
  });
162
159
 
163
160
  app.use((err, req, res) => {
164
- res.statusCode = err.statusCode || 500;
165
- res.json({ error: { message: err.message } });
161
+ res.status(err.statusCode || 500).json({ error: { message: err.message } });
166
162
  });
167
163
  ```
168
164
 
@@ -170,23 +166,18 @@ app.use((err, req, res) => {
170
166
 
171
167
  ### Menentukan status code
172
168
 
173
- Ada dua cara:
169
+ Status code diambil dari properti `statusCode` pada error. Tanpa properti itu, hasilnya `500`.
174
170
 
175
171
  ```js
176
- // 1. Properti statusCode pada error
177
172
  app.get("/admin", () => {
178
173
  const err = new Error("Forbidden");
179
174
  err.statusCode = 403;
180
175
  throw err;
181
176
  });
182
-
183
- // 2. Set res.statusCode sebelum throw
184
- app.get("/limit", (req, res) => {
185
- res.statusCode = 429;
186
- throw new Error("Too Many Requests");
187
- });
188
177
  ```
189
178
 
179
+ > `res.statusCode` atau `res.status()` yang diset **sebelum** error dilempar tidak dipakai oleh respons default. Gunakan `err.statusCode`, atau tangani sendiri di error handler.
180
+
190
181
  ### Respons default
191
182
 
192
183
  Jika tidak ada handler yang mengirim response, router mengirim JSON otomatis:
@@ -196,35 +187,44 @@ Jika tidak ada handler yang mengirim response, router mengirim JSON otomatis:
196
187
  ```
197
188
 
198
189
  - Tidak ada route cocok: `404 Not Found`
199
- - Error tanpa status code: `500`
200
- - Jika `res.statusCode` sudah `>= 400`, nilainya dipertahankan
190
+ - Error dengan `statusCode`: status tersebut dipakai
191
+ - Error tanpa `statusCode`: `500`
201
192
  - Jika error handler sendiri melempar error, error baru dibuat dengan `cause` menunjuk error asli
202
-
203
193
  - Field `stack` hanya disertakan saat `NODE_ENV=development`
204
194
 
205
195
  ## Timeout
206
196
 
207
- Set `req.timeout` (milidetik) untuk membatasi waktu eksekusi handler. Jika terlampaui, error `504 Gateway Timeout` dilempar dan diteruskan ke error handler.
197
+ Aktifkan lewat opsi `timeout` (milidetik) pada `App`. Jika seluruh chain handler melebihi waktu tersebut, error `504 Gateway Timeout` dilempar dan diteruskan ke error handler.
208
198
 
209
199
  ```js
210
- app.use((req) => {
211
- req.timeout = 5000;
200
+ const app = new App({ timeout: 5000 });
201
+ ```
202
+
203
+ - Tanpa opsi `timeout` (atau bernilai `0`), tidak ada batas waktu
204
+ - Handler yang sudah berjalan tidak dihentikan paksa. Tetapi setelah response dikirim, `res.json()` dan `res.send()` mengabaikan penulisan berikutnya, dan handler selanjutnya di chain tidak dipanggil
205
+
206
+ ### `req.signal`
207
+
208
+ Saat `timeout` aktif, `req.signal` adalah `AbortSignal` yang di-abort ketika timeout terjadi (reason-nya error 504). Teruskan ke operasi yang mendukungnya agar pekerjaan ikut dibatalkan:
209
+
210
+ ```js
211
+ app.get("/proxy", async (req, res) => {
212
+ const upstream = await fetch("https://example.com/api", { signal: req.signal });
213
+ res.json(await upstream.json());
212
214
  });
213
215
  ```
214
216
 
215
- - Nilai default `0` (tanpa timeout)
216
- - Harus diset **secara sinkron di middleware pertama** pada chain, karena dibaca sesaat setelah chain mulai berjalan
217
- - Timeout tidak membatalkan handler yang sedang berjalan; hanya menghentikan penantian dan mengirim error
217
+ `req.signal` bernilai `undefined` jika `timeout` tidak diaktifkan.
218
218
 
219
219
  ## Request
220
220
 
221
- | Properti | Keterangan |
222
- | --- | --- |
223
- | `req.path` | Pathname tanpa query string |
224
- | `req.query` | Object query string, di-parse secara lazy. Key yang berulang menjadi array |
225
- | `req.params` | Parameter dari route dinamis (sudah di-decode dengan `decodeURIComponent`) |
226
- | `req.body` | Hasil parse body (butuh middleware `json()`) |
227
- | `req.timeout` | Batas waktu handler dalam ms (default `0`) |
221
+ | Properti | Keterangan |
222
+ | ------------ | -------------------------------------------------------------------------- |
223
+ | `req.path` | Pathname tanpa query string |
224
+ | `req.query` | Object query string, di-parse secara lazy. Key yang berulang menjadi array |
225
+ | `req.params` | Parameter dari route dinamis (sudah di-decode) |
226
+ | `req.body` | Hasil parse body (butuh middleware `json()`) |
227
+ | `req.signal` | `AbortSignal` yang di-abort saat timeout (hanya jika `timeout` aktif) |
228
228
 
229
229
  ```js
230
230
  // GET /search?tag=a&tag=b&page=2
@@ -233,14 +233,21 @@ app.get("/search", (req, res) => {
233
233
  });
234
234
  ```
235
235
 
236
+ `req.query` dan `req.params` memakai object tanpa prototype, jadi gunakan `Object.hasOwn(req.query, "key")` atau `"key" in req.query`, bukan `req.query.hasOwnProperty(...)`.
237
+
236
238
  `Request` adalah turunan `http.IncomingMessage`, jadi `req.headers`, `req.method`, dan lainnya tetap tersedia.
237
239
 
238
240
  ## Response
239
241
 
240
- | Method | Keterangan |
241
- | --- | --- |
242
- | `res.send(chunk)` | Kirim response dan akhiri |
243
- | `res.json(value)` | Set `Content-Type: application/json`, serialize, lalu kirim |
242
+ | Method | Keterangan |
243
+ | ------------------ | ----------------------------------------------------------- |
244
+ | `res.status(code)` | Set status code, mengembalikan `res` sehingga bisa di-chain |
245
+ | `res.send(chunk)` | Kirim response dan akhiri |
246
+ | `res.json(value)` | Set `Content-Type: application/json`, serialize, lalu kirim |
247
+
248
+ ```js
249
+ res.status(201).json({ created: true });
250
+ ```
244
251
 
245
252
  `Response` adalah turunan `http.ServerResponse`, jadi `res.statusCode`, `res.setHeader()`, dan lainnya tetap tersedia.
246
253
 
@@ -252,8 +259,8 @@ app.get("/search", (req, res) => {
252
259
  app.use(App.json({ limit: 100 * 1024 })); // default 100 KB
253
260
  ```
254
261
 
255
- | Opsi | Default | Keterangan |
256
- | --- | --- | --- |
262
+ | Opsi | Default | Keterangan |
263
+ | ------- | -------- | ------------------------------- |
257
264
  | `limit` | `102400` | Ukuran body maksimum dalam byte |
258
265
 
259
266
  Perilaku:
@@ -278,36 +285,42 @@ app.use(
278
285
  );
279
286
  ```
280
287
 
281
- | Opsi | Default | Keterangan |
282
- | --- | --- | --- |
283
- | `origin` | `"*"` | `"*"`, string origin, atau `null` |
284
- | `credentials` | `false` | Mengirim `Access-Control-Allow-Credentials: true` |
285
- | `headers` | - | `"*"` atau array nama header |
286
- | `methods` | - | `"*"` atau array nama method |
287
- | `maxAge` | `0` | Lama cache preflight (detik). `0` berarti header tidak dikirim |
288
+ | Opsi | Default | Keterangan |
289
+ | ------------- | ------- | -------------------------------------------------------------- |
290
+ | `origin` | `"*"` | `"*"`, string origin, atau `null` |
291
+ | `credentials` | `false` | Mengirim `Access-Control-Allow-Credentials: true` |
292
+ | `headers` | - | `"*"` atau array nama header |
293
+ | `methods` | - | `"*"` atau array nama method |
294
+ | `maxAge` | `0` | Lama cache preflight (detik). `0` berarti header tidak dikirim |
288
295
 
289
296
  Perilaku:
290
297
 
291
298
  - Request `OPTIONS` dijawab langsung dengan `204` dan chain berhenti
299
+ - `Access-Control-Allow-Headers` dan `Access-Control-Allow-Methods` hanya dikirim pada preflight, jika nilai yang diminta ada di daftar
292
300
  - Dengan `origin: "*"` dan `credentials: true`, origin dari request dipantulkan dan header `Vary: Origin` ditambahkan
293
301
  - Nilai `"*"` untuk `headers` dan `methods` diabaikan saat `credentials: true`, sesuai spesifikasi CORS
294
302
  - Pada request non-preflight, `headers` (jika array) dikirim sebagai `Access-Control-Expose-Headers`
295
303
 
296
- ## Opsi Router
304
+ > `origin: "*"` bersama `credentials: true` memantulkan **semua** origin dengan credentials. Pakai hanya untuk development; di production sebutkan origin secara eksplisit.
305
+
306
+ ## Opsi
297
307
 
298
308
  ```js
299
- const app = new App({ size: 1000 });
309
+ const app = new App({ size: 1000, timeout: 5000 });
300
310
  ```
301
311
 
302
- | Opsi | Default | Keterangan |
303
- | --- | --- | --- |
304
- | `size` | `1000` | Jumlah maksimum entri LRU cache untuk hasil lookup `METHOD path` |
312
+ | Opsi | Default | Keterangan |
313
+ | --------- | ------- | ----------------------------------------------------------------------------- |
314
+ | `size` | `1000` | Jumlah maksimum entri LRU cache untuk hasil lookup `METHOD path` |
315
+ | `timeout` | - | Batas waktu handler dalam ms (khusus `App`). Lihat bagian [Timeout](#timeout) |
316
+
317
+ `Router` hanya menerima `size`.
305
318
 
306
319
  ## API
307
320
 
308
321
  ### `new App([options])` / `new Router([options])`
309
322
 
310
- Membuat instance baru. `App` adalah `Router` ditambah `listen()`.
323
+ Membuat instance baru. `App` adalah `Router` ditambah `listen()` dan dukungan `timeout`.
311
324
 
312
325
  ### `router.use([path], ...handlers | router)`
313
326
 
@@ -338,11 +351,11 @@ console.log(server.address().port);
338
351
 
339
352
  Hello world dengan satu route `GET /`, diuji memakai `autocannon -c 100 -d 40 -p 10` di mesin lokal.
340
353
 
341
- | | Req/sec (avg) | Latency (avg) |
342
- | --- | --- | --- |
343
- | Fastify | 31.475 | 31,27 ms |
344
- | **@ndiinginc/router** | 29.735 | 33,12 ms |
345
- | Express | 20.211 | 48,93 ms |
354
+ | | Req/sec (avg) | Latency (avg) |
355
+ | --------------------- | ------------- | ------------- |
356
+ | Fastify | 31.475 | 31,27 ms |
357
+ | **@ndiinginc/router** | 29.735 | 33,12 ms |
358
+ | Express | 20.211 | 48,93 ms |
346
359
 
347
360
  Hasil bervariasi tergantung mesin, versi Node.js, dan jumlah route. Benchmark ini hanya menggambarkan overhead dasar, bukan performa aplikasi nyata.
348
361
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ndiinginc/router",
3
- "version": "4.5.0",
3
+ "version": "4.6.0",
4
4
  "description": "",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
package/src/app.js CHANGED
@@ -11,13 +11,26 @@ const SERVER_OPTIONS = {
11
11
  };
12
12
 
13
13
  class App extends Router {
14
- async withTimeout(handlerPromise, delay) {
15
- let timeout;
14
+ async withTimeout(req, handlerPromise) {
15
+ const controller = new AbortController();
16
+ const { signal } = controller;
17
+
18
+ req.signal = signal;
19
+
20
+ const timeout = setTimeout(() => {
21
+ controller.abort(ErrorResponse.status(504));
22
+ }, this.timeout);
23
+
24
+ let handleAbort;
16
25
  const timeoutPromise = new Promise((_, reject) => {
17
- timeout = setTimeout(() => reject(ErrorResponse.status(504)), delay);
26
+ handleAbort = () => reject(signal.reason);
27
+ signal.addEventListener("abort", handleAbort, { once: true });
18
28
  });
19
29
 
20
- await Promise.race([handlerPromise, timeoutPromise]).finally(() => clearTimeout(timeout));
30
+ await Promise.race([handlerPromise(), timeoutPromise]).finally(() => {
31
+ signal.removeEventListener("abort", handleAbort);
32
+ clearTimeout(timeout);
33
+ });
21
34
  }
22
35
 
23
36
  /**
@@ -26,22 +39,19 @@ class App extends Router {
26
39
  */
27
40
  async handle(req, res) {
28
41
  const { params, handlers, errorHandlers } = this.find(req.method, req.path);
29
- req.params = params;
30
-
42
+ req.params = Object.assign(Object.create(null), params);
31
43
  let err;
32
44
  try {
33
- const handlerPromise = (async () => {
45
+ const handlerPromise = async () => {
34
46
  for (const handler of handlers) {
35
47
  if (res.writableEnded) break;
36
48
  await handler(req, res);
37
49
  }
38
- })();
39
-
40
- if (req.timeout) await this.withTimeout(handlerPromise, req.timeout);
41
- else await handlerPromise;
50
+ };
51
+ if (this.timeout) await this.withTimeout(req, handlerPromise);
52
+ else await handlerPromise();
42
53
  } catch (error) {
43
54
  err = error;
44
-
45
55
  try {
46
56
  for (const handler of errorHandlers) {
47
57
  if (res.writableEnded) break;
@@ -51,14 +61,9 @@ class App extends Router {
51
61
  err = new Error(error.message, { cause: err });
52
62
  }
53
63
  }
54
-
55
64
  if (!res.writableEnded) {
56
65
  if (!err) err = ErrorResponse.status(404);
57
-
58
- if (!res.statusCode || res.statusCode < 400) {
59
- res.statusCode = err.statusCode || 500;
60
- }
61
-
66
+ res.statusCode = err.statusCode || 500;
62
67
  const error = { message: err.message };
63
68
  if (process.env.NODE_ENV === "development") {
64
69
  error.stack = err.stack;
package/src/error.js CHANGED
@@ -5,21 +5,21 @@ const http = require("node:http");
5
5
  class ErrorResponse extends Error {
6
6
  constructor({ statusCode, message } = {}, options) {
7
7
  super(message, options);
8
-
9
8
  this.name = this.constructor.name;
10
-
11
9
  if (Error.captureStackTrace) {
12
10
  Error.captureStackTrace(this, this.constructor);
13
11
  }
14
-
15
12
  this.statusCode = statusCode;
16
13
  }
17
14
 
18
- static status(statusCode) {
19
- return new ErrorResponse({
20
- statusCode,
21
- message: http.STATUS_CODES[statusCode],
22
- });
15
+ static status(statusCode, options) {
16
+ return new ErrorResponse(
17
+ {
18
+ statusCode,
19
+ message: http.STATUS_CODES[statusCode],
20
+ },
21
+ options,
22
+ );
23
23
  }
24
24
  }
25
25
 
package/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- require('./polyfill.js')
1
+ require("./polyfill.js");
2
2
  const { json } = require("./middleware/json.js");
3
3
  const { cors } = require("./middleware/cors.js");
4
4
  const { App } = require("./app.js");
@@ -1,10 +1,10 @@
1
1
  function pattern(string) {
2
2
  return new RegExp(
3
- `(${String(string)
3
+ `^(${String(string)
4
4
  .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
5
5
  .replace(/ /g, "")
6
6
  .replace(/,/g, "|")
7
- .replace(/\\\*/g, ".*")})`,
7
+ .replace(/\\\*/g, ".*")})$`,
8
8
  "i",
9
9
  );
10
10
  }
@@ -42,7 +42,6 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
42
42
  function setAllowHeaders(req, res) {
43
43
  const reqHeaders = req.headers["access-control-request-headers"];
44
44
  if (!reqHeaders) return;
45
-
46
45
  let value;
47
46
  if (headers === "*" && !credentials) {
48
47
  value = "*";
@@ -59,7 +58,6 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
59
58
  function setAllowMethods(req, res) {
60
59
  const reqMethod = req.headers["access-control-request-method"];
61
60
  if (!reqMethod) return;
62
-
63
61
  let value;
64
62
  if (methods === "*" && !credentials) {
65
63
  value = "*";
@@ -73,25 +71,21 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
73
71
  * @param {import('http').IncomingMessage} req
74
72
  * @param {import('http').ServerResponse} res
75
73
  */
76
- function setAllowOrigin(req, res) {
74
+ function setAllowOrigin(req, res, preflight = true) {
77
75
  const reqOrigin = req.headers["origin"];
78
76
 
79
77
  let value;
80
78
  let explicit;
81
- if (origin === "*") {
82
- if (!credentials) value = "*";
83
- else {
84
- value = reqOrigin;
85
- explicit = true;
86
- }
79
+ if (origin === "*" && !credentials) {
80
+ value = "*";
87
81
  } else if (origin === null) {
88
82
  value = "null";
89
- } else if (typeof origin === "string" && ORIGIN_PATTERN.test(reqOrigin)) {
90
- value = origin;
83
+ } else if (ORIGIN_PATTERN.test(reqOrigin)) {
84
+ value = reqOrigin;
91
85
  explicit = true;
92
86
  }
93
87
  if (value) res.setHeader("Access-Control-Allow-Origin", value);
94
- if (explicit) res.appendHeader("Vary", "Origin");
88
+ if (!preflight && explicit) res.appendHeader("Vary", "Origin");
95
89
  }
96
90
 
97
91
  /**
@@ -122,16 +116,16 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
122
116
  */
123
117
  return (req, res) => {
124
118
  setAllowCredentials(res);
125
- setAllowOrigin(req, res);
126
119
  if (req.method === "OPTIONS") {
120
+ setAllowOrigin(req, res);
127
121
  setAllowHeaders(req, res);
128
122
  setAllowMethods(req, res);
129
123
  setMaxAge(res);
130
-
131
124
  res.statusCode = 204;
132
125
  res.setHeader("Content-Length", "0");
133
126
  res.end();
134
127
  } else {
128
+ setAllowOrigin(req, res, false);
135
129
  setExposeHeaders(res);
136
130
  }
137
131
  };
@@ -1,6 +1,6 @@
1
1
  const http = require("node:http");
2
+ const { ErrorResponse } = require("../error.js");
2
3
 
3
- // middleware
4
4
  function json({ limit = 100 * 1024 } = {}) {
5
5
  const reviver = (key, value) => {
6
6
  if (key === "__proto__" || key === "constructor" || key === "prototype") {
@@ -8,46 +8,35 @@ function json({ limit = 100 * 1024 } = {}) {
8
8
  }
9
9
  return value;
10
10
  };
11
-
12
11
  return async (req, res) => {
13
12
  if (!["POST", "PUT", "PATCH", "QUERY"].includes(req.method)) {
14
13
  return;
15
14
  }
16
-
17
15
  const contentType = req.headers["content-type"];
18
16
  if (!contentType || !contentType.includes("application/json")) {
19
17
  return;
20
18
  }
21
-
22
19
  const contentLength = parseInt(req.headers["content-length"], 10);
23
20
  if (contentLength && contentLength > limit) {
24
- res.statusCode = 413;
25
- throw new Error(http.STATUS_CODES[res.statusCode]);
21
+ throw ErrorResponse.status(413);
26
22
  }
27
-
28
23
  let chunkLength = 0;
29
-
30
24
  const chunks = [];
31
-
32
25
  for await (const chunk of req) {
33
26
  chunkLength += chunk.length;
34
27
  if (chunkLength > limit) {
35
- res.statusCode = 413;
36
- throw new Error(http.STATUS_CODES[res.statusCode]);
28
+ throw ErrorResponse.status(413);
37
29
  }
38
30
  chunks.push(chunk);
39
31
  }
40
-
41
32
  if (chunks.length === 0) {
42
33
  req.body = null;
43
34
  return;
44
35
  }
45
-
46
36
  try {
47
37
  req.body = JSON.parse(Buffer.concat(chunks), reviver);
48
38
  } catch (error) {
49
- res.statusCode = 400;
50
- throw new Error("Invalid JSON body");
39
+ throw new ErrorResponse({ statusCode: 400, message: "Invalid JSON body" }, { cause: error });
51
40
  }
52
41
  };
53
42
  }
package/src/node.js CHANGED
@@ -1,6 +1,5 @@
1
1
  const { Pattern } = require("./pattern.js");
2
2
 
3
-
4
3
  class Node {
5
4
  constructor(segment = "") {
6
5
  this.hasRegExpChildren = false;
@@ -11,58 +10,37 @@ class Node {
11
10
  }
12
11
 
13
12
  add(segment) {
14
- const node = new Node(segment);
15
- if (node.pattern) this.hasRegExpChildren = true; // node.hasRegExp;
16
- return this.children.getOrInsert(segment,node);
13
+ return this.children.getOrInsertComputed(segment, () => {
14
+ const node = new Node(segment);
15
+ if (node.pattern) this.hasRegExpChildren = true;
16
+ return node;
17
+ });
17
18
  }
18
19
 
19
- /**
20
- * @param {Node} node
21
- */
22
20
  merge(node, offset) {
23
- for (const [method, handlers] of node.handlers) {
24
- for (const handler of handlers) {
25
- handler.seq += offset;
26
- this.handlers.getOrInsert(method,[]).push(handler);
21
+ for (const [method, entries] of node.handlers) {
22
+ const target = this.handlers.getOrInsert(method, []);
23
+ for (const entry of entries) {
24
+ target.push({ ...entry, seq: entry.seq + offset });
27
25
  }
28
26
  }
29
-
30
27
  for (const [segment, child] of node.children) {
31
- if (child.pattern) this.hasRegExpChildren = true; // child.hasRegExp;
32
- if (!this.children.has(segment)) {
33
- child.setSeqOffset(offset);
34
- this.children.set(segment, child);
35
- } else {
36
- this.children.get(segment).merge(child, offset);
37
- }
38
- }
39
- }
40
-
41
- setSeqOffset(offset) {
42
- for (const [, handlers] of this.handlers) {
43
- for (const handler of handlers) {
44
- handler.seq += offset;
45
- }
46
- }
47
-
48
- for (const [, child] of this.children) {
49
- child.setSeqOffset(offset);
28
+ const target = this.children.getOrInsertComputed(segment, () => new Node(segment));
29
+ if (target.pattern) this.hasRegExpChildren = true;
30
+ target.merge(child, offset);
50
31
  }
51
32
  }
52
33
 
53
34
  getMaxSeq() {
54
35
  let max = -1;
55
-
56
36
  for (const [, handlers] of this.handlers) {
57
- for (const handler of handlers) {
58
- max = Math.max(max, handler.seq);
37
+ for (const entry of handlers) {
38
+ max = Math.max(max, entry.seq);
59
39
  }
60
40
  }
61
-
62
41
  for (const [, child] of this.children) {
63
42
  max = Math.max(max, child.getMaxSeq());
64
43
  }
65
-
66
44
  return max;
67
45
  }
68
46
  }
package/src/polyfill.js CHANGED
@@ -1,21 +1,21 @@
1
- // Polyfill for Map.prototype.getOrInsert
2
- if (!Map.prototype.getOrInsert) {
3
- Map.prototype.getOrInsert = function (key, defaultValue) {
4
- if (!this.has(key)) {
5
- this.set(key, defaultValue);
6
- }
7
- return this.get(key);
8
- };
9
- }
10
- // Polyfill for Map.prototype.getOrInsertComputed
11
- if (!Map.prototype.getOrInsertComputed) {
12
- Map.prototype.getOrInsertComputed = function (key, callbackFunction) {
13
- if (typeof callbackFunction !== 'function') {
14
- throw new TypeError('The callback argument must be a function');
15
- }
16
- if (!this.has(key)) {
17
- this.set(key, callbackFunction(key));
18
- }
19
- return this.get(key);
20
- };
21
- }
1
+ // Polyfill for Map.prototype.getOrInsert
2
+ if (!Map.prototype.getOrInsert) {
3
+ Map.prototype.getOrInsert = function (key, defaultValue) {
4
+ if (!this.has(key)) {
5
+ this.set(key, defaultValue);
6
+ }
7
+ return this.get(key);
8
+ };
9
+ }
10
+ // Polyfill for Map.prototype.getOrInsertComputed
11
+ if (!Map.prototype.getOrInsertComputed) {
12
+ Map.prototype.getOrInsertComputed = function (key, callback) {
13
+ if (typeof callback !== "function") {
14
+ throw new TypeError("The callback argument must be a function");
15
+ }
16
+ if (!this.has(key)) {
17
+ this.set(key, callback(key));
18
+ }
19
+ return this.get(key);
20
+ };
21
+ }
package/src/request.js CHANGED
@@ -6,16 +6,13 @@ class Request extends http.IncomingMessage {
6
6
  _parsed;
7
7
  _query;
8
8
  params = {};
9
- timeout = 0;
10
-
9
+ signal;
11
10
  get path() {
12
11
  if (this._path) return this._path;
13
-
14
- if (this.url.indexOf("?") === -1) {
12
+ if (!/[?#]/.test(this.url)) {
15
13
  this._path = this.url;
16
14
  return this._path;
17
15
  }
18
-
19
16
  this._parsed = parseUrl(this.url);
20
17
  this._path = this._parsed.path;
21
18
  return this._path;
@@ -23,12 +20,10 @@ class Request extends http.IncomingMessage {
23
20
 
24
21
  get query() {
25
22
  if (this._query) return this._query;
26
-
27
23
  if (!this._parsed) {
28
24
  this._query = {};
29
25
  return this._query;
30
26
  }
31
-
32
27
  this._query = parseQueryString(this._parsed.queryString);
33
28
  return this._query;
34
29
  }
package/src/response.js CHANGED
@@ -1,11 +1,20 @@
1
1
  const http = require("node:http");
2
2
 
3
3
  class Response extends http.ServerResponse {
4
+ status(statusCode) {
5
+ this.statusCode = statusCode;
6
+ return this;
7
+ }
4
8
  send(chunk) {
9
+ if (this.writableEnded) return;
5
10
  this.end(chunk);
6
11
  }
7
-
8
12
  json(value) {
13
+ if (this.writableEnded) return;
14
+ if (this.headersSent) {
15
+ this.end();
16
+ return;
17
+ }
9
18
  this.setHeader("Content-Type", "application/json");
10
19
  this.end(JSON.stringify(value));
11
20
  }
package/src/router.js CHANGED
@@ -3,8 +3,8 @@ const { Cache } = require("./cache.js");
3
3
 
4
4
  class Router {
5
5
  constructor(options = {}) {
6
+ this.timeout = options.timeout ?? 0;
6
7
  this.cacheSize = options.size ?? 1000;
7
-
8
8
  this.root = new Node();
9
9
  this.seq = 0;
10
10
  this.cache = new Cache(this.cacheSize);
@@ -19,9 +19,7 @@ class Router {
19
19
  handlers = [path, ...handlers];
20
20
  path = null;
21
21
  }
22
-
23
22
  let node = this.root;
24
-
25
23
  if (path !== null) {
26
24
  for (const segment of this.split(path)) {
27
25
  node = node.add(segment);
@@ -31,10 +29,13 @@ class Router {
31
29
  for (const handler of handlers) {
32
30
  if (handler.root) {
33
31
  node.merge(handler.root, this.seq);
34
- this.seq = node.getMaxSeq() + 1;
32
+ this.seq += handler.root.getMaxSeq() + 1;
35
33
  } else {
36
- handler.seq = this.seq++;
37
- node.handlers.getOrInsert(method,[]).push(handler);
34
+ node.handlers.getOrInsert(method, []).push({
35
+ handler,
36
+ seq: this.seq++,
37
+ isErrorHandler: handler.length === 3,
38
+ });
38
39
  }
39
40
  }
40
41
  }
@@ -94,6 +95,27 @@ class Router {
94
95
  return this;
95
96
  }
96
97
 
98
+ matchRegExp(node, input, params) {
99
+ let matched;
100
+ for (const [, child] of node.children) {
101
+ if (!child.pattern) continue;
102
+ const match = child.pattern.exec(input);
103
+ if (match) {
104
+ matched = child;
105
+ for (const name in match.groups) {
106
+ const raw = match.groups[name];
107
+ try {
108
+ params[name] = decodeURIComponent(raw);
109
+ } catch {
110
+ params[name] = raw;
111
+ }
112
+ }
113
+ break;
114
+ }
115
+ }
116
+ return matched;
117
+ }
118
+
97
119
  /**
98
120
  * @param {'GET'|'QUERY'|'HEAD'|'OPTIONS'|'TRACE'|'PUT'|'DELETE'|'POST'|'PATCH'|'CONNECT'} method
99
121
  * @param {String} path
@@ -101,73 +123,47 @@ class Router {
101
123
  */
102
124
  find(method, path) {
103
125
  const key = `${method} ${path}`;
104
-
105
126
  const cached = this.cache.get(key);
106
127
  if (cached) return cached;
107
-
108
128
  let node = this.root;
109
-
110
129
  let params = Object.create(null);
111
130
  const handlers = {};
112
131
  const errorHandlers = {};
113
-
114
132
  for (const input of this.split(path)) {
115
133
  let matched = node.children.get(input);
116
-
117
134
  if (!matched && node.hasRegExpChildren) {
118
- for (const [, child] of node.children) {
119
- if (!child.pattern) continue;
120
- const match = child.pattern.exec(input);
121
- if (match) {
122
- matched = child;
123
- for (const name in match.groups) {
124
- const raw = match.groups[name];
125
- try {
126
- params[name] = decodeURIComponent(raw);
127
- } catch {
128
- params[name] = raw;
129
- }
130
- }
131
- break;
132
- }
133
- }
135
+ matched = this.matchRegExp(node, input, params);
134
136
  }
135
-
136
137
  if (!matched) {
137
138
  node = null;
138
139
  params = Object.create(null);
139
140
  break;
140
141
  }
141
-
142
142
  node = matched;
143
-
144
- if(node.handlers.has("ALL"))
145
- for (const handler of node.handlers.get("ALL")) {
146
- if (handler.length === 3) errorHandlers[handler.seq] = handler;
147
- else handlers[handler.seq] = handler;
143
+ if (node.handlers.has("ALL")) {
144
+ for (const entry of node.handlers.get("ALL")) {
145
+ if (entry.isErrorHandler) errorHandlers[entry.seq] = entry.handler;
146
+ else handlers[entry.seq] = entry.handler;
147
+ }
148
148
  }
149
149
  }
150
-
151
- if(this.root.handlers.has("ALL"))
152
- for (const handler of this.root.handlers.get("ALL")) {
153
- if (handler.length === 3) errorHandlers[handler.seq] = handler;
154
- else handlers[handler.seq] = handler;
150
+ if (this.root.handlers.has("ALL")) {
151
+ for (const entry of this.root.handlers.get("ALL")) {
152
+ if (entry.isErrorHandler) errorHandlers[entry.seq] = entry.handler;
153
+ else handlers[entry.seq] = entry.handler;
154
+ }
155
155
  }
156
-
157
- if (node&&node.handlers.has(method)) {
158
- for (const handler of node.handlers.get(method)) {
159
- handlers[handler.seq] = handler;
156
+ if (node && node.handlers.has(method)) {
157
+ for (const entry of node.handlers.get(method)) {
158
+ handlers[entry.seq] = entry.handler;
160
159
  }
161
160
  }
162
-
163
161
  const result = {
164
162
  params,
165
163
  handlers: Object.values(handlers),
166
164
  errorHandlers: Object.values(errorHandlers),
167
165
  };
168
-
169
166
  this.cache.set(key, result);
170
-
171
167
  return result;
172
168
  }
173
169
  }
package/src/util.js CHANGED
@@ -3,37 +3,32 @@ const QUERY_STRING_REGEX = /([^=&]*)?=([^&]*)?/g;
3
3
 
4
4
  function parseQueryString(queryString) {
5
5
  const result = Object.create(null);
6
-
7
6
  let match;
8
7
  while ((match = QUERY_STRING_REGEX.exec(queryString)) !== null) {
9
8
  const [, rawName = "", rawValue = ""] = match;
10
-
11
- let name
9
+ let name;
12
10
  try {
13
- name = decodeURIComponent(rawName)
11
+ name = decodeURIComponent(rawName);
14
12
  } catch (error) {
15
- name=rawName
13
+ name = rawName;
16
14
  }
17
-
18
- let value
15
+ let value;
19
16
  try {
20
- value = decodeURIComponent(rawValue)
17
+ value = decodeURIComponent(rawValue);
21
18
  } catch (error) {
22
- value=rawValue
19
+ value = rawValue;
23
20
  }
24
-
25
- if (result[name]) {
21
+ if (Object.hasOwn(result, name)) {
26
22
  if (Array.isArray(result[name])) result[name].push(value);
27
23
  else result[name] = [result[name], value];
28
24
  } else result[name] = value;
29
25
  }
30
-
31
26
  return result;
32
27
  }
33
28
  exports.parseQueryString = parseQueryString;
34
29
 
35
30
  function parseUrl(url) {
36
- const [, path = "/", , queryString = ""] = URL_REGEX.exec(url);
37
- return { path, queryString };
31
+ const [, rawPath = "", , queryString = ""] = URL_REGEX.exec(url);
32
+ return { path: rawPath || "/", queryString };
38
33
  }
39
34
  exports.parseUrl = parseUrl;