@ndiinginc/router 4.6.0 → 4.6.2

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
@@ -51,19 +51,23 @@ app.listen(3000);
51
51
 
52
52
  Yang diekspor adalah class `App`, dengan tiga properti tambahan:
53
53
 
54
- | Export | Keterangan |
55
- | ------------ | ---------------------------------------- |
56
- | `App` | Router + HTTP server (`listen`) |
54
+ | Export | Keterangan |
55
+ | --- | --- |
56
+ | `App` | Router + HTTP server (`listen`) |
57
57
  | `App.Router` | Router tanpa server, untuk nested router |
58
- | `App.json` | Middleware JSON body parser |
59
- | `App.cors` | Middleware CORS |
58
+ | `App.json` | Middleware JSON body parser |
59
+ | `App.cors` | Middleware CORS |
60
60
 
61
61
  ## Routing
62
62
 
63
63
  Semua method mengembalikan `this`, jadi bisa di-chain.
64
64
 
65
65
  ```js
66
- app.get("/users", handler).post("/users", handler).put("/users/:id", handler).patch("/users/:id", handler).delete("/users/:id", handler);
66
+ app.get("/users", handler)
67
+ .post("/users", handler)
68
+ .put("/users/:id", handler)
69
+ .patch("/users/:id", handler)
70
+ .delete("/users/:id", handler);
67
71
  ```
68
72
 
69
73
  Method yang tersedia: `get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `trace`, `connect`, `query`, dan `use` (semua method).
@@ -194,18 +198,18 @@ Jika tidak ada handler yang mengirim response, router mengirim JSON otomatis:
194
198
 
195
199
  ## Timeout
196
200
 
197
- Aktifkan lewat opsi `timeout` (milidetik) pada `App`. Jika seluruh chain handler melebihi waktu tersebut, error `504 Gateway Timeout` dilempar dan diteruskan ke error handler.
201
+ Aktifkan lewat opsi `requestTimeout` (milidetik) pada `App`. Jika seluruh chain handler melebihi waktu tersebut, error `504 Gateway Timeout` dilempar dan diteruskan ke error handler.
198
202
 
199
203
  ```js
200
- const app = new App({ timeout: 5000 });
204
+ const app = new App({ requestTimeout: 5000 });
201
205
  ```
202
206
 
203
- - Tanpa opsi `timeout` (atau bernilai `0`), tidak ada batas waktu
207
+ - Tanpa opsi `requestTimeout` (atau bernilai `0`), tidak ada batas waktu
204
208
  - Handler yang sudah berjalan tidak dihentikan paksa. Tetapi setelah response dikirim, `res.json()` dan `res.send()` mengabaikan penulisan berikutnya, dan handler selanjutnya di chain tidak dipanggil
205
209
 
206
210
  ### `req.signal`
207
211
 
208
- Saat `timeout` aktif, `req.signal` adalah `AbortSignal` yang di-abort ketika timeout terjadi (reason-nya error 504). Teruskan ke operasi yang mendukungnya agar pekerjaan ikut dibatalkan:
212
+ Saat `requestTimeout` aktif, `req.signal` adalah `AbortSignal` yang di-abort ketika timeout terjadi (reason-nya error 504) atau ketika salah satu handler melempar error (reason-nya error tersebut). Teruskan ke operasi yang mendukungnya agar pekerjaan ikut dibatalkan:
209
213
 
210
214
  ```js
211
215
  app.get("/proxy", async (req, res) => {
@@ -214,17 +218,17 @@ app.get("/proxy", async (req, res) => {
214
218
  });
215
219
  ```
216
220
 
217
- `req.signal` bernilai `undefined` jika `timeout` tidak diaktifkan.
221
+ `req.signal` bernilai `undefined` jika `requestTimeout` tidak diaktifkan.
218
222
 
219
223
  ## Request
220
224
 
221
- | Properti | Keterangan |
222
- | ------------ | -------------------------------------------------------------------------- |
223
- | `req.path` | Pathname tanpa query string |
224
- | `req.query` | Object query string, di-parse secara lazy. Key yang berulang menjadi array |
225
- | `req.params` | Parameter dari route dinamis (sudah di-decode) |
226
- | `req.body` | Hasil parse body (butuh middleware `json()`) |
227
- | `req.signal` | `AbortSignal` yang di-abort saat timeout (hanya jika `timeout` aktif) |
225
+ | Properti | Keterangan |
226
+ | --- | --- |
227
+ | `req.path` | Pathname tanpa query string |
228
+ | `req.query` | Object query string, di-parse secara lazy. Key yang berulang menjadi array, dan `+` di-decode menjadi spasi |
229
+ | `req.params` | Parameter dari route dinamis (sudah di-decode) |
230
+ | `req.body` | Hasil parse body (butuh middleware `json()`) |
231
+ | `req.signal` | `AbortSignal` yang di-abort saat timeout atau error handler (hanya jika `requestTimeout` aktif) |
228
232
 
229
233
  ```js
230
234
  // GET /search?tag=a&tag=b&page=2
@@ -239,11 +243,11 @@ app.get("/search", (req, res) => {
239
243
 
240
244
  ## Response
241
245
 
242
- | Method | Keterangan |
243
- | ------------------ | ----------------------------------------------------------- |
246
+ | Method | Keterangan |
247
+ | --- | --- |
244
248
  | `res.status(code)` | Set status code, mengembalikan `res` sehingga bisa di-chain |
245
- | `res.send(chunk)` | Kirim response dan akhiri |
246
- | `res.json(value)` | Set `Content-Type: application/json`, serialize, lalu kirim |
249
+ | `res.send(chunk)` | Kirim response dan akhiri |
250
+ | `res.json(value)` | Set `Content-Type: application/json`, serialize, lalu kirim |
247
251
 
248
252
  ```js
249
253
  res.status(201).json({ created: true });
@@ -259,8 +263,8 @@ res.status(201).json({ created: true });
259
263
  app.use(App.json({ limit: 100 * 1024 })); // default 100 KB
260
264
  ```
261
265
 
262
- | Opsi | Default | Keterangan |
263
- | ------- | -------- | ------------------------------- |
266
+ | Opsi | Default | Keterangan |
267
+ | --- | --- | --- |
264
268
  | `limit` | `102400` | Ukuran body maksimum dalam byte |
265
269
 
266
270
  Perilaku:
@@ -285,19 +289,19 @@ app.use(
285
289
  );
286
290
  ```
287
291
 
288
- | Opsi | Default | Keterangan |
289
- | ------------- | ------- | -------------------------------------------------------------- |
290
- | `origin` | `"*"` | `"*"`, string origin, atau `null` |
291
- | `credentials` | `false` | Mengirim `Access-Control-Allow-Credentials: true` |
292
- | `headers` | - | `"*"` atau array nama header |
293
- | `methods` | - | `"*"` atau array nama method |
294
- | `maxAge` | `0` | Lama cache preflight (detik). `0` berarti header tidak dikirim |
292
+ | Opsi | Default | Keterangan |
293
+ | --- | --- | --- |
294
+ | `origin` | `"*"` | `"*"`, satu origin, daftar dipisah koma, pola dengan `*` (misalnya `https://*.example.com`), atau `null` |
295
+ | `credentials` | `false` | Mengirim `Access-Control-Allow-Credentials: true` |
296
+ | `headers` | - | `"*"` atau array nama header |
297
+ | `methods` | - | `"*"` atau array nama method |
298
+ | `maxAge` | `0` | Lama cache preflight (detik). `0` berarti header tidak dikirim |
295
299
 
296
300
  Perilaku:
297
301
 
298
302
  - Request `OPTIONS` dijawab langsung dengan `204` dan chain berhenti
299
303
  - `Access-Control-Allow-Headers` dan `Access-Control-Allow-Methods` hanya dikirim pada preflight, jika nilai yang diminta ada di daftar
300
- - Dengan `origin: "*"` dan `credentials: true`, origin dari request dipantulkan dan header `Vary: Origin` ditambahkan
304
+ - Jika `origin` berupa daftar atau pola (atau `"*"` dengan `credentials: true`), origin dari request dipantulkan bila cocok. Pencocokan dilakukan pada seluruh string (tanpa memperhatikan huruf besar/kecil), dan header `Vary: Origin` ditambahkan pada request non-preflight
301
305
  - Nilai `"*"` untuk `headers` dan `methods` diabaikan saat `credentials: true`, sesuai spesifikasi CORS
302
306
  - Pada request non-preflight, `headers` (jika array) dikirim sebagai `Access-Control-Expose-Headers`
303
307
 
@@ -306,21 +310,21 @@ Perilaku:
306
310
  ## Opsi
307
311
 
308
312
  ```js
309
- const app = new App({ size: 1000, timeout: 5000 });
313
+ const app = new App({ cacheSize: 1000, requestTimeout: 5000 });
310
314
  ```
311
315
 
312
- | Opsi | Default | Keterangan |
313
- | --------- | ------- | ----------------------------------------------------------------------------- |
314
- | `size` | `1000` | Jumlah maksimum entri LRU cache untuk hasil lookup `METHOD path` |
315
- | `timeout` | - | Batas waktu handler dalam ms (khusus `App`). Lihat bagian [Timeout](#timeout) |
316
+ | Opsi | Default | Keterangan |
317
+ | --- | --- | --- |
318
+ | `cacheSize` | `1000` | Jumlah maksimum entri LRU cache untuk hasil lookup `METHOD path` |
319
+ | `requestTimeout` | - | Batas waktu handler dalam ms (khusus `App`). Lihat bagian [Timeout](#requestTimeout) |
316
320
 
317
- `Router` hanya menerima `size`.
321
+ `Router` hanya menerima `cacheSize`.
318
322
 
319
323
  ## API
320
324
 
321
325
  ### `new App([options])` / `new Router([options])`
322
326
 
323
- Membuat instance baru. `App` adalah `Router` ditambah `listen()` dan dukungan `timeout`.
327
+ Membuat instance baru. `App` adalah `Router` ditambah `listen()` dan dukungan `requestTimeout`.
324
328
 
325
329
  ### `router.use([path], ...handlers | router)`
326
330
 
@@ -351,11 +355,11 @@ console.log(server.address().port);
351
355
 
352
356
  Hello world dengan satu route `GET /`, diuji memakai `autocannon -c 100 -d 40 -p 10` di mesin lokal.
353
357
 
354
- | | Req/sec (avg) | Latency (avg) |
355
- | --------------------- | ------------- | ------------- |
356
- | Fastify | 31.475 | 31,27 ms |
357
- | **@ndiinginc/router** | 29.735 | 33,12 ms |
358
- | Express | 20.211 | 48,93 ms |
358
+ | | Req/sec (avg) | Latency (avg) |
359
+ | --- | --- | --- |
360
+ | Fastify | 31.475 | 31,27 ms |
361
+ | **@ndiinginc/router** | 29.735 | 33,12 ms |
362
+ | Express | 20.211 | 48,93 ms |
359
363
 
360
364
  Hasil bervariasi tergantung mesin, versi Node.js, dan jumlah route. Benchmark ini hanya menggambarkan overhead dasar, bukan performa aplikasi nyata.
361
365
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ndiinginc/router",
3
- "version": "4.6.0",
3
+ "version": "4.6.2",
4
4
  "description": "",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
package/src/app.js CHANGED
@@ -19,7 +19,7 @@ class App extends Router {
19
19
 
20
20
  const timeout = setTimeout(() => {
21
21
  controller.abort(ErrorResponse.status(504));
22
- }, this.timeout);
22
+ }, this.requestTimeout);
23
23
 
24
24
  let handleAbort;
25
25
  const timeoutPromise = new Promise((_, reject) => {
@@ -27,7 +27,13 @@ class App extends Router {
27
27
  signal.addEventListener("abort", handleAbort, { once: true });
28
28
  });
29
29
 
30
- await Promise.race([handlerPromise(), timeoutPromise]).finally(() => {
30
+ const promise = handlerPromise().catch((error) => {
31
+ if (!signal.aborted) {
32
+ controller.abort(error);
33
+ }
34
+ });
35
+
36
+ await Promise.race([promise, timeoutPromise]).finally(() => {
31
37
  signal.removeEventListener("abort", handleAbort);
32
38
  clearTimeout(timeout);
33
39
  });
@@ -48,7 +54,7 @@ class App extends Router {
48
54
  await handler(req, res);
49
55
  }
50
56
  };
51
- if (this.timeout) await this.withTimeout(req, handlerPromise);
57
+ if (this.requestTimeout) await this.withTimeout(req, handlerPromise);
52
58
  else await handlerPromise();
53
59
  } catch (error) {
54
60
  err = error;
package/src/request.js CHANGED
@@ -21,7 +21,7 @@ class Request extends http.IncomingMessage {
21
21
  get query() {
22
22
  if (this._query) return this._query;
23
23
  if (!this._parsed) {
24
- this._query = {};
24
+ this._query = Object.create(null);
25
25
  return this._query;
26
26
  }
27
27
  this._query = parseQueryString(this._parsed.queryString);
package/src/router.js CHANGED
@@ -1,10 +1,11 @@
1
1
  const { Node } = require("./node.js");
2
2
  const { Cache } = require("./cache.js");
3
+ const { decodePathParam } = require("./util.js");
3
4
 
4
5
  class Router {
5
6
  constructor(options = {}) {
6
- this.timeout = options.timeout ?? 0;
7
- this.cacheSize = options.size ?? 1000;
7
+ this.requestTimeout = options.requestTimeout ?? 0;
8
+ this.cacheSize = options.cacheSize ?? 1000;
8
9
  this.root = new Node();
9
10
  this.seq = 0;
10
11
  this.cache = new Cache(this.cacheSize);
@@ -103,12 +104,7 @@ class Router {
103
104
  if (match) {
104
105
  matched = child;
105
106
  for (const name in match.groups) {
106
- const raw = match.groups[name];
107
- try {
108
- params[name] = decodeURIComponent(raw);
109
- } catch {
110
- params[name] = raw;
111
- }
107
+ params[name] = decodePathParam(match.groups[name]);
112
108
  }
113
109
  break;
114
110
  }
package/src/util.js CHANGED
@@ -1,23 +1,31 @@
1
1
  const URL_REGEX = /^([^?#]*)(\?([^#]*))?(#.*)?$/;
2
2
  const QUERY_STRING_REGEX = /([^=&]*)?=([^&]*)?/g;
3
3
 
4
+ function decodeQueryParam(encoded) {
5
+ try {
6
+ return decodeURIComponent(encoded.replace(/\+/g, " "));
7
+ } catch (error) {
8
+ return encoded;
9
+ }
10
+ }
11
+ exports.decodeQueryParam = decodeQueryParam;
12
+
13
+ function decodePathParam(encoded) {
14
+ try {
15
+ return decodeURIComponent(encoded);
16
+ } catch {
17
+ return encoded;
18
+ }
19
+ }
20
+ exports.decodePathParam=decodePathParam
21
+
4
22
  function parseQueryString(queryString) {
5
23
  const result = Object.create(null);
6
24
  let match;
7
25
  while ((match = QUERY_STRING_REGEX.exec(queryString)) !== null) {
8
26
  const [, rawName = "", rawValue = ""] = match;
9
- let name;
10
- try {
11
- name = decodeURIComponent(rawName);
12
- } catch (error) {
13
- name = rawName;
14
- }
15
- let value;
16
- try {
17
- value = decodeURIComponent(rawValue);
18
- } catch (error) {
19
- value = rawValue;
20
- }
27
+ const name = decodeQueryParam(rawName);
28
+ const value = decodeQueryParam(rawValue);
21
29
  if (Object.hasOwn(result, name)) {
22
30
  if (Array.isArray(result[name])) result[name].push(value);
23
31
  else result[name] = [result[name], value];