fulmine.js 5.1.9 → 5.3.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/NOTICE +29 -2
- package/README.md +148 -62
- package/package.json +6 -2
- package/src/application.js +97 -34
- package/src/cli.js +302 -5
- package/src/declarative.js +19 -0
- package/src/index.js +2 -0
- package/src/middlewares.js +97 -25
- package/src/node-shim.js +5 -3
- package/src/options.d.ts +110 -0
- package/src/parse-query.js +19 -2
- package/src/request.js +357 -38
- package/src/response.js +79 -15
- package/src/router.js +686 -160
- package/src/types.d.ts +52 -3
- package/src/usage.js +16 -0
- package/src/utils.js +267 -36
- package/src/view.js +2 -0
- package/src/websocket.js +239 -0
- package/src/worker.js +2 -0
package/src/websocket.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
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
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
const { canBeOptimizedWithParams, decodeParam, NullObject } = require("./utils.js");
|
|
20
|
+
|
|
21
|
+
// the parameter names in a path, in the order µWS numbers them
|
|
22
|
+
const PARAM = /:(\w+)/g;
|
|
23
|
+
|
|
24
|
+
// Handlers µWS calls with the socket. Everything else in a behavior object is a µWS setting
|
|
25
|
+
// (maxPayloadLength, idleTimeout, compression, ...) and rides through untouched.
|
|
26
|
+
const SOCKET_HANDLERS = ["open", "message", "dropped", "drain", "close", "ping", "pong", "subscription"];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Joins a mount path and a route path the way the router does, without the empty-string edges
|
|
30
|
+
* that would leave a double slash.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} prefix
|
|
33
|
+
* @param {string} path
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function joinPaths(prefix, path) {
|
|
37
|
+
if (!prefix || prefix === "/") {
|
|
38
|
+
return path;
|
|
39
|
+
}
|
|
40
|
+
if (!path || path === "/") {
|
|
41
|
+
return prefix;
|
|
42
|
+
}
|
|
43
|
+
return prefix + path;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Every websocket route reachable from this router, with the mount paths already applied.
|
|
48
|
+
*
|
|
49
|
+
* Walked separately from the HTTP routes: those fall back to ordinary routing when µWS cannot
|
|
50
|
+
* match them, and a websocket has no fallback to fall back to, so an unmountable one has to be
|
|
51
|
+
* refused out loud instead.
|
|
52
|
+
*
|
|
53
|
+
* @param {any} router
|
|
54
|
+
* @param {string|null} prefix the mount path accumulated so far, or null once a mount was a
|
|
55
|
+
* shape µWS cannot match, which makes everything below it unreachable
|
|
56
|
+
* @param {any[]} out
|
|
57
|
+
* @param {Set<any>} seen routers already walked, since a router may be mounted twice
|
|
58
|
+
*/
|
|
59
|
+
function collectRoutes(router, prefix, out, seen) {
|
|
60
|
+
if (seen.has(router)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
seen.add(router);
|
|
64
|
+
|
|
65
|
+
for (const entry of router._wsRoutes ?? []) {
|
|
66
|
+
if (prefix === null) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`websocket route "${entry.path}" sits under a mount µWS cannot match. ` +
|
|
69
|
+
"Mount the router on a literal path, or on one whose parameters are whole segments."
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const path = joinPaths(prefix, entry.path);
|
|
73
|
+
if (!canBeOptimizedWithParams(path)) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`websocket path "${path}" is not one µWS can match. Use a literal path, or ` +
|
|
76
|
+
"parameters that are a whole segment, as in /room/:id."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
out.push({ path, behavior: entry.behavior, owner: entry.owner });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const route of router._routes) {
|
|
83
|
+
if (!route.use) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
for (const callback of route.callbacks) {
|
|
87
|
+
// a mounted Router, or a callable sub-app, which is a function carrying routes
|
|
88
|
+
if (callback && callback._routes) {
|
|
89
|
+
const mount =
|
|
90
|
+
prefix === null || typeof route.path !== "string" || !canBeOptimizedWithParams(route.path)
|
|
91
|
+
? null
|
|
92
|
+
: joinPaths(prefix, route.path);
|
|
93
|
+
collectRoutes(callback, mount, out, seen);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The µWS upgrade handler for one route: it builds this project's request and response, offers
|
|
101
|
+
* them to the application's own `upgrade` hook, and completes the handshake unless that hook
|
|
102
|
+
* answered the request itself.
|
|
103
|
+
*
|
|
104
|
+
* @param {any} app the application whose request and response classes serve this route
|
|
105
|
+
* @param {string} path the composed path, whose parameters are read back by index
|
|
106
|
+
* @param {any} behavior what the caller registered
|
|
107
|
+
* @returns {(res: any, req: any, context: any) => void}
|
|
108
|
+
*/
|
|
109
|
+
function makeUpgradeHandler(app, path, behavior) {
|
|
110
|
+
const paramNames = [...path.matchAll(PARAM)].map((match) => match[1]);
|
|
111
|
+
const userUpgrade = behavior.upgrade;
|
|
112
|
+
|
|
113
|
+
return (res, req, context) => {
|
|
114
|
+
// read off the µWS request before anything can await: it is neutered on return, and the
|
|
115
|
+
// handshake needs these three even when the upgrade is decided asynchronously
|
|
116
|
+
const key = req.getHeader("sec-websocket-key");
|
|
117
|
+
const protocol = req.getHeader("sec-websocket-protocol");
|
|
118
|
+
const extensions = req.getHeader("sec-websocket-extensions");
|
|
119
|
+
|
|
120
|
+
const request = new app._request(req, res, app);
|
|
121
|
+
if (paramNames.length) {
|
|
122
|
+
const params = new NullObject();
|
|
123
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
124
|
+
params[paramNames[i]] = decodeParam(req.getParameter(i));
|
|
125
|
+
}
|
|
126
|
+
request.params = params;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let aborted = false;
|
|
130
|
+
|
|
131
|
+
/** Completes the handshake, unless the hook answered or the client already left. */
|
|
132
|
+
const accept = () => {
|
|
133
|
+
if (aborted || request.res?.finished) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// the socket outlives the response, so what only the response can answer is read
|
|
137
|
+
// while it is still alive: reading it later would be a use after free
|
|
138
|
+
request._detachFromResponse();
|
|
139
|
+
res.cork(() => {
|
|
140
|
+
res.upgrade({ req: request }, key, protocol, extensions, context);
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (!userUpgrade) {
|
|
145
|
+
accept();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const response = new app._response(res, request, app);
|
|
150
|
+
request.res = response;
|
|
151
|
+
|
|
152
|
+
let decision;
|
|
153
|
+
try {
|
|
154
|
+
decision = userUpgrade(request, response);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// an upgrade that throws refuses the socket, and says so the way an unhandled route
|
|
157
|
+
// would rather than leaving the client hanging on a half-open handshake
|
|
158
|
+
if (!response.finished) {
|
|
159
|
+
res.cork(() => {
|
|
160
|
+
response.status(500).end();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
app.emit("error", err);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!decision || typeof decision.then !== "function") {
|
|
168
|
+
accept();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// an async hook (a session lookup, a token check) outlives this callback, so µWS has to
|
|
173
|
+
// be told who to call if the client leaves first. Registered now, still inside the
|
|
174
|
+
// handler, which is the only place µWS accepts it
|
|
175
|
+
res.onAborted(() => {
|
|
176
|
+
aborted = true;
|
|
177
|
+
});
|
|
178
|
+
// and whatever the hook writes now lands outside the cork µWS holds for this callback,
|
|
179
|
+
// so the response opens its own, exactly as a route handler answering late does
|
|
180
|
+
response._corkNeeded = true;
|
|
181
|
+
decision.then(accept, (err) => {
|
|
182
|
+
if (!aborted && !response.finished) {
|
|
183
|
+
res.cork(() => {
|
|
184
|
+
response.status(500).end();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
app.emit("error", err);
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Hands every websocket route this application can reach to µWS. Called from listen(), before
|
|
194
|
+
* the catch-all goes on: µWS routes an upgrade to the websocket route even when a catch-all
|
|
195
|
+
* covers the same path, so the two live side by side.
|
|
196
|
+
*
|
|
197
|
+
* @param {any} app
|
|
198
|
+
*/
|
|
199
|
+
function registerWebSocketRoutes(app) {
|
|
200
|
+
const routes = [];
|
|
201
|
+
collectRoutes(app, "", routes, new Set());
|
|
202
|
+
for (const route of routes) {
|
|
203
|
+
const uwsBehavior = { ...route.behavior };
|
|
204
|
+
delete uwsBehavior.upgrade;
|
|
205
|
+
// bound to the owner's classes, so a mounted sub-app's request layer is the one its own
|
|
206
|
+
// handlers expect
|
|
207
|
+
uwsBehavior.upgrade = makeUpgradeHandler(route.owner ?? app, route.path, route.behavior);
|
|
208
|
+
app.uwsApp.ws(route.path, uwsBehavior);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Whatever a caller passed as a behavior, checked where it is written rather than where it is
|
|
214
|
+
* used: a handler under a misspelled name would otherwise never run and never say why.
|
|
215
|
+
*
|
|
216
|
+
* @param {string} path
|
|
217
|
+
* @param {any} behavior
|
|
218
|
+
*/
|
|
219
|
+
function checkBehavior(path, behavior) {
|
|
220
|
+
if (typeof path !== "string") {
|
|
221
|
+
throw new TypeError("app.ws() requires a path string");
|
|
222
|
+
}
|
|
223
|
+
if (!behavior || typeof behavior !== "object") {
|
|
224
|
+
throw new TypeError("app.ws() requires a behavior object, as µWS takes");
|
|
225
|
+
}
|
|
226
|
+
if (!canBeOptimizedWithParams(path)) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`websocket path "${path}" is not one µWS can match. Use a literal path, or ` +
|
|
229
|
+
"parameters that are a whole segment, as in /room/:id."
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
for (const name of [...SOCKET_HANDLERS, "upgrade"]) {
|
|
233
|
+
if (behavior[name] !== undefined && typeof behavior[name] !== "function") {
|
|
234
|
+
throw new TypeError(`app.ws() behavior.${name} must be a function`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = { registerWebSocketRoutes, checkBehavior };
|
package/src/worker.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Copyright 2024 dimden.dev
|
|
3
3
|
Copyright 2026 Nigro Simone
|
|
4
4
|
|
|
5
|
+
This file is derived from Ultimate Express and has been modified.
|
|
6
|
+
|
|
5
7
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
8
|
you may not use this file except in compliance with the License.
|
|
7
9
|
You may obtain a copy of the License at
|