fulmine.js 5.19.3 → 5.19.4
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 +1 -1
- package/src/application.js +25 -36
- package/src/cli.js +22 -16
- package/src/cluster.js +2 -2
- package/src/compression.js +102 -61
- package/src/declarative.js +47 -24
- package/src/hot-settings.js +5 -5
- package/src/lazy-readable.js +8 -6
- package/src/lazy-writable.js +3 -3
- package/src/middlewares.js +94 -55
- package/src/nest.js +3 -2
- package/src/node-shim.js +10 -5
- package/src/optimizer.js +9 -7
- package/src/options.d.ts +9 -4
- package/src/request-utils.js +3 -2
- package/src/request.js +49 -38
- package/src/response.js +145 -109
- package/src/route.js +3 -3
- package/src/router-utils.js +62 -14
- package/src/router.js +46 -28
- package/src/server-shape.js +14 -10
- package/src/server-timing.js +16 -4
- package/src/socket.js +3 -3
- package/src/testing.js +4 -3
- package/src/usage.js +10 -5
- package/src/utils.js +120 -23
- package/src/verify.js +4 -3
- package/src/view.js +1 -1
- package/src/walk.js +8 -7
- package/src/websocket.js +17 -8
- package/src/work.js +3 -3
package/package.json
CHANGED
package/src/application.js
CHANGED
|
@@ -163,11 +163,11 @@ class Application extends Router {
|
|
|
163
163
|
/**
|
|
164
164
|
* The base constructor's arguments, written out rather than spread. See Request.
|
|
165
165
|
*
|
|
166
|
-
* @param {
|
|
167
|
-
* @param {
|
|
168
|
-
* @param {
|
|
169
|
-
* @param {
|
|
170
|
-
* @param {
|
|
166
|
+
* @param {import("uWebSockets.js").HttpRequest} req uWS request
|
|
167
|
+
* @param {import("uWebSockets.js").HttpResponse} res uWS response
|
|
168
|
+
* @param {Application} app the application this request arrived at
|
|
169
|
+
* @param {import("./router-utils.js").NativePreset} [preset] a literal registration's constants
|
|
170
|
+
* @param {import("./router-utils.js").SkipHolder} [skipHolder] where a granted header skip lives
|
|
171
171
|
*/
|
|
172
172
|
constructor(req, res, app, preset, skipHolder) {
|
|
173
173
|
super(req, res, app, preset, skipHolder);
|
|
@@ -177,27 +177,13 @@ class Application extends Router {
|
|
|
177
177
|
/**
|
|
178
178
|
* The base constructor's arguments, written out rather than spread. See Response.
|
|
179
179
|
*
|
|
180
|
-
* @param {
|
|
181
|
-
* @param {
|
|
182
|
-
* @param {
|
|
180
|
+
* @param {import("uWebSockets.js").HttpResponse} res uWS response
|
|
181
|
+
* @param {Request} req the Request, already built
|
|
182
|
+
* @param {Application} app the application this request arrived at
|
|
183
183
|
*/
|
|
184
184
|
constructor(res, req, app) {
|
|
185
185
|
super(res, req, app);
|
|
186
186
|
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Node counts an explicit writeHead as the head gone out; remembered here so the
|
|
190
|
-
* automatic OPTIONS reply can refuse to add headers after it, as express's does.
|
|
191
|
-
*
|
|
192
|
-
* @param {number} statusCode
|
|
193
|
-
* @param {string|Record<string, any>} [statusMessage]
|
|
194
|
-
* @param {Record<string, any>} [headers]
|
|
195
|
-
* @returns {this}
|
|
196
|
-
*/
|
|
197
|
-
writeHead(statusCode, statusMessage, headers) {
|
|
198
|
-
this._headWritten = true;
|
|
199
|
-
return super.writeHead(statusCode, statusMessage, headers);
|
|
200
|
-
}
|
|
201
187
|
};
|
|
202
188
|
this.request = this._request.prototype;
|
|
203
189
|
this.response = this._response.prototype;
|
|
@@ -253,9 +239,9 @@ class Application extends Router {
|
|
|
253
239
|
// stores where a Set paid identity hashing and table upkeep per request. A holder object
|
|
254
240
|
// rather than a bare field, because the callable app copies own scalars by value and two
|
|
255
241
|
// copies of a head would disagree; an object rides by reference, the way the Set did
|
|
256
|
-
this._pending = /** @type {{ head:
|
|
242
|
+
this._pending = /** @type {{ head: Response|null }} */ ({ head: null });
|
|
257
243
|
// on the per-app prototype layer, not per response, same as the Set was
|
|
258
|
-
/** @type {
|
|
244
|
+
/** @type {{_pendingIn?: {head: Response|null}}} */ (this.response)._pendingIn = this._pending;
|
|
259
245
|
this._draining = false;
|
|
260
246
|
// read here, at construction, the way express does; an empty NODE_ENV means development,
|
|
261
247
|
// which the ?? in the shared default would miss
|
|
@@ -285,8 +271,8 @@ class Application extends Router {
|
|
|
285
271
|
* message carries data and not closures. The counter wraps rather than growing without bound,
|
|
286
272
|
* a million tasks being far more than can be outstanding at once.
|
|
287
273
|
*
|
|
288
|
-
* @param {(value:
|
|
289
|
-
* @param {(err:
|
|
274
|
+
* @param {(value: Buffer) => void} resolve
|
|
275
|
+
* @param {(err: Error) => void} reject
|
|
290
276
|
* @returns {number} the key to send to the worker
|
|
291
277
|
*/
|
|
292
278
|
createWorkerTask(resolve, reject) {
|
|
@@ -493,12 +479,14 @@ class Application extends Router {
|
|
|
493
479
|
* is held in a set until it finishes, so close() knows when the last one is done. Native
|
|
494
480
|
* routes and the catch-all both come through here, since both call it on the app.
|
|
495
481
|
*
|
|
496
|
-
* @param {
|
|
497
|
-
* @param {
|
|
498
|
-
* @param {
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
482
|
+
* @param {import("uWebSockets.js").HttpResponse} res uWS response
|
|
483
|
+
* @param {import("uWebSockets.js").HttpRequest} req uWS request, readable only during this call
|
|
484
|
+
* @param {import("./router-utils.js").NativePreset} [preset] a literal registration's constants,
|
|
485
|
+
* see nativePreset in the router
|
|
486
|
+
* @param {import("./router-utils.js").SkipHolder} [skipHolder] where a granted header skip lives,
|
|
487
|
+
* forwarded whole: dropping it here silently turned every skip off, since the native closures
|
|
488
|
+
* call this override
|
|
489
|
+
* @returns {Request} the request, with the response reachable as request.res
|
|
502
490
|
*/
|
|
503
491
|
handleRequest(res, req, preset, skipHolder) {
|
|
504
492
|
const request = super.handleRequest(res, req, preset, skipHolder);
|
|
@@ -532,8 +520,8 @@ class Application extends Router {
|
|
|
532
520
|
* what the catch-all runs, and also what a native registration falls back to when it sees a
|
|
533
521
|
* request it must not answer itself, see the case guard in Router#_registerUwsRoute.
|
|
534
522
|
*
|
|
535
|
-
* @param {
|
|
536
|
-
* @param {
|
|
523
|
+
* @param {import("uWebSockets.js").HttpResponse} res the uWS response
|
|
524
|
+
* @param {import("uWebSockets.js").HttpRequest} req the uWS request
|
|
537
525
|
*/
|
|
538
526
|
_serveGeneric(res, req) {
|
|
539
527
|
const request = this.handleRequest(res, req);
|
|
@@ -768,13 +756,14 @@ class Application extends Router {
|
|
|
768
756
|
* otherwise. A function in the options position is taken as the callback.
|
|
769
757
|
*
|
|
770
758
|
* @param {string} name view name, resolved against the "views" setting
|
|
771
|
-
* @param {Record<string, any
|
|
759
|
+
* @param {Record<string, any>|((err: Error|null, html?: string) => void)} [options] locals for
|
|
760
|
+
* the view, or the callback in its place
|
|
772
761
|
* @param {(err: Error|null, html?: string) => void} [callback] receives the rendered view. It
|
|
773
762
|
* is what render is for, so leaving it out throws, as it does in Express
|
|
774
763
|
*/
|
|
775
764
|
render(name, options, callback) {
|
|
776
765
|
if (typeof options === "function") {
|
|
777
|
-
callback = /** @type {
|
|
766
|
+
callback = /** @type {(err: Error|null, html?: string) => void} */ (options);
|
|
778
767
|
options = new NullObject();
|
|
779
768
|
}
|
|
780
769
|
// render exists to hand the result somewhere, so there is always a callback by this point:
|
package/src/cli.js
CHANGED
|
@@ -46,6 +46,9 @@ const { collectRoutes } = require("./testing.js");
|
|
|
46
46
|
const { verify } = require("./verify.js");
|
|
47
47
|
const { override, angular } = require("./adopt.js");
|
|
48
48
|
|
|
49
|
+
/** @typedef {import("./application.js").Application} Application */
|
|
50
|
+
/** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
|
|
51
|
+
|
|
49
52
|
const FROM = "express";
|
|
50
53
|
const TO = "fulmine.js";
|
|
51
54
|
|
|
@@ -187,7 +190,7 @@ function loadTypeScript(target) {
|
|
|
187
190
|
*
|
|
188
191
|
* @param {string} source
|
|
189
192
|
* @param {string} fileName decides whether JSX is allowed, so a .tsx angle bracket is not a cast
|
|
190
|
-
* @param {
|
|
193
|
+
* @param {typeof import("typescript")} ts the compiler
|
|
191
194
|
* @param {Set<string>} [seen] as in findSpecifiers
|
|
192
195
|
* @returns {{start: number, end: number}[]}
|
|
193
196
|
*/
|
|
@@ -202,7 +205,7 @@ function findSpecifiersTypeScript(source, fileName, ts, seen) {
|
|
|
202
205
|
|
|
203
206
|
/** @type {{start: number, end: number}[]} */
|
|
204
207
|
const found = [];
|
|
205
|
-
/** @param {
|
|
208
|
+
/** @param {import("typescript").StringLiteral} node a string literal naming a module */
|
|
206
209
|
const take = (node) => {
|
|
207
210
|
if (node.text === FROM) {
|
|
208
211
|
found.push({ start: node.getStart(sourceFile), end: node.getEnd() });
|
|
@@ -249,7 +252,8 @@ function findSpecifiersTypeScript(source, fileName, ts, seen) {
|
|
|
249
252
|
*
|
|
250
253
|
* @param {string} source
|
|
251
254
|
* @param {string} fileName decides whether JSX is allowed, as above
|
|
252
|
-
* @param {any} ts the scanner and the two enums loadTypeScript kept
|
|
255
|
+
* @param {any} ts the scanner and the two enums loadTypeScript kept, from typescript 7's unstable
|
|
256
|
+
* API, which the typings this project compiles against do not describe
|
|
253
257
|
* @param {Set<string>} [seen] as in findSpecifiers
|
|
254
258
|
* @returns {{start: number, end: number}[]}
|
|
255
259
|
*/
|
|
@@ -300,7 +304,7 @@ function findSpecifiersScanner(source, fileName, ts, seen) {
|
|
|
300
304
|
* @returns {{start: number, end: number}[]|null} null when the file does not parse
|
|
301
305
|
*/
|
|
302
306
|
function findSpecifiers(source, seen) {
|
|
303
|
-
/** @type {
|
|
307
|
+
/** @type {import("acorn").Program|null|undefined} */
|
|
304
308
|
let tree;
|
|
305
309
|
// A file is either a module or a script and the parser has to be told which. Try module first,
|
|
306
310
|
// since it also accepts everything a script can contain except a bare `return`.
|
|
@@ -308,7 +312,7 @@ function findSpecifiers(source, seen) {
|
|
|
308
312
|
try {
|
|
309
313
|
tree = acorn.parse(source, {
|
|
310
314
|
ecmaVersion: "latest",
|
|
311
|
-
sourceType: /** @type {
|
|
315
|
+
sourceType: /** @type {"module"|"script"} */ (sourceType),
|
|
312
316
|
allowReturnOutsideFunction: true,
|
|
313
317
|
allowAwaitOutsideFunction: true,
|
|
314
318
|
allowHashBang: true
|
|
@@ -324,7 +328,7 @@ function findSpecifiers(source, seen) {
|
|
|
324
328
|
|
|
325
329
|
/** @type {{start: number, end: number}[]} */
|
|
326
330
|
const found = [];
|
|
327
|
-
/** @param {
|
|
331
|
+
/** @param {import("acorn").Literal & {value: string}} node a string literal naming a module */
|
|
328
332
|
const record = (node) => {
|
|
329
333
|
if (node.value === FROM) {
|
|
330
334
|
found.push({ start: node.start, end: node.end });
|
|
@@ -361,8 +365,10 @@ function findSpecifiers(source, seen) {
|
|
|
361
365
|
* Visits every node. acorn produces plain objects, so the shape is walked rather than dispatched
|
|
362
366
|
* on: a table of node types would have to be kept in step with the parser, and being out of step
|
|
363
367
|
* would mean silently skipping an import.
|
|
364
|
-
* @param {any} node an acorn
|
|
365
|
-
*
|
|
368
|
+
* @param {any} node an acorn node, or an array or a scalar under one: walked by key, so no shape
|
|
369
|
+
* is assumed
|
|
370
|
+
* @param {(node: any) => void} visit handed every node, loose because the visitor reads edges of
|
|
371
|
+
* its own off each
|
|
366
372
|
*/
|
|
367
373
|
function walk(node, visit) {
|
|
368
374
|
if (!node || typeof node !== "object") return;
|
|
@@ -459,7 +465,7 @@ function findEntry(given) {
|
|
|
459
465
|
* owns listen.
|
|
460
466
|
*
|
|
461
467
|
* @param {string} entry
|
|
462
|
-
* @returns {
|
|
468
|
+
* @returns {object[]} the prototypes to stub, this command's copy first
|
|
463
469
|
*/
|
|
464
470
|
function listenOwners(entry) {
|
|
465
471
|
const builds = new Set([require("./index.js")]);
|
|
@@ -507,7 +513,7 @@ function listenOwners(entry) {
|
|
|
507
513
|
*
|
|
508
514
|
* @param {string[]} argv
|
|
509
515
|
* @param {string} command the word for the message when there is nothing to load
|
|
510
|
-
* @returns {{apps:
|
|
516
|
+
* @returns {{apps: Application[], entry: string}|null} null once the reason has been printed
|
|
511
517
|
*/
|
|
512
518
|
function loadApps(argv, command) {
|
|
513
519
|
const entry = findEntry(argv.find((arg) => !arg.startsWith("--")));
|
|
@@ -539,7 +545,7 @@ function loadApps(argv, command) {
|
|
|
539
545
|
try {
|
|
540
546
|
require(entry);
|
|
541
547
|
} catch (e) {
|
|
542
|
-
const error = /** @type {
|
|
548
|
+
const error = /** @type {Error} */ (e);
|
|
543
549
|
restore();
|
|
544
550
|
console.error(`${path.relative(process.cwd(), entry)} could not be loaded:
|
|
545
551
|
${error.stack ?? error}`);
|
|
@@ -584,7 +590,7 @@ ${error.stack ?? error}`);
|
|
|
584
590
|
*
|
|
585
591
|
* Best effort throughout: a build with no workers, or a worker already gone, is not an error.
|
|
586
592
|
*
|
|
587
|
-
* @param {
|
|
593
|
+
* @param {Application[]} apps
|
|
588
594
|
* @returns {void}
|
|
589
595
|
*/
|
|
590
596
|
function stopFileWorkers(apps) {
|
|
@@ -762,9 +768,9 @@ function matchesWanted(full, method, wanted) {
|
|
|
762
768
|
* application with a thousand cold routes and one hot one that fell back would score well and
|
|
763
769
|
* serve badly. What is printed is counted rather than judged.
|
|
764
770
|
*
|
|
765
|
-
* @param {
|
|
766
|
-
* @param {
|
|
767
|
-
* @param {
|
|
771
|
+
* @param {{route: RouteEntry, full: string}[]} routes
|
|
772
|
+
* @param {{route: RouteEntry, full: string}[]} native
|
|
773
|
+
* @param {{route: RouteEntry, full: string}[]} declarative
|
|
768
774
|
*/
|
|
769
775
|
function printSummary(routes, native, declarative) {
|
|
770
776
|
console.log("\nWhat this adds up to\n");
|
|
@@ -809,7 +815,7 @@ function printSummary(routes, native, declarative) {
|
|
|
809
815
|
}
|
|
810
816
|
|
|
811
817
|
/**
|
|
812
|
-
* @param {
|
|
818
|
+
* @param {Application} app the application the entry file built
|
|
813
819
|
* @param {boolean} several whether to say which application this is
|
|
814
820
|
*/
|
|
815
821
|
function printProfile(app, several) {
|
package/src/cluster.js
CHANGED
|
@@ -156,7 +156,7 @@ function becomeSupervisor() {
|
|
|
156
156
|
function forkWorkers(count) {
|
|
157
157
|
let stopping = false;
|
|
158
158
|
supervising = true;
|
|
159
|
-
/** @param {
|
|
159
|
+
/** @param {import("cluster").Worker} worker @param {number} code @param {string} signal */
|
|
160
160
|
const onExit = (worker, code, signal) => {
|
|
161
161
|
if (!stopping) {
|
|
162
162
|
console.error(`worker ${worker.process.pid} exited (${signal || code}), starting another`);
|
|
@@ -172,7 +172,7 @@ function forkWorkers(count) {
|
|
|
172
172
|
supervising = false;
|
|
173
173
|
cluster.off("exit", onExit);
|
|
174
174
|
for (const id of Object.keys(cluster.workers ?? {})) {
|
|
175
|
-
/** @type {
|
|
175
|
+
/** @type {NodeJS.Dict<import("cluster").Worker>} */ (cluster.workers)[id]?.kill();
|
|
176
176
|
}
|
|
177
177
|
};
|
|
178
178
|
for (const signal of ["SIGTERM", "SIGINT"]) {
|
package/src/compression.js
CHANGED
|
@@ -16,6 +16,10 @@ limitations under the License.
|
|
|
16
16
|
|
|
17
17
|
/** @typedef {import("./request.js")} Request */
|
|
18
18
|
/** @typedef {import("./response.js")} Response */
|
|
19
|
+
/**
|
|
20
|
+
* What res.on was given, parked until there is a compressor to hang it on.
|
|
21
|
+
* @typedef {Parameters<import("stream").Writable["on"]>} OnArgs
|
|
22
|
+
*/
|
|
19
23
|
|
|
20
24
|
// express.compression(), which answers with a compressed body when the client asked for one.
|
|
21
25
|
//
|
|
@@ -46,7 +50,8 @@ const {
|
|
|
46
50
|
ENCODING_GZIP,
|
|
47
51
|
ENCODING_DEFLATE,
|
|
48
52
|
ENCODING_ZSTD,
|
|
49
|
-
memoizeByString
|
|
53
|
+
memoizeByString,
|
|
54
|
+
applyWriteHead
|
|
50
55
|
} = require("./utils.js");
|
|
51
56
|
|
|
52
57
|
// zstd arrived in node's zlib during the range of versions this supports, so whether it can be
|
|
@@ -114,7 +119,8 @@ const noop = () => {};
|
|
|
114
119
|
*
|
|
115
120
|
* Only for the deflate formats. A brotli stream carries context across a reset.
|
|
116
121
|
*
|
|
117
|
-
* @param {() => any} create
|
|
122
|
+
* @param {() => any} create makes the stream. Loose because what is checked below is node's zlib
|
|
123
|
+
* internals, which its typings do not declare
|
|
118
124
|
* @param {number} finishFlag
|
|
119
125
|
* @param {(body: Buffer) => Buffer} oneShot
|
|
120
126
|
* @returns {(body: Buffer) => Buffer}
|
|
@@ -251,7 +257,7 @@ function toBuffer(chunk, encoding) {
|
|
|
251
257
|
* @param {object} [options]
|
|
252
258
|
* @param {number|string} [options.threshold] the smallest body worth compressing, bytes or "1kb".
|
|
253
259
|
* Default 1024. A response whose size is not known in advance is compressed whatever its size.
|
|
254
|
-
* @param {(req:
|
|
260
|
+
* @param {(req: Request, res: Response) => boolean} [options.filter] whether this response should be
|
|
255
261
|
* compressed at all. The default says yes to any compressible content type.
|
|
256
262
|
* @param {string} [options.enforceEncoding] what to use when the request carries no
|
|
257
263
|
* Accept-Encoding at all. Default "identity", which is to say nothing is compressed.
|
|
@@ -266,17 +272,19 @@ function toBuffer(chunk, encoding) {
|
|
|
266
272
|
* @param {number} [options.memLevel] zlib memory level.
|
|
267
273
|
* @param {number} [options.strategy] zlib strategy.
|
|
268
274
|
* @param {number} [options.windowBits] zlib window size.
|
|
269
|
-
* @returns {(req: any, res: any, next: (err?:
|
|
275
|
+
* @returns {(req: any, res: any, next: (err?: unknown) => void) => void} the middleware. The pair is
|
|
276
|
+
* loose because this is written against node's end() and write() shapes, which this project's
|
|
277
|
+
* own narrow
|
|
270
278
|
*/
|
|
271
279
|
function compression(options) {
|
|
272
280
|
const opts = options || {};
|
|
273
281
|
// the whole bag goes to zlib, as the compression module does: level, memLevel, strategy,
|
|
274
282
|
// windowBits and chunkSize arrive under their own names and zlib ignores the rest
|
|
275
|
-
const zlibOptions = /** @type {
|
|
283
|
+
const zlibOptions = /** @type {import("zlib").ZlibOptions} */ (opts);
|
|
276
284
|
const brotliOptions = { ...opts.brotli };
|
|
277
285
|
brotliOptions.params = {
|
|
278
286
|
[zlib.constants.BROTLI_PARAM_QUALITY]: 4,
|
|
279
|
-
...(opts.brotli && /** @type {
|
|
287
|
+
...(opts.brotli && /** @type {import("zlib").BrotliOptions} */ (opts.brotli).params)
|
|
280
288
|
};
|
|
281
289
|
// node's default level, unlike brotli above: zstd at its default is already in the band where
|
|
282
290
|
// this middleware wants to be, and dropping it further buys nothing worth the ratio
|
|
@@ -285,7 +293,7 @@ function compression(options) {
|
|
|
285
293
|
const enforceEncoding = opts.enforceEncoding || "identity";
|
|
286
294
|
// bytes.parse reads "1kb" and hands back null for anything it cannot, an absent option
|
|
287
295
|
// included, which is where the default comes in
|
|
288
|
-
const threshold = bytes.parse(/** @type {
|
|
296
|
+
const threshold = bytes.parse(/** @type {string|number} */ (opts.threshold)) ?? 1024;
|
|
289
297
|
// the mask handed to the negotiation, built once here: a name nobody knows is a config
|
|
290
298
|
// mistake and throws now rather than serving the wrong bytes later
|
|
291
299
|
let allowed = ENCODING_DEFAULT;
|
|
@@ -362,7 +370,8 @@ function compression(options) {
|
|
|
362
370
|
|
|
363
371
|
/**
|
|
364
372
|
* @param {string} method
|
|
365
|
-
* @returns {
|
|
373
|
+
* @returns {import("stream").Transform & import("zlib").Zlib} the transform stream for a body that
|
|
374
|
+
* arrives in pieces
|
|
366
375
|
*/
|
|
367
376
|
function compressStream(method) {
|
|
368
377
|
if (method === "gzip") {
|
|
@@ -393,19 +402,32 @@ function compression(options) {
|
|
|
393
402
|
if (!chosen || chosen === "identity" || req.method === "HEAD") {
|
|
394
403
|
res.flush = noFlush;
|
|
395
404
|
const _plainEnd = res.end;
|
|
405
|
+
const _plainWriteHead = res.writeHead;
|
|
396
406
|
let varied = false;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
407
|
+
/** Says the answer varies, once, before the head is settled. */
|
|
408
|
+
const vary = () => {
|
|
409
|
+
if (varied) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
varied = true;
|
|
413
|
+
const cacheControl = res.headersSent ? undefined : res.getHeader("Cache-Control");
|
|
414
|
+
if (
|
|
415
|
+
!res.headersSent &&
|
|
416
|
+
filter(req, res) &&
|
|
417
|
+
!(cacheControl && NO_TRANSFORM.test(String(cacheControl)))
|
|
418
|
+
) {
|
|
419
|
+
addVary(res);
|
|
408
420
|
}
|
|
421
|
+
};
|
|
422
|
+
// writeHead settles the head, so a handler that calls it is answered there, with the
|
|
423
|
+
// headers it carries applied first, as on-headers orders it for the compression module
|
|
424
|
+
res.writeHead = function writeHead(statusCode, statusMessage, headers) {
|
|
425
|
+
const reason = applyWriteHead(this, statusMessage, headers);
|
|
426
|
+
vary();
|
|
427
|
+
return _plainWriteHead.call(this, statusCode, reason);
|
|
428
|
+
};
|
|
429
|
+
res.end = function end(chunk, encoding, callback) {
|
|
430
|
+
vary();
|
|
409
431
|
return _plainEnd.call(this, chunk, encoding, callback);
|
|
410
432
|
};
|
|
411
433
|
return next();
|
|
@@ -414,15 +436,18 @@ function compression(options) {
|
|
|
414
436
|
const _write = res.write;
|
|
415
437
|
const _end = res.end;
|
|
416
438
|
const _on = res.on;
|
|
439
|
+
const _writeHead = res.writeHead;
|
|
417
440
|
|
|
418
441
|
/** drain listeners parked until there is a compressor to hang them on, see res.on below */
|
|
419
|
-
let listeners = /** @type {
|
|
420
|
-
/** @type {
|
|
442
|
+
let listeners = /** @type {OnArgs[]|null} */ ([]);
|
|
443
|
+
/** @type {(import("stream").Transform & import("zlib").Zlib)|null} */
|
|
421
444
|
let stream = null;
|
|
422
445
|
let decided = false;
|
|
446
|
+
// the encoding decided on, "" for none; the compressor itself starts with the first byte
|
|
447
|
+
let method = "";
|
|
423
448
|
let ended = false;
|
|
424
449
|
/** what end() was given to call back, held until the compressor has finished */
|
|
425
|
-
let endCallback = /** @type {
|
|
450
|
+
let endCallback = /** @type {(() => void)|undefined} */ (undefined);
|
|
426
451
|
|
|
427
452
|
// the compression module adds this, and code written against it calls it: an SSE feed
|
|
428
453
|
// pushes its event out with res.flush(). Nothing to flush before there is a compressor
|
|
@@ -497,27 +522,39 @@ function compression(options) {
|
|
|
497
522
|
* @param {string} method
|
|
498
523
|
*/
|
|
499
524
|
function startStream(method) {
|
|
500
|
-
|
|
525
|
+
// the closures below read the local: inside them the checker forgets the field is set
|
|
526
|
+
const compressor = (stream = compressStream(method));
|
|
501
527
|
// The parked listeners, and the list itself stays rather than being emptied: res.on
|
|
502
528
|
// reads it to know a drain listener belongs on the compressor from here on. A pipe
|
|
503
529
|
// registers its own the first time write() says to slow down, which is after this
|
|
504
|
-
for (const listener of /** @type {
|
|
505
|
-
|
|
530
|
+
for (const listener of /** @type {OnArgs[]} */ (listeners)) {
|
|
531
|
+
compressor.on(listener[0], listener[1]);
|
|
506
532
|
}
|
|
507
|
-
|
|
533
|
+
compressor.on("data", (chunk) => {
|
|
508
534
|
if (_write.call(res, chunk) === false) {
|
|
509
|
-
|
|
535
|
+
compressor.pause();
|
|
510
536
|
}
|
|
511
537
|
});
|
|
512
|
-
|
|
538
|
+
compressor.on("end", () => {
|
|
513
539
|
_end.call(res, endCallback);
|
|
514
540
|
});
|
|
515
|
-
_on.call(res, "drain", () =>
|
|
541
|
+
_on.call(res, "drain", () => compressor.resume());
|
|
516
542
|
// an aborted response never reaches the end of the stream, and the zlib context behind
|
|
517
543
|
// it is native memory that a garbage collector is in no hurry to reach
|
|
518
|
-
_on.call(res, "close", () =>
|
|
544
|
+
_on.call(res, "close", () => compressor.destroy());
|
|
519
545
|
}
|
|
520
546
|
|
|
547
|
+
// writeHead settles the head, so the decision is taken there when a handler calls it, as
|
|
548
|
+
// on-headers takes it for the compression module: with the headers it carries applied
|
|
549
|
+
// first, since a Content-Length among them is what the decision removes
|
|
550
|
+
res.writeHead = function writeHead(statusCode, statusMessage, headers) {
|
|
551
|
+
const reason = applyWriteHead(this, statusMessage, headers);
|
|
552
|
+
if (!decided) {
|
|
553
|
+
method = decide();
|
|
554
|
+
}
|
|
555
|
+
return _writeHead.call(this, statusCode, reason);
|
|
556
|
+
};
|
|
557
|
+
|
|
521
558
|
res.write = function write(chunk, encoding, callback) {
|
|
522
559
|
if (typeof encoding === "function") {
|
|
523
560
|
callback = encoding;
|
|
@@ -527,10 +564,10 @@ function compression(options) {
|
|
|
527
564
|
return false;
|
|
528
565
|
}
|
|
529
566
|
if (!decided) {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
567
|
+
method = decide();
|
|
568
|
+
}
|
|
569
|
+
if (method && !stream) {
|
|
570
|
+
startStream(method);
|
|
534
571
|
}
|
|
535
572
|
if (stream) {
|
|
536
573
|
return stream.write(toBuffer(chunk, encoding), callback);
|
|
@@ -539,8 +576,7 @@ function compression(options) {
|
|
|
539
576
|
};
|
|
540
577
|
|
|
541
578
|
res.end = function end(chunk, encoding, callback) {
|
|
542
|
-
// node's shapes
|
|
543
|
-
// argument only arrives from code written against node's ServerResponse
|
|
579
|
+
// node's shapes: the callback may sit in either position
|
|
544
580
|
if (typeof chunk === "function") {
|
|
545
581
|
callback = chunk;
|
|
546
582
|
chunk = undefined;
|
|
@@ -552,6 +588,14 @@ function compression(options) {
|
|
|
552
588
|
if (ended) {
|
|
553
589
|
return this;
|
|
554
590
|
}
|
|
591
|
+
if (!decided) {
|
|
592
|
+
method = decide(chunkLength(chunk, encoding));
|
|
593
|
+
}
|
|
594
|
+
// a head settled by writeHead can no longer take the length the whole-body answer
|
|
595
|
+
// below sets, so the body goes out in pieces, as node's does after one
|
|
596
|
+
if (method && !stream && res.headersSent) {
|
|
597
|
+
startStream(method);
|
|
598
|
+
}
|
|
555
599
|
if (stream) {
|
|
556
600
|
ended = true;
|
|
557
601
|
endCallback = callback;
|
|
@@ -562,32 +606,29 @@ function compression(options) {
|
|
|
562
606
|
}
|
|
563
607
|
return this;
|
|
564
608
|
}
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
res.setHeader("Content-Length", String(body.length));
|
|
575
|
-
return _end.call(this, body, callback);
|
|
576
|
-
}
|
|
577
|
-
compressWholeAsync(method, input, (err, body) => {
|
|
578
|
-
// the client can leave while the pool is working, and writing to a
|
|
579
|
-
// response that is already gone is not something uWS survives
|
|
580
|
-
if (res.aborted || res.finished) {
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
if (err) {
|
|
584
|
-
return res.destroy(err);
|
|
585
|
-
}
|
|
586
|
-
res.setHeader("Content-Length", String(body.length));
|
|
587
|
-
_end.call(res, body, callback);
|
|
588
|
-
});
|
|
589
|
-
return this;
|
|
609
|
+
if (method) {
|
|
610
|
+
// the whole answer is here, so it is compressed in one call rather than through a
|
|
611
|
+
// stream, and goes out with the length it ended up being
|
|
612
|
+
ended = true;
|
|
613
|
+
const input = toBuffer(chunk, encoding);
|
|
614
|
+
if (input.length <= SYNC_LIMIT) {
|
|
615
|
+
const body = compressWhole(method, input);
|
|
616
|
+
res.setHeader("Content-Length", String(body.length));
|
|
617
|
+
return _end.call(this, body, callback);
|
|
590
618
|
}
|
|
619
|
+
compressWholeAsync(method, input, (err, body) => {
|
|
620
|
+
// the client can leave while the pool is working, and writing to a response
|
|
621
|
+
// that is already gone is not something uWS survives
|
|
622
|
+
if (res.aborted || res.finished) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (err) {
|
|
626
|
+
return res.destroy(err);
|
|
627
|
+
}
|
|
628
|
+
res.setHeader("Content-Length", String(body.length));
|
|
629
|
+
_end.call(res, body, callback);
|
|
630
|
+
});
|
|
631
|
+
return this;
|
|
591
632
|
}
|
|
592
633
|
ended = true;
|
|
593
634
|
return _end.call(this, chunk, callback);
|