fulmine.js 5.21.1 → 5.21.2
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 +2 -2
- package/package.json +1 -2
- package/src/adopt.js +9 -23
- package/src/application.js +168 -246
- package/src/cli.js +71 -163
- package/src/cluster.js +5 -15
- package/src/compression.js +52 -114
- package/src/create.js +3 -6
- package/src/declarative.js +85 -167
- package/src/hot-settings.js +29 -21
- package/src/index.js +2 -5
- package/src/lazy-readable.js +6 -18
- package/src/lazy-writable.js +5 -16
- package/src/middlewares.js +151 -366
- package/src/nest.js +10 -16
- package/src/node-shim.js +75 -83
- package/src/optimizer.js +68 -153
- package/src/parse-query.js +7 -15
- package/src/request-utils.js +12 -29
- package/src/request.js +268 -406
- package/src/response.js +333 -560
- package/src/route.js +10 -6
- package/src/router-utils.js +124 -249
- package/src/router.js +227 -360
- package/src/server-shape.js +4 -16
- package/src/server-timing.js +2 -14
- package/src/socket.js +23 -6
- package/src/testing.js +19 -44
- package/src/usage.js +10 -20
- package/src/utils.js +173 -379
- package/src/uws.js +6 -8
- package/src/verify.js +20 -39
- package/src/view.js +29 -10
- package/src/walk.js +109 -156
- package/src/websocket.js +4 -7
- package/src/work.js +4 -11
package/src/application.js
CHANGED
|
@@ -43,8 +43,7 @@ const { workerCount, forkWorkers, isSupervising, becomeSupervisor } = require(".
|
|
|
43
43
|
|
|
44
44
|
const cpuCount = os.cpus().length;
|
|
45
45
|
|
|
46
|
-
// marks a "trust proxy"
|
|
47
|
-
// mounted sub-app knows it may inherit the parent's
|
|
46
|
+
// marks a "trust proxy" the application never set, under express's key, so a sub-app may inherit
|
|
48
47
|
const trustProxyDefaultSymbol = "@@symbol:trust_proxy_default";
|
|
49
48
|
|
|
50
49
|
const workers = /** @type {FSWorker[]} */ ([]);
|
|
@@ -52,29 +51,29 @@ let taskKey = 0;
|
|
|
52
51
|
const workerTasks = new NullObject();
|
|
53
52
|
|
|
54
53
|
class FSWorker {
|
|
54
|
+
/** Whether a read is in flight on it. @type {boolean} */
|
|
55
|
+
busy = false;
|
|
56
|
+
|
|
57
|
+
/** @type {Worker} */
|
|
58
|
+
worker;
|
|
59
|
+
|
|
55
60
|
/**
|
|
56
|
-
* A worker thread that
|
|
57
|
-
* It is unref'd, so an idle one does not keep the process alive, and it is shared between every
|
|
58
|
-
* app in the process rather than started per app.
|
|
61
|
+
* A worker thread that only reads files, unref'd and shared by every app in the process.
|
|
59
62
|
*/
|
|
60
63
|
constructor() {
|
|
61
|
-
|
|
62
|
-
//
|
|
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
|
|
64
|
+
// its own execArgv: a --import written for the parent (Angular's route extraction loader
|
|
65
|
+
// reads workerData) throws in a thread that only reads files
|
|
65
66
|
this.worker = new Worker(path.join(__dirname, "worker.js"), { execArgv: [] });
|
|
66
67
|
|
|
67
68
|
this.worker.on("message", (message) => {
|
|
68
|
-
//
|
|
69
|
-
// as {"watch:import": [...]}, which carries no key of ours
|
|
69
|
+
// under --watch node reports {"watch:import": [...]} here too, with no key of ours
|
|
70
70
|
if (workerTasks[message.key] === undefined) return;
|
|
71
71
|
this.busy = false;
|
|
72
72
|
if (message.err) {
|
|
73
73
|
workerTasks[message.key].reject(new Error(message.err));
|
|
74
74
|
} else {
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// res.end() implementations (e.g. express-session calls Buffer.byteLength on the chunk).
|
|
75
|
+
// the transferred ArrayBuffer as a Buffer, zero-copy: express-session calls
|
|
76
|
+
// Buffer.byteLength on what res.end() gets
|
|
78
77
|
workerTasks[message.key].resolve(
|
|
79
78
|
message.data instanceof ArrayBuffer ? Buffer.from(message.data) : message.data
|
|
80
79
|
);
|
|
@@ -87,31 +86,27 @@ class FSWorker {
|
|
|
87
86
|
}
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
// the worker path's
|
|
91
|
-
// holds an entry the read path would not have produced whole
|
|
89
|
+
// the worker path's bound, a bigger file streams
|
|
92
90
|
const FILE_CACHE_MAX_ENTRY = 768 * 1024;
|
|
93
|
-
// oldest-first once the budget is spent
|
|
94
|
-
// something other than an application server anyway
|
|
91
|
+
// oldest-first once the budget is spent
|
|
95
92
|
const FILE_CACHE_BUDGET = 64 * 1024 * 1024;
|
|
96
93
|
|
|
97
94
|
class Application extends Router {
|
|
98
95
|
/**
|
|
99
|
-
*
|
|
100
|
-
* Router does not: express chains a mounted app's settings onto its parent's.
|
|
96
|
+
* A mounted app's settings chain onto its parent's, as in express.
|
|
101
97
|
*
|
|
102
98
|
* @type {boolean}
|
|
103
99
|
*/
|
|
104
100
|
_inheritsSettings = true;
|
|
105
101
|
|
|
106
102
|
/**
|
|
107
|
-
*
|
|
103
|
+
* See Router#_isApplication.
|
|
108
104
|
* @type {boolean}
|
|
109
105
|
*/
|
|
110
106
|
_isApplication = true;
|
|
111
107
|
|
|
112
108
|
/**
|
|
113
|
-
* Whether express.testing already compiled the routes
|
|
114
|
-
* else: a second compilation would register everything with uWS twice. See src/testing.js.
|
|
109
|
+
* Whether express.testing already compiled the routes, a second time would register them twice.
|
|
115
110
|
* @type {boolean|undefined}
|
|
116
111
|
*/
|
|
117
112
|
_testingCompiled;
|
|
@@ -125,12 +120,71 @@ class Application extends Router {
|
|
|
125
120
|
/** What uWS.App or uWS.SSLApp is given, kept until the app is made. */
|
|
126
121
|
_uwsOptions;
|
|
127
122
|
|
|
123
|
+
/** The forks the cluster setting asks for, 0 when none. @type {number} */
|
|
124
|
+
_clusterWorkers;
|
|
125
|
+
|
|
126
|
+
/** Whether uwsOptions carries a key and a certificate, which picks uWS.SSLApp. @type {string|undefined} */
|
|
127
|
+
ssl;
|
|
128
|
+
|
|
129
|
+
/** express's app.cache, the view cache. @type {Record<string, any>} */
|
|
130
|
+
cache = new NullObject();
|
|
131
|
+
|
|
132
|
+
/** The view engines by extension, chained onto the parent's on mount. @type {Record<string, any>} */
|
|
133
|
+
engines = { __proto__: null };
|
|
134
|
+
|
|
135
|
+
/** A null prototype, as express gives app.locals. @type {Record<string, any>} */
|
|
136
|
+
locals = Object.create(null);
|
|
137
|
+
|
|
138
|
+
/** The request prototype layer of this app, what app.request extends. @type {Request} */
|
|
139
|
+
request;
|
|
140
|
+
|
|
141
|
+
/** @type {Response} */
|
|
142
|
+
response;
|
|
143
|
+
|
|
144
|
+
/** @type {boolean} */
|
|
145
|
+
listenCalled = false;
|
|
146
|
+
|
|
147
|
+
/** @type {FSWorker[]} */
|
|
148
|
+
workers = [];
|
|
149
|
+
|
|
150
|
+
/** @type {number|undefined} */
|
|
151
|
+
port;
|
|
152
|
+
|
|
153
|
+
/** @type {boolean} */
|
|
154
|
+
listening = false;
|
|
155
|
+
|
|
156
|
+
/** What address() has to go on. @type {string|undefined} */
|
|
157
|
+
_listenHost;
|
|
158
|
+
|
|
159
|
+
/** close() stops the listen socket, then waits for the pending responses, as node does. @type {import("uWebSockets.js").us_listen_socket|undefined} */
|
|
160
|
+
_listenSocket;
|
|
161
|
+
|
|
162
|
+
/** The fork supervisor, in the primary of a clustered app only. @type {{stop: () => void}|undefined} */
|
|
163
|
+
_clusterHandle;
|
|
164
|
+
|
|
165
|
+
/** readSmallFile's cache, by absolute path. @type {Map<string, {mtimeMs: number, size: number, data: Buffer}>} */
|
|
166
|
+
_fileCache = new Map();
|
|
167
|
+
|
|
168
|
+
/** @type {number} */
|
|
169
|
+
_fileCacheBytes = 0;
|
|
170
|
+
|
|
171
|
+
/** readSmallFile's reads in flight, so concurrent asks share one. @type {Map<string, Promise<Buffer>>} */
|
|
172
|
+
_fileReadsInFlight = new Map();
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The responses being served, an intrusive list (a Set paid hashing per request). A holder
|
|
176
|
+
* object: the callable app copies own scalars by value.
|
|
177
|
+
* @type {{head: Response|null}}
|
|
178
|
+
*/
|
|
179
|
+
_pending = { head: null };
|
|
180
|
+
|
|
181
|
+
/** Whether close() is waiting for the pending responses. @type {boolean} */
|
|
182
|
+
_draining = false;
|
|
183
|
+
|
|
128
184
|
/**
|
|
129
|
-
* @param {
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* existing uWS app instead of making one. Everything else is an application setting and
|
|
133
|
-
* lands next to the defaults.
|
|
185
|
+
* @param {Record<string, any>} [settings] the options express() takes: uwsOptions (HTTP or HTTPS), threads
|
|
186
|
+
* (the file-reading pool, 0 off), cluster, uwsApp (an existing uWS app); the rest are
|
|
187
|
+
* application settings
|
|
134
188
|
*/
|
|
135
189
|
constructor(settings = new NullObject()) {
|
|
136
190
|
super(settings);
|
|
@@ -140,42 +194,30 @@ class Application extends Router {
|
|
|
140
194
|
if (typeof settings.threads !== "number") {
|
|
141
195
|
settings.threads = cpuCount > 1 ? 1 : 0;
|
|
142
196
|
}
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
// settles it for the whole process before any app has listened, see becomeSupervisor
|
|
197
|
+
// counted here so a bad setting throws where the app is written, and the process becomes
|
|
198
|
+
// the supervisor before any app listens
|
|
146
199
|
this._clusterWorkers = workerCount(settings.cluster);
|
|
147
200
|
if (this._clusterWorkers > 0 && cluster.isPrimary) {
|
|
148
201
|
becomeSupervisor();
|
|
149
202
|
}
|
|
150
203
|
if (settings.http3) {
|
|
151
|
-
// uWS.H3App
|
|
152
|
-
//
|
|
153
|
-
// verified 2026-08-05 with uWS alone. A clear throw beats a native crash; this
|
|
154
|
-
// branch goes back to H3App once uNetworking ships working QUIC in the prebuilts.
|
|
204
|
+
// uWS.H3App segfaults on Linux and hangs on Windows in the pinned build, verified
|
|
205
|
+
// 2026-08-05 with uWS alone
|
|
155
206
|
throw new Error(
|
|
156
207
|
"http3 is not usable with the pinned uWebSockets.js build: its H3App crashes " +
|
|
157
208
|
"during construction. Track uNetworking/uWebSockets.js for working QUIC support."
|
|
158
209
|
);
|
|
159
210
|
}
|
|
160
211
|
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
|
|
162
|
-
// optimizer need, and what an app served through node's http never asks for
|
|
212
|
+
// the uWS app is made on first use, see the uwsApp getter
|
|
163
213
|
this._uwsApp = settings.uwsApp;
|
|
164
214
|
this._uwsOptions = settings.uwsOptions;
|
|
165
|
-
this.cache = new NullObject();
|
|
166
|
-
this.engines = { __proto__: null };
|
|
167
|
-
// a null prototype, as express gives app.locals, so a local named like an Object method
|
|
168
|
-
// is just a local
|
|
169
|
-
this.locals = Object.create(null);
|
|
170
215
|
this.locals.settings = this.settings;
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
// was an allocation on every request
|
|
216
|
+
// a request/response prototype layer per app, so extending app.request cannot leak into
|
|
217
|
+
// another app. The constructors are written out: the implicit one spreads its arguments,
|
|
218
|
+
// an allocation per request
|
|
175
219
|
this._request = class extends Request {
|
|
176
220
|
/**
|
|
177
|
-
* The base constructor's arguments, written out rather than spread. See Request.
|
|
178
|
-
*
|
|
179
221
|
* @param {import("uWebSockets.js").HttpRequest} req uWS request
|
|
180
222
|
* @param {import("uWebSockets.js").HttpResponse} res uWS response
|
|
181
223
|
* @param {Application} app the application this request arrived at
|
|
@@ -188,8 +230,6 @@ class Application extends Router {
|
|
|
188
230
|
};
|
|
189
231
|
this._response = class extends Response {
|
|
190
232
|
/**
|
|
191
|
-
* The base constructor's arguments, written out rather than spread. See Response.
|
|
192
|
-
*
|
|
193
233
|
* @param {import("uWebSockets.js").HttpResponse} res uWS response
|
|
194
234
|
* @param {Request} req the Request, already built
|
|
195
235
|
* @param {Application} app the application this request arrived at
|
|
@@ -201,22 +241,17 @@ class Application extends Router {
|
|
|
201
241
|
this.request = this._request.prototype;
|
|
202
242
|
this.response = this._response.prototype;
|
|
203
243
|
this.on("mount", (parent) => {
|
|
204
|
-
// the parent's extensions show through,
|
|
205
|
-
// application has a layer to hang onto: a plain router mount leaves things alone
|
|
244
|
+
// the parent's extensions and engines show through, as express chains them
|
|
206
245
|
if (parent.request) {
|
|
207
246
|
Object.setPrototypeOf(this.request, parent.request);
|
|
208
247
|
}
|
|
209
248
|
if (parent.response) {
|
|
210
249
|
Object.setPrototypeOf(this.response, parent.response);
|
|
211
250
|
}
|
|
212
|
-
// and the engines with them, which is the same chaining express does: a sub-app renders
|
|
213
|
-
// with whatever the parent registered unless it registered its own. Without this a
|
|
214
|
-
// render inside a mounted app looked for a module named after the extension.
|
|
215
251
|
if (parent.engines) {
|
|
216
252
|
Object.setPrototypeOf(this.engines, parent.engines);
|
|
217
253
|
}
|
|
218
|
-
// a "trust proxy"
|
|
219
|
-
// the defaults are deleted so get() falls through to the parent's value
|
|
254
|
+
// a "trust proxy" never set here is inherited: the defaults are deleted so get() falls through
|
|
220
255
|
if (
|
|
221
256
|
this._settings[trustProxyDefaultSymbol] === true &&
|
|
222
257
|
typeof parent._settings["trust proxy fn"] === "function"
|
|
@@ -225,8 +260,6 @@ class Application extends Router {
|
|
|
225
260
|
delete this._settings["trust proxy fn"];
|
|
226
261
|
}
|
|
227
262
|
});
|
|
228
|
-
this.listenCalled = false;
|
|
229
|
-
this.workers = [];
|
|
230
263
|
for (let i = 0; i < settings.threads; i++) {
|
|
231
264
|
if (workers[i]) {
|
|
232
265
|
this.workers[i] = workers[i];
|
|
@@ -234,30 +267,8 @@ class Application extends Router {
|
|
|
234
267
|
this.workers[i] = new FSWorker();
|
|
235
268
|
}
|
|
236
269
|
}
|
|
237
|
-
this.port = undefined;
|
|
238
|
-
this.listening = false;
|
|
239
|
-
// the host handed to listen(), which is all address() has to go on
|
|
240
|
-
this._listenHost = undefined;
|
|
241
|
-
// the uWS listen socket, and the responses being served right now: close() stops the
|
|
242
|
-
// first and waits for the second, the way node's server.close() does
|
|
243
|
-
this._listenSocket = undefined;
|
|
244
|
-
// the fork supervisor, in the primary of a clustered app and nowhere else
|
|
245
|
-
/** @type {{stop: () => void}|undefined} */
|
|
246
|
-
this._clusterHandle = undefined;
|
|
247
|
-
// readSmallFile's cache and its in-flight reads, see the method
|
|
248
|
-
this._fileCache = new Map();
|
|
249
|
-
this._fileCacheBytes = 0;
|
|
250
|
-
this._fileReadsInFlight = new Map();
|
|
251
|
-
// the responses being served right now, an intrusive list: linking is three pointer
|
|
252
|
-
// stores where a Set paid identity hashing and table upkeep per request. A holder object
|
|
253
|
-
// rather than a bare field, because the callable app copies own scalars by value and two
|
|
254
|
-
// copies of a head would disagree; an object rides by reference, the way the Set did
|
|
255
|
-
this._pending = /** @type {{ head: Response|null }} */ ({ head: null });
|
|
256
|
-
// on the per-app prototype layer, not per response, same as the Set was
|
|
257
270
|
/** @type {{_pendingIn?: {head: Response|null}}} */ (this.response)._pendingIn = this._pending;
|
|
258
|
-
|
|
259
|
-
// read here, at construction, the way express does; an empty NODE_ENV means development,
|
|
260
|
-
// which the ?? in the shared default would miss
|
|
271
|
+
// at construction as express reads it; an empty NODE_ENV is development
|
|
261
272
|
if (typeof this._settings.env === "undefined") {
|
|
262
273
|
this._settings.env = process.env.NODE_ENV || "development";
|
|
263
274
|
}
|
|
@@ -270,7 +281,6 @@ class Application extends Router {
|
|
|
270
281
|
}
|
|
271
282
|
}
|
|
272
283
|
}
|
|
273
|
-
// non-enumerable, so the marker never shows up walking the settings
|
|
274
284
|
Object.defineProperty(this._settings, trustProxyDefaultSymbol, {
|
|
275
285
|
configurable: true,
|
|
276
286
|
value: true
|
|
@@ -280,9 +290,8 @@ class Application extends Router {
|
|
|
280
290
|
}
|
|
281
291
|
|
|
282
292
|
/**
|
|
283
|
-
* Parks a promise's settle functions under a key the worker
|
|
284
|
-
*
|
|
285
|
-
* a million tasks being far more than can be outstanding at once.
|
|
293
|
+
* Parks a promise's settle functions under a key the worker sends back. The counter wraps at a
|
|
294
|
+
* million.
|
|
286
295
|
*
|
|
287
296
|
* @param {(value: Buffer) => void} resolve
|
|
288
297
|
* @param {(err: Error) => void} reject
|
|
@@ -298,9 +307,8 @@ class Application extends Router {
|
|
|
298
307
|
}
|
|
299
308
|
|
|
300
309
|
/**
|
|
301
|
-
* Reads a file on
|
|
302
|
-
*
|
|
303
|
-
* res.sendFile uses it for small files and streams the rest.
|
|
310
|
+
* Reads a file on a file thread picked at random. Only worth it for a small file, res.sendFile
|
|
311
|
+
* streams the rest.
|
|
304
312
|
*
|
|
305
313
|
* @param {string} path absolute path to read
|
|
306
314
|
* @returns {Promise<Buffer>}
|
|
@@ -315,11 +323,9 @@ class Application extends Router {
|
|
|
315
323
|
}
|
|
316
324
|
|
|
317
325
|
/**
|
|
318
|
-
* A small file through the worker pool
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
* on a macrotask, which is when a worker's answer would have arrived.
|
|
322
|
-
* `app.set("file cache", false)` turns the cache off, the shared read stays.
|
|
326
|
+
* A small file through the worker pool: concurrent asks share one read, and an unchanged file
|
|
327
|
+
* (by the stat the caller paid for) comes from a bounded cache, on a macrotask as a worker's
|
|
328
|
+
* answer would. `app.set("file cache", false)` keeps only the shared read.
|
|
323
329
|
*
|
|
324
330
|
* @param {string} fullpath
|
|
325
331
|
* @param {import("fs").Stats} stat
|
|
@@ -357,17 +363,14 @@ class Application extends Router {
|
|
|
357
363
|
return data;
|
|
358
364
|
});
|
|
359
365
|
this._fileReadsInFlight.set(fullpath, pending);
|
|
360
|
-
// never cached past settlement: a rejection clears the slot the same way
|
|
361
366
|
const clear = () => this._fileReadsInFlight.delete(fullpath);
|
|
362
367
|
pending.then(clear, clear);
|
|
363
368
|
return pending;
|
|
364
369
|
}
|
|
365
370
|
|
|
366
371
|
/**
|
|
367
|
-
* Reads or writes
|
|
368
|
-
*
|
|
369
|
-
* `trust proxy`, `query parser` and `etag` compile the value into a function kept beside it,
|
|
370
|
-
* and `views` becomes an absolute path.
|
|
372
|
+
* Reads or writes a setting; `set(key, undefined)` still writes. `trust proxy`, `query parser`
|
|
373
|
+
* and `etag` compile the value into a function kept beside it.
|
|
371
374
|
*
|
|
372
375
|
* @param {string} key setting name
|
|
373
376
|
* @param {*} [value] value to store; omit to read instead
|
|
@@ -379,19 +382,16 @@ class Application extends Router {
|
|
|
379
382
|
}
|
|
380
383
|
if (key === "trust proxy") {
|
|
381
384
|
if (!value) {
|
|
382
|
-
// compiled, not deleted: an explicit false must shadow a parent's setting
|
|
383
|
-
// this app is mounted, and a deleted key would read straight through to it
|
|
385
|
+
// compiled, not deleted: an explicit false must shadow a parent's setting
|
|
384
386
|
this._settings["trust proxy fn"] = compileTrust(false);
|
|
385
387
|
} else {
|
|
386
388
|
this._settings["trust proxy fn"] = compileTrust(value);
|
|
387
389
|
}
|
|
388
|
-
// set explicitly, so a mount no longer inherits the parent's
|
|
389
390
|
Object.defineProperty(this._settings, trustProxyDefaultSymbol, {
|
|
390
391
|
configurable: true,
|
|
391
392
|
value: false
|
|
392
393
|
});
|
|
393
394
|
} else if (key === "stat cache") {
|
|
394
|
-
// compiled here so the read path is a number and not a duration to parse per request
|
|
395
395
|
this._settings["stat cache ms"] = durationSetting(value, "stat cache");
|
|
396
396
|
} else if (key === "query parser") {
|
|
397
397
|
if (value === "extended") {
|
|
@@ -403,24 +403,18 @@ class Application extends Router {
|
|
|
403
403
|
} else if (value === false) {
|
|
404
404
|
this._settings["query parser fn"] = undefined;
|
|
405
405
|
} else {
|
|
406
|
-
// express's wording, which applications match on
|
|
407
406
|
throw new TypeError("unknown value for query parser function: " + value);
|
|
408
407
|
}
|
|
409
408
|
} else if (key === "etag methods") {
|
|
410
|
-
// fulmine's own: the methods whose send() computes
|
|
411
|
-
//
|
|
412
|
-
// ["GET", "HEAD"] skips the digest everywhere a validator can never match, which
|
|
413
|
-
// measured +21% on a 4KB POST answer. See issue #10.
|
|
409
|
+
// fulmine's own: the methods whose send() computes an ETag, all of them unset as
|
|
410
|
+
// express does; ["GET", "HEAD"] measured +21% on a 4KB POST answer, see issue #10
|
|
414
411
|
if (value != null && (!Array.isArray(value) || value.some((m) => typeof m !== "string"))) {
|
|
415
412
|
throw new TypeError('"etag methods" wants an array of method names, or null for all of them');
|
|
416
413
|
}
|
|
417
414
|
value = value == null ? undefined : value.map((/** @type {string} */ m) => m.toUpperCase());
|
|
418
415
|
} else if (key === "etag") {
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
// whatever this setting says, and req.fresh reads nothing else off the request.
|
|
422
|
-
// Registering a route after listen still takes them back: that is a different
|
|
423
|
-
// question, about code the analysis never saw
|
|
416
|
+
// the header skips stay: the skip branch reads the conditional pair by name whatever
|
|
417
|
+
// this says
|
|
424
418
|
if (typeof value === "function") {
|
|
425
419
|
this._settings["etag fn"] = value;
|
|
426
420
|
} else {
|
|
@@ -436,14 +430,13 @@ class Application extends Router {
|
|
|
436
430
|
delete this._settings["etag fn"];
|
|
437
431
|
break;
|
|
438
432
|
default:
|
|
439
|
-
// express's wording, which applications match on
|
|
440
433
|
throw new TypeError("unknown value for etag function: " + value);
|
|
441
434
|
}
|
|
442
435
|
}
|
|
443
436
|
}
|
|
444
437
|
|
|
445
438
|
this._settings[key] = value;
|
|
446
|
-
//
|
|
439
|
+
// see Router#_hot
|
|
447
440
|
settingsEpoch.n++;
|
|
448
441
|
return this;
|
|
449
442
|
}
|
|
@@ -469,8 +462,7 @@ class Application extends Router {
|
|
|
469
462
|
}
|
|
470
463
|
|
|
471
464
|
/**
|
|
472
|
-
* Whether a setting is truthy
|
|
473
|
-
* and as express does: mounting chains a sub-app's settings onto its parent's.
|
|
465
|
+
* Whether a setting is truthy, through the parent as get() reads it.
|
|
474
466
|
* @param {string} key setting name
|
|
475
467
|
* @returns {boolean}
|
|
476
468
|
*/
|
|
@@ -488,25 +480,19 @@ class Application extends Router {
|
|
|
488
480
|
}
|
|
489
481
|
|
|
490
482
|
/**
|
|
491
|
-
* Router's handleRequest plus the
|
|
492
|
-
* is held in a set until it finishes, so close() knows when the last one is done. Native
|
|
493
|
-
* routes and the catch-all both come through here, since both call it on the app.
|
|
483
|
+
* Router's handleRequest plus the pending list a graceful close() drains.
|
|
494
484
|
*
|
|
495
485
|
* @param {import("uWebSockets.js").HttpResponse} res uWS response
|
|
496
486
|
* @param {import("uWebSockets.js").HttpRequest} req uWS request, readable only during this call
|
|
497
|
-
* @param {import("./router-utils.js").NativePreset} [preset]
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
* call this override
|
|
502
|
-
* @returns {Request} the request, with the response reachable as request.res
|
|
487
|
+
* @param {import("./router-utils.js").NativePreset} [preset] see nativePreset
|
|
488
|
+
* @param {import("./router-utils.js").SkipHolder} [skipHolder] forwarded whole, dropping it
|
|
489
|
+
* silently turned every skip off
|
|
490
|
+
* @returns {Request} the request, with the response as request.res
|
|
503
491
|
*/
|
|
504
492
|
handleRequest(res, req, preset, skipHolder) {
|
|
505
493
|
const request = super.handleRequest(res, req, preset, skipHolder);
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
// An aborted response only flips its flags without emitting 'close', which is why
|
|
509
|
-
// close()'s drain also sweeps the list by those flags instead of trusting this alone
|
|
494
|
+
// unlinked by the close listener the Response already has; an aborted response only
|
|
495
|
+
// flips its flags, so close()'s drain sweeps by them too
|
|
510
496
|
const response = request.res;
|
|
511
497
|
const pending = this._pending;
|
|
512
498
|
response._pendingLinked = true;
|
|
@@ -520,10 +506,8 @@ class Application extends Router {
|
|
|
520
506
|
}
|
|
521
507
|
|
|
522
508
|
/**
|
|
523
|
-
* The µWS app
|
|
524
|
-
*
|
|
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.
|
|
509
|
+
* The µWS app, for what µWS offers that this does not (socket.io attaches to it). Made on
|
|
510
|
+
* first ask, so an app served through node's http never loads the binary, see src/uws.js.
|
|
527
511
|
*
|
|
528
512
|
* @returns {any}
|
|
529
513
|
*/
|
|
@@ -535,19 +519,14 @@ class Application extends Router {
|
|
|
535
519
|
return this._uwsApp;
|
|
536
520
|
}
|
|
537
521
|
|
|
538
|
-
/**
|
|
539
|
-
* Registers the catch-all uWS handler, which is what serves every request that no optimized
|
|
540
|
-
* route took natively. It walks this app's own chain and, when nothing in it answered, decides
|
|
541
|
-
* between an error, the automatic OPTIONS reply and a 404.
|
|
542
|
-
*/
|
|
522
|
+
/** The catch-all uWS handler, for every request no native route took. */
|
|
543
523
|
_createRequestHandler() {
|
|
544
524
|
this.uwsApp.any("/*", (res, req) => this._serveGeneric(res, req));
|
|
545
525
|
}
|
|
546
526
|
|
|
547
527
|
/**
|
|
548
|
-
* Serves one request by walking
|
|
549
|
-
*
|
|
550
|
-
* request it must not answer itself, see the case guard in Router#_registerUwsRoute.
|
|
528
|
+
* Serves one request by walking the chain: the catch-all, and what a native registration
|
|
529
|
+
* falls back to on a request it must not answer itself, see the case guard in _registerUwsRoute.
|
|
551
530
|
*
|
|
552
531
|
* @param {import("uWebSockets.js").HttpResponse} res the uWS response
|
|
553
532
|
* @param {import("uWebSockets.js").HttpRequest} req the uWS request
|
|
@@ -561,11 +540,9 @@ class Application extends Router {
|
|
|
561
540
|
try {
|
|
562
541
|
this._routeRequestDirect(request, response);
|
|
563
542
|
} finally {
|
|
564
|
-
// the synchronous stretch
|
|
565
|
-
// whatever comes after it is outside
|
|
543
|
+
// the synchronous stretch ran under uWS's own cork
|
|
566
544
|
response._corkNeeded = true;
|
|
567
|
-
// an abort can only arrive after this callback returns
|
|
568
|
-
// finally says: a response that finished inside it never needs uWS told at all
|
|
545
|
+
// an abort can only arrive after this callback returns
|
|
569
546
|
if (!response.finished) {
|
|
570
547
|
this._armAbort(res, response);
|
|
571
548
|
}
|
|
@@ -573,12 +550,8 @@ class Application extends Router {
|
|
|
573
550
|
}
|
|
574
551
|
|
|
575
552
|
/**
|
|
576
|
-
* Binds
|
|
577
|
-
*
|
|
578
|
-
* Returns the app and not an `http.Server`, since there is no node server underneath. The app
|
|
579
|
-
* carries `address()`, `close()`, `listening` and the 'listening' and 'close' events; anything
|
|
580
|
-
* needing a real server, socket.io being the usual case, wants `app.uwsApp`. A path instead of
|
|
581
|
-
* a port is a unix socket.
|
|
553
|
+
* Binds and starts accepting. Returns the app, which answers as an `http.Server`; socket.io
|
|
554
|
+
* wants `app.uwsApp`. A path instead of a port is a unix socket.
|
|
582
555
|
*
|
|
583
556
|
* @param {number|string} [port] port, or a unix socket path; 0 picks a free port
|
|
584
557
|
* @param {string} [host] interface to bind; every interface when omitted
|
|
@@ -587,13 +560,9 @@ class Application extends Router {
|
|
|
587
560
|
* @returns {this} the app, which doubles as the server handle
|
|
588
561
|
*/
|
|
589
562
|
listen(port, host, backlog, callback) {
|
|
590
|
-
// With { cluster } the primary
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
// workers, listen callback included, so once per worker rather than once.
|
|
594
|
-
//
|
|
595
|
-
// The test is the process and not this app: a second app on a TLS port, without a cluster
|
|
596
|
-
// setting of its own, would take that port here exclusively and every worker would fail
|
|
563
|
+
// With { cluster } the primary only forks: each worker binds this port with SO_REUSEPORT
|
|
564
|
+
// and everything below runs once per worker. The test is the process, not this app: a
|
|
565
|
+
// second app without a cluster setting would take its port here and every worker would fail
|
|
597
566
|
if (cluster.isPrimary && isSupervising()) {
|
|
598
567
|
if (this._clusterWorkers > 0 && !this._clusterHandle) {
|
|
599
568
|
this._clusterHandle = forkWorkers(this._clusterWorkers);
|
|
@@ -601,11 +570,9 @@ class Application extends Router {
|
|
|
601
570
|
return this;
|
|
602
571
|
}
|
|
603
572
|
this._compileOptimizedRoutes();
|
|
604
|
-
// before the catch-all: µWS sends an upgrade to the websocket route even when a
|
|
605
|
-
// catch-all covers the same path, so the two coexist and the order is only tidiness
|
|
606
573
|
registerWebSocketRoutes(this);
|
|
607
574
|
this._createRequestHandler();
|
|
608
|
-
// node's shapes: (cb), (port, cb), (port, host, cb)
|
|
575
|
+
// node's shapes: (cb), (port, cb), (port, host, cb), (port, host, backlog, cb)
|
|
609
576
|
if (typeof port === "function") {
|
|
610
577
|
callback = port;
|
|
611
578
|
port = 0;
|
|
@@ -615,45 +582,36 @@ class Application extends Router {
|
|
|
615
582
|
} else if (typeof backlog === "function") {
|
|
616
583
|
callback = backlog;
|
|
617
584
|
}
|
|
618
|
-
// bare listen()
|
|
619
|
-
// undefined the port fell through to the unix-socket branch below
|
|
585
|
+
// a bare listen() binds an OS-assigned port, as node does
|
|
620
586
|
if (port == null) {
|
|
621
587
|
port = 0;
|
|
622
588
|
}
|
|
623
|
-
// uWS runs this
|
|
624
|
-
//
|
|
625
|
-
// and node emits both 'listening' and 'error' from a process.nextTick.
|
|
589
|
+
// uWS runs this inside its own listen(), so everything reported to the caller is deferred
|
|
590
|
+
// a tick, as node emits 'listening' and 'error'
|
|
626
591
|
const onListen = (/** @type {import("uWebSockets.js").us_listen_socket|false} */ socket) => {
|
|
627
592
|
if (!socket) {
|
|
628
593
|
/** @type {NodeJS.ErrnoException} */
|
|
629
594
|
const err = new Error("listen EADDRINUSE: address already in use :::" + port);
|
|
630
595
|
err.code = "EADDRINUSE";
|
|
631
|
-
// Express 5
|
|
632
|
-
// so a failed bind arrives at the callback rather than being thrown past it
|
|
596
|
+
// Express 5 hands a failed bind to the listen callback
|
|
633
597
|
if (callback) {
|
|
634
598
|
return process.nextTick(() => callback.call(this, err));
|
|
635
599
|
}
|
|
636
|
-
//
|
|
637
|
-
// unhandled error rethrows it from the tick that emitted it, not from listen()
|
|
600
|
+
// without one it is thrown from the tick, as an unhandled 'error' would be
|
|
638
601
|
return process.nextTick(() => {
|
|
639
602
|
throw err;
|
|
640
603
|
});
|
|
641
604
|
}
|
|
642
|
-
// the port
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
// the Express docs use - would die on the temporal dead zone.
|
|
605
|
+
// the port synchronously, so address() works as soon as listen() returns; the
|
|
606
|
+
// callback on a tick, `const server = app.listen(p, () => server.address())` would
|
|
607
|
+
// hit the temporal dead zone
|
|
646
608
|
this.port = loadUWS().us_socket_local_port(socket);
|
|
647
609
|
this.listening = true;
|
|
648
610
|
this._listenHost = host;
|
|
649
|
-
// kept so close() can stop accepting without dropping what is in flight
|
|
650
611
|
this._listenSocket = socket;
|
|
651
612
|
process.nextTick(() => {
|
|
652
|
-
// `this` is
|
|
653
|
-
//
|
|
654
|
-
// `function () { this.address() }` reads the same on both.
|
|
655
|
-
// The callback goes first: in Express it is registered as a 'listening' listener
|
|
656
|
-
// before the caller can add any of their own.
|
|
613
|
+
// `this` is what listen() returns, as in Express; the callback first, as the
|
|
614
|
+
// first 'listening' listener
|
|
657
615
|
if (callback) callback.call(this);
|
|
658
616
|
this.emit("listening");
|
|
659
617
|
});
|
|
@@ -685,10 +643,7 @@ class Application extends Router {
|
|
|
685
643
|
}
|
|
686
644
|
|
|
687
645
|
/**
|
|
688
|
-
* Publishes a message to every socket subscribed to a topic, from outside any
|
|
689
|
-
*
|
|
690
|
-
* The socket's own `publish` reaches the same topics; this one is for the sender that is
|
|
691
|
-
* not a socket, a timer or a route handler broadcasting to a room.
|
|
646
|
+
* Publishes a message to every socket subscribed to a topic, from outside any socket.
|
|
692
647
|
*
|
|
693
648
|
* @param {string} topic
|
|
694
649
|
* @param {string|ArrayBuffer|Buffer} message
|
|
@@ -711,9 +666,7 @@ class Application extends Router {
|
|
|
711
666
|
}
|
|
712
667
|
|
|
713
668
|
/**
|
|
714
|
-
*
|
|
715
|
-
* walk `app.router.stack`. Here the application is the router, so it hands back itself and the
|
|
716
|
-
* walk finds the same layers.
|
|
669
|
+
* express 5's `app.router`, for a caller walking `app.router.stack`: the application itself.
|
|
717
670
|
*
|
|
718
671
|
* @returns {this}
|
|
719
672
|
*/
|
|
@@ -729,10 +682,8 @@ class Application extends Router {
|
|
|
729
682
|
if (!this.listening || !this.port) {
|
|
730
683
|
return null;
|
|
731
684
|
}
|
|
732
|
-
// uWS hands back the port
|
|
733
|
-
//
|
|
734
|
-
// reported as written, since what it resolved to is not readable back from here: node
|
|
735
|
-
// would say "::1" where this says "localhost".
|
|
685
|
+
// uWS hands back only the port: no host is "::" as node reports it, a hostname is
|
|
686
|
+
// reported as written where node would say "::1"
|
|
736
687
|
const host = this._listenHost;
|
|
737
688
|
if (!host) {
|
|
738
689
|
return { address: "::", family: "IPv6", port: this.port };
|
|
@@ -741,8 +692,7 @@ class Application extends Router {
|
|
|
741
692
|
}
|
|
742
693
|
|
|
743
694
|
/**
|
|
744
|
-
* The full mount path
|
|
745
|
-
* A top level app returns the empty string rather than "/".
|
|
695
|
+
* The full mount path through every parent, "" at the top level.
|
|
746
696
|
* @returns {string}
|
|
747
697
|
*/
|
|
748
698
|
path() {
|
|
@@ -757,9 +707,7 @@ class Application extends Router {
|
|
|
757
707
|
}
|
|
758
708
|
|
|
759
709
|
/**
|
|
760
|
-
* Registers a template engine for
|
|
761
|
-
*
|
|
762
|
-
* The leading dot is optional: "pug" and ".pug" register the same thing.
|
|
710
|
+
* Registers a template engine for an extension, with or without the dot.
|
|
763
711
|
*
|
|
764
712
|
* @param {string} ext file extension the engine handles
|
|
765
713
|
* @param {(path: string, options: object, callback: (err: Error|null, rendered?: string) => void) => void} fn
|
|
@@ -777,29 +725,22 @@ class Application extends Router {
|
|
|
777
725
|
}
|
|
778
726
|
|
|
779
727
|
/**
|
|
780
|
-
* Renders a view
|
|
781
|
-
* `
|
|
782
|
-
*
|
|
783
|
-
* `app.locals` and `options._locals` are merged into the options, in that order, so a
|
|
784
|
-
* per-request local wins. Caching follows the "view cache" setting unless `options.cache` says
|
|
785
|
-
* otherwise. A function in the options position is taken as the callback.
|
|
728
|
+
* Renders a view into the callback, without sending: `res.render()` is the one that responds.
|
|
729
|
+
* `app.locals` then `options._locals` are merged in, so a per-request local wins. Caching
|
|
730
|
+
* follows the "view cache" setting unless `options.cache` says otherwise.
|
|
786
731
|
*
|
|
787
732
|
* @param {string} name view name, resolved against the "views" setting
|
|
788
|
-
* @param {Record<string, any>|((err: Error|null, html?: string) => void)} [options] locals
|
|
789
|
-
* the
|
|
790
|
-
* @param {(err: Error|null, html?: string) => void} [callback]
|
|
791
|
-
* is what render is for, so leaving it out throws, as it does in Express
|
|
733
|
+
* @param {Record<string, any>|((err: Error|null, html?: string) => void)} [options] locals, or
|
|
734
|
+
* the callback in its place
|
|
735
|
+
* @param {(err: Error|null, html?: string) => void} [callback] required, as in Express
|
|
792
736
|
*/
|
|
793
737
|
render(name, options, callback) {
|
|
794
738
|
if (typeof options === "function") {
|
|
795
739
|
callback = /** @type {(err: Error|null, html?: string) => void} */ (options);
|
|
796
740
|
options = new NullObject();
|
|
797
741
|
}
|
|
798
|
-
// render exists to hand the result somewhere, so there is always a callback by this point:
|
|
799
|
-
// either the third argument or the second one, shuffled above
|
|
800
742
|
const done = /** @type {(err: Error|null, html?: string) => void} */ (callback);
|
|
801
|
-
// express's order
|
|
802
|
-
// and what was passed to this call wins over both
|
|
743
|
+
// express's order: app.locals, then res.locals as _locals, then what was passed
|
|
803
744
|
const opts = options || new NullObject();
|
|
804
745
|
options = new NullObject();
|
|
805
746
|
for (const key in this.locals) {
|
|
@@ -828,11 +769,8 @@ class Application extends Router {
|
|
|
828
769
|
view = new View(name, {
|
|
829
770
|
defaultEngine: this.get("view engine"),
|
|
830
771
|
root: this.get("views"),
|
|
831
|
-
// the object itself,
|
|
832
|
-
// through the prototype chain, and a
|
|
833
|
-
// sub-app rendering with the parent's engine went off to require() a module named
|
|
834
|
-
// after the extension. Express hands its own object over too, and means to: a view
|
|
835
|
-
// that loads an engine by require caches it back here
|
|
772
|
+
// the object itself, as express hands it: a sub-app reaches its parent's engines
|
|
773
|
+
// through the prototype chain, and a view caches a required engine back here
|
|
836
774
|
engines: this.engines
|
|
837
775
|
});
|
|
838
776
|
if (!view.path) {
|
|
@@ -864,23 +802,17 @@ class Application extends Router {
|
|
|
864
802
|
}
|
|
865
803
|
|
|
866
804
|
/**
|
|
867
|
-
* Stops accepting connections, lets in-flight requests finish, then emits 'close'.
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
*
|
|
871
|
-
* was waiting for. It still runs, but only once the last pending response is done, to drop the
|
|
872
|
-
* idle keep-alive connections nothing else would close.
|
|
873
|
-
*
|
|
874
|
-
* The callback is the first 'close' listener. Closing a server that was not listening still
|
|
875
|
-
* calls back with ERR_SERVER_NOT_RUNNING, the way node does.
|
|
805
|
+
* Stops accepting connections, lets in-flight requests finish, then emits 'close'. uWS's
|
|
806
|
+
* close() kills every connection, so it runs only after the last pending response, to drop the
|
|
807
|
+
* idle keep-alive ones. Closing a server that was not listening calls back with
|
|
808
|
+
* ERR_SERVER_NOT_RUNNING, as node does.
|
|
876
809
|
*
|
|
877
810
|
* @param {(err?: Error) => void} [callback] called once closed
|
|
878
811
|
* @returns {this} the app, for chaining
|
|
879
812
|
*/
|
|
880
813
|
close(callback) {
|
|
881
|
-
// the primary of a clustered app never bound anything
|
|
882
|
-
//
|
|
883
|
-
// and drains itself when the signal reaches it
|
|
814
|
+
// the primary of a clustered app never bound anything: it stops the workers, each of
|
|
815
|
+
// which drains itself
|
|
884
816
|
if (this._clusterHandle) {
|
|
885
817
|
this._clusterHandle.stop();
|
|
886
818
|
this._clusterHandle = undefined;
|
|
@@ -892,8 +824,7 @@ class Application extends Router {
|
|
|
892
824
|
}
|
|
893
825
|
const wasListening = this.listening;
|
|
894
826
|
this.listening = false;
|
|
895
|
-
//
|
|
896
|
-
// server that was not running still gets called back, with an error
|
|
827
|
+
// the callback is the first 'close' listener, as in Express
|
|
897
828
|
if (callback) {
|
|
898
829
|
this.once("close", () => {
|
|
899
830
|
if (wasListening) {
|
|
@@ -906,8 +837,7 @@ class Application extends Router {
|
|
|
906
837
|
});
|
|
907
838
|
}
|
|
908
839
|
if (!this.listenCalled || !wasListening) {
|
|
909
|
-
// a close
|
|
910
|
-
// 'close' serves both calls, which is what node does too
|
|
840
|
+
// a close during a drain does not emit again, as in node
|
|
911
841
|
if (!this._draining) {
|
|
912
842
|
process.nextTick(() => this.emit("close"));
|
|
913
843
|
}
|
|
@@ -927,12 +857,11 @@ class Application extends Router {
|
|
|
927
857
|
process.nextTick(finish);
|
|
928
858
|
return this;
|
|
929
859
|
}
|
|
930
|
-
//
|
|
931
|
-
//
|
|
860
|
+
// an aborted response only flips its flags, so the drain sweeps by them; the timer keeps
|
|
861
|
+
// the loop alive
|
|
932
862
|
const sweep = setInterval(() => {
|
|
933
863
|
let response = this._pending.head;
|
|
934
864
|
while (response !== null) {
|
|
935
|
-
// taken before the unlink, which nulls the pointers
|
|
936
865
|
const next = response._pendingNext;
|
|
937
866
|
if (response.finished || response.aborted) {
|
|
938
867
|
response._unlinkPending();
|
|
@@ -948,22 +877,15 @@ class Application extends Router {
|
|
|
948
877
|
}
|
|
949
878
|
}
|
|
950
879
|
|
|
951
|
-
// An app is a function, as
|
|
952
|
-
//
|
|
953
|
-
// given something it could not call.
|
|
954
|
-
//
|
|
955
|
-
// Tried once before and reverted the same day, because a callable app broke supertest: `request(app)`
|
|
956
|
-
// reads `typeof app === "function"` and wraps what it finds in http.createServer, and there was
|
|
957
|
-
// nothing underneath that could serve node's IncomingMessage. src/node-shim.js closes that hole.
|
|
880
|
+
// An app is a function, as in Express: vhost and the like call it. supertest then wraps it in
|
|
881
|
+
// http.createServer, which is what src/node-shim.js serves.
|
|
958
882
|
/** @param {object} [options] the settings express() takes, see the Application constructor */
|
|
959
883
|
module.exports = function (options) {
|
|
960
884
|
return new Application(options)._asCallable();
|
|
961
885
|
};
|
|
962
886
|
|
|
963
|
-
// the class
|
|
964
|
-
// method to that prototype adds it to every app, which is what the property is for.
|
|
887
|
+
// the class, so index.js exposes its prototype as express.application
|
|
965
888
|
module.exports.Application = Application;
|
|
966
889
|
|
|
967
|
-
//
|
|
968
|
-
// result of listen() looks for. See server-shape.js for what is answered and what is not.
|
|
890
|
+
// what makes an application answer as an http.Server, see server-shape.js
|
|
969
891
|
addServerMembers(Application.prototype);
|