@ndiinginc/router 4.6.0 → 4.6.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 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).
@@ -205,7 +209,7 @@ const app = new App({ timeout: 5000 });
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 `timeout` 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) => {
@@ -218,13 +222,13 @@ app.get("/proxy", async (req, res) => {
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 `timeout` 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
 
@@ -309,10 +313,10 @@ Perilaku:
309
313
  const app = new App({ size: 1000, timeout: 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
+ | `size` | `1000` | Jumlah maksimum entri LRU cache untuk hasil lookup `METHOD path` |
319
+ | `timeout` | - | Batas waktu handler dalam ms (khusus `App`). Lihat bagian [Timeout](#timeout) |
316
320
 
317
321
  `Router` hanya menerima `size`.
318
322
 
@@ -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.1",
4
4
  "description": "",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
package/src/app.js CHANGED
@@ -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
  });
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,5 +1,6 @@
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 = {}) {
@@ -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];