fulmine.js 5.20.0 → 5.21.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/package.json +2 -2
- package/src/application.js +37 -11
- package/src/cli.js +5 -4
- package/src/declarative.js +8 -3
- package/src/index.js +0 -12
- package/src/optimizer.js +3 -2
- package/src/router.js +4 -2
- package/src/testing.js +5 -2
- package/src/types.d.ts +11 -1
- package/src/utils.js +5 -0
- package/src/uws.js +45 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.21.1",
|
|
4
4
|
"description": "Drop-in Express 5 replacement on uWebSockets.js, up to 20x faster. Same API, your middleware and framework keep working.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"statuses": "^2.0.2",
|
|
117
117
|
"tseep": "^1.3.1",
|
|
118
118
|
"type-is": "^2.1.0",
|
|
119
|
-
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.
|
|
119
|
+
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.71.0",
|
|
120
120
|
"vary": "^1.1.2"
|
|
121
121
|
},
|
|
122
122
|
"peerDependencies": {
|
package/src/application.js
CHANGED
|
@@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
|
|
|
17
17
|
limitations under the License.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const { loadUWS } = require("./uws.js");
|
|
21
21
|
const Router = require("./router.js");
|
|
22
22
|
const {
|
|
23
23
|
removeDuplicateSlashes,
|
|
@@ -59,7 +59,10 @@ class FSWorker {
|
|
|
59
59
|
*/
|
|
60
60
|
constructor() {
|
|
61
61
|
this.busy = false;
|
|
62
|
-
|
|
62
|
+
// its own execArgv, not the parent thread's: a worker inherits them, and a --require or
|
|
63
|
+
// --import written for the parent (Angular's route extraction registers a loader that reads
|
|
64
|
+
// workerData) throws inside a thread that only reads files
|
|
65
|
+
this.worker = new Worker(path.join(__dirname, "worker.js"), { execArgv: [] });
|
|
63
66
|
|
|
64
67
|
this.worker.on("message", (message) => {
|
|
65
68
|
// node speaks on this channel too: under --watch a worker reports the files it loaded
|
|
@@ -113,6 +116,15 @@ class Application extends Router {
|
|
|
113
116
|
*/
|
|
114
117
|
_testingCompiled;
|
|
115
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The uWS app once made, or the one settings.uwsApp handed in. See the uwsApp getter.
|
|
121
|
+
* @type {any}
|
|
122
|
+
*/
|
|
123
|
+
_uwsApp;
|
|
124
|
+
|
|
125
|
+
/** What uWS.App or uWS.SSLApp is given, kept until the app is made. */
|
|
126
|
+
_uwsOptions;
|
|
127
|
+
|
|
116
128
|
/**
|
|
117
129
|
* @param {object} [settings] the options express() takes. uwsOptions goes to uWS and decides
|
|
118
130
|
* between an HTTP, an HTTPS and an HTTP/3 server; threads sizes the file-reading pool, and 0
|
|
@@ -135,9 +147,7 @@ class Application extends Router {
|
|
|
135
147
|
if (this._clusterWorkers > 0 && cluster.isPrimary) {
|
|
136
148
|
becomeSupervisor();
|
|
137
149
|
}
|
|
138
|
-
if (settings.
|
|
139
|
-
this.uwsApp = /** @type {import("uWebSockets.js").TemplatedApp} */ (settings.uwsApp);
|
|
140
|
-
} else if (settings.http3) {
|
|
150
|
+
if (settings.http3) {
|
|
141
151
|
// uWS.H3App exists in the pinned build but its QUIC stack does not: the constructor
|
|
142
152
|
// segfaults on Linux and hangs forever on Windows before serving a single request,
|
|
143
153
|
// verified 2026-08-05 with uWS alone. A clear throw beats a native crash; this
|
|
@@ -146,12 +156,12 @@ class Application extends Router {
|
|
|
146
156
|
"http3 is not usable with the pinned uWebSockets.js build: its H3App crashes " +
|
|
147
157
|
"during construction. Track uNetworking/uWebSockets.js for working QUIC support."
|
|
148
158
|
);
|
|
149
|
-
} else if (settings.uwsOptions.key_file_name && settings.uwsOptions.cert_file_name) {
|
|
150
|
-
this.uwsApp = uWS.SSLApp(settings.uwsOptions);
|
|
151
|
-
} else {
|
|
152
|
-
this.uwsApp = uWS.App(settings.uwsOptions);
|
|
153
159
|
}
|
|
154
160
|
this.ssl = settings.uwsOptions.key_file_name && settings.uwsOptions.cert_file_name;
|
|
161
|
+
// the uWS app is made on first use, see the uwsApp getter: what listen(), ws() and the
|
|
162
|
+
// optimizer need, and what an app served through node's http never asks for
|
|
163
|
+
this._uwsApp = settings.uwsApp;
|
|
164
|
+
this._uwsOptions = settings.uwsOptions;
|
|
155
165
|
this.cache = new NullObject();
|
|
156
166
|
this.engines = { __proto__: null };
|
|
157
167
|
// a null prototype, as express gives app.locals, so a local named like an Object method
|
|
@@ -509,6 +519,22 @@ class Application extends Router {
|
|
|
509
519
|
return request;
|
|
510
520
|
}
|
|
511
521
|
|
|
522
|
+
/**
|
|
523
|
+
* The µWS app underneath, for anything µWS offers that this does not: socket.io attaches to
|
|
524
|
+
* it. Made the first time it is asked for, so an application that only ever answers through
|
|
525
|
+
* node's http, a test through supertest or Angular's build extracting routes in a worker
|
|
526
|
+
* thread, never loads the binary. See src/uws.js for why that matters.
|
|
527
|
+
*
|
|
528
|
+
* @returns {any}
|
|
529
|
+
*/
|
|
530
|
+
get uwsApp() {
|
|
531
|
+
if (this._uwsApp === undefined) {
|
|
532
|
+
const uWS = loadUWS();
|
|
533
|
+
this._uwsApp = this.ssl ? uWS.SSLApp(this._uwsOptions) : uWS.App(this._uwsOptions);
|
|
534
|
+
}
|
|
535
|
+
return this._uwsApp;
|
|
536
|
+
}
|
|
537
|
+
|
|
512
538
|
/**
|
|
513
539
|
* Registers the catch-all uWS handler, which is what serves every request that no optimized
|
|
514
540
|
* route took natively. It walks this app's own chain and, when nothing in it answered, decides
|
|
@@ -617,7 +643,7 @@ class Application extends Router {
|
|
|
617
643
|
// listen() returns. The callback is not: running it here would run it before listen()
|
|
618
644
|
// had returned, and `const server = app.listen(p, () => server.address())` - the form
|
|
619
645
|
// the Express docs use - would die on the temporal dead zone.
|
|
620
|
-
this.port =
|
|
646
|
+
this.port = loadUWS().us_socket_local_port(socket);
|
|
621
647
|
this.listening = true;
|
|
622
648
|
this._listenHost = host;
|
|
623
649
|
// kept so close() can stop accepting without dropping what is in flight
|
|
@@ -888,7 +914,7 @@ class Application extends Router {
|
|
|
888
914
|
return this;
|
|
889
915
|
}
|
|
890
916
|
if (this._listenSocket) {
|
|
891
|
-
|
|
917
|
+
loadUWS().us_listen_socket_close(this._listenSocket);
|
|
892
918
|
this._listenSocket = undefined;
|
|
893
919
|
}
|
|
894
920
|
this._draining = true;
|
package/src/cli.js
CHANGED
|
@@ -107,10 +107,11 @@ const DIFFERENCES = [
|
|
|
107
107
|
[
|
|
108
108
|
"a compiled route keeps its connection header",
|
|
109
109
|
"A handler simple enough to be read at registration time is answered natively, and a client\n" +
|
|
110
|
-
"that sent Connection: close is still told keep-alive, though the socket does close. A
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
'
|
|
110
|
+
"that sent Connection: close is still told keep-alive, though the socket does close. A\n" +
|
|
111
|
+
"response that would carry a validator is never compiled, so conditional requests behave as\n" +
|
|
112
|
+
'on Express. app.set("declarative responses", false) turns it off. A body with a piece of\n' +
|
|
113
|
+
'the query or a route parameter in it is compiled only under app.set("declarative request\n' +
|
|
114
|
+
'values", true), which takes the value as uWS reads it: undecoded, the first one, or none.'
|
|
114
115
|
],
|
|
115
116
|
[
|
|
116
117
|
"headers are capped at 4096 bytes by default",
|
package/src/declarative.js
CHANGED
|
@@ -21,8 +21,7 @@ const acorn = require("acorn");
|
|
|
21
21
|
const { stringify, contentTypeSet, withUtf8Charset, contentTypeFor, headerIsWritable } = require("./utils.js");
|
|
22
22
|
// H3App, DeclarativeResponse and _cfg exist at runtime but are missing from the .d.ts the
|
|
23
23
|
// package ships, so the module is read through a loose alias
|
|
24
|
-
const
|
|
25
|
-
const uWSAny = /** @type {any} */ (uWS);
|
|
24
|
+
const { loadUWS } = require("./uws.js");
|
|
26
25
|
const statuses = require("statuses");
|
|
27
26
|
|
|
28
27
|
/** @typedef {import("./application.js").Application} Application */
|
|
@@ -818,6 +817,12 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
818
817
|
}
|
|
819
818
|
const { sendUsed, bodyFromSend } = read;
|
|
820
819
|
|
|
820
|
+
// a part copied out of the request is written by uWS with its own reading of it, so the
|
|
821
|
+
// route is compiled only where the application asked for that
|
|
822
|
+
if (!app.get("declarative request values") && body.some((part) => part.type !== "text")) {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
|
|
821
826
|
// a handler that never sends is not a response: Express leaves the request waiting, so this
|
|
822
827
|
// has to fall back instead of answering a bare 200
|
|
823
828
|
if (!sendUsed && !sendStatusUsed) {
|
|
@@ -830,7 +835,7 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
830
835
|
return false;
|
|
831
836
|
}
|
|
832
837
|
|
|
833
|
-
let decRes = new
|
|
838
|
+
let decRes = new (loadUWS().DeclarativeResponse)();
|
|
834
839
|
|
|
835
840
|
if (statusCode !== 200) {
|
|
836
841
|
const statusMessage = statuses.message[statusCode] ?? "unknown";
|
package/src/index.js
CHANGED
|
@@ -17,11 +17,6 @@ See the License for the specific language governing permissions and
|
|
|
17
17
|
limitations under the License.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
// H3App, DeclarativeResponse and _cfg exist at runtime but are missing from the .d.ts the
|
|
21
|
-
// package ships, so the module is read through a loose alias
|
|
22
|
-
const uWS = require("uWebSockets.js");
|
|
23
|
-
const uWSAny = /** @type {any} */ (uWS);
|
|
24
|
-
|
|
25
20
|
// A project on pnpm owns the uWebSockets.js dependency itself, see `npx fulmine.js pnpm`, so the
|
|
26
21
|
// one installed can drift from the one this package pins and was tested against. Said once, at
|
|
27
22
|
// require time, where it reaches every deployment rather than only the ones that run verify.
|
|
@@ -50,13 +45,6 @@ const middlewares = require("./middlewares.js");
|
|
|
50
45
|
const Request = require("./request.js");
|
|
51
46
|
const Response = require("./response.js");
|
|
52
47
|
|
|
53
|
-
try {
|
|
54
|
-
// disable Uwebsockets header
|
|
55
|
-
uWSAny._cfg("999999990007");
|
|
56
|
-
} catch (error) {
|
|
57
|
-
// older uWS builds do not expose _cfg; there is nothing to fall back to
|
|
58
|
-
}
|
|
59
|
-
|
|
60
48
|
try {
|
|
61
49
|
// the compile cache, node 22.8 and up: the next boot skips compiling the same code. Respects
|
|
62
50
|
// NODE_DISABLE_COMPILE_CACHE, and booting without a cache is not an error
|
package/src/optimizer.js
CHANGED
|
@@ -558,8 +558,9 @@ function registerUwsRoute(router, route, optimizedPath) {
|
|
|
558
558
|
route.paramCallbacks.size === 0 && // a param callback has to run, and this answers without running anything
|
|
559
559
|
// a captured value is decoded when the route runs, and one that cannot be decoded is a
|
|
560
560
|
// 400 in express and on the ordinary path here. Nothing runs to raise it on a
|
|
561
|
-
// declarative response, so GET /a-b%5Ec@d%e came back 200 from app.get("/:p12")
|
|
562
|
-
|
|
561
|
+
// declarative response, so GET /a-b%5Ec@d%e came back 200 from app.get("/:p12").
|
|
562
|
+
// "declarative request values" is the application accepting that
|
|
563
|
+
(route.optimizedParams === undefined || router.get("declarative request values")) &&
|
|
563
564
|
// a declarative response is answered by µWS itself, so no javascript runs and the case
|
|
564
565
|
// guard could not: a route that needs one has to stay an ordinary handler
|
|
565
566
|
caseGuards === null &&
|
package/src/router.js
CHANGED
|
@@ -100,9 +100,11 @@ module.exports = class Router extends EventEmitter {
|
|
|
100
100
|
* it have already established that, so the honest `TemplatedApp|undefined` would only add
|
|
101
101
|
* casts where the guard already is.
|
|
102
102
|
*
|
|
103
|
-
* @
|
|
103
|
+
* @returns {any}
|
|
104
104
|
*/
|
|
105
|
-
uwsApp
|
|
105
|
+
get uwsApp() {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
106
108
|
|
|
107
109
|
/**
|
|
108
110
|
* Whether an unset routing flag reads on through the mount parent. Only an application does,
|
package/src/testing.js
CHANGED
|
@@ -187,8 +187,11 @@ function whyNotCompiled(app, entry) {
|
|
|
187
187
|
if (!app.get("declarative responses")) {
|
|
188
188
|
return "answered by µWS, but declarative responses are turned off";
|
|
189
189
|
}
|
|
190
|
-
if (entry.path.includes(":")) {
|
|
191
|
-
return
|
|
190
|
+
if (entry.path.includes(":") && !app.get("declarative request values")) {
|
|
191
|
+
return (
|
|
192
|
+
"answered by µWS, but the route captures, and nothing runs to decode the value: " +
|
|
193
|
+
'app.set("declarative request values", true) is what puts a route here'
|
|
194
|
+
);
|
|
192
195
|
}
|
|
193
196
|
if (app.get("etag")) {
|
|
194
197
|
return (
|
package/src/types.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ declare module "fulmine.js" {
|
|
|
18
18
|
import e from "express";
|
|
19
19
|
import uWS from "uWebSockets.js";
|
|
20
20
|
import { ZlibOptions, BrotliOptions } from "zlib";
|
|
21
|
+
import serveStatic = require("serve-static");
|
|
21
22
|
|
|
22
23
|
type Settings = {
|
|
23
24
|
uwsOptions?: uWS.AppOptions;
|
|
@@ -36,7 +37,16 @@ declare module "fulmine.js" {
|
|
|
36
37
|
export import request = e.request;
|
|
37
38
|
export import response = e.response;
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
/** serve-static's options plus preCompressed, which is this project's. */
|
|
41
|
+
interface StaticOptions extends serveStatic.ServeStaticOptions<e.Response> {
|
|
42
|
+
/**
|
|
43
|
+
* Serve `file.br` or `file.gz` in place of `file` when one is on disk and the client
|
|
44
|
+
* takes it. Off by default. Which twins a path has is remembered for a second;
|
|
45
|
+
* `{ cache: false }` asks the disk every time, a duration sets how long.
|
|
46
|
+
*/
|
|
47
|
+
preCompressed?: boolean | { cache?: number | string | false };
|
|
48
|
+
}
|
|
49
|
+
function static(root: string, options?: StaticOptions): e.RequestHandler;
|
|
40
50
|
// export import query = e.query;
|
|
41
51
|
|
|
42
52
|
// express has no compression middleware, so there is nothing to re-export: these are the
|
package/src/utils.js
CHANGED
|
@@ -1028,6 +1028,11 @@ const defaultSettings = {
|
|
|
1028
1028
|
// The native µWS router matches bytes, so the compiler in _compileOptimizedRoutes only hands
|
|
1029
1029
|
// it routes whose earlier siblings it can prove agree under either case rule.
|
|
1030
1030
|
"declarative responses": true,
|
|
1031
|
+
// off: a compiled body copies nothing out of the request. On, res.send(req.query.q) and
|
|
1032
|
+
// res.send(req.params.id) compile too, written by uWS as it reads them and not as Express
|
|
1033
|
+
// does: a query key repeated or missing gives the first value or nothing, a route parameter
|
|
1034
|
+
// goes out undecoded, and one that Express refuses with a 400 is answered
|
|
1035
|
+
"declarative request values": false,
|
|
1031
1036
|
// on. Off hands every request to the ordinary chain instead of letting uWS match what it can,
|
|
1032
1037
|
// which is slower and answers the same. Not a tuning knob: it exists so one application can be
|
|
1033
1038
|
// served both ways and the answers compared, see `npm run fuzz -- --self`. A compiled response
|
package/src/uws.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2024 dimden.dev
|
|
3
|
+
Copyright 2026 Nigro Simone
|
|
4
|
+
|
|
5
|
+
This file is derived from Ultimate Express and has been modified.
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
|
|
17
|
+
// µWebSockets.js, loaded the first time something needs it rather than when this package is
|
|
18
|
+
// required. An application that is built and never listens never loads the binary. Angular's
|
|
19
|
+
// build imports server.ts in a worker thread to extract the routes and serves it through node's
|
|
20
|
+
// http, and on Windows the binary crashes the process when a thread that loaded it exits
|
|
21
|
+
// (uNetworking/uWebSockets.js#668), so the build only works if nothing in that thread loads it.
|
|
22
|
+
|
|
23
|
+
/** @type {any} */
|
|
24
|
+
let uWS;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The µWS module. H3App, DeclarativeResponse and _cfg exist at runtime but are missing from the
|
|
28
|
+
* .d.ts the package ships, so it is handed back loosely typed.
|
|
29
|
+
*
|
|
30
|
+
* @returns {any}
|
|
31
|
+
*/
|
|
32
|
+
function loadUWS() {
|
|
33
|
+
if (uWS === undefined) {
|
|
34
|
+
uWS = /** @type {any} */ (require("uWebSockets.js"));
|
|
35
|
+
try {
|
|
36
|
+
// disable Uwebsockets header
|
|
37
|
+
uWS._cfg("999999990007");
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// older uWS builds do not expose _cfg; there is nothing to fall back to
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return uWS;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { loadUWS };
|