@ndiinginc/router 4.11.0 → 4.11.2

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
@@ -50,23 +50,19 @@ app.listen(3000);
50
50
 
51
51
  Yang diekspor adalah class `App`, dengan tiga properti tambahan:
52
52
 
53
- | Export | Keterangan |
54
- | --- | --- |
55
- | `App` | Router + HTTP server (`listen`) |
53
+ | Export | Keterangan |
54
+ | ------------ | ---------------------------------------- |
55
+ | `App` | Router + HTTP server (`listen`) |
56
56
  | `App.Router` | Router tanpa server, untuk nested router |
57
- | `App.json` | Middleware JSON body parser |
58
- | `App.cors` | Middleware CORS |
57
+ | `App.json` | Middleware JSON body parser |
58
+ | `App.cors` | Middleware CORS |
59
59
 
60
60
  ## Routing
61
61
 
62
62
  Semua method mengembalikan `this`, jadi bisa di-chain.
63
63
 
64
64
  ```js
65
- app.get("/users", handler)
66
- .post("/users", handler)
67
- .put("/users/:id", handler)
68
- .patch("/users/:id", handler)
69
- .delete("/users/:id", handler);
65
+ app.get("/users", handler).post("/users", handler).put("/users/:id", handler).patch("/users/:id", handler).delete("/users/:id", handler);
70
66
  ```
71
67
 
72
68
  Method yang tersedia: `get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `trace`, `connect`, `query`, dan `use` (semua method).
@@ -236,12 +232,12 @@ app.get("/proxy", async (req, res) => {
236
232
 
237
233
  ## Request
238
234
 
239
- | Properti | Keterangan |
240
- | --- | --- |
241
- | `req.path` | Pathname tanpa query string |
242
- | `req.query` | Object query string, di-parse secara lazy. Key yang berulang menjadi array, dan `+` di-decode menjadi spasi |
243
- | `req.params` | Parameter dari route dinamis (sudah di-decode). Wildcard `*` ada di `req.params.wildcard` |
244
- | `req.body` | Hasil parse body (butuh middleware `json()`) |
235
+ | Properti | Keterangan |
236
+ | ---------------- | ----------------------------------------------------------------------------------------------------------- |
237
+ | `req.path` | Pathname tanpa query string |
238
+ | `req.query` | Object query string, di-parse secara lazy. Key yang berulang menjadi array, dan `+` di-decode menjadi spasi |
239
+ | `req.params` | Parameter dari route dinamis (sudah di-decode). Wildcard `*` ada di `req.params.wildcard` |
240
+ | `req.body` | Hasil parse body (butuh middleware `json()`) |
245
241
  | `req.controller` | `AbortController` untuk request ini. Di-abort saat timeout atau error handler (jika `requestTimeout` aktif) |
246
242
 
247
243
  ```js
@@ -257,11 +253,11 @@ app.get("/search", (req, res) => {
257
253
 
258
254
  ## Response
259
255
 
260
- | Method | Keterangan |
261
- | --- | --- |
262
- | `res.status(code)` | Set status code, mengembalikan `res` sehingga bisa di-chain |
263
- | `res.send(chunk)` | Kirim response dan akhiri |
264
- | `res.json(value)` | Set `Content-Type: application/json; charset=utf-8`, serialize, lalu kirim |
256
+ | Method | Keterangan |
257
+ | ------------------ | -------------------------------------------------------------------------- |
258
+ | `res.status(code)` | Set status code, mengembalikan `res` sehingga bisa di-chain |
259
+ | `res.send(chunk)` | Kirim response dan akhiri |
260
+ | `res.json(value)` | Set `Content-Type: application/json; charset=utf-8`, serialize, lalu kirim |
265
261
 
266
262
  ```js
267
263
  res.status(201).json({ created: true });
@@ -277,8 +273,8 @@ res.status(201).json({ created: true });
277
273
  app.use(App.json({ limit: 100 * 1024 })); // default 100 KB
278
274
  ```
279
275
 
280
- | Opsi | Default | Keterangan |
281
- | --- | --- | --- |
276
+ | Opsi | Default | Keterangan |
277
+ | ------- | -------- | ------------------------------- |
282
278
  | `limit` | `102400` | Ukuran body maksimum dalam byte |
283
279
 
284
280
  Perilaku:
@@ -303,13 +299,13 @@ app.use(
303
299
  );
304
300
  ```
305
301
 
306
- | Opsi | Default | Keterangan |
307
- | --- | --- | --- |
308
- | `origin` | `"*"` | `"*"`, satu origin, atau array origin. `null` tidak mengirim header `Access-Control-Allow-Origin` sama sekali |
309
- | `credentials` | `false` | Mengirim `Access-Control-Allow-Credentials: true` |
310
- | `headers` | - | `"*"` atau array nama header (tidak membedakan huruf besar/kecil) |
311
- | `methods` | - | `"*"` atau array nama method (tidak membedakan huruf besar/kecil) |
312
- | `maxAge` | `0` | Lama cache preflight (detik). `0` berarti header tidak dikirim |
302
+ | Opsi | Default | Keterangan |
303
+ | ------------- | ------- | ------------------------------------------------------------------------------------------------------------- |
304
+ | `origin` | `"*"` | `"*"`, satu origin, atau array origin. `null` tidak mengirim header `Access-Control-Allow-Origin` sama sekali |
305
+ | `credentials` | `false` | Mengirim `Access-Control-Allow-Credentials: true` |
306
+ | `headers` | - | `"*"` atau array nama header (tidak membedakan huruf besar/kecil) |
307
+ | `methods` | - | `"*"` atau array nama method (tidak membedakan huruf besar/kecil) |
308
+ | `maxAge` | `0` | Lama cache preflight (detik). `0` berarti header tidak dikirim |
313
309
 
314
310
  Perilaku:
315
311
 
@@ -329,9 +325,9 @@ Perilaku:
329
325
  const app = new App({ requestTimeout: 5000 });
330
326
  ```
331
327
 
332
- | Opsi | Default | Keterangan |
333
- | --- | --- | --- |
334
- | `requestTimeout` | `0` | Batas waktu handler dalam ms (khusus `App`). Lihat bagian [Timeout](#timeout) |
328
+ | Opsi | Default | Keterangan |
329
+ | ---------------- | ------- | ----------------------------------------------------------------------------- |
330
+ | `requestTimeout` | `0` | Batas waktu handler dalam ms (khusus `App`). Lihat bagian [Timeout](#timeout) |
335
331
 
336
332
  `Router` tidak memiliki opsi sendiri; `requestTimeout` hanya dipakai oleh `App`.
337
333
 
@@ -373,10 +369,7 @@ console.log(server.address().port);
373
369
  ```js
374
370
  const https = require("https");
375
371
 
376
- const server = https.createServer(
377
- { ...App.SERVER_OPTIONS, key, cert },
378
- (req, res) => app.handle(req, res),
379
- );
372
+ const server = https.createServer({ ...App.SERVER_OPTIONS, key, cert }, (req, res) => app.handle(req, res));
380
373
  server.listen(443);
381
374
  ```
382
375
 
@@ -384,11 +377,11 @@ server.listen(443);
384
377
 
385
378
  Hello world dengan satu route `GET /`, diuji memakai `autocannon -c 100 -d 40 -p 10` di mesin lokal.
386
379
 
387
- | | Req/sec (avg) | Latency (avg) |
388
- | --- | --- | --- |
389
- | Fastify | 31.475 | 31,27 ms |
390
- | **@ndiinginc/router** | 29.735 | 33,12 ms |
391
- | Express | 20.211 | 48,93 ms |
380
+ | | Req/sec (avg) | Latency (avg) |
381
+ | --------------------- | ------------- | ------------- |
382
+ | Fastify | 31.475 | 31,27 ms |
383
+ | **@ndiinginc/router** | 29.735 | 33,12 ms |
384
+ | Express | 20.211 | 48,93 ms |
392
385
 
393
386
  Hasil bervariasi tergantung mesin, versi Node.js, dan jumlah route. Benchmark ini hanya menggambarkan overhead dasar, bukan performa aplikasi nyata.
394
387
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ndiinginc/router",
3
- "version": "4.11.0",
3
+ "version": "4.11.2",
4
4
  "description": "",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -22,6 +22,6 @@
22
22
  "@ndiinginc/test": "^1.17.0",
23
23
  "@types/jest": "^30.0.0",
24
24
  "@types/node": "^26.6.5",
25
- "autocannon": "^8.0.0"
25
+ "autocannon": "^2.0.1"
26
26
  }
27
27
  }
package/src/app.js CHANGED
@@ -11,26 +11,27 @@ const SERVER_OPTIONS = {
11
11
  };
12
12
 
13
13
  class App extends Router {
14
+ /**
15
+ * @param {Request} req
16
+ * @param {Response} res
17
+ * @param {Promise} handlerPromise
18
+ */
14
19
  async withTimeout(req, res, handlerPromise) {
15
20
  let handleAbort;
16
21
  const timeoutPromise = new Promise((_, reject) => {
17
22
  handleAbort = () => reject(req.controller.signal.reason);
18
23
  req.controller.signal.addEventListener("abort", handleAbort, { once: true });
19
24
  });
20
-
21
25
  const timeout = setTimeout(() => {
22
26
  res.statusCode = 504;
23
27
  req.controller.abort(new Error(http.STATUS_CODES[res.statusCode]));
24
28
  }, this.requestTimeout);
25
-
26
29
  const promise = handlerPromise().catch((error) => {
27
30
  if (!req.controller.signal.aborted) {
28
31
  req.controller.abort(error);
29
32
  } else {
30
- console.error(error); // swallow error?
31
33
  }
32
34
  });
33
-
34
35
  try {
35
36
  await Promise.race([promise, timeoutPromise]);
36
37
  } finally {
@@ -39,34 +40,34 @@ class App extends Router {
39
40
  }
40
41
  }
41
42
 
43
+ /**
44
+ * @param {Response} res
45
+ * @param {Error} err
46
+ */
42
47
  fallback(res, err) {
43
48
  if (!res.writableEnded) {
44
49
  if (!err) {
45
50
  res.statusCode = 404;
46
51
  err = new Error(http.STATUS_CODES[res.statusCode]);
47
52
  }
48
-
49
53
  if (res.statusCode < 400) {
50
54
  res.statusCode = 500;
51
55
  }
52
-
53
56
  const error = { message: err.message };
54
57
  if (process.env.NODE_ENV === "development") {
55
58
  error.stack = err.stack;
56
59
  }
57
-
58
60
  res.json({ error });
59
61
  }
60
62
  }
61
63
 
62
64
  /**
63
- * @param {import('http').IncomingMessage & import('./request').Request} req
64
- * @param {import('http').ServerResponse & import('./response').Response} res
65
+ * @param {Request} req
66
+ * @param {Response} res
65
67
  */
66
68
  async handle(req, res) {
67
69
  const { params, paramsLength, handlers, errorHandlers } = this.find(req.method, req.path);
68
70
  req.params = paramsLength ? Object.assign(Object.create(null), params) : EMPTY_PARAMS;
69
-
70
71
  let err;
71
72
  try {
72
73
  const handlerPromise = async () => {
@@ -75,7 +76,6 @@ class App extends Router {
75
76
  await handler(req, res);
76
77
  }
77
78
  };
78
-
79
79
  if (this.requestTimeout) {
80
80
  await this.withTimeout(req, res, handlerPromise);
81
81
  } else {
@@ -83,7 +83,6 @@ class App extends Router {
83
83
  }
84
84
  } catch (error) {
85
85
  err = error;
86
-
87
86
  try {
88
87
  for (const handler of errorHandlers) {
89
88
  if (res.writableEnded) break;
@@ -93,10 +92,13 @@ class App extends Router {
93
92
  err = new Error(error.message, { cause: err });
94
93
  }
95
94
  }
96
-
97
95
  this.fallback(res, err);
98
96
  }
99
97
 
98
+ /**
99
+ * @param {import('net').ListenOptions} args
100
+ * @returns {import('http').Server}
101
+ */
100
102
  listen(...args) {
101
103
  const requestListener = (req, res) => this.handle(req, res);
102
104
  const server = http.createServer(SERVER_OPTIONS, requestListener);
@@ -105,6 +107,6 @@ class App extends Router {
105
107
  }
106
108
  }
107
109
 
108
- App.SERVER_OPTIONS=SERVER_OPTIONS
110
+ App.SERVER_OPTIONS = SERVER_OPTIONS;
109
111
 
110
112
  exports.App = App;
@@ -1,14 +1,21 @@
1
1
  /**
2
- * @typedef CORSOptions
3
- * @property {String|Array|Null} [origin="*"]
4
- * @property {Boolean} [credentials=false]
5
- * @property {String|Array} [headers]
6
- * @property {Number} [maxAge=0]
7
- * @property {String|Array} [methods]
2
+ * @typedef {Object} Options
3
+ * @property {string|array} origin
4
+ * @property {boolean} credentials
5
+ * @property {string|array} headers
6
+ * @property {Number} maxAge
7
+ * @property {string|array} methods
8
8
  */
9
9
 
10
10
  /**
11
- * @param {CORSOptions} options
11
+ * @callback Callback
12
+ * @param {import('../request')} req
13
+ * @param {import('../response')} res
14
+ */
15
+
16
+ /**
17
+ * @param {Options} Options
18
+ * @returns {Callback}
12
19
  */
13
20
  function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods } = {}) {
14
21
  if (Array.isArray(headers)) {
@@ -28,15 +35,12 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
28
35
  const reqOrigin = req.headers["origin"];
29
36
  let value;
30
37
  let explicit = false;
31
-
32
38
  if (origin === "*" && !credentials) {
33
39
  value = "*";
34
40
  } else if (origin === "*" || origin === reqOrigin || (Array.isArray(origin) && origin.includes(reqOrigin))) {
35
41
  value = reqOrigin;
36
42
  explicit = true;
37
43
  }
38
- // origin === null -> skip (jangan set "null", celah keamanan)
39
-
40
44
  if (value) res.setHeader("Access-Control-Allow-Origin", value);
41
45
  if (explicit) res.appendHeader("Vary", "Origin");
42
46
  }
@@ -44,9 +48,7 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
44
48
  function setAllowHeaders(req, res) {
45
49
  const reqHeaders = req.headers["access-control-request-headers"];
46
50
  if (!reqHeaders) return;
47
-
48
51
  const requested = reqHeaders.split(",").map((h) => h.trim().toLowerCase());
49
-
50
52
  let value;
51
53
  if (headers === "*" && !credentials) {
52
54
  value = "*";
@@ -54,7 +56,6 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
54
56
  const allowed = requested.filter((h) => headers.includes(h));
55
57
  if (allowed.length === requested.length) value = headers;
56
58
  } else {
57
- // headers undefined -> echo request headers (wajib per spec)
58
59
  value = requested;
59
60
  }
60
61
  if (value) res.setHeader("Access-Control-Allow-Headers", value);
@@ -63,16 +64,13 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
63
64
  function setAllowMethods(req, res) {
64
65
  const reqMethod = req.headers["access-control-request-method"];
65
66
  if (!reqMethod) return;
66
-
67
67
  const method = reqMethod.toUpperCase();
68
-
69
68
  let value;
70
69
  if (methods === "*" && !credentials) {
71
70
  value = "*";
72
71
  } else if (Array.isArray(methods)) {
73
72
  if (methods.includes(method)) value = methods;
74
73
  } else {
75
- // methods undefined -> echo request method
76
74
  value = [method];
77
75
  }
78
76
  if (value) res.setHeader("Access-Control-Allow-Methods", value);
@@ -96,9 +94,7 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
96
94
 
97
95
  return (req, res) => {
98
96
  setAllowCredentials(res);
99
-
100
97
  if (req.method === "OPTIONS" && req.headers["access-control-request-method"]) {
101
- // preflight
102
98
  setAllowOrigin(req, res);
103
99
  setAllowHeaders(req, res);
104
100
  setAllowMethods(req, res);
@@ -107,7 +103,6 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
107
103
  res.setHeader("Content-Length", "0");
108
104
  res.end();
109
105
  } else {
110
- // normal response (termasuk OPTIONS non-preflight)
111
106
  setAllowOrigin(req, res);
112
107
  setExposeHeaders(res);
113
108
  }
@@ -1,12 +1,27 @@
1
1
  const http = require("http");
2
+ /**
3
+ * @typedef {Object} Options
4
+ * @property {Number} [limit=1.024e+5]
5
+ */
2
6
 
3
- function json({ limit = 100 * 1024 } = {}) {
7
+ /**
8
+ * @callback Callback
9
+ * @param {import('../request')} req
10
+ * @param {import('../response')} res
11
+ */
12
+
13
+ /**
14
+ * @param {Options} Options
15
+ * @returns {Callback}
16
+ */
17
+ function json({ limit = 1.024e5 } = {}) {
4
18
  const reviver = (key, value) => {
5
19
  if (key === "__proto__" || key === "constructor" || key === "prototype") {
6
20
  return undefined;
7
21
  }
8
22
  return value;
9
23
  };
24
+
10
25
  return async (req, res) => {
11
26
  if (!["POST", "PUT", "PATCH", "QUERY"].includes(req.method)) {
12
27
  return;
package/src/node.js CHANGED
@@ -1,32 +1,49 @@
1
1
  class Node {
2
+ /**
3
+ * @param {String} segment
4
+ */
2
5
  constructor(segment = "") {
3
6
  this.segment = segment;
4
- /** @type {Map<string, Node>} static children */
7
+ if (segment.startsWith(":")) {
8
+ /**@type {String}*/
9
+ this.name = segment.slice(1);
10
+ }
11
+
12
+ /**@type {Map<string,Node>}*/
5
13
  this.children = new Map();
6
- /** @type {Map<string, Node>} param children, key = ':name' */
14
+
15
+ /**@type {Map<string,Node>}*/
7
16
  this.params = new Map();
8
- /** @type {Node | null} wildcard child */
17
+
18
+ /**@type {Node}*/
9
19
  this.wildcard = null;
20
+ /**@type {Map<string,Function[]>}*/
10
21
  this.handlers = new Map();
11
22
  }
12
23
 
24
+ /**
25
+ * @param {String} segment
26
+ * @returns {Node}
27
+ */
13
28
  add(segment) {
14
29
  if (segment === "*") {
15
30
  if (!this.wildcard) this.wildcard = new Node("*");
16
31
  return this.wildcard;
17
32
  }
18
-
19
33
  if (segment.startsWith(":")) {
20
34
  return this.params.getOrInsertComputed(segment, (segment) => {
21
35
  return new Node(segment);
22
36
  });
23
37
  }
24
-
25
38
  return this.children.getOrInsertComputed(segment, (segment) => {
26
39
  return new Node(segment);
27
40
  });
28
41
  }
29
42
 
43
+ /**
44
+ * @param {Node} node
45
+ * @param {Number} offset
46
+ */
30
47
  merge(node, offset) {
31
48
  for (const [method, entries] of node.handlers) {
32
49
  for (const entry of entries) {
@@ -36,41 +53,36 @@ class Node {
36
53
  });
37
54
  }
38
55
  }
39
-
40
56
  if (node.wildcard) {
41
57
  this.add("*").merge(node.wildcard, offset);
42
58
  }
43
-
44
59
  for (const [segment, child] of node.params) {
45
60
  this.add(segment).merge(child, offset);
46
61
  }
47
-
48
62
  for (const [segment, child] of node.children) {
49
63
  this.add(segment).merge(child, offset);
50
64
  }
51
65
  }
52
66
 
67
+ /**
68
+ * @returns {Number}
69
+ */
53
70
  getMaxSeq() {
54
71
  let max = -1;
55
-
56
72
  for (const [, entries] of this.handlers) {
57
73
  for (const entry of entries) {
58
74
  max = Math.max(max, entry.seq);
59
75
  }
60
76
  }
61
-
62
77
  if (this.wildcard) {
63
78
  max = Math.max(max, this.wildcard.getMaxSeq());
64
79
  }
65
-
66
80
  for (const [, child] of this.params) {
67
81
  max = Math.max(max, child.getMaxSeq());
68
82
  }
69
-
70
83
  for (const [, child] of this.children) {
71
84
  max = Math.max(max, child.getMaxSeq());
72
85
  }
73
-
74
86
  return max;
75
87
  }
76
88
  }
package/src/polyfill.js CHANGED
@@ -1,5 +1,3 @@
1
-
2
- // Polyfill for Map.prototype.getOrInsert
3
1
  if (!Map.prototype.getOrInsert) {
4
2
  Map.prototype.getOrInsert = function (key, defaultValue) {
5
3
  if (!this.has(key)) {
@@ -8,7 +6,7 @@ if (!Map.prototype.getOrInsert) {
8
6
  return this.get(key);
9
7
  };
10
8
  }
11
- // Polyfill for Map.prototype.getOrInsertComputed
9
+
12
10
  if (!Map.prototype.getOrInsertComputed) {
13
11
  Map.prototype.getOrInsertComputed = function (key, callback) {
14
12
  if (typeof callback !== "function") {
package/src/request.js CHANGED
@@ -13,7 +13,6 @@ class Request extends http.IncomingMessage {
13
13
  this._path = path;
14
14
  this._queryString = queryString;
15
15
  }
16
-
17
16
  return this._path;
18
17
  }
19
18
 
@@ -21,18 +20,13 @@ class Request extends http.IncomingMessage {
21
20
  if (!this._query) {
22
21
  this._query = parseQueryString(this._queryString);
23
22
  }
24
-
25
23
  return this._query;
26
24
  }
27
25
 
28
- /**
29
- * @returns {AbortController}
30
- */
31
26
  get controller() {
32
27
  if (!this._controller) {
33
28
  this._controller = new AbortController();
34
29
  }
35
-
36
30
  return this._controller;
37
31
  }
38
32
  }
package/src/response.js CHANGED
@@ -1,16 +1,26 @@
1
1
  const http = require("http");
2
2
 
3
3
  class Response extends http.ServerResponse {
4
+ /**
5
+ * @param {Number} code
6
+ * @returns {Response}
7
+ */
4
8
  status(code) {
5
9
  this.statusCode = code;
6
10
  return this;
7
11
  }
8
12
 
13
+ /**
14
+ * @param {String} chunk
15
+ */
9
16
  send(chunk) {
10
17
  if (this.writableEnded) return;
11
18
  this.end(chunk, null, null);
12
19
  }
13
-
20
+
21
+ /**
22
+ * @param {Object} value
23
+ */
14
24
  json(value) {
15
25
  if (!this.headersSent) {
16
26
  this.setHeader("Content-Type", "application/json; charset=utf-8");
package/src/router.js CHANGED
@@ -1,31 +1,45 @@
1
1
  const { decodeParam } = require("./util");
2
2
  const { Node } = require("./node");
3
3
 
4
+ /**
5
+ * @typedef {Object} Options
6
+ * @property {Number} requestTimeout
7
+ */
8
+
4
9
  class Router {
10
+ /**
11
+ * @param {Options} options
12
+ */
5
13
  constructor(options = {}) {
6
14
  this.requestTimeout = options.requestTimeout ?? 0;
7
15
  this.root = new Node();
8
16
  this.seq = 0;
9
17
  }
10
18
 
19
+ /**
20
+ * @param {String} path
21
+ * @returns {String[]}
22
+ */
11
23
  split(path) {
12
24
  return path.split("/").filter(Boolean);
13
25
  }
14
26
 
27
+ /**
28
+ * @param {String} method
29
+ * @param {String|Function} path
30
+ * @param {Function[]} handlers
31
+ */
15
32
  add(method, path, ...handlers) {
16
33
  if (typeof path === "function") {
17
34
  handlers = [path, ...handlers];
18
35
  path = null;
19
36
  }
20
-
21
37
  let node = this.root;
22
-
23
38
  if (path) {
24
39
  for (const segment of this.split(path)) {
25
40
  node = node.add(segment);
26
41
  }
27
42
  }
28
-
29
43
  for (const handler of handlers) {
30
44
  if (handler.root) {
31
45
  node.merge(handler.root, this.seq);
@@ -40,128 +54,193 @@ class Router {
40
54
  }
41
55
  }
42
56
 
57
+ /**
58
+ * @param {String|Function} path
59
+ * @param {Function[]} handlers
60
+ * @returns {this}
61
+ */
43
62
  use(path, ...handlers) {
44
63
  this.add("ALL", path, ...handlers);
45
64
  return this;
46
65
  }
47
66
 
67
+ /**
68
+ * @param {String|Function} path
69
+ * @param {Function[]} handlers
70
+ * @returns {this}
71
+ */
48
72
  get(path, ...handlers) {
49
73
  this.add("GET", path, ...handlers);
50
74
  return this;
51
75
  }
52
76
 
77
+ /**
78
+ * @param {String|Function} path
79
+ * @param {Function[]} handlers
80
+ * @returns {this}
81
+ */
53
82
  query(path, ...handlers) {
54
83
  this.add("QUERY", path, ...handlers);
55
84
  return this;
56
85
  }
57
86
 
87
+ /**
88
+ * @param {String|Function} path
89
+ * @param {Function[]} handlers
90
+ * @returns {this}
91
+ */
58
92
  head(path, ...handlers) {
59
93
  this.add("HEAD", path, ...handlers);
60
94
  return this;
61
95
  }
62
96
 
97
+ /**
98
+ * @param {String|Function} path
99
+ * @param {Function[]} handlers
100
+ * @returns {this}
101
+ */
63
102
  options(path, ...handlers) {
64
103
  this.add("OPTIONS", path, ...handlers);
65
104
  return this;
66
105
  }
67
106
 
107
+ /**
108
+ * @param {String|Function} path
109
+ * @param {Function[]} handlers
110
+ * @returns {this}
111
+ */
68
112
  trace(path, ...handlers) {
69
113
  this.add("TRACE", path, ...handlers);
70
114
  return this;
71
115
  }
72
116
 
117
+ /**
118
+ * @param {String|Function} path
119
+ * @param {Function[]} handlers
120
+ * @returns {this}
121
+ */
73
122
  put(path, ...handlers) {
74
123
  this.add("PUT", path, ...handlers);
75
124
  return this;
76
125
  }
77
126
 
127
+ /**
128
+ * @param {String|Function} path
129
+ * @param {Function[]} handlers
130
+ * @returns {this}
131
+ */
78
132
  delete(path, ...handlers) {
79
133
  this.add("DELETE", path, ...handlers);
80
134
  return this;
81
135
  }
82
136
 
137
+ /**
138
+ * @param {String|Function} path
139
+ * @param {Function[]} handlers
140
+ * @returns {this}
141
+ */
83
142
  post(path, ...handlers) {
84
143
  this.add("POST", path, ...handlers);
85
144
  return this;
86
145
  }
87
146
 
147
+ /**
148
+ * @param {String|Function} path
149
+ * @param {Function[]} handlers
150
+ * @returns {this}
151
+ */
88
152
  patch(path, ...handlers) {
89
153
  this.add("PATCH", path, ...handlers);
90
154
  return this;
91
155
  }
92
156
 
157
+ /**
158
+ * @param {String|Function} path
159
+ * @param {Function[]} handlers
160
+ * @returns {this}
161
+ */
93
162
  connect(path, ...handlers) {
94
163
  this.add("CONNECT", path, ...handlers);
95
164
  return this;
96
165
  }
97
166
 
98
167
  /**
168
+ * @param {String} method
169
+ * @param {String[]} segments
99
170
  * @param {Node} node
100
- * @param {string[]} segments
101
- * @param {number} index
102
- * @param {Node[]} path
103
- * @param {Record<string, string>} params
104
- * @param {{ best: { nodes: Node[], params: Record<string, string> } }} state
105
- * @returns {{ nodes: Node[], params: Record<string, string>, matched: boolean }}
106
- */
107
- search(node, segments, index, path, params, state,method) {
171
+ * @param {Array} path
172
+ * @param {Object} params
173
+ * @param {Object} state
174
+ * @param {Number} index
175
+ * @returns {{path:Array,params:Object,matched:Boolean}}
176
+ */
177
+ search(method, segments, node, path = [], params = {}, state = { path: [], params: {} }, index = 0) {
108
178
  path.push(node);
109
-
110
- // habis segmen -> full match
111
- // if (index === segments.length) {
112
- // return { nodes: path.slice(), params: { ...params }, matched: true };
113
- // }
114
179
  if (index === segments.length) {
115
- if (node.handlers.has(method)) {
116
- return { nodes: path.slice(), params: { ...params }, matched: true };
180
+ if (node.handlers.has(method) || node.handlers.has("ALL")) {
181
+ return { path, params, matched: true };
117
182
  }
118
- // node ada tapi tidak punya handler method ini: catat sebagai partial, lalu coba cabang lain
119
- if (path.length > state.best.nodes.length) {
120
- state.best = { nodes: path.slice(), params: { ...params } };
183
+ if (path.length > state.path.length) {
184
+ state.path = path.slice();
185
+ state.params = { ...params };
121
186
  }
122
187
  path.pop();
123
- return { matched: false };
188
+ return { path: state.path, params: state.params, matched: false };
124
189
  }
125
-
126
190
  const segment = segments[index];
127
-
128
- // 1. static (prioritas tertinggi)
129
- const staticChild = node.children.get(segment);
130
- if (staticChild) {
131
- const res = this.search(staticChild, segments, index + 1, path, params, state,method);
191
+ if (node.children.has(segment)) {
192
+ const res = this.search(
193
+ //
194
+ method,
195
+ segments,
196
+ node.children.get(segment),
197
+ path,
198
+ params,
199
+ state,
200
+ index + 1,
201
+ );
132
202
  if (res.matched) return res;
133
203
  }
134
-
135
- // 2. semua param child (bisa lebih dari satu)
136
- for (const [key, child] of node.params) {
137
- const name = key.slice(1); // buang ':'
138
- const prev = params[name];
139
- params[name] = decodeParam(segment);
140
- const res = this.search(child, segments, index + 1, path, params, state,method);
141
- if (res.matched) return res;
142
- // backtrack param value
143
- if (prev === undefined) delete params[name];
144
- else params[name] = prev;
204
+ if (node.params.size) {
205
+ for (const [, child] of node.params) {
206
+ const prev = params[child.name];
207
+ params[child.name] = decodeParam(segment);
208
+ const res = this.search(
209
+ //
210
+ method,
211
+ segments,
212
+ child,
213
+ path,
214
+ params,
215
+ state,
216
+ index + 1,
217
+ );
218
+ if (res.matched) return res;
219
+ if (prev === undefined) {
220
+ const { [child.name]: _, ...rest } = params;
221
+ params = rest;
222
+ } else params[child.name] = prev;
223
+ }
145
224
  }
146
-
147
- // 3. wildcard (prioritas terendah, menyerap sisa segmen)
148
- if (node.wildcard) {
225
+ if (node.wildcard && (node.wildcard.handlers.get(method) || node.wildcard.handlers.get("ALL"))) {
149
226
  const rest = segments.slice(index).join("/");
150
227
  params["wildcard"] = decodeParam(rest);
151
228
  path.push(node.wildcard);
152
- return { nodes: path.slice(), params: { ...params }, matched: true };
229
+ return { path, params, matched: true };
153
230
  }
154
-
155
- // tidak match di sini -> update partial terpanjang
156
- // path.length > state.best.nodes.length artinya kita sudah lebih dalam
157
- if (path.length > state.best.nodes.length) {
158
- state.best = { nodes: path.slice(), params: { ...params } };
231
+ if (path.length > state.path.length) {
232
+ state.path = path.slice();
233
+ state.params = { ...params };
159
234
  }
160
-
161
235
  path.pop();
162
- return { matched: false };
236
+ return { path: state.path, params: state.params, matched: false };
163
237
  }
164
238
 
239
+ /**
240
+ * @param {Node} r
241
+ * @param {String} method
242
+ * @param {Object} handlers
243
+ */
165
244
  collectHandler(r, method, handlers) {
166
245
  if (r.handlers.has(method)) {
167
246
  for (const entry of r.handlers.get(method)) {
@@ -170,49 +249,40 @@ class Router {
170
249
  }
171
250
  }
172
251
 
173
- collectErrorHandler(r, errorHandlers) {
174
- if (r.handlers.has("CATCH")) {
175
- for (const entry of r.handlers.get("CATCH")) {
176
- errorHandlers[entry.seq] = entry.handler;
177
- }
178
- }
179
- }
180
-
252
+ /**
253
+ * @param {Node} r
254
+ * @param {Object} handlers
255
+ * @param {Object} errorHandlers
256
+ */
181
257
  collectMiddleware(r, handlers, errorHandlers) {
182
258
  this.collectHandler(r, "ALL", handlers);
183
- this.collectErrorHandler(r, errorHandlers);
259
+ this.collectHandler(r, "CATCH", errorHandlers);
184
260
  }
185
261
 
262
+ /**
263
+ * @param {String} method
264
+ * @param {String} path
265
+ * @returns {{matched: boolean;params: Object;paramsLength: number;handlers: any[];errorHandlers: any[];}}
266
+ */
186
267
  find(method, path) {
187
- const segments = this.split(path);
188
-
189
- const params = {};
268
+ const res = this.search(
269
+ //
270
+ method,
271
+ this.split(path),
272
+ this.root,
273
+ );
190
274
  const handlers = {};
191
275
  const errorHandlers = {};
192
-
193
- /** @type {{ best: { nodes: Node[], params: Record<string, string> } }} */
194
- const state = { best: { nodes: [], params: {} } };
195
-
196
- const res = this.search(this.root, segments, 0, [], params, state,method);
197
-
198
- const matchedNodes = res.matched ? res.nodes : state.best.nodes;
199
- const finalParams = res.matched ? res.params : state.best.params;
200
-
201
- // middleware dari semua node di jalur (partial atau full)
202
- for (const r of matchedNodes) {
276
+ for (const r of res.path) {
203
277
  this.collectMiddleware(r, handlers, errorHandlers);
204
278
  }
205
-
206
- // method handler HANYA kalau full match
207
279
  if (res.matched) {
208
- const leaf = matchedNodes[matchedNodes.length - 1];
209
- this.collectHandler(leaf, method, handlers);
280
+ this.collectHandler(res.path.at(-1), method, handlers);
210
281
  }
211
-
212
282
  return {
213
283
  matched: res.matched,
214
- params: finalParams,
215
- paramsLength: Object.keys(finalParams).length,
284
+ params: res.params,
285
+ paramsLength: Object.keys(res.params).length,
216
286
  handlers: Object.values(handlers),
217
287
  errorHandlers: Object.values(errorHandlers),
218
288
  };
package/src/util.js CHANGED
@@ -1,7 +1,10 @@
1
- // const EMPTY_PARAMS = Object.freeze(Object.create(null));
2
- const EMPTY_PARAMS = Object.create(null)
1
+ const EMPTY_PARAMS = Object.create(null);
3
2
  exports.EMPTY_PARAMS = EMPTY_PARAMS;
4
3
 
4
+ /**
5
+ * @param {String} raw
6
+ * @returns {String}
7
+ */
5
8
  function decodeParam(raw) {
6
9
  try {
7
10
  return decodeURIComponent(raw);
@@ -11,9 +14,12 @@ function decodeParam(raw) {
11
14
  }
12
15
  exports.decodeParam = decodeParam;
13
16
 
17
+ /**
18
+ * @param {String} raw
19
+ * @returns {String}
20
+ */
14
21
  function decodeQuery(raw) {
15
22
  const normalized = raw.replace(/\+/g, " ");
16
-
17
23
  try {
18
24
  return decodeURIComponent(normalized);
19
25
  } catch (error) {
@@ -21,19 +27,19 @@ function decodeQuery(raw) {
21
27
  }
22
28
  }
23
29
 
30
+ /**
31
+ * @param {String} queryString
32
+ * @returns {Object}
33
+ */
24
34
  function parseQueryString(queryString) {
25
35
  if (queryString === "") return Object.create(null);
26
-
27
36
  const regex = /([^=&]*)=([^&]*)/g;
28
37
  const result = Object.create(null);
29
-
30
38
  let match;
31
39
  while ((match = regex.exec(queryString))) {
32
40
  const [, rawName, rawValue] = match;
33
-
34
41
  const name = decodeQuery(rawName);
35
42
  const value = decodeQuery(rawValue);
36
-
37
43
  if (Object.hasOwn(result, name)) {
38
44
  if (Array.isArray(result[name])) result[name].push(value);
39
45
  else result[name] = [result[name], value];
@@ -44,14 +50,16 @@ function parseQueryString(queryString) {
44
50
  }
45
51
  exports.parseQueryString = parseQueryString;
46
52
 
53
+ /**
54
+ * @param {String} url
55
+ * @returns {{path: any;queryString: any;}}
56
+ */
47
57
  function parseUrl(url) {
48
58
  if (!(url.includes("?") || url.includes("#"))) {
49
59
  return { path: url, queryString: "" };
50
60
  }
51
-
52
61
  const regex = /^([^?#]*)(\?([^#]*))?(#.*)?$/;
53
62
  const [, path = "/", , queryString = ""] = regex.exec(url);
54
-
55
63
  return { path, queryString };
56
64
  }
57
65
  exports.parseUrl = parseUrl;