fulmine.js 5.0.0 → 5.1.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/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;