@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 +41 -37
- package/package.json +1 -1
- package/src/app.js +7 -1
- package/src/request.js +1 -1
- package/src/router.js +2 -6
- package/src/util.js +20 -12
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
|
|
55
|
-
|
|
|
56
|
-
| `App`
|
|
54
|
+
| Export | Keterangan |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `App` | Router + HTTP server (`listen`) |
|
|
57
57
|
| `App.Router` | Router tanpa server, untuk nested router |
|
|
58
|
-
| `App.json`
|
|
59
|
-
| `App.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)
|
|
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
|
|
222
|
-
|
|
|
223
|
-
| `req.path`
|
|
224
|
-
| `req.query`
|
|
225
|
-
| `req.params` | Parameter dari route dinamis (sudah di-decode)
|
|
226
|
-
| `req.body`
|
|
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
|
|
243
|
-
|
|
|
246
|
+
| Method | Keterangan |
|
|
247
|
+
| --- | --- |
|
|
244
248
|
| `res.status(code)` | Set status code, mengembalikan `res` sehingga bisa di-chain |
|
|
245
|
-
| `res.send(chunk)`
|
|
246
|
-
| `res.json(value)`
|
|
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
|
|
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
|
|
289
|
-
|
|
|
290
|
-
| `origin`
|
|
291
|
-
| `credentials` | `false` | Mengirim `Access-Control-Allow-Credentials: true`
|
|
292
|
-
| `headers`
|
|
293
|
-
| `methods`
|
|
294
|
-
| `maxAge`
|
|
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
|
-
-
|
|
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
|
|
313
|
-
|
|
|
314
|
-
| `size`
|
|
315
|
-
| `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
|
-
|
|
|
355
|
-
|
|
|
356
|
-
| Fastify
|
|
357
|
-
| **@ndiinginc/router** | 29.735
|
|
358
|
-
| Express
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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];
|