@ndiinginc/router 4.1.3 → 4.2.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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@ndiinginc/router",
3
- "version": "4.1.3",
3
+ "version": "4.2.0",
4
4
  "description": "",
5
- "main": "./src/app.js",
5
+ "main": "./src/index.js",
6
6
  "scripts": {
7
7
  "test": "nodemon --exec \"node -e \\\"require('@ndiinginc/test')(require('path').resolve('tests'),{verbose:true})\\\"\"",
8
- "dev": "nodemon src/app.js",
8
+ "dev": "nodemon src/index.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/src/app.js CHANGED
@@ -1,67 +1,76 @@
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;
1
+ const http = require("node:http");
2
+ const { Router } = require("./router.js");
3
+ const { Request } = require("./request.js");
4
+ const { Response } = require("./response.js");
5
+ const { ErrorResponse } = require("./error.js");
6
+
7
+ /**@type {import('http').ServerOptions}*/
8
+ const SERVER_OPTIONS = {
9
+ IncomingMessage: Request,
10
+ ServerResponse: Response,
11
+ };
12
+
13
+ class App extends Router {
14
+ async withTimeout(handlerPromise, delay) {
15
+ let timeout;
16
+ const timeoutPromise = new Promise((_, reject) => {
17
+ timeout = setTimeout(() => reject(ErrorResponse.status(504)), delay);
18
+ });
19
+
20
+ await Promise.race([handlerPromise, timeoutPromise]).finally(() => clearTimeout(timeout));
21
+ }
22
+
23
+ /**
24
+ * @param {import('http').IncomingMessage} req
25
+ * @param {import('http').ServerResponse} res
26
+ */
27
+ async handle(req, res) {
28
+ const { params, handlers, errorHandlers } = this.find(req.method, req.path);
29
+ req.params = params;
30
+
31
+ let err;
32
+ try {
33
+ const handlerPromise = (async () => {
34
+ for (const handler of handlers) {
35
+ if (res.writableEnded) break;
36
+ await handler(req, res);
37
+ }
38
+ })();
39
+
40
+ if (req.timeout) await this.withTimeout(handlerPromise, req.timeout);
41
+ else await handlerPromise;
42
+ } catch (error) {
43
+ err = error;
44
+
45
+ try {
46
+ for (const handler of errorHandlers) {
47
+ if (res.writableEnded) break;
48
+ await handler(err, req, res);
49
+ }
50
+ } catch (error) {
51
+ err = new Error(error.message, { cause: err });
52
+ }
53
+ }
54
+
55
+ if (!res.writableEnded) {
56
+ if (!err) err = ErrorResponse.status(404);
57
+
58
+ if (!res.statusCode || res.statusCode < 400) {
59
+ res.statusCode = err.statusCode || 500;
60
+ }
61
+
62
+ res.json({ error: { message: err.message, stack: err.stack } });
63
+ }
64
+ }
65
+
66
+ listen(...params) {
67
+ const requestListener = (req, res) => this.handle(req, res);
68
+ const server = http.createServer(SERVER_OPTIONS, requestListener);
69
+ server.listen(...params);
70
+ return server;
71
+ }
72
+ }
73
+
74
+ App.SERVER_OPTIONS = SERVER_OPTIONS;
75
+
76
+ exports.App = App;
package/src/cache.js CHANGED
@@ -1,28 +1,24 @@
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;
1
+ class Cache {
2
+ constructor(size) {
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.map.has(key)) this.map.delete(key);
17
+ else if (this.map.size >= this.size) {
18
+ this.map.delete(this.map.keys().next().value);
19
+ }
20
+ this.map.set(key, value);
21
+ }
22
+ }
23
+
24
+ exports.Cache = Cache;
package/src/error.js ADDED
@@ -0,0 +1,26 @@
1
+ const http = require("node:http");
2
+
3
+ // Client error responses (400 – 499)
4
+ // Server error responses (500 – 599)
5
+ class ErrorResponse extends Error {
6
+ constructor({ statusCode, message } = {}, options) {
7
+ super(message, options);
8
+
9
+ this.name = this.constructor.name;
10
+
11
+ if (Error.captureStackTrace) {
12
+ Error.captureStackTrace(this, this.constructor);
13
+ }
14
+
15
+ this.statusCode = statusCode;
16
+ }
17
+
18
+ static status(statusCode) {
19
+ return new ErrorResponse({
20
+ statusCode,
21
+ message: http.STATUS_CODES[statusCode],
22
+ });
23
+ }
24
+ }
25
+
26
+ exports.ErrorResponse = ErrorResponse;
package/src/index.js ADDED
@@ -0,0 +1,9 @@
1
+ const { inspect } = require("node:util");
2
+ const { json } = require("./json.js");
3
+ const { App } = require("./app.js");
4
+ const { Router } = require("./router.js");
5
+
6
+ App.Router=Router
7
+ App.json=json
8
+
9
+ module.exports=App
@@ -1,52 +1,55 @@
1
- const http = require("node:http");
2
-
3
- function json({ limit = 100 * 1024 } = {}) {
4
- const reviver = (key, value) => {
5
- if (key === "__proto__" || key === "constructor" || key === "prototype") {
6
- return undefined;
7
- }
8
- return value;
9
- };
10
-
11
- return async (req, res) => {
12
- if (!["POST", "PUT", "PATCH", "QUERY"].includes(req.method)) {
13
- return;
14
- }
15
-
16
- const contentType = req.headers["content-type"];
17
- if (!contentType || !contentType.includes("application/json")) {
18
- return;
19
- }
20
-
21
- const contentLength = parseInt(req.headers["content-length"], 10);
22
- if (contentLength && contentLength > limit) {
23
- res.statusCode = 413;
24
- throw new Error(http.STATUS_CODES[res.statusCode]);
25
- }
26
-
27
- let chunkLength = 0;
28
- const chunks = [];
29
- for await (const chunk of req) {
30
- chunkLength += chunk.length;
31
- if (chunkLength > limit) {
32
- res.statusCode = 413;
33
- throw new Error(http.STATUS_CODES[res.statusCode]);
34
- }
35
-
36
- chunks.push(chunk);
37
- }
38
-
39
- if (chunks.length === 0) {
40
- req.body = null;
41
- return;
42
- }
43
-
44
- try {
45
- req.body = JSON.parse(Buffer.concat(chunks), reviver);
46
- } catch (error) {
47
- res.statusCode = 400;
48
- throw new Error("Invalid JSON body");
49
- }
50
- };
51
- }
52
- module.exports.json = json;
1
+ const http = require("node:http");
2
+
3
+ // middleware
4
+ function json({ limit = 100 * 1024 } = {}) {
5
+ const reviver = (key, value) => {
6
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
7
+ return undefined;
8
+ }
9
+ return value;
10
+ };
11
+
12
+ return async (req, res) => {
13
+ if (!["POST", "PUT", "PATCH", "QUERY"].includes(req.method)) {
14
+ return;
15
+ }
16
+
17
+ const contentType = req.headers["content-type"];
18
+ if (!contentType || !contentType.includes("application/json")) {
19
+ return;
20
+ }
21
+
22
+ const contentLength = parseInt(req.headers["content-length"], 10);
23
+ if (contentLength && contentLength > limit) {
24
+ res.statusCode = 413;
25
+ throw new Error(http.STATUS_CODES[res.statusCode]);
26
+ }
27
+
28
+ let chunkLength = 0;
29
+
30
+ const chunks = [];
31
+
32
+ for await (const chunk of req) {
33
+ chunkLength += chunk.length;
34
+ if (chunkLength > limit) {
35
+ res.statusCode = 413;
36
+ throw new Error(http.STATUS_CODES[res.statusCode]);
37
+ }
38
+ chunks.push(chunk);
39
+ }
40
+
41
+ if (chunks.length === 0) {
42
+ req.body = null;
43
+ return;
44
+ }
45
+
46
+ try {
47
+ req.body = JSON.parse(Buffer.concat(chunks), reviver);
48
+ } catch (error) {
49
+ res.statusCode = 400;
50
+ throw new Error("Invalid JSON body");
51
+ }
52
+ };
53
+ }
54
+
55
+ exports.json = json;
package/src/node.js CHANGED
@@ -1,86 +1,81 @@
1
- const { Pattern } = require("./pattern.js");
2
-
3
- class Node {
4
- constructor(segment) {
5
- this.segment = segment;
6
- this.pattern = new Pattern(segment);
7
- /**@type {Map<string, Node>}*/
8
- this.children = 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
- ]);
23
- }
24
-
25
- add(segment) {
26
- if (!this.children.has(segment)) {
27
- this.children.set(segment, new Node(segment));
28
- }
29
-
30
- return this.children.get(segment);
31
- }
32
-
33
- /**
34
- * @param {Node} node
35
- * @param {Number} offset
36
- */
37
- merge(node, offset) {
38
- for (const [method, handlers] of node.handlers) {
39
- for (const handler of handlers) {
40
- handler.seq += offset;
41
- this.handlers.get(method).push(handler);
42
- }
43
- }
44
-
45
- for (const [segment, child] of node.children) {
46
- if (!this.children.has(segment)) {
47
- child.setSeqOffset(offset);
48
- this.children.set(segment, child);
49
- } else {
50
- this.children.get(segment).merge(child, offset);
51
- }
52
- }
53
- }
54
-
55
- /**
56
- * @param {Number} offset
57
- */
58
- setSeqOffset(offset) {
59
- for (const [, handlers] of this.handlers) {
60
- for (const handler of handlers) {
61
- handler.seq += offset;
62
- }
63
- }
64
-
65
- for (const [, child] of this.children) {
66
- child.setSeqOffset(offset);
67
- }
68
- }
69
-
70
- getMaxSeq() {
71
- let max = -1;
72
-
73
- for (const [, handlers] of this.handlers) {
74
- for (const handler of handlers) {
75
- max = Math.max(max, handler.seq);
76
- }
77
- }
78
-
79
- for (const [, child] of this.children) {
80
- max = Math.max(max, child.getMaxSeq());
81
- }
82
-
83
- return max;
84
- }
85
- }
86
- exports.Node = Node;
1
+ const { Pattern } = require("./pattern.js");
2
+
3
+ class Node {
4
+ constructor(segment = "") {
5
+ this.segment = segment;
6
+ this.pattern = new Pattern(segment);
7
+ /**@type {Map<string,Node>}*/
8
+ this.children = new Map();
9
+ this.handlers = new Map([
10
+ ["ALL", []],
11
+ ["GET", []],
12
+ ["QUERY", []],
13
+ ["HEAD", []],
14
+ ["OPTIONS", []],
15
+ ["TRACE", []],
16
+ ["PUT", []],
17
+ ["DELETE", []],
18
+ ["POST", []],
19
+ ["PATCH", []],
20
+ ["CONNECT", []],
21
+ ]);
22
+ }
23
+
24
+ add(segment) {
25
+ if (!this.children.has(segment)) {
26
+ this.children.set(segment, new Node(segment));
27
+ }
28
+ return this.children.get(segment);
29
+ }
30
+
31
+ /**
32
+ * @param {Node} node
33
+ */
34
+ merge(node, offset) {
35
+ for (const [method, handlers] of node.handlers) {
36
+ for (const handler of handlers) {
37
+ handler.seq += offset;
38
+ this.handlers.get(method).push(handler);
39
+ }
40
+ }
41
+
42
+ for (const [segment, child] of node.children) {
43
+ if (!this.children.has(segment)) {
44
+ child.setSeqOffset(offset);
45
+ this.children.set(segment, child);
46
+ } else {
47
+ this.children.get(segment).merge(child, offset);
48
+ }
49
+ }
50
+ }
51
+
52
+ setSeqOffset(offset) {
53
+ for (const [, handlers] of this.handlers) {
54
+ for (const handler of handlers) {
55
+ handler.seq += offset;
56
+ }
57
+ }
58
+
59
+ for (const [, child] of this.children) {
60
+ child.setSeqOffset(offset);
61
+ }
62
+ }
63
+
64
+ getMaxSeq() {
65
+ let max = -1;
66
+
67
+ for (const [, handlers] of this.handlers) {
68
+ for (const handler of handlers) {
69
+ max = Math.max(max, handler.seq);
70
+ }
71
+ }
72
+
73
+ for (const [, child] of this.children) {
74
+ max = Math.max(max, child.getMaxSeq());
75
+ }
76
+
77
+ return max;
78
+ }
79
+ }
80
+
81
+ exports.Node = Node;
package/src/pattern.js CHANGED
@@ -1,34 +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
- */
7
- class Pattern {
8
- constructor(input) {
9
- const pattern = input
10
- .replace(/[.+*?^${}()|[\]\\]/g, "\\$&")
11
- .replace(/:(\w+)/g, "(?<$1>[^/]+)")
12
- .replace(/\\\*/g, ".*");
13
-
14
- Object.defineProperty(this, "re", {
15
- value: new RegExp(`^${pattern}$`),
16
- writable: true,
17
- enumerable: false,
18
- configurable: true,
19
- });
20
- }
21
-
22
- 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);
32
- }
33
- }
34
- exports.Pattern = Pattern;
1
+ /**
2
+ * @typedef PatternExecResult
3
+ * @property {number} index
4
+ * @property {string} input
5
+ * @property {Record<string, string>} [groups]
6
+ * @property {number} length
7
+ */
8
+ class Pattern {
9
+ constructor(input) {
10
+ const pattern = `^${input
11
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
12
+ .replace(/:(\w+)/g, "(?<$1>[^/]+)")
13
+ .replace(/\*/, ".*")}$`;
14
+ Object.defineProperty(this, "regex", {
15
+ value: new RegExp(pattern),
16
+ configurable: true,
17
+ enumerable: false,
18
+ });
19
+ }
20
+
21
+ /**
22
+ * @param {string} input
23
+ * @returns {PatternExecResult | null}
24
+ */
25
+ exec(input) {
26
+ return this.regex.exec(input);
27
+ }
28
+
29
+ test(input) {
30
+ return this.regex.test(input);
31
+ }
32
+ }
33
+
34
+ exports.Pattern = Pattern;
package/src/request.js CHANGED
@@ -1,43 +1,37 @@
1
- const http = require("node:http");
2
-
3
- class Request extends http.IncomingMessage {
4
- static base = "http://127.0.0";
5
-
6
- get path() {
7
- if (this._path) return this._path;
8
-
9
- if (this.url.indexOf('?')===-1) {
10
- this._path = this.url;
11
- return this._path;
12
- }
13
-
14
- this.parsed = URL.parse(this.url, Request.base);
15
- this._path = this.parsed.pathname;
16
- return this._path;
17
- }
18
-
19
- get query() {
20
- if (this._query) return this._query;
21
-
22
- if (!this.parsed) {
23
- this._query = {};
24
- return this._query;
25
- }
26
-
27
- const query = {};
28
- for (const [name, value] of this.parsed.searchParams) {
29
- if (query[name]) {
30
- if (Array.isArray(query[name])) {
31
- query[name].push(value);
32
- } else {
33
- query[name] = [query[name], value];
34
- }
35
- } else {
36
- query[name] = value;
37
- }
38
- }
39
- this._query = query;
40
- return this._query;
41
- }
42
- }
43
- exports.Request = Request;
1
+ const http = require("node:http");
2
+ const { parseUrl, parseQueryString } = require("./util.js");
3
+
4
+ class Request extends http.IncomingMessage {
5
+ _path;
6
+ _parsed;
7
+ _query;
8
+ params={};
9
+ timeout=0;
10
+
11
+ get path() {
12
+ if (this._path) return this._path;
13
+
14
+ if (this.url.indexOf("?") === -1) {
15
+ this._path = this.url;
16
+ return this._path;
17
+ }
18
+
19
+ this._parsed = parseUrl(this.url)
20
+ this._path = this._parsed.path;
21
+ return this._path;
22
+ }
23
+
24
+ get query() {
25
+ if (this._query) return this._query;
26
+
27
+ if (!this._parsed) {
28
+ this._query = {};
29
+ return this._query;
30
+ }
31
+
32
+ this._query = parseQueryString(this._parsed.queryString);
33
+ return this._query;
34
+ }
35
+ }
36
+
37
+ exports.Request = Request;
package/src/response.js CHANGED
@@ -1,14 +1,13 @@
1
- const http = require("node:http");
2
-
3
- class Response extends http.ServerResponse {
4
- send(string) {
5
- this.end(string);
6
- this.done = true;
7
- }
8
-
9
- json(object) {
10
- this.setHeader("content-type", "application/json");
11
- this.send(JSON.stringify(object));
12
- }
13
- }
14
- exports.Response = Response;
1
+ const http = require("node:http");
2
+
3
+ class Response extends http.ServerResponse {
4
+ send(chunk) {
5
+ this.end(chunk);
6
+ }
7
+
8
+ json(value) {
9
+ this.setHeader("Content-Type","application/json")
10
+ this.end(JSON.stringify(value));
11
+ }
12
+ }
13
+ exports.Response = Response;
package/src/router.js CHANGED
@@ -1,179 +1,164 @@
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
- */
22
-
23
- class Router {
24
- constructor(options = {}) {
25
- this.cacheSize = options.cacheSize ?? 30_000;
26
-
27
- this.root = new Node("/");
28
- this.seq = 0;
29
- this.cache = new Cache(this.cacheSize);
30
- }
31
-
32
- split(path) {
33
- if (path === "/") return [];
34
-
35
- return path.split("/").filter(Boolean);
36
- }
37
-
38
- /**
39
- * @param {*} method
40
- * @param {*} path
41
- * @param {FunctionHandler[]} handler
42
- */
43
- add(method, path, ...handlers) {
44
- if (typeof path === "function") {
45
- handlers = [path, ...handlers];
46
- path = null;
47
- }
48
-
49
- let node = this.root;
50
-
51
- if (path !== null) {
52
- for (const segment of this.split(path)) {
53
- node = node.add(segment);
54
- }
55
- }
56
-
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
- }
65
- }
66
- }
67
-
68
- use(path, ...handlers) {
69
- this.add("ALL", path, ...handlers);
70
- return this;
71
- }
72
-
73
- get(path, ...handlers) {
74
- this.add("GET", path, ...handlers);
75
- return this;
76
- }
77
-
78
- query(path, ...handlers) {
79
- this.add("QUERY", path, ...handlers);
80
- return this;
81
- }
82
-
83
- head(path, ...handlers) {
84
- this.add("HEAD", path, ...handlers);
85
- return this;
86
- }
87
-
88
- options(path, ...handlers) {
89
- this.add("OPTIONS", path, ...handlers);
90
- return this;
91
- }
92
-
93
- trace(path, ...handlers) {
94
- this.add("TRACE", path, ...handlers);
95
- return this;
96
- }
97
-
98
- put(path, ...handlers) {
99
- this.add("PUT", path, ...handlers);
100
- return this;
101
- }
102
-
103
- delete(path, ...handlers) {
104
- this.add("DELETE", path, ...handlers);
105
- return this;
106
- }
107
-
108
- post(path, ...handlers) {
109
- this.add("POST", path, ...handlers);
110
- return this;
111
- }
112
-
113
- patch(path, ...handlers) {
114
- this.add("PATCH", path, ...handlers);
115
- return this;
116
- }
117
-
118
- connect(path, ...handlers) {
119
- this.add("CONNECT", path, ...handlers);
120
- return this;
121
- }
122
-
123
- find(method, path) {
124
- const key = `${method} ${path}`;
125
-
126
- const cached = this.cache.get(key);
127
- if (cached) return cached;
128
-
129
- let node = this.root;
130
-
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;
145
- }
146
- }
147
- }
148
-
149
- if (!matched) {
150
- node = null;
151
- params = {};
152
- break;
153
- }
154
-
155
- node = matched;
156
-
157
- for (const handler of node.handlers.get("ALL")) {
158
- if (handler.length === 3) errorHandlers[handler.seq] = handler;
159
- else handlers[handler.seq] = handler;
160
- }
161
- }
162
-
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;
166
- }
167
-
168
- if (node) {
169
- for (const handler of node.handlers.get(method)) {
170
- handlers[handler.seq] = handler;
171
- }
172
- }
173
-
174
- const value = { params, handlers: Object.values(handlers), errorHandlers: Object.values(errorHandlers) };
175
- this.cache.set(key, value);
176
- return value;
177
- }
178
- }
179
- module.exports.Router = Router;
1
+ const { Node } = require("./node.js");
2
+ const { Cache } = require("./cache.js");
3
+
4
+ class Router {
5
+ constructor(options = {}) {
6
+ this.cacheSize = options.size ?? 1000;
7
+
8
+ this.root = new Node();
9
+ this.seq = 0;
10
+ this.cache = new Cache(this.cacheSize);
11
+ }
12
+
13
+ split(path) {
14
+ return path.split("/").filter(Boolean);
15
+ }
16
+
17
+ add(method, path, ...handlers) {
18
+ if (typeof path === "function") {
19
+ handlers = [path, ...handlers];
20
+ path = null;
21
+ }
22
+
23
+ let node = this.root;
24
+
25
+ if (path !== null) {
26
+ for (const segment of this.split(path)) {
27
+ node = node.add(segment);
28
+ }
29
+ }
30
+
31
+ for (const handler of handlers) {
32
+ if (handler.root) {
33
+ node.merge(handler.root, this.seq);
34
+ this.seq = node.getMaxSeq() + 1;
35
+ } else {
36
+ handler.seq = this.seq++;
37
+ node.handlers.get(method).push(handler);
38
+ }
39
+ }
40
+ }
41
+
42
+ use(path, ...handlers) {
43
+ this.add("ALL", path, ...handlers);
44
+ return this;
45
+ }
46
+
47
+ get(path, ...handlers) {
48
+ this.add("GET", path, ...handlers);
49
+ return this;
50
+ }
51
+
52
+ query(path, ...handlers) {
53
+ this.add("QUERY", path, ...handlers);
54
+ return this;
55
+ }
56
+
57
+ head(path, ...handlers) {
58
+ this.add("HEAD", path, ...handlers);
59
+ return this;
60
+ }
61
+
62
+ options(path, ...handlers) {
63
+ this.add("OPTIONS", path, ...handlers);
64
+ return this;
65
+ }
66
+
67
+ trace(path, ...handlers) {
68
+ this.add("TRACE", path, ...handlers);
69
+ return this;
70
+ }
71
+
72
+ put(path, ...handlers) {
73
+ this.add("PUT", path, ...handlers);
74
+ return this;
75
+ }
76
+
77
+ delete(path, ...handlers) {
78
+ this.add("DELETE", path, ...handlers);
79
+ return this;
80
+ }
81
+
82
+ post(path, ...handlers) {
83
+ this.add("POST", path, ...handlers);
84
+ return this;
85
+ }
86
+
87
+ patch(path, ...handlers) {
88
+ this.add("PATCH", path, ...handlers);
89
+ return this;
90
+ }
91
+
92
+ connect(path, ...handlers) {
93
+ this.add("CONNECT", path, ...handlers);
94
+ return this;
95
+ }
96
+
97
+ /**
98
+ * @param {'GET'|'QUERY'|'HEAD'|'OPTIONS'|'TRACE'|'PUT'|'DELETE'|'POST'|'PATCH'|'CONNECT'} method
99
+ * @param {String} path
100
+ * @returns {{params: {};handlers: any[];errorHandlers: any[];}}
101
+ */
102
+ find(method, path) {
103
+ const key = `${method} ${path}`;
104
+
105
+ const cached = this.cache.get(key);
106
+ if (cached) return cached;
107
+
108
+ let node = this.root;
109
+
110
+ let params = {};
111
+ const handlers = {};
112
+ const errorHandlers = {};
113
+
114
+ for (const input of this.split(path)) {
115
+ let matched = node.children.get(input);
116
+
117
+ if (!matched) {
118
+ for (const [, child] of node.children) {
119
+ const match = child.pattern.exec(input);
120
+ if (match) {
121
+ matched = child;
122
+ Object.assign(params, match.groups);
123
+ break;
124
+ }
125
+ }
126
+ }
127
+
128
+ if (!matched) {
129
+ node = null;
130
+ params = {};
131
+ break;
132
+ }
133
+
134
+ node = matched;
135
+
136
+ for (const handler of node.handlers.get("ALL")) {
137
+ if (handler.length === 3) errorHandlers[handler.seq] = handler;
138
+ else handlers[handler.seq] = handler;
139
+ }
140
+ }
141
+
142
+ for (const handler of this.root.handlers.get("ALL")) {
143
+ if (handler.length === 3) errorHandlers[handler.seq] = handler;
144
+ else handlers[handler.seq] = handler;
145
+ }
146
+
147
+ if (node) {
148
+ for (const handler of node.handlers.get(method)) {
149
+ handlers[handler.seq] = handler
150
+ }
151
+ }
152
+
153
+ const result = {
154
+ params,
155
+ handlers: Object.values(handlers),
156
+ errorHandlers: Object.values(errorHandlers),
157
+ };
158
+
159
+ this.cache.set(key, result);
160
+
161
+ return result;
162
+ }
163
+ }
164
+ exports.Router = Router;
package/src/util.js ADDED
@@ -0,0 +1,27 @@
1
+ const URL_REGEX = /^([^?#]*)(\?([^#]*))?(#.*)?$/;
2
+ const QUERY_STRING_REGEX = /([^=&]*)?=([^&]*)?/g;
3
+
4
+ function parseQueryString(queryString) {
5
+ const result = {};
6
+
7
+ let match;
8
+ while ((match = QUERY_STRING_REGEX.exec(queryString)) !== null) {
9
+ const [, name = "", raw = ""] = match;
10
+
11
+ const value = decodeURIComponent(raw);
12
+
13
+ if (result[name]) {
14
+ if (Array.isArray(result[name])) result[name].push(value);
15
+ else result[name] = [result[name], value];
16
+ } else result[name] = value;
17
+ }
18
+
19
+ return result;
20
+ }
21
+ exports.parseQueryString = parseQueryString;
22
+
23
+ function parseUrl(url) {
24
+ const [, path = "/", , queryString = ""] = URL_REGEX.exec(url);
25
+ return { path, queryString };
26
+ }
27
+ exports.parseUrl = parseUrl;