@ndiinginc/router 4.5.0 → 4.6.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 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);
@@ -82,6 +82,8 @@ app.get("/profile", authenticate, loadUser, (req, res) => {
82
82
 
83
83
  ### Parameter dinamis
84
84
 
85
+ Nilai parameter otomatis di-decode dengan `decodeURIComponent`. Jika formatnya tidak valid (misalnya `%`), nilai mentah dipakai apa adanya.
86
+
85
87
  ```js
86
88
  app.get("/posts/:postId/comments/:commentId", (req, res) => {
87
89
  const { postId, commentId } = req.params;
@@ -118,8 +120,7 @@ app.use((req, res) => {
118
120
  // Scoped: jalan untuk /admin dan semua sub-path-nya
119
121
  app.use("/admin", async (req, res) => {
120
122
  if (req.headers.authorization !== "secret") {
121
- res.statusCode = 401;
122
- res.json({ error: "Unauthorized" });
123
+ res.status(401).json({ error: "Unauthorized" });
123
124
  }
124
125
  });
125
126
  ```
@@ -149,7 +150,7 @@ app.use("/admin", admin);
149
150
  // GET /admin/users/42 -> { id: "42" }
150
151
  ```
151
152
 
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.
153
+ > 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
154
 
154
155
  ## Error Handling
155
156
 
@@ -161,8 +162,7 @@ app.get("/boom", () => {
161
162
  });
162
163
 
163
164
  app.use((err, req, res) => {
164
- res.statusCode = err.statusCode || 500;
165
- res.json({ error: { message: err.message } });
165
+ res.status(err.statusCode || 500).json({ error: { message: err.message } });
166
166
  });
167
167
  ```
168
168
 
@@ -170,23 +170,18 @@ app.use((err, req, res) => {
170
170
 
171
171
  ### Menentukan status code
172
172
 
173
- Ada dua cara:
173
+ Status code diambil dari properti `statusCode` pada error. Tanpa properti itu, hasilnya `500`.
174
174
 
175
175
  ```js
176
- // 1. Properti statusCode pada error
177
176
  app.get("/admin", () => {
178
177
  const err = new Error("Forbidden");
179
178
  err.statusCode = 403;
180
179
  throw err;
181
180
  });
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
181
  ```
189
182
 
183
+ > `res.statusCode` atau `res.status()` yang diset **sebelum** error dilempar tidak dipakai oleh respons default. Gunakan `err.statusCode`, atau tangani sendiri di error handler.
184
+
190
185
  ### Respons default
191
186
 
192
187
  Jika tidak ada handler yang mengirim response, router mengirim JSON otomatis:
@@ -196,35 +191,44 @@ Jika tidak ada handler yang mengirim response, router mengirim JSON otomatis:
196
191
  ```
197
192
 
198
193
  - Tidak ada route cocok: `404 Not Found`
199
- - Error tanpa status code: `500`
200
- - Jika `res.statusCode` sudah `>= 400`, nilainya dipertahankan
194
+ - Error dengan `statusCode`: status tersebut dipakai
195
+ - Error tanpa `statusCode`: `500`
201
196
  - Jika error handler sendiri melempar error, error baru dibuat dengan `cause` menunjuk error asli
202
-
203
197
  - Field `stack` hanya disertakan saat `NODE_ENV=development`
204
198
 
205
199
  ## Timeout
206
200
 
207
- Set `req.timeout` (milidetik) untuk membatasi waktu eksekusi handler. Jika terlampaui, error `504 Gateway Timeout` dilempar dan diteruskan ke error handler.
201
+ 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
202
 
209
203
  ```js
210
- app.use((req) => {
211
- req.timeout = 5000;
204
+ const app = new App({ timeout: 5000 });
205
+ ```
206
+
207
+ - Tanpa opsi `timeout` (atau bernilai `0`), tidak ada batas waktu
208
+ - 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
209
+
210
+ ### `req.signal`
211
+
212
+ Saat `timeout` aktif, `req.signal` adalah `AbortSignal` yang di-abort ketika timeout terjadi (reason-nya error 504) atau ketika salah satu handler melempar error (reason-nya error tersebut). Teruskan ke operasi yang mendukungnya agar pekerjaan ikut dibatalkan:
213
+
214
+ ```js
215
+ app.get("/proxy", async (req, res) => {
216
+ const upstream = await fetch("https://example.com/api", { signal: req.signal });
217
+ res.json(await upstream.json());
212
218
  });
213
219
  ```
214
220
 
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
221
+ `req.signal` bernilai `undefined` jika `timeout` tidak diaktifkan.
218
222
 
219
223
  ## Request
220
224
 
221
225
  | Properti | Keterangan |
222
226
  | --- | --- |
223
227
  | `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`) |
228
+ | `req.query` | Object query string, di-parse secara lazy. Key yang berulang menjadi array, dan `+` di-decode menjadi spasi |
229
+ | `req.params` | Parameter dari route dinamis (sudah di-decode) |
226
230
  | `req.body` | Hasil parse body (butuh middleware `json()`) |
227
- | `req.timeout` | Batas waktu handler dalam ms (default `0`) |
231
+ | `req.signal` | `AbortSignal` yang di-abort saat timeout atau error handler (hanya jika `timeout` aktif) |
228
232
 
229
233
  ```js
230
234
  // GET /search?tag=a&tag=b&page=2
@@ -233,15 +237,22 @@ app.get("/search", (req, res) => {
233
237
  });
234
238
  ```
235
239
 
240
+ `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(...)`.
241
+
236
242
  `Request` adalah turunan `http.IncomingMessage`, jadi `req.headers`, `req.method`, dan lainnya tetap tersedia.
237
243
 
238
244
  ## Response
239
245
 
240
246
  | Method | Keterangan |
241
247
  | --- | --- |
248
+ | `res.status(code)` | Set status code, mengembalikan `res` sehingga bisa di-chain |
242
249
  | `res.send(chunk)` | Kirim response dan akhiri |
243
250
  | `res.json(value)` | Set `Content-Type: application/json`, serialize, lalu kirim |
244
251
 
252
+ ```js
253
+ res.status(201).json({ created: true });
254
+ ```
255
+
245
256
  `Response` adalah turunan `http.ServerResponse`, jadi `res.statusCode`, `res.setHeader()`, dan lainnya tetap tersedia.
246
257
 
247
258
  ## Middleware Bawaan
@@ -280,7 +291,7 @@ app.use(
280
291
 
281
292
  | Opsi | Default | Keterangan |
282
293
  | --- | --- | --- |
283
- | `origin` | `"*"` | `"*"`, string origin, atau `null` |
294
+ | `origin` | `"*"` | `"*"`, satu origin, daftar dipisah koma, pola dengan `*` (misalnya `https://*.example.com`), atau `null` |
284
295
  | `credentials` | `false` | Mengirim `Access-Control-Allow-Credentials: true` |
285
296
  | `headers` | - | `"*"` atau array nama header |
286
297
  | `methods` | - | `"*"` atau array nama method |
@@ -289,25 +300,31 @@ app.use(
289
300
  Perilaku:
290
301
 
291
302
  - Request `OPTIONS` dijawab langsung dengan `204` dan chain berhenti
292
- - Dengan `origin: "*"` dan `credentials: true`, origin dari request dipantulkan dan header `Vary: Origin` ditambahkan
303
+ - `Access-Control-Allow-Headers` dan `Access-Control-Allow-Methods` hanya dikirim pada preflight, jika nilai yang diminta ada di daftar
304
+ - Jika `origin` berupa daftar atau pola (atau `"*"` dengan `credentials: true`), origin dari request dipantulkan bila cocok. Pencocokan dilakukan pada seluruh string (tanpa memperhatikan huruf besar/kecil), dan header `Vary: Origin` ditambahkan pada request non-preflight
293
305
  - Nilai `"*"` untuk `headers` dan `methods` diabaikan saat `credentials: true`, sesuai spesifikasi CORS
294
306
  - Pada request non-preflight, `headers` (jika array) dikirim sebagai `Access-Control-Expose-Headers`
295
307
 
296
- ## Opsi Router
308
+ > `origin: "*"` bersama `credentials: true` memantulkan **semua** origin dengan credentials. Pakai hanya untuk development; di production sebutkan origin secara eksplisit.
309
+
310
+ ## Opsi
297
311
 
298
312
  ```js
299
- const app = new App({ size: 1000 });
313
+ const app = new App({ size: 1000, timeout: 5000 });
300
314
  ```
301
315
 
302
316
  | Opsi | Default | Keterangan |
303
317
  | --- | --- | --- |
304
318
  | `size` | `1000` | Jumlah maksimum entri LRU cache untuk hasil lookup `METHOD path` |
319
+ | `timeout` | - | Batas waktu handler dalam ms (khusus `App`). Lihat bagian [Timeout](#timeout) |
320
+
321
+ `Router` hanya menerima `size`.
305
322
 
306
323
  ## API
307
324
 
308
325
  ### `new App([options])` / `new Router([options])`
309
326
 
310
- Membuat instance baru. `App` adalah `Router` ditambah `listen()`.
327
+ Membuat instance baru. `App` adalah `Router` ditambah `listen()` dan dukungan `timeout`.
311
328
 
312
329
  ### `router.use([path], ...handlers | router)`
313
330
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ndiinginc/router",
3
- "version": "4.5.0",
3
+ "version": "4.6.1",
4
4
  "description": "",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
package/src/app.js CHANGED
@@ -11,13 +11,32 @@ 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
+ const promise = handlerPromise().catch((error) => {
31
+ if (!signal.aborted) {
32
+ controller.abort(error);
33
+ }
34
+ });
35
+
36
+ await Promise.race([promise, timeoutPromise]).finally(() => {
37
+ signal.removeEventListener("abort", handleAbort);
38
+ clearTimeout(timeout);
39
+ });
21
40
  }
22
41
 
23
42
  /**
@@ -26,22 +45,19 @@ class App extends Router {
26
45
  */
27
46
  async handle(req, res) {
28
47
  const { params, handlers, errorHandlers } = this.find(req.method, req.path);
29
- req.params = params;
30
-
48
+ req.params = Object.assign(Object.create(null), params);
31
49
  let err;
32
50
  try {
33
- const handlerPromise = (async () => {
51
+ const handlerPromise = async () => {
34
52
  for (const handler of handlers) {
35
53
  if (res.writableEnded) break;
36
54
  await handler(req, res);
37
55
  }
38
- })();
39
-
40
- if (req.timeout) await this.withTimeout(handlerPromise, req.timeout);
41
- else await handlerPromise;
56
+ };
57
+ if (this.timeout) await this.withTimeout(req, handlerPromise);
58
+ else await handlerPromise();
42
59
  } catch (error) {
43
60
  err = error;
44
-
45
61
  try {
46
62
  for (const handler of errorHandlers) {
47
63
  if (res.writableEnded) break;
@@ -51,14 +67,9 @@ class App extends Router {
51
67
  err = new Error(error.message, { cause: err });
52
68
  }
53
69
  }
54
-
55
70
  if (!res.writableEnded) {
56
71
  if (!err) err = ErrorResponse.status(404);
57
-
58
- if (!res.statusCode || res.statusCode < 400) {
59
- res.statusCode = err.statusCode || 500;
60
- }
61
-
72
+ res.statusCode = err.statusCode || 500;
62
73
  const error = { message: err.message };
63
74
  if (process.env.NODE_ENV === "development") {
64
75
  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
- this._query = {};
24
+ this._query = Object.create(null);
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
@@ -1,10 +1,11 @@
1
1
  const { Node } = require("./node.js");
2
2
  const { Cache } = require("./cache.js");
3
+ const { decodePathParam } = require("./util.js");
3
4
 
4
5
  class Router {
5
6
  constructor(options = {}) {
7
+ this.timeout = options.timeout ?? 0;
6
8
  this.cacheSize = options.size ?? 1000;
7
-
8
9
  this.root = new Node();
9
10
  this.seq = 0;
10
11
  this.cache = new Cache(this.cacheSize);
@@ -19,9 +20,7 @@ class Router {
19
20
  handlers = [path, ...handlers];
20
21
  path = null;
21
22
  }
22
-
23
23
  let node = this.root;
24
-
25
24
  if (path !== null) {
26
25
  for (const segment of this.split(path)) {
27
26
  node = node.add(segment);
@@ -31,10 +30,13 @@ class Router {
31
30
  for (const handler of handlers) {
32
31
  if (handler.root) {
33
32
  node.merge(handler.root, this.seq);
34
- this.seq = node.getMaxSeq() + 1;
33
+ this.seq += handler.root.getMaxSeq() + 1;
35
34
  } else {
36
- handler.seq = this.seq++;
37
- node.handlers.getOrInsert(method,[]).push(handler);
35
+ node.handlers.getOrInsert(method, []).push({
36
+ handler,
37
+ seq: this.seq++,
38
+ isErrorHandler: handler.length === 3,
39
+ });
38
40
  }
39
41
  }
40
42
  }
@@ -94,6 +96,22 @@ class Router {
94
96
  return this;
95
97
  }
96
98
 
99
+ matchRegExp(node, input, params) {
100
+ let matched;
101
+ for (const [, child] of node.children) {
102
+ if (!child.pattern) continue;
103
+ const match = child.pattern.exec(input);
104
+ if (match) {
105
+ matched = child;
106
+ for (const name in match.groups) {
107
+ params[name] = decodePathParam(match.groups[name]);
108
+ }
109
+ break;
110
+ }
111
+ }
112
+ return matched;
113
+ }
114
+
97
115
  /**
98
116
  * @param {'GET'|'QUERY'|'HEAD'|'OPTIONS'|'TRACE'|'PUT'|'DELETE'|'POST'|'PATCH'|'CONNECT'} method
99
117
  * @param {String} path
@@ -101,73 +119,47 @@ class Router {
101
119
  */
102
120
  find(method, path) {
103
121
  const key = `${method} ${path}`;
104
-
105
122
  const cached = this.cache.get(key);
106
123
  if (cached) return cached;
107
-
108
124
  let node = this.root;
109
-
110
125
  let params = Object.create(null);
111
126
  const handlers = {};
112
127
  const errorHandlers = {};
113
-
114
128
  for (const input of this.split(path)) {
115
129
  let matched = node.children.get(input);
116
-
117
130
  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
- }
131
+ matched = this.matchRegExp(node, input, params);
134
132
  }
135
-
136
133
  if (!matched) {
137
134
  node = null;
138
135
  params = Object.create(null);
139
136
  break;
140
137
  }
141
-
142
138
  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;
139
+ if (node.handlers.has("ALL")) {
140
+ for (const entry of node.handlers.get("ALL")) {
141
+ if (entry.isErrorHandler) errorHandlers[entry.seq] = entry.handler;
142
+ else handlers[entry.seq] = entry.handler;
143
+ }
148
144
  }
149
145
  }
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;
146
+ if (this.root.handlers.has("ALL")) {
147
+ for (const entry of this.root.handlers.get("ALL")) {
148
+ if (entry.isErrorHandler) errorHandlers[entry.seq] = entry.handler;
149
+ else handlers[entry.seq] = entry.handler;
150
+ }
155
151
  }
156
-
157
- if (node&&node.handlers.has(method)) {
158
- for (const handler of node.handlers.get(method)) {
159
- handlers[handler.seq] = handler;
152
+ if (node && node.handlers.has(method)) {
153
+ for (const entry of node.handlers.get(method)) {
154
+ handlers[entry.seq] = entry.handler;
160
155
  }
161
156
  }
162
-
163
157
  const result = {
164
158
  params,
165
159
  handlers: Object.values(handlers),
166
160
  errorHandlers: Object.values(errorHandlers),
167
161
  };
168
-
169
162
  this.cache.set(key, result);
170
-
171
163
  return result;
172
164
  }
173
165
  }
package/src/util.js CHANGED
@@ -1,39 +1,42 @@
1
1
  const URL_REGEX = /^([^?#]*)(\?([^#]*))?(#.*)?$/;
2
2
  const QUERY_STRING_REGEX = /([^=&]*)?=([^&]*)?/g;
3
3
 
4
+ function decodeQueryParam(encoded) {
5
+ try {
6
+ return decodeURIComponent(encoded.replace(/\+/g, " "));
7
+ } catch (error) {
8
+ return encoded;
9
+ }
10
+ }
11
+ exports.decodeQueryParam = decodeQueryParam;
12
+
13
+ function decodePathParam(encoded) {
14
+ try {
15
+ return decodeURIComponent(encoded);
16
+ } catch {
17
+ return encoded;
18
+ }
19
+ }
20
+ exports.decodePathParam=decodePathParam
21
+
4
22
  function parseQueryString(queryString) {
5
23
  const result = Object.create(null);
6
-
7
24
  let match;
8
25
  while ((match = QUERY_STRING_REGEX.exec(queryString)) !== null) {
9
26
  const [, rawName = "", rawValue = ""] = match;
10
-
11
- let name
12
- try {
13
- name = decodeURIComponent(rawName)
14
- } catch (error) {
15
- name=rawName
16
- }
17
-
18
- let value
19
- try {
20
- value = decodeURIComponent(rawValue)
21
- } catch (error) {
22
- value=rawValue
23
- }
24
-
25
- if (result[name]) {
27
+ const name = decodeQueryParam(rawName);
28
+ const value = decodeQueryParam(rawValue);
29
+ if (Object.hasOwn(result, name)) {
26
30
  if (Array.isArray(result[name])) result[name].push(value);
27
31
  else result[name] = [result[name], value];
28
32
  } else result[name] = value;
29
33
  }
30
-
31
34
  return result;
32
35
  }
33
36
  exports.parseQueryString = parseQueryString;
34
37
 
35
38
  function parseUrl(url) {
36
- const [, path = "/", , queryString = ""] = URL_REGEX.exec(url);
37
- return { path, queryString };
39
+ const [, rawPath = "", , queryString = ""] = URL_REGEX.exec(url);
40
+ return { path: rawPath || "/", queryString };
38
41
  }
39
42
  exports.parseUrl = parseUrl;