@ndiinginc/router 4.0.0 → 4.1.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
@@ -46,11 +46,7 @@ app.listen(3000);
46
46
  Semua method mengembalikan `this`, jadi bisa di-chain.
47
47
 
48
48
  ```js
49
- app.get("/users", handler)
50
- .post("/users", handler)
51
- .put("/users/:id", handler)
52
- .patch("/users/:id", handler)
53
- .delete("/users/:id", handler);
49
+ app.get("/users", handler).post("/users", handler).put("/users/:id", handler).patch("/users/:id", handler).delete("/users/:id", handler);
54
50
  ```
55
51
 
56
52
  Method yang tersedia: `get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `trace`, `connect`, `query`, dan `use` (semua method).
@@ -149,12 +145,12 @@ Jika tidak ada handler yang mengirim response, router mengirim JSON otomatis:
149
145
 
150
146
  ## Request
151
147
 
152
- | Properti | Keterangan |
153
- | --- | --- |
154
- | `req.path` | Pathname tanpa query string |
155
- | `req.query` | Object query string. Key yang berulang menjadi array |
156
- | `req.params` | Parameter dari route dinamis |
157
- | `req.body` | Hasil parse body (butuh middleware `json()`) |
148
+ | Properti | Keterangan |
149
+ | ------------ | ---------------------------------------------------- |
150
+ | `req.path` | Pathname tanpa query string |
151
+ | `req.query` | Object query string. Key yang berulang menjadi array |
152
+ | `req.params` | Parameter dari route dinamis |
153
+ | `req.body` | Hasil parse body (butuh middleware `json()`) |
158
154
 
159
155
  ```js
160
156
  // GET /search?tag=a&tag=b&page=2
@@ -165,9 +161,9 @@ app.get("/search", (req, res) => {
165
161
 
166
162
  ## Response
167
163
 
168
- | Method | Keterangan |
169
- | --- | --- |
170
- | `res.send(string)` | Kirim response dan akhiri chain |
164
+ | Method | Keterangan |
165
+ | ------------------ | ----------------------------------------------------------- |
166
+ | `res.send(string)` | Kirim response dan akhiri chain |
171
167
  | `res.json(object)` | Set `content-type: application/json`, serialize, lalu kirim |
172
168
 
173
169
  Keduanya juga turunan dari `http.ServerResponse`, jadi `res.statusCode`, `res.setHeader()`, dan lainnya tetap tersedia.
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@ndiinginc/router",
3
- "version": "4.0.0",
3
+ "version": "4.1.2",
4
4
  "description": "",
5
- "main": "./src/router.js",
5
+ "main": "./src/app.js",
6
6
  "scripts": {
7
7
  "test": "nodemon --exec \"node -e \\\"require('@ndiinginc/test')(require('path').resolve('tests'),{verbose:true})\\\"\"",
8
- "dev": "nodemon src/router.js",
8
+ "dev": "nodemon src/app.js",
9
9
  "prettier": "npx prettier . --write",
10
10
  "commit": "node -e \"require('@ndiinginc/commit')()\"",
11
11
  "npm:dry-run": "npm publish --dry-run",
package/rest/router.http CHANGED
@@ -13,6 +13,3 @@ GET http://localhost:4000
13
13
  ###
14
14
  GET http://localhost:5000
15
15
 
16
- autocannon -c 100 -d 10 -p 10 localhost:3000
17
- autocannon -c 100 -d 10 -p 10 localhost:4000
18
- autocannon -c 100 -d 10 -p 10 localhost:5000
package/src/app.js ADDED
@@ -0,0 +1,67 @@
1
+ const http = require("node:http");
2
+ const { Response } = require("./response.js");
3
+ const { Request } = require("./request.js");
4
+ const { Router } = require("./Router.js");
5
+
6
+ class App extends Router {
7
+ /**
8
+ * @param {import('http').IncomingMessage} req
9
+ * @param {import('http').ServerResponse} res
10
+ */
11
+ async handleRequest(req, res) {
12
+ let err;
13
+
14
+ const { params, handlers, errorHandlers } = this.find(req.method, req.path);
15
+ req.params = params;
16
+
17
+ try {
18
+ for (const handler of handlers) {
19
+ if (res.done) break;
20
+ await handler(req, res);
21
+ }
22
+ } catch (error) {
23
+ err = error;
24
+
25
+ try {
26
+ for (const handler of errorHandlers) {
27
+ if (res.done) break;
28
+ await handler(err, req, res);
29
+ }
30
+ } catch (error) {
31
+ err = error;
32
+ }
33
+ }
34
+
35
+ if (!res.done) {
36
+ if (!err) {
37
+ res.statusCode = 404;
38
+ err = new Error(http.STATUS_CODES[res.statusCode]);
39
+ }
40
+
41
+ res.statusCode = res.statusCode >= 200 && res.statusCode < 300 ? 500 : res.statusCode;
42
+ const error = { message: err.message };
43
+ if (process.env.NODE_ENV === "development") error.stack = err.stack;
44
+ res.json({ error });
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @param {import('net').ListenOptions} options
50
+ * @returns {import('http').Server}
51
+ */
52
+ listen(...args) {
53
+ const serverOptions = {
54
+ IncomingMessage: Request,
55
+ ServerResponse: Response,
56
+ };
57
+ const requestListener = (req, res) => this.handleRequest(req, res);
58
+
59
+ const server = http.createServer(serverOptions, requestListener);
60
+
61
+ server.listen(...args);
62
+
63
+ return server;
64
+ }
65
+ }
66
+
67
+ module.exports = App;
package/src/cache.js ADDED
@@ -0,0 +1,28 @@
1
+ class Cache {
2
+ constructor(size) {
3
+ this.size = size;
4
+
5
+ this.map = new Map();
6
+ }
7
+
8
+ get(key) {
9
+ if (!this.map.has(key)) return undefined;
10
+
11
+ const value = this.map.get(key);
12
+
13
+ this.map.delete(key);
14
+ this.map.set(key, value);
15
+
16
+ return value;
17
+ }
18
+
19
+ set(key, value) {
20
+ if (this.map.has(key)) this.map.delete(key);
21
+ else if (this.map.size >= this.size) {
22
+ this.map.delete(this.map.keys().next().value);
23
+ }
24
+
25
+ this.map.set(key, value);
26
+ }
27
+ }
28
+ exports.Cache = Cache;
package/src/middleware.js CHANGED
@@ -24,14 +24,15 @@ function json({ limit = 100 * 1024 } = {}) {
24
24
  throw new Error(http.STATUS_CODES[res.statusCode]);
25
25
  }
26
26
 
27
- let bodySize = 0;
27
+ let chunkLength = 0;
28
28
  const chunks = [];
29
29
  for await (const chunk of req) {
30
- bodySize += chunk.length;
31
- if (bodySize > limit) {
30
+ chunkLength += chunk.length;
31
+ if (chunkLength > limit) {
32
32
  res.statusCode = 413;
33
33
  throw new Error(http.STATUS_CODES[res.statusCode]);
34
34
  }
35
+
35
36
  chunks.push(chunk);
36
37
  }
37
38
 
@@ -40,9 +41,8 @@ function json({ limit = 100 * 1024 } = {}) {
40
41
  return;
41
42
  }
42
43
 
43
- const rawBody = Buffer.concat(chunks).toString("utf8");
44
44
  try {
45
- req.body = JSON.parse(rawBody, reviver);
45
+ req.body = JSON.parse(Buffer.concat(chunks), reviver);
46
46
  } catch (error) {
47
47
  res.statusCode = 400;
48
48
  throw new Error("Invalid JSON body");
package/src/node.js CHANGED
@@ -6,14 +6,27 @@ class Node {
6
6
  this.pattern = new Pattern(segment);
7
7
  /**@type {Map<string, Node>}*/
8
8
  this.children = new Map();
9
- /**@type {Map<string, Function[]>}*/
10
- this.handlers = new Map();
9
+ /**@type {Map<string, FunctionHandler[]>}*/
10
+ this.handlers = new Map([
11
+ ["ALL", []],
12
+ ["GET", []],
13
+ ["QUERY", []],
14
+ ["HEAD", []],
15
+ ["OPTIONS", []],
16
+ ["TRACE", []],
17
+ ["PUT", []],
18
+ ["DELETE", []],
19
+ ["POST", []],
20
+ ["PATCH", []],
21
+ ["CONNECT", []],
22
+ ]);
11
23
  }
12
24
 
13
25
  add(segment) {
14
26
  if (!this.children.has(segment)) {
15
27
  this.children.set(segment, new Node(segment));
16
28
  }
29
+
17
30
  return this.children.get(segment);
18
31
  }
19
32
 
@@ -25,7 +38,7 @@ class Node {
25
38
  for (const [method, handlers] of node.handlers) {
26
39
  for (const handler of handlers) {
27
40
  handler.seq += offset;
28
- this.handlers.getOrInsert(method, []).push(handler);
41
+ this.handlers.get(method).push(handler);
29
42
  }
30
43
  }
31
44
 
@@ -39,6 +52,9 @@ class Node {
39
52
  }
40
53
  }
41
54
 
55
+ /**
56
+ * @param {Number} offset
57
+ */
42
58
  setSeqOffset(offset) {
43
59
  for (const [, handlers] of this.handlers) {
44
60
  for (const handler of handlers) {
package/src/pattern.js CHANGED
@@ -1,24 +1,34 @@
1
+ /**
2
+ * @typedef {Object} PatternExec
3
+ * @property {number} index - The zero-based index of the match in the input string.
4
+ * @property {string} input - The original input string.
5
+ * @property {Object.<string, string>} [groups] - Named capture groups, or undefined.
6
+ */
1
7
  class Pattern {
2
- static baseURL = "http://localhost";
3
-
4
8
  constructor(input) {
5
- const regexStr = input
9
+ const pattern = input
6
10
  .replace(/[.+*?^${}()|[\]\\]/g, "\\$&")
7
11
  .replace(/:(\w+)/g, "(?<$1>[^/]+)")
8
12
  .replace(/\\\*/g, ".*");
9
- this.regex = new RegExp(`^${regexStr}$`);
10
- }
11
13
 
12
- exec(input) {
13
- const match = this.regex.exec(input);
14
- if (!match) return null;
15
- return {
16
- pathname: { groups: match.groups || {} },
17
- };
14
+ Object.defineProperty(this, "re", {
15
+ value: new RegExp(`^${pattern}$`),
16
+ writable: true,
17
+ enumerable: false,
18
+ configurable: true,
19
+ });
18
20
  }
19
21
 
20
22
  test(input) {
21
- return this.regex.test(input);
23
+ return this.re.test(input);
24
+ }
25
+
26
+ /**
27
+ * @param {String} input
28
+ * @returns {PatternExec | null}
29
+ */
30
+ exec(input) {
31
+ return this.re.exec(input);
22
32
  }
23
33
  }
24
34
  exports.Pattern = Pattern;
package/src/request.js CHANGED
@@ -1,17 +1,30 @@
1
- const { IncomingMessage } = require("node:http");
1
+ const http = require("node:http");
2
+
3
+ class Request extends http.IncomingMessage {
4
+ static base = "http://127.0.0";
2
5
 
3
- class Request extends IncomingMessage {
4
6
  get path() {
5
- if (this.url.indexOf("?") !== -1) {
6
- this.parsed = URL.parse(this.url, "http://localhost");
7
- return this.parsed.pathname;
7
+ if (this._path) return this._path;
8
+
9
+ if (this.url === "/") {
10
+ this._path = this.url;
11
+ return this._path;
8
12
  }
9
- return this.url;
13
+
14
+ this.parsed = URL.parse(this.url, Request.base);
15
+ this._path = this.parsed.pathname;
16
+ return this._path;
10
17
  }
11
18
 
12
19
  get query() {
20
+ if (this._query) return this._query;
21
+
22
+ if (!this.parsed) {
23
+ this._query = {};
24
+ return this._query;
25
+ }
26
+
13
27
  const query = {};
14
- if (!this.parsed) return query;
15
28
  for (const [name, value] of this.parsed.searchParams) {
16
29
  if (query[name]) {
17
30
  if (Array.isArray(query[name])) {
@@ -23,7 +36,8 @@ class Request extends IncomingMessage {
23
36
  query[name] = value;
24
37
  }
25
38
  }
26
- return query;
39
+ this._query = query;
40
+ return this._query;
27
41
  }
28
42
  }
29
43
  exports.Request = Request;
package/src/response.js CHANGED
@@ -1,9 +1,9 @@
1
- const { ServerResponse } = require("node:http");
1
+ const http = require("node:http");
2
2
 
3
- class Response extends ServerResponse {
3
+ class Response extends http.ServerResponse {
4
4
  send(string) {
5
- this.done = true;
6
5
  this.end(string);
6
+ this.done = true;
7
7
  }
8
8
 
9
9
  json(object) {
package/src/router.js CHANGED
@@ -1,25 +1,48 @@
1
- require("./polyfill.js");
2
- const http = require("node:http");
3
- const { Response } = require("./response.js");
4
- const { Request } = require("./request.js");
5
- const { Node } = require("./node.js");
1
+ const { Node } = require("./node");
2
+ const { Cache } = require("./cache");
3
+
4
+ /**
5
+ * @callback Handler
6
+ * @param {*} req
7
+ * @param {*} res
8
+ */
9
+ /**
10
+ * @callback Middleware
11
+ * @param {*} req
12
+ */
13
+ /**
14
+ * @callback ErrorHandler
15
+ * @param {*} err
16
+ * @param {*} req
17
+ * @param {*} res
18
+ */
19
+ /**
20
+ * @typedef {Handler|Middleware|ErrorHandler} FunctionHandler
21
+ */
6
22
 
7
23
  class Router {
8
- /**@type {Node}*/
9
- root;
24
+ constructor(options = {}) {
25
+ this.cacheSize = options.cacheSize ?? 30_000;
10
26
 
11
- constructor() {
12
- this.root = new Node("");
27
+ this.root = new Node("/");
13
28
  this.seq = 0;
29
+ this.cache = new Cache(this.cacheSize);
14
30
  }
15
31
 
16
32
  split(path) {
33
+ if (path === "/") return [];
34
+
17
35
  return path.split("/").filter(Boolean);
18
36
  }
19
37
 
20
- add(method, path, handler) {
38
+ /**
39
+ * @param {*} method
40
+ * @param {*} path
41
+ * @param {FunctionHandler[]} handler
42
+ */
43
+ add(method, path, ...handlers) {
21
44
  if (typeof path === "function") {
22
- handler = path;
45
+ handlers = [path, ...handlers];
23
46
  path = null;
24
47
  }
25
48
 
@@ -31,182 +54,126 @@ class Router {
31
54
  }
32
55
  }
33
56
 
34
- if (handler.root) {
35
- node.merge(handler.root, this.seq);
36
- this.seq = node.getMaxSeq() + 1;
37
- } else {
38
- handler.seq = this.seq++;
39
- node.handlers.getOrInsert(method, []).push(handler);
57
+ for (const handler of handlers) {
58
+ if (handler.root) {
59
+ node.merge(handler.root, this.seq);
60
+ this.seq = node.getMaxSeq() + 1;
61
+ } else {
62
+ handler.seq = this.seq++;
63
+ node.handlers.get(method).push(handler);
64
+ }
40
65
  }
41
66
  }
42
67
 
43
- use(path, handler) {
44
- this.add("ALL", path, handler);
68
+ use(path, ...handlers) {
69
+ this.add("ALL", path, ...handlers);
45
70
  return this;
46
71
  }
47
72
 
48
- get(path, handler) {
49
- this.add("GET", path, handler);
73
+ get(path, ...handlers) {
74
+ this.add("GET", path, ...handlers);
50
75
  return this;
51
76
  }
52
- query(path, handler) {
53
- this.add("QUERY", path, handler);
77
+
78
+ query(path, ...handlers) {
79
+ this.add("QUERY", path, ...handlers);
54
80
  return this;
55
81
  }
56
- head(path, handler) {
57
- this.add("HEAD", path, handler);
82
+
83
+ head(path, ...handlers) {
84
+ this.add("HEAD", path, ...handlers);
58
85
  return this;
59
86
  }
60
- options(path, handler) {
61
- this.add("OPTIONS", path, handler);
87
+
88
+ options(path, ...handlers) {
89
+ this.add("OPTIONS", path, ...handlers);
62
90
  return this;
63
91
  }
64
- trace(path, handler) {
65
- this.add("TRACE", path, handler);
92
+
93
+ trace(path, ...handlers) {
94
+ this.add("TRACE", path, ...handlers);
66
95
  return this;
67
96
  }
68
- put(path, handler) {
69
- this.add("PUT", path, handler);
97
+
98
+ put(path, ...handlers) {
99
+ this.add("PUT", path, ...handlers);
70
100
  return this;
71
101
  }
72
- delete(path, handler) {
73
- this.add("DELETE", path, handler);
102
+
103
+ delete(path, ...handlers) {
104
+ this.add("DELETE", path, ...handlers);
74
105
  return this;
75
106
  }
76
- post(path, handler) {
77
- this.add("POST", path, handler);
107
+
108
+ post(path, ...handlers) {
109
+ this.add("POST", path, ...handlers);
78
110
  return this;
79
111
  }
80
- patch(path, handler) {
81
- this.add("PATCH", path, handler);
112
+
113
+ patch(path, ...handlers) {
114
+ this.add("PATCH", path, ...handlers);
82
115
  return this;
83
116
  }
84
- connect(path, handler) {
85
- this.add("CONNECT", path, handler);
117
+
118
+ connect(path, ...handlers) {
119
+ this.add("CONNECT", path, ...handlers);
86
120
  return this;
87
121
  }
88
122
 
89
123
  find(method, path) {
90
- let params = {};
91
- let handlers = {};
92
- let errorHandlers = {};
124
+ const key = `${method} ${path}`;
93
125
 
94
- let node = this.root;
126
+ const cached = this.cache.get(key);
127
+ if (cached) return cached;
95
128
 
96
- const inputs = this.split(path);
97
- if (inputs.length) {
98
- for (const input of inputs) {
99
- let matched = node.children.get(input);
100
-
101
- if (!matched) {
102
- for (const [, child] of node.children) {
103
- const match = child.pattern.exec(input);
104
- if (match) {
105
- matched = child;
106
- Object.assign(params, match.pathname.groups);
107
- break;
108
- }
109
- }
110
- }
111
-
112
- if (!matched) {
113
- node = null;
114
- params = {};
115
- break;
116
- }
117
-
118
- node = matched;
129
+ let node = this.root;
119
130
 
120
- if (node.handlers.has("ALL")) {
121
- for (const handler of node.handlers.get("ALL")) {
122
- if (handler.length === 3) errorHandlers[handler.seq] = handler;
123
- else handlers[handler.seq] = handler;
131
+ let params = {};
132
+ const handlers = {};
133
+ const errorHandlers = {};
134
+
135
+ for (const input of this.split(path)) {
136
+ let matched = node.children.get(input);
137
+
138
+ if (!matched) {
139
+ for (const [, child] of node.children) {
140
+ const match = child.pattern.exec(input);
141
+ if (match) {
142
+ matched = child;
143
+ Object.assign(params, match.groups);
144
+ break;
124
145
  }
125
146
  }
126
147
  }
127
- }
128
148
 
129
- if (this.root.handlers.has("ALL")) {
130
- for (const handler of this.root.handlers.get("ALL")) {
131
- if (handler.length === 3) errorHandlers[handler.seq] = handler;
132
- else handlers[handler.seq] = handler;
149
+ if (!matched) {
150
+ node = null;
151
+ params = {};
152
+ break;
133
153
  }
134
- }
135
154
 
136
- if (node && node.handlers.has(method)) {
137
- for (const handler of node.handlers.get(method)) {
155
+ node = matched;
156
+
157
+ for (const handler of node.handlers.get("ALL")) {
138
158
  if (handler.length === 3) errorHandlers[handler.seq] = handler;
139
159
  else handlers[handler.seq] = handler;
140
160
  }
141
161
  }
142
162
 
143
- return {
144
- params,
145
- handlers: Object.values(handlers),
146
- errorHandlers: Object.values(errorHandlers),
147
- };
148
- }
149
-
150
- /**
151
- * @param {import('http').RequestOptions} req
152
- * @param {import('http').ServerResponse} res
153
- */
154
- async handleRequest(req, res) {
155
- const result = this.find(req.method, req.path);
156
- req.params = result.params;
157
-
158
- try {
159
- for (const handler of result.handlers) {
160
- if (res.done) break;
161
- await handler(req, res);
162
- }
163
- } catch (error) {
164
- res.error = error;
165
-
166
- try {
167
- for (const handler of result.errorHandlers) {
168
- if (res.done) break;
169
- await handler(res.error, req, res);
170
- }
171
- } catch (error) {
172
- res.error = error;
173
- }
163
+ for (const handler of this.root.handlers.get("ALL")) {
164
+ if (handler.length === 3) errorHandlers[handler.seq] = handler;
165
+ else handlers[handler.seq] = handler;
174
166
  }
175
167
 
176
- if (!res.done) {
177
- if (!res.error) {
178
- res.statusCode = 404;
179
- res.error = new Error(http.STATUS_CODES[res.statusCode]);
168
+ if (node) {
169
+ for (const handler of node.handlers.get(method)) {
170
+ handlers[handler.seq] = handler;
180
171
  }
181
-
182
- res.statusCode = res.statusCode >= 200 && res.statusCode < 300 ? 500 : res.statusCode;
183
-
184
- const error = {
185
- code: res.error.code,
186
- message: res.error.message,
187
- };
188
- if (process.env.NODE_ENV === "development") error.stack = res.error.stack;
189
-
190
- res.json({ error });
191
172
  }
192
- }
193
173
 
194
- /**
195
- * @returns {import('http').Server}
196
- */
197
- listen(...params) {
198
- const server = http.createServer(
199
- {
200
- IncomingMessage: Request,
201
- ServerResponse: Response,
202
- },
203
- (req, res) => this.handleRequest(req, res),
204
- );
205
-
206
- server.listen(...params);
207
-
208
- return server;
174
+ const value = { params, handlers: Object.values(handlers), errorHandlers: Object.values(errorHandlers) };
175
+ this.cache.set(key, value);
176
+ return value;
209
177
  }
210
178
  }
211
-
212
- module.exports = Router;
179
+ module.exports.Router = Router;
package/src/polyfill.js DELETED
@@ -1,9 +0,0 @@
1
- // Polyfill for Map.prototype.getOrInsert
2
- if (!Map.prototype.getOrInsert) {
3
- Map.prototype.getOrInsert = function (key, defaultValue) {
4
- if (!this.has(key)) {
5
- this.set(key, defaultValue);
6
- }
7
- return this.get(key);
8
- };
9
- }