@ndiinginc/router 4.11.0 → 4.11.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
@@ -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.1",
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
@@ -17,20 +17,16 @@ class App extends Router {
17
17
  handleAbort = () => reject(req.controller.signal.reason);
18
18
  req.controller.signal.addEventListener("abort", handleAbort, { once: true });
19
19
  });
20
-
21
20
  const timeout = setTimeout(() => {
22
21
  res.statusCode = 504;
23
22
  req.controller.abort(new Error(http.STATUS_CODES[res.statusCode]));
24
23
  }, this.requestTimeout);
25
-
26
24
  const promise = handlerPromise().catch((error) => {
27
25
  if (!req.controller.signal.aborted) {
28
26
  req.controller.abort(error);
29
27
  } else {
30
- console.error(error); // swallow error?
31
28
  }
32
29
  });
33
-
34
30
  try {
35
31
  await Promise.race([promise, timeoutPromise]);
36
32
  } finally {
@@ -45,16 +41,13 @@ class App extends Router {
45
41
  res.statusCode = 404;
46
42
  err = new Error(http.STATUS_CODES[res.statusCode]);
47
43
  }
48
-
49
44
  if (res.statusCode < 400) {
50
45
  res.statusCode = 500;
51
46
  }
52
-
53
47
  const error = { message: err.message };
54
48
  if (process.env.NODE_ENV === "development") {
55
49
  error.stack = err.stack;
56
50
  }
57
-
58
51
  res.json({ error });
59
52
  }
60
53
  }
@@ -66,7 +59,6 @@ class App extends Router {
66
59
  async handle(req, res) {
67
60
  const { params, paramsLength, handlers, errorHandlers } = this.find(req.method, req.path);
68
61
  req.params = paramsLength ? Object.assign(Object.create(null), params) : EMPTY_PARAMS;
69
-
70
62
  let err;
71
63
  try {
72
64
  const handlerPromise = async () => {
@@ -75,7 +67,6 @@ class App extends Router {
75
67
  await handler(req, res);
76
68
  }
77
69
  };
78
-
79
70
  if (this.requestTimeout) {
80
71
  await this.withTimeout(req, res, handlerPromise);
81
72
  } else {
@@ -83,7 +74,6 @@ class App extends Router {
83
74
  }
84
75
  } catch (error) {
85
76
  err = error;
86
-
87
77
  try {
88
78
  for (const handler of errorHandlers) {
89
79
  if (res.writableEnded) break;
@@ -93,7 +83,6 @@ class App extends Router {
93
83
  err = new Error(error.message, { cause: err });
94
84
  }
95
85
  }
96
-
97
86
  this.fallback(res, err);
98
87
  }
99
88
 
@@ -105,6 +94,6 @@ class App extends Router {
105
94
  }
106
95
  }
107
96
 
108
- App.SERVER_OPTIONS=SERVER_OPTIONS
97
+ App.SERVER_OPTIONS = SERVER_OPTIONS;
109
98
 
110
99
  exports.App = App;
@@ -28,15 +28,12 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
28
28
  const reqOrigin = req.headers["origin"];
29
29
  let value;
30
30
  let explicit = false;
31
-
32
31
  if (origin === "*" && !credentials) {
33
32
  value = "*";
34
33
  } else if (origin === "*" || origin === reqOrigin || (Array.isArray(origin) && origin.includes(reqOrigin))) {
35
34
  value = reqOrigin;
36
35
  explicit = true;
37
36
  }
38
- // origin === null -> skip (jangan set "null", celah keamanan)
39
-
40
37
  if (value) res.setHeader("Access-Control-Allow-Origin", value);
41
38
  if (explicit) res.appendHeader("Vary", "Origin");
42
39
  }
@@ -44,9 +41,7 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
44
41
  function setAllowHeaders(req, res) {
45
42
  const reqHeaders = req.headers["access-control-request-headers"];
46
43
  if (!reqHeaders) return;
47
-
48
44
  const requested = reqHeaders.split(",").map((h) => h.trim().toLowerCase());
49
-
50
45
  let value;
51
46
  if (headers === "*" && !credentials) {
52
47
  value = "*";
@@ -54,7 +49,6 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
54
49
  const allowed = requested.filter((h) => headers.includes(h));
55
50
  if (allowed.length === requested.length) value = headers;
56
51
  } else {
57
- // headers undefined -> echo request headers (wajib per spec)
58
52
  value = requested;
59
53
  }
60
54
  if (value) res.setHeader("Access-Control-Allow-Headers", value);
@@ -63,16 +57,13 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
63
57
  function setAllowMethods(req, res) {
64
58
  const reqMethod = req.headers["access-control-request-method"];
65
59
  if (!reqMethod) return;
66
-
67
60
  const method = reqMethod.toUpperCase();
68
-
69
61
  let value;
70
62
  if (methods === "*" && !credentials) {
71
63
  value = "*";
72
64
  } else if (Array.isArray(methods)) {
73
65
  if (methods.includes(method)) value = methods;
74
66
  } else {
75
- // methods undefined -> echo request method
76
67
  value = [method];
77
68
  }
78
69
  if (value) res.setHeader("Access-Control-Allow-Methods", value);
@@ -96,9 +87,7 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
96
87
 
97
88
  return (req, res) => {
98
89
  setAllowCredentials(res);
99
-
100
90
  if (req.method === "OPTIONS" && req.headers["access-control-request-method"]) {
101
- // preflight
102
91
  setAllowOrigin(req, res);
103
92
  setAllowHeaders(req, res);
104
93
  setAllowMethods(req, res);
@@ -107,7 +96,6 @@ function cors({ origin = "*", credentials = false, headers, maxAge = 0, methods
107
96
  res.setHeader("Content-Length", "0");
108
97
  res.end();
109
98
  } else {
110
- // normal response (termasuk OPTIONS non-preflight)
111
99
  setAllowOrigin(req, res);
112
100
  setExposeHeaders(res);
113
101
  }
@@ -7,6 +7,7 @@ function json({ limit = 100 * 1024 } = {}) {
7
7
  }
8
8
  return value;
9
9
  };
10
+
10
11
  return async (req, res) => {
11
12
  if (!["POST", "PUT", "PATCH", "QUERY"].includes(req.method)) {
12
13
  return;
package/src/node.js CHANGED
@@ -1,6 +1,9 @@
1
1
  class Node {
2
2
  constructor(segment = "") {
3
3
  this.segment = segment;
4
+ if (segment.startsWith(":")) {
5
+ this.name = segment.slice(1);
6
+ }
4
7
  /** @type {Map<string, Node>} static children */
5
8
  this.children = new Map();
6
9
  /** @type {Map<string, Node>} param children, key = ':name' */
@@ -15,13 +18,11 @@ class Node {
15
18
  if (!this.wildcard) this.wildcard = new Node("*");
16
19
  return this.wildcard;
17
20
  }
18
-
19
21
  if (segment.startsWith(":")) {
20
22
  return this.params.getOrInsertComputed(segment, (segment) => {
21
23
  return new Node(segment);
22
24
  });
23
25
  }
24
-
25
26
  return this.children.getOrInsertComputed(segment, (segment) => {
26
27
  return new Node(segment);
27
28
  });
@@ -36,15 +37,12 @@ class Node {
36
37
  });
37
38
  }
38
39
  }
39
-
40
40
  if (node.wildcard) {
41
41
  this.add("*").merge(node.wildcard, offset);
42
42
  }
43
-
44
43
  for (const [segment, child] of node.params) {
45
44
  this.add(segment).merge(child, offset);
46
45
  }
47
-
48
46
  for (const [segment, child] of node.children) {
49
47
  this.add(segment).merge(child, offset);
50
48
  }
@@ -52,25 +50,20 @@ class Node {
52
50
 
53
51
  getMaxSeq() {
54
52
  let max = -1;
55
-
56
53
  for (const [, entries] of this.handlers) {
57
54
  for (const entry of entries) {
58
55
  max = Math.max(max, entry.seq);
59
56
  }
60
57
  }
61
-
62
58
  if (this.wildcard) {
63
59
  max = Math.max(max, this.wildcard.getMaxSeq());
64
60
  }
65
-
66
61
  for (const [, child] of this.params) {
67
62
  max = Math.max(max, child.getMaxSeq());
68
63
  }
69
-
70
64
  for (const [, child] of this.children) {
71
65
  max = Math.max(max, child.getMaxSeq());
72
66
  }
73
-
74
67
  return max;
75
68
  }
76
69
  }
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,7 +20,6 @@ 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
 
@@ -32,7 +30,6 @@ class Request extends http.IncomingMessage {
32
30
  if (!this._controller) {
33
31
  this._controller = new AbortController();
34
32
  }
35
-
36
33
  return this._controller;
37
34
  }
38
35
  }
package/src/response.js CHANGED
@@ -10,7 +10,7 @@ class Response extends http.ServerResponse {
10
10
  if (this.writableEnded) return;
11
11
  this.end(chunk, null, null);
12
12
  }
13
-
13
+
14
14
  json(value) {
15
15
  if (!this.headersSent) {
16
16
  this.setHeader("Content-Type", "application/json; charset=utf-8");
package/src/router.js CHANGED
@@ -17,15 +17,12 @@ class Router {
17
17
  handlers = [path, ...handlers];
18
18
  path = null;
19
19
  }
20
-
21
20
  let node = this.root;
22
-
23
21
  if (path) {
24
22
  for (const segment of this.split(path)) {
25
23
  node = node.add(segment);
26
24
  }
27
25
  }
28
-
29
26
  for (const handler of handlers) {
30
27
  if (handler.root) {
31
28
  node.merge(handler.root, this.seq);
@@ -104,62 +101,66 @@ class Router {
104
101
  * @param {{ best: { nodes: Node[], params: Record<string, string> } }} state
105
102
  * @returns {{ nodes: Node[], params: Record<string, string>, matched: boolean }}
106
103
  */
107
- search(node, segments, index, path, params, state,method) {
104
+ search(method, segments, node, path = [], params = {}, state = { path: [], params: {} }, index = 0) {
108
105
  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
106
  if (index === segments.length) {
115
- if (node.handlers.has(method)) {
116
- return { nodes: path.slice(), params: { ...params }, matched: true };
107
+ if (node.handlers.has(method) || node.handlers.has("ALL")) {
108
+ return { path, params, matched: true };
117
109
  }
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 } };
110
+ if (path.length > state.path.length) {
111
+ state.path = path.slice();
112
+ state.params = { ...params };
121
113
  }
122
114
  path.pop();
123
- return { matched: false };
115
+ return { path: state.path, params: state.params, matched: false };
124
116
  }
125
-
126
117
  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);
118
+ if (node.children.has(segment)) {
119
+ const res = this.search(
120
+ //
121
+ method,
122
+ segments,
123
+ node.children.get(segment),
124
+ path,
125
+ params,
126
+ state,
127
+ index + 1,
128
+ );
132
129
  if (res.matched) return res;
133
130
  }
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;
131
+ if (node.params.size) {
132
+ for (const [, child] of node.params) {
133
+ const prev = params[child.name];
134
+ params[child.name] = decodeParam(segment);
135
+ const res = this.search(
136
+ //
137
+ method,
138
+ segments,
139
+ child,
140
+ path,
141
+ params,
142
+ state,
143
+ index + 1,
144
+ );
145
+ if (res.matched) return res;
146
+ if (prev === undefined) {
147
+ const { [child.name]: _, ...rest } = params;
148
+ params = rest;
149
+ } else params[child.name] = prev;
150
+ }
145
151
  }
146
-
147
- // 3. wildcard (prioritas terendah, menyerap sisa segmen)
148
- if (node.wildcard) {
152
+ if (node.wildcard && (node.wildcard.handlers.get(method) || node.wildcard.handlers.get("ALL"))) {
149
153
  const rest = segments.slice(index).join("/");
150
154
  params["wildcard"] = decodeParam(rest);
151
155
  path.push(node.wildcard);
152
- return { nodes: path.slice(), params: { ...params }, matched: true };
156
+ return { path, params, matched: true };
153
157
  }
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 } };
158
+ if (path.length > state.path.length) {
159
+ state.path = path.slice();
160
+ state.params = { ...params };
159
161
  }
160
-
161
162
  path.pop();
162
- return { matched: false };
163
+ return { path: state.path, params: state.params, matched: false };
163
164
  }
164
165
 
165
166
  collectHandler(r, method, handlers) {
@@ -170,49 +171,30 @@ class Router {
170
171
  }
171
172
  }
172
173
 
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
-
181
174
  collectMiddleware(r, handlers, errorHandlers) {
182
175
  this.collectHandler(r, "ALL", handlers);
183
- this.collectErrorHandler(r, errorHandlers);
176
+ this.collectHandler(r, "CATCH", errorHandlers);
184
177
  }
185
178
 
186
179
  find(method, path) {
187
- const segments = this.split(path);
188
-
189
- const params = {};
180
+ const res = this.search(
181
+ //
182
+ method,
183
+ this.split(path),
184
+ this.root,
185
+ );
190
186
  const handlers = {};
191
187
  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) {
188
+ for (const r of res.path) {
203
189
  this.collectMiddleware(r, handlers, errorHandlers);
204
190
  }
205
-
206
- // method handler HANYA kalau full match
207
191
  if (res.matched) {
208
- const leaf = matchedNodes[matchedNodes.length - 1];
209
- this.collectHandler(leaf, method, handlers);
192
+ this.collectHandler(res.path.at(-1), method, handlers);
210
193
  }
211
-
212
194
  return {
213
195
  matched: res.matched,
214
- params: finalParams,
215
- paramsLength: Object.keys(finalParams).length,
196
+ params: res.params,
197
+ paramsLength: Object.keys(res.params).length,
216
198
  handlers: Object.values(handlers),
217
199
  errorHandlers: Object.values(errorHandlers),
218
200
  };
package/src/util.js CHANGED
@@ -1,5 +1,4 @@
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
 
5
4
  function decodeParam(raw) {
@@ -13,7 +12,6 @@ exports.decodeParam = decodeParam;
13
12
 
14
13
  function decodeQuery(raw) {
15
14
  const normalized = raw.replace(/\+/g, " ");
16
-
17
15
  try {
18
16
  return decodeURIComponent(normalized);
19
17
  } catch (error) {
@@ -23,17 +21,13 @@ function decodeQuery(raw) {
23
21
 
24
22
  function parseQueryString(queryString) {
25
23
  if (queryString === "") return Object.create(null);
26
-
27
24
  const regex = /([^=&]*)=([^&]*)/g;
28
25
  const result = Object.create(null);
29
-
30
26
  let match;
31
27
  while ((match = regex.exec(queryString))) {
32
28
  const [, rawName, rawValue] = match;
33
-
34
29
  const name = decodeQuery(rawName);
35
30
  const value = decodeQuery(rawValue);
36
-
37
31
  if (Object.hasOwn(result, name)) {
38
32
  if (Array.isArray(result[name])) result[name].push(value);
39
33
  else result[name] = [result[name], value];
@@ -48,10 +42,8 @@ function parseUrl(url) {
48
42
  if (!(url.includes("?") || url.includes("#"))) {
49
43
  return { path: url, queryString: "" };
50
44
  }
51
-
52
45
  const regex = /^([^?#]*)(\?([^#]*))?(#.*)?$/;
53
46
  const [, path = "/", , queryString = ""] = regex.exec(url);
54
-
55
47
  return { path, queryString };
56
48
  }
57
49
  exports.parseUrl = parseUrl;