fulmine.js 5.0.0 → 5.1.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 +26 -8
- package/package.json +1 -1
- package/src/application.js +109 -19
- package/src/cli.js +3 -3
- package/src/index.js +5 -0
- package/src/middlewares.js +87 -36
- package/src/node-shim.js +2 -1
- package/src/request.js +95 -37
- package/src/response.js +141 -77
- package/src/route.js +180 -0
- package/src/router.js +503 -124
- package/src/utils.js +41 -3
package/src/route.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2026 Nigro Simone
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { METHODS } = require("http");
|
|
18
|
+
const { NullObject } = require("./utils.js");
|
|
19
|
+
|
|
20
|
+
// how many handlers may run one after another before the stack is let go of. Express uses the
|
|
21
|
+
// same number for the same reason: a route with thousands of synchronous handlers would otherwise
|
|
22
|
+
// grow the call stack until it blows
|
|
23
|
+
const SYNC_LIMIT = 100;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The handlers registered for one path, and the walk over them.
|
|
27
|
+
*
|
|
28
|
+
* Express exports this, and code that builds a route by hand rather than through a router uses it:
|
|
29
|
+
* `new Route(path)`, a handler per verb, then `dispatch(req, res, done)`. It does no matching, the
|
|
30
|
+
* caller having decided the route is the right one; all it knows is which verb a handler answers.
|
|
31
|
+
*/
|
|
32
|
+
class Route {
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} path what this route was registered for. Kept for whoever reads it, since
|
|
35
|
+
* matching happens before dispatch
|
|
36
|
+
*/
|
|
37
|
+
constructor(path) {
|
|
38
|
+
this.path = path;
|
|
39
|
+
/** @type {{method: string|undefined, handle: Function}[]} */
|
|
40
|
+
this.stack = [];
|
|
41
|
+
// which verbs have a handler, which is what an OPTIONS reply is built from
|
|
42
|
+
this.methods = new NullObject();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether this route answers the verb, HEAD falling back to GET as it does over the wire.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} method
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
handlesMethod(method) {
|
|
52
|
+
const lowered = method.toLowerCase();
|
|
53
|
+
return Boolean(this.methods[lowered] || (lowered === "head" && this.methods.get));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The verbs this route answers, in upper case, HEAD included when GET is there.
|
|
58
|
+
*
|
|
59
|
+
* @returns {string[]}
|
|
60
|
+
*/
|
|
61
|
+
_methods() {
|
|
62
|
+
const methods = Object.keys(this.methods);
|
|
63
|
+
if (this.methods.get && !this.methods.head) {
|
|
64
|
+
methods.push("head");
|
|
65
|
+
}
|
|
66
|
+
return methods.map((method) => method.toUpperCase());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Runs the handlers this request's verb reaches, one after another through next().
|
|
71
|
+
*
|
|
72
|
+
* @param {any} req
|
|
73
|
+
* @param {any} res
|
|
74
|
+
* @param {(err?: any) => void} done called when the route is finished with the request, with
|
|
75
|
+
* whatever error it ended on
|
|
76
|
+
*/
|
|
77
|
+
dispatch(req, res, done) {
|
|
78
|
+
let index = 0;
|
|
79
|
+
let sync = 0;
|
|
80
|
+
const stack = this.stack;
|
|
81
|
+
if (stack.length === 0) {
|
|
82
|
+
return done();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let method = String(req.method ?? "").toLowerCase();
|
|
86
|
+
if (method === "head" && !this.methods.head) {
|
|
87
|
+
method = "get";
|
|
88
|
+
}
|
|
89
|
+
req.route = this;
|
|
90
|
+
|
|
91
|
+
const next = (err) => {
|
|
92
|
+
// next("route") leaves this route, and next("router") leaves whoever is running it
|
|
93
|
+
if (err === "route") {
|
|
94
|
+
return done();
|
|
95
|
+
}
|
|
96
|
+
if (err === "router") {
|
|
97
|
+
return done(err);
|
|
98
|
+
}
|
|
99
|
+
if (index >= stack.length) {
|
|
100
|
+
return done(err);
|
|
101
|
+
}
|
|
102
|
+
if (++sync > SYNC_LIMIT) {
|
|
103
|
+
sync = 0;
|
|
104
|
+
return setImmediate(next, err);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let layer;
|
|
108
|
+
let matched = false;
|
|
109
|
+
while (!matched && index < stack.length) {
|
|
110
|
+
layer = stack[index++];
|
|
111
|
+
matched = !layer.method || layer.method === method;
|
|
112
|
+
}
|
|
113
|
+
if (!matched) {
|
|
114
|
+
return done(err);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const handle = /** @type {any} */ (layer).handle;
|
|
118
|
+
// an error only reaches the four-argument handlers, and everything else only runs
|
|
119
|
+
// while there is no error, which is the same rule ordinary dispatch follows
|
|
120
|
+
if (err) {
|
|
121
|
+
if (handle.length !== 4) {
|
|
122
|
+
return next(err);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
handle(err, req, res, next);
|
|
126
|
+
} catch (thrown) {
|
|
127
|
+
next(thrown);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
if (handle.length === 4) {
|
|
131
|
+
return next();
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
handle(req, res, next);
|
|
135
|
+
} catch (thrown) {
|
|
136
|
+
next(thrown);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
sync = 0;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
next();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Refuses a handler that could never be called, worded as express words it.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} method the verb this was registered for, for the message
|
|
150
|
+
* @param {any[]} handlers
|
|
151
|
+
*/
|
|
152
|
+
function checkRouteHandlers(method, handlers) {
|
|
153
|
+
for (const handle of handlers) {
|
|
154
|
+
if (typeof handle !== "function") {
|
|
155
|
+
const type = Object.prototype.toString.call(handle);
|
|
156
|
+
throw new TypeError(`Route.${method}() requires a callback function but got a ${type}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// every verb, plus all(), registered the same way: flattened, checked, then pushed with the verb
|
|
162
|
+
// they answer. all() pushes handlers with no verb at all, which matches every request
|
|
163
|
+
for (const method of ["all", ...METHODS.map((verb) => verb.toLowerCase())]) {
|
|
164
|
+
if (method !== "all" && typeof Route.prototype[method] === "function") {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
Route.prototype[method] = function (...handlers) {
|
|
168
|
+
const flattened = handlers.flat(Infinity);
|
|
169
|
+
checkRouteHandlers(method, flattened);
|
|
170
|
+
for (const handle of flattened) {
|
|
171
|
+
this.stack.push({ method: method === "all" ? undefined : method, handle });
|
|
172
|
+
if (method !== "all") {
|
|
173
|
+
this.methods[method] = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return this;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = Route;
|