@ndiinginc/router 4.10.0 → 4.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -7
- package/package.json +1 -1
- package/src/router.js +16 -7
package/README.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# @ndiinginc/router
|
|
2
2
|
|
|
3
|
-
Router HTTP kecil, cepat, dan **zero-dependency** untuk Node.js. Dibangun langsung di atas `node:http`, dengan routing berbasis tree,
|
|
3
|
+
Router HTTP kecil, cepat, dan **zero-dependency** untuk Node.js. Dibangun langsung di atas `node:http`, dengan routing berbasis tree, nested router, middleware async, error handler, timeout per request, serta middleware `json` dan `cors` bawaan.
|
|
4
4
|
|
|
5
5
|
## Fitur
|
|
6
6
|
|
|
7
7
|
- Zero dependency, murni `node:http`
|
|
8
8
|
- Routing berbasis tree per segmen: static, lalu `:param`, lalu wildcard `*`, dengan backtracking
|
|
9
9
|
- Parameter dinamis `:id` dan wildcard `*` yang menangkap sisa path
|
|
10
|
-
- LRU cache untuk hasil lookup route
|
|
11
10
|
- Nested router dengan `use(path, router)`
|
|
12
11
|
- Middleware global dan per-path, mendukung `async`
|
|
13
12
|
- Error handler dengan signature `(err, req, res)`
|
|
@@ -119,6 +118,7 @@ Wildcard butuh minimal satu segmen tersisa, jadi `/files/*` tidak cocok dengan `
|
|
|
119
118
|
|
|
120
119
|
- Di setiap level, router mencoba **static dulu, lalu parameter (sesuai urutan pendaftaran), lalu wildcard**. Jadi `/users/me` menang atas `/users/:id` bila keduanya terdaftar.
|
|
121
120
|
- Jika sebuah cabang gagal di tengah jalan, router mundur (backtrack) dan mencoba cabang berikutnya.
|
|
121
|
+
- Pemilihan cabang memperhatikan method. Jika route static tidak punya handler untuk method request, router mencoba cabang parameter atau wildcard. Contoh: dengan `GET /users/me` dan `PUT /users/:id`, request `PUT /users/me` ditangani oleh `PUT /users/:id`.
|
|
122
122
|
- Trailing slash dan slash ganda diabaikan: `/users`, `/users/`, dan `//users` dianggap sama.
|
|
123
123
|
|
|
124
124
|
## Middleware
|
|
@@ -212,7 +212,7 @@ Jika tidak ada handler yang mengirim response, router mengirim JSON otomatis:
|
|
|
212
212
|
|
|
213
213
|
## Timeout
|
|
214
214
|
|
|
215
|
-
Aktifkan lewat opsi `requestTimeout` (milidetik) pada `App`. Jika seluruh chain handler melebihi waktu tersebut, `res.statusCode` diset `
|
|
215
|
+
Aktifkan lewat opsi `requestTimeout` (milidetik) pada `App`. Jika seluruh chain handler melebihi waktu tersebut, `res.statusCode` diset `504` dan error `Gateway Timeout` dilempar ke error handler.
|
|
216
216
|
|
|
217
217
|
```js
|
|
218
218
|
const app = new App({ requestTimeout: 5000 });
|
|
@@ -326,15 +326,14 @@ Perilaku:
|
|
|
326
326
|
## Opsi
|
|
327
327
|
|
|
328
328
|
```js
|
|
329
|
-
const app = new App({
|
|
329
|
+
const app = new App({ requestTimeout: 5000 });
|
|
330
330
|
```
|
|
331
331
|
|
|
332
332
|
| Opsi | Default | Keterangan |
|
|
333
333
|
| --- | --- | --- |
|
|
334
|
-
| `cacheSize` | `1000` | Jumlah maksimum entri LRU cache untuk hasil lookup `METHOD path`. `0` menonaktifkan cache. Cache dikosongkan setiap ada route baru didaftarkan |
|
|
335
334
|
| `requestTimeout` | `0` | Batas waktu handler dalam ms (khusus `App`). Lihat bagian [Timeout](#timeout) |
|
|
336
335
|
|
|
337
|
-
`Router` hanya
|
|
336
|
+
`Router` tidak memiliki opsi sendiri; `requestTimeout` hanya dipakai oleh `App`.
|
|
338
337
|
|
|
339
338
|
## API
|
|
340
339
|
|
|
@@ -352,7 +351,7 @@ Mendaftarkan handler untuk method tertentu. `path` opsional; jika dihilangkan, h
|
|
|
352
351
|
|
|
353
352
|
### `router.find(method, path)`
|
|
354
353
|
|
|
355
|
-
Mencari route tanpa menjalankannya.
|
|
354
|
+
Mencari route tanpa menjalankannya. Berguna untuk testing.
|
|
356
355
|
|
|
357
356
|
```js
|
|
358
357
|
const { matched, params, paramsLength, handlers, errorHandlers } = app.find("GET", "/users/1");
|
|
@@ -367,6 +366,32 @@ const server = app.listen(0); // port acak
|
|
|
367
366
|
console.log(server.address().port);
|
|
368
367
|
```
|
|
369
368
|
|
|
369
|
+
### Server sendiri
|
|
370
|
+
|
|
371
|
+
`App.SERVER_OPTIONS` berisi opsi `IncomingMessage` dan `ServerResponse` yang dipakai `listen()`. Jika ingin membuat server sendiri (misalnya HTTPS), sertakan opsi itu dan panggil `app.handle(req, res)`:
|
|
372
|
+
|
|
373
|
+
```js
|
|
374
|
+
const https = require("https");
|
|
375
|
+
|
|
376
|
+
const server = https.createServer(
|
|
377
|
+
{ ...App.SERVER_OPTIONS, key, cert },
|
|
378
|
+
(req, res) => app.handle(req, res),
|
|
379
|
+
);
|
|
380
|
+
server.listen(443);
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Benchmark
|
|
384
|
+
|
|
385
|
+
Hello world dengan satu route `GET /`, diuji memakai `autocannon -c 100 -d 40 -p 10` di mesin lokal.
|
|
386
|
+
|
|
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 |
|
|
392
|
+
|
|
393
|
+
Hasil bervariasi tergantung mesin, versi Node.js, dan jumlah route. Benchmark ini hanya menggambarkan overhead dasar, bukan performa aplikasi nyata.
|
|
394
|
+
|
|
370
395
|
## Testing
|
|
371
396
|
|
|
372
397
|
```bash
|
package/package.json
CHANGED
package/src/router.js
CHANGED
|
@@ -13,7 +13,6 @@ class Router {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
add(method, path, ...handlers) {
|
|
16
|
-
|
|
17
16
|
if (typeof path === "function") {
|
|
18
17
|
handlers = [path, ...handlers];
|
|
19
18
|
path = null;
|
|
@@ -105,12 +104,23 @@ class Router {
|
|
|
105
104
|
* @param {{ best: { nodes: Node[], params: Record<string, string> } }} state
|
|
106
105
|
* @returns {{ nodes: Node[], params: Record<string, string>, matched: boolean }}
|
|
107
106
|
*/
|
|
108
|
-
search(node, segments, index, path, params, state) {
|
|
107
|
+
search(node, segments, index, path, params, state,method) {
|
|
109
108
|
path.push(node);
|
|
110
109
|
|
|
111
110
|
// habis segmen -> full match
|
|
111
|
+
// if (index === segments.length) {
|
|
112
|
+
// return { nodes: path.slice(), params: { ...params }, matched: true };
|
|
113
|
+
// }
|
|
112
114
|
if (index === segments.length) {
|
|
113
|
-
|
|
115
|
+
if (node.handlers.has(method)) {
|
|
116
|
+
return { nodes: path.slice(), params: { ...params }, matched: true };
|
|
117
|
+
}
|
|
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 } };
|
|
121
|
+
}
|
|
122
|
+
path.pop();
|
|
123
|
+
return { matched: false };
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
const segment = segments[index];
|
|
@@ -118,7 +128,7 @@ class Router {
|
|
|
118
128
|
// 1. static (prioritas tertinggi)
|
|
119
129
|
const staticChild = node.children.get(segment);
|
|
120
130
|
if (staticChild) {
|
|
121
|
-
const res = this.search(staticChild, segments, index + 1, path, params, state);
|
|
131
|
+
const res = this.search(staticChild, segments, index + 1, path, params, state,method);
|
|
122
132
|
if (res.matched) return res;
|
|
123
133
|
}
|
|
124
134
|
|
|
@@ -127,7 +137,7 @@ class Router {
|
|
|
127
137
|
const name = key.slice(1); // buang ':'
|
|
128
138
|
const prev = params[name];
|
|
129
139
|
params[name] = decodeParam(segment);
|
|
130
|
-
const res = this.search(child, segments, index + 1, path, params, state);
|
|
140
|
+
const res = this.search(child, segments, index + 1, path, params, state,method);
|
|
131
141
|
if (res.matched) return res;
|
|
132
142
|
// backtrack param value
|
|
133
143
|
if (prev === undefined) delete params[name];
|
|
@@ -174,7 +184,6 @@ class Router {
|
|
|
174
184
|
}
|
|
175
185
|
|
|
176
186
|
find(method, path) {
|
|
177
|
-
|
|
178
187
|
const segments = this.split(path);
|
|
179
188
|
|
|
180
189
|
const params = {};
|
|
@@ -184,7 +193,7 @@ class Router {
|
|
|
184
193
|
/** @type {{ best: { nodes: Node[], params: Record<string, string> } }} */
|
|
185
194
|
const state = { best: { nodes: [], params: {} } };
|
|
186
195
|
|
|
187
|
-
const res = this.search(this.root, segments, 0, [], params, state);
|
|
196
|
+
const res = this.search(this.root, segments, 0, [], params, state,method);
|
|
188
197
|
|
|
189
198
|
const matchedNodes = res.matched ? res.nodes : state.best.nodes;
|
|
190
199
|
const finalParams = res.matched ? res.params : state.best.params;
|