@ndiinginc/router 4.9.0 → 4.10.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 +0 -12
- package/package.json +3 -2
- package/src/app.js +5 -0
- package/src/polyfill.js +1 -0
- package/src/request.js +2 -0
- package/src/response.js +2 -0
- package/src/router.js +1 -15
- package/src/util.js +11 -2
- package/src/cache.js +0 -28
package/README.md
CHANGED
|
@@ -367,18 +367,6 @@ const server = app.listen(0); // port acak
|
|
|
367
367
|
console.log(server.address().port);
|
|
368
368
|
```
|
|
369
369
|
|
|
370
|
-
## Benchmark
|
|
371
|
-
|
|
372
|
-
Hello world dengan satu route `GET /`, diuji memakai `autocannon -c 100 -d 40 -p 10` di mesin lokal.
|
|
373
|
-
|
|
374
|
-
| | Req/sec (avg) | Latency (avg) |
|
|
375
|
-
| --- | --- | --- |
|
|
376
|
-
| Fastify | 31.475 | 31,27 ms |
|
|
377
|
-
| **@ndiinginc/router** | 29.735 | 33,12 ms |
|
|
378
|
-
| Express | 20.211 | 48,93 ms |
|
|
379
|
-
|
|
380
|
-
Hasil bervariasi tergantung mesin, versi Node.js, dan jumlah route. Benchmark ini hanya menggambarkan overhead dasar, bukan performa aplikasi nyata.
|
|
381
|
-
|
|
382
370
|
## Testing
|
|
383
371
|
|
|
384
372
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ndiinginc/router",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.10.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
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,7 +13,6 @@ class Router {
|
|
|
16
13
|
}
|
|
17
14
|
|
|
18
15
|
add(method, path, ...handlers) {
|
|
19
|
-
this.cache.clear();
|
|
20
16
|
|
|
21
17
|
if (typeof path === "function") {
|
|
22
18
|
handlers = [path, ...handlers];
|
|
@@ -178,10 +174,6 @@ class Router {
|
|
|
178
174
|
}
|
|
179
175
|
|
|
180
176
|
find(method, path) {
|
|
181
|
-
const key = `${method} ${path}`;
|
|
182
|
-
|
|
183
|
-
const cached = this.cache.get(key);
|
|
184
|
-
if (cached) return cached;
|
|
185
177
|
|
|
186
178
|
const segments = this.split(path);
|
|
187
179
|
|
|
@@ -208,19 +200,13 @@ class Router {
|
|
|
208
200
|
this.collectHandler(leaf, method, handlers);
|
|
209
201
|
}
|
|
210
202
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const result = {
|
|
203
|
+
return {
|
|
214
204
|
matched: res.matched,
|
|
215
205
|
params: finalParams,
|
|
216
206
|
paramsLength: Object.keys(finalParams).length,
|
|
217
207
|
handlers: Object.values(handlers),
|
|
218
208
|
errorHandlers: Object.values(errorHandlers),
|
|
219
209
|
};
|
|
220
|
-
|
|
221
|
-
this.cache.set(key, result);
|
|
222
|
-
|
|
223
|
-
return result;
|
|
224
210
|
}
|
|
225
211
|
}
|
|
226
212
|
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;
|