@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.
Files changed (3) hide show
  1. package/README.md +32 -7
  2. package/package.json +1 -1
  3. 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, LRU cache untuk lookup route, nested router, middleware async, error handler, timeout per request, serta middleware `json` dan `cors` bawaan.
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 `502` dan error `Bad Gateway` dilempar ke error handler.
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({ cacheSize: 1000, requestTimeout: 5000 });
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 menerima `cacheSize`.
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. Hasilnya di-cache dan `params`-nya di-freeze. Berguna untuk testing.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ndiinginc/router",
3
- "version": "4.10.0",
3
+ "version": "4.11.0",
4
4
  "description": "",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
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
- return { nodes: path.slice(), params: { ...params }, matched: true };
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;