@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 +35 -42
- package/package.json +2 -2
- package/src/app.js +1 -12
- package/src/middleware/cors.js +0 -12
- package/src/middleware/json.js +1 -0
- package/src/node.js +3 -10
- package/src/polyfill.js +1 -3
- package/src/request.js +0 -3
- package/src/response.js +1 -1
- package/src/router.js +55 -73
- package/src/util.js +1 -9
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
|
|
54
|
-
|
|
|
55
|
-
| `App`
|
|
53
|
+
| Export | Keterangan |
|
|
54
|
+
| ------------ | ---------------------------------------- |
|
|
55
|
+
| `App` | Router + HTTP server (`listen`) |
|
|
56
56
|
| `App.Router` | Router tanpa server, untuk nested router |
|
|
57
|
-
| `App.json`
|
|
58
|
-
| `App.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
|
|
240
|
-
|
|
|
241
|
-
| `req.path`
|
|
242
|
-
| `req.query`
|
|
243
|
-
| `req.params`
|
|
244
|
-
| `req.body`
|
|
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
|
|
261
|
-
|
|
|
262
|
-
| `res.status(code)` | Set status code, mengembalikan `res` sehingga bisa di-chain
|
|
263
|
-
| `res.send(chunk)`
|
|
264
|
-
| `res.json(value)`
|
|
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
|
|
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
|
|
307
|
-
|
|
|
308
|
-
| `origin`
|
|
309
|
-
| `credentials` | `false` | Mengirim `Access-Control-Allow-Credentials: true`
|
|
310
|
-
| `headers`
|
|
311
|
-
| `methods`
|
|
312
|
-
| `maxAge`
|
|
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
|
|
333
|
-
|
|
|
334
|
-
| `requestTimeout` | `0`
|
|
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
|
-
|
|
|
388
|
-
|
|
|
389
|
-
| Fastify
|
|
390
|
-
| **@ndiinginc/router** | 29.735
|
|
391
|
-
| Express
|
|
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.
|
|
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": "^
|
|
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;
|
package/src/middleware/cors.js
CHANGED
|
@@ -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
|
}
|
package/src/middleware/json.js
CHANGED
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
|
-
|
|
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
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(
|
|
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 {
|
|
107
|
+
if (node.handlers.has(method) || node.handlers.has("ALL")) {
|
|
108
|
+
return { path, params, matched: true };
|
|
117
109
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
state.
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 {
|
|
156
|
+
return { path, params, matched: true };
|
|
153
157
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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.
|
|
176
|
+
this.collectHandler(r, "CATCH", errorHandlers);
|
|
184
177
|
}
|
|
185
178
|
|
|
186
179
|
find(method, path) {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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:
|
|
215
|
-
paramsLength: Object.keys(
|
|
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
|
-
|
|
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;
|