@ndiinginc/router 4.9.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 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,20 @@ 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
+
370
383
  ## Benchmark
371
384
 
372
385
  Hello world dengan satu route `GET /`, diuji memakai `autocannon -c 100 -d 40 -p 10` di mesin lokal.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ndiinginc/router",
3
- "version": "4.9.0",
3
+ "version": "4.11.0",
4
4
  "description": "",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -21,6 +21,7 @@
21
21
  "@ndiinginc/commit": "^1.0.1",
22
22
  "@ndiinginc/test": "^1.17.0",
23
23
  "@types/jest": "^30.0.0",
24
- "@types/node": "^26.6.5"
24
+ "@types/node": "^26.6.5",
25
+ "autocannon": "^8.0.0"
25
26
  }
26
27
  }
package/src/app.js CHANGED
@@ -45,13 +45,16 @@ class App extends Router {
45
45
  res.statusCode = 404;
46
46
  err = new Error(http.STATUS_CODES[res.statusCode]);
47
47
  }
48
+
48
49
  if (res.statusCode < 400) {
49
50
  res.statusCode = 500;
50
51
  }
52
+
51
53
  const error = { message: err.message };
52
54
  if (process.env.NODE_ENV === "development") {
53
55
  error.stack = err.stack;
54
56
  }
57
+
55
58
  res.json({ error });
56
59
  }
57
60
  }
@@ -72,6 +75,7 @@ class App extends Router {
72
75
  await handler(req, res);
73
76
  }
74
77
  };
78
+
75
79
  if (this.requestTimeout) {
76
80
  await this.withTimeout(req, res, handlerPromise);
77
81
  } else {
@@ -79,6 +83,7 @@ class App extends Router {
79
83
  }
80
84
  } catch (error) {
81
85
  err = error;
86
+
82
87
  try {
83
88
  for (const handler of errorHandlers) {
84
89
  if (res.writableEnded) break;
package/src/polyfill.js CHANGED
@@ -1,3 +1,4 @@
1
+
1
2
  // Polyfill for Map.prototype.getOrInsert
2
3
  if (!Map.prototype.getOrInsert) {
3
4
  Map.prototype.getOrInsert = function (key, defaultValue) {
package/src/request.js CHANGED
@@ -21,6 +21,7 @@ class Request extends http.IncomingMessage {
21
21
  if (!this._query) {
22
22
  this._query = parseQueryString(this._queryString);
23
23
  }
24
+
24
25
  return this._query;
25
26
  }
26
27
 
@@ -31,6 +32,7 @@ class Request extends http.IncomingMessage {
31
32
  if (!this._controller) {
32
33
  this._controller = new AbortController();
33
34
  }
35
+
34
36
  return this._controller;
35
37
  }
36
38
  }
package/src/response.js CHANGED
@@ -5,10 +5,12 @@ class Response extends http.ServerResponse {
5
5
  this.statusCode = code;
6
6
  return this;
7
7
  }
8
+
8
9
  send(chunk) {
9
10
  if (this.writableEnded) return;
10
11
  this.end(chunk, null, null);
11
12
  }
13
+
12
14
  json(value) {
13
15
  if (!this.headersSent) {
14
16
  this.setHeader("Content-Type", "application/json; charset=utf-8");
package/src/router.js CHANGED
@@ -1,14 +1,11 @@
1
1
  const { decodeParam } = require("./util");
2
2
  const { Node } = require("./node");
3
- const { Cache } = require("./cache");
4
3
 
5
4
  class Router {
6
5
  constructor(options = {}) {
7
6
  this.requestTimeout = options.requestTimeout ?? 0;
8
- this.cacheSize = options.cacheSize ?? 1000;
9
7
  this.root = new Node();
10
8
  this.seq = 0;
11
- this.cache = new Cache(this.cacheSize);
12
9
  }
13
10
 
14
11
  split(path) {
@@ -16,8 +13,6 @@ class Router {
16
13
  }
17
14
 
18
15
  add(method, path, ...handlers) {
19
- this.cache.clear();
20
-
21
16
  if (typeof path === "function") {
22
17
  handlers = [path, ...handlers];
23
18
  path = null;
@@ -109,12 +104,23 @@ class Router {
109
104
  * @param {{ best: { nodes: Node[], params: Record<string, string> } }} state
110
105
  * @returns {{ nodes: Node[], params: Record<string, string>, matched: boolean }}
111
106
  */
112
- search(node, segments, index, path, params, state) {
107
+ search(node, segments, index, path, params, state,method) {
113
108
  path.push(node);
114
109
 
115
110
  // habis segmen -> full match
111
+ // if (index === segments.length) {
112
+ // return { nodes: path.slice(), params: { ...params }, matched: true };
113
+ // }
116
114
  if (index === segments.length) {
117
- 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 };
118
124
  }
119
125
 
120
126
  const segment = segments[index];
@@ -122,7 +128,7 @@ class Router {
122
128
  // 1. static (prioritas tertinggi)
123
129
  const staticChild = node.children.get(segment);
124
130
  if (staticChild) {
125
- const res = this.search(staticChild, segments, index + 1, path, params, state);
131
+ const res = this.search(staticChild, segments, index + 1, path, params, state,method);
126
132
  if (res.matched) return res;
127
133
  }
128
134
 
@@ -131,7 +137,7 @@ class Router {
131
137
  const name = key.slice(1); // buang ':'
132
138
  const prev = params[name];
133
139
  params[name] = decodeParam(segment);
134
- const res = this.search(child, segments, index + 1, path, params, state);
140
+ const res = this.search(child, segments, index + 1, path, params, state,method);
135
141
  if (res.matched) return res;
136
142
  // backtrack param value
137
143
  if (prev === undefined) delete params[name];
@@ -178,11 +184,6 @@ class Router {
178
184
  }
179
185
 
180
186
  find(method, path) {
181
- const key = `${method} ${path}`;
182
-
183
- const cached = this.cache.get(key);
184
- if (cached) return cached;
185
-
186
187
  const segments = this.split(path);
187
188
 
188
189
  const params = {};
@@ -192,7 +193,7 @@ class Router {
192
193
  /** @type {{ best: { nodes: Node[], params: Record<string, string> } }} */
193
194
  const state = { best: { nodes: [], params: {} } };
194
195
 
195
- const res = this.search(this.root, segments, 0, [], params, state);
196
+ const res = this.search(this.root, segments, 0, [], params, state,method);
196
197
 
197
198
  const matchedNodes = res.matched ? res.nodes : state.best.nodes;
198
199
  const finalParams = res.matched ? res.params : state.best.params;
@@ -208,19 +209,13 @@ class Router {
208
209
  this.collectHandler(leaf, method, handlers);
209
210
  }
210
211
 
211
- Object.freeze(finalParams);
212
-
213
- const result = {
212
+ return {
214
213
  matched: res.matched,
215
214
  params: finalParams,
216
215
  paramsLength: Object.keys(finalParams).length,
217
216
  handlers: Object.values(handlers),
218
217
  errorHandlers: Object.values(errorHandlers),
219
218
  };
220
-
221
- this.cache.set(key, result);
222
-
223
- return result;
224
219
  }
225
220
  }
226
221
  exports.Router = Router;
package/src/util.js CHANGED
@@ -1,4 +1,5 @@
1
- const EMPTY_PARAMS = Object.freeze(Object.create(null));
1
+ // const EMPTY_PARAMS = Object.freeze(Object.create(null));
2
+ const EMPTY_PARAMS = Object.create(null)
2
3
  exports.EMPTY_PARAMS = EMPTY_PARAMS;
3
4
 
4
5
  function decodeParam(raw) {
@@ -12,6 +13,7 @@ exports.decodeParam = decodeParam;
12
13
 
13
14
  function decodeQuery(raw) {
14
15
  const normalized = raw.replace(/\+/g, " ");
16
+
15
17
  try {
16
18
  return decodeURIComponent(normalized);
17
19
  } catch (error) {
@@ -21,18 +23,23 @@ function decodeQuery(raw) {
21
23
 
22
24
  function parseQueryString(queryString) {
23
25
  if (queryString === "") return Object.create(null);
26
+
24
27
  const regex = /([^=&]*)=([^&]*)/g;
25
- let match;
26
28
  const result = Object.create(null);
29
+
30
+ let match;
27
31
  while ((match = regex.exec(queryString))) {
28
32
  const [, rawName, rawValue] = match;
33
+
29
34
  const name = decodeQuery(rawName);
30
35
  const value = decodeQuery(rawValue);
36
+
31
37
  if (Object.hasOwn(result, name)) {
32
38
  if (Array.isArray(result[name])) result[name].push(value);
33
39
  else result[name] = [result[name], value];
34
40
  } else result[name] = value;
35
41
  }
42
+
36
43
  return result;
37
44
  }
38
45
  exports.parseQueryString = parseQueryString;
@@ -41,8 +48,10 @@ function parseUrl(url) {
41
48
  if (!(url.includes("?") || url.includes("#"))) {
42
49
  return { path: url, queryString: "" };
43
50
  }
51
+
44
52
  const regex = /^([^?#]*)(\?([^#]*))?(#.*)?$/;
45
53
  const [, path = "/", , queryString = ""] = regex.exec(url);
54
+
46
55
  return { path, queryString };
47
56
  }
48
57
  exports.parseUrl = parseUrl;
package/src/cache.js DELETED
@@ -1,28 +0,0 @@
1
- class Cache {
2
- constructor(size = 0) {
3
- this.size = size;
4
- this.map = new Map();
5
- }
6
-
7
- get(key) {
8
- if (!this.map.has(key)) return undefined;
9
- const value = this.map.get(key);
10
- this.map.delete(key);
11
- this.map.set(key, value);
12
- return value;
13
- }
14
-
15
- set(key, value) {
16
- if (this.size <= 0) return; // disabled
17
- if (this.map.has(key)) this.map.delete(key);
18
- else if (this.map.size >= this.size) {
19
- this.map.delete(this.map.keys().next().value);
20
- }
21
- this.map.set(key, value);
22
- }
23
-
24
- clear() {
25
- this.map.clear();
26
- }
27
- }
28
- exports.Cache = Cache;