fulmine.js 5.19.2 → 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 +2 -1
- package/src/adopt.js +20 -26
- package/src/application.js +63 -73
- package/src/cli.js +48 -48
- package/src/cluster.js +18 -27
- package/src/compression.js +141 -112
- package/src/declarative.js +611 -540
- package/src/hot-settings.js +80 -0
- package/src/index.js +11 -17
- package/src/lazy-readable.js +131 -0
- package/src/lazy-writable.js +97 -0
- package/src/middlewares.js +153 -130
- package/src/nest.js +22 -36
- package/src/node-shim.js +19 -16
- package/src/optimizer.js +600 -0
- package/src/options.d.ts +9 -4
- package/src/parse-query.js +3 -3
- package/src/request-utils.js +307 -0
- package/src/request.js +147 -548
- package/src/response-utils.js +88 -0
- package/src/response.js +228 -535
- package/src/route.js +7 -8
- package/src/router-utils.js +998 -0
- package/src/router.js +166 -2170
- package/src/server-shape.js +40 -51
- package/src/server-timing.js +32 -33
- package/src/socket.js +208 -0
- package/src/testing.js +43 -45
- package/src/usage.js +25 -25
- package/src/utils.js +165 -78
- package/src/verify.js +22 -31
- package/src/view.js +6 -8
- package/src/walk.js +581 -0
- package/src/websocket.js +34 -26
- package/src/work.js +22 -28
package/src/cluster.js
CHANGED
|
@@ -16,16 +16,12 @@ limitations under the License.
|
|
|
16
16
|
|
|
17
17
|
// express({ cluster: "auto" }): one process per core, all on the same port.
|
|
18
18
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// the
|
|
23
|
-
// socket on the same port and the kernel picks which one gets each connection. No primary in the
|
|
24
|
-
// path, no handle to pass, nothing serialised between processes.
|
|
19
|
+
// The usual answer is the cluster module, where the primary holds the listening socket and passes
|
|
20
|
+
// each accepted connection to a worker over IPC. uWS does not need that: it binds with the port
|
|
21
|
+
// marked shared, which is SO_REUSEPORT, so every worker has its own listening socket on the same
|
|
22
|
+
// port and the kernel picks who gets each connection. No primary in the path, nothing serialised.
|
|
25
23
|
//
|
|
26
|
-
//
|
|
27
|
-
// process binds exclusive. What was missing is the fork, which every application had to write for
|
|
28
|
-
// itself, and an application that does not write it uses one core.
|
|
24
|
+
// Application#listen already passes the flag. What was missing is the fork.
|
|
29
25
|
|
|
30
26
|
"use strict";
|
|
31
27
|
|
|
@@ -54,8 +50,7 @@ function parallelism() {
|
|
|
54
50
|
* The CPU quota a cgroup puts on this process, in cores, or undefined where there is none.
|
|
55
51
|
*
|
|
56
52
|
* This is the number that matters in a container: os.availableParallelism() reports the machine,
|
|
57
|
-
*
|
|
58
|
-
* processes that fight over two cores. Both cgroup layouts are read, v2 first.
|
|
53
|
+
* so a 2-core pod on a 64-core node would fork 64 processes. Both cgroup layouts are read, v2 first.
|
|
59
54
|
*
|
|
60
55
|
* @param {(file: string) => string} [read] the file reader, for a test that has no cgroup
|
|
61
56
|
* @returns {number|undefined}
|
|
@@ -103,8 +98,8 @@ function availableCores(read = readFile, cores = parallelism) {
|
|
|
103
98
|
* How many workers a `cluster` setting asks for. Zero means the setting is off and the process
|
|
104
99
|
* serves by itself, which is the default.
|
|
105
100
|
*
|
|
106
|
-
* A value nobody can read
|
|
107
|
-
* core in production
|
|
101
|
+
* A value nobody can read throws instead of quietly meaning zero: `cluster: "atuo"` would run on
|
|
102
|
+
* one core in production and say nothing about it.
|
|
108
103
|
*
|
|
109
104
|
* @param {boolean|number|"auto"|undefined} setting
|
|
110
105
|
* @param {number} [cores] counted only when the setting asks for it: every application calls this,
|
|
@@ -124,9 +119,8 @@ function workerCount(setting, cores) {
|
|
|
124
119
|
throw new TypeError(`cluster must be "auto", a boolean or a positive number, not ${JSON.stringify(setting)}`);
|
|
125
120
|
}
|
|
126
121
|
|
|
127
|
-
// Whether this process
|
|
128
|
-
//
|
|
129
|
-
// otherwise bind that one here, exclusively, and every worker would fail on it.
|
|
122
|
+
// Whether this process forked workers and serves nothing itself. About the process, not the app:
|
|
123
|
+
// an entry with a second app on a TLS port would bind that one here and every worker would fail.
|
|
130
124
|
let supervising = false;
|
|
131
125
|
|
|
132
126
|
/**
|
|
@@ -141,9 +135,8 @@ function isSupervising() {
|
|
|
141
135
|
/**
|
|
142
136
|
* Says this process is the primary of a clustered application, before it has forked anything.
|
|
143
137
|
*
|
|
144
|
-
* Written when the application is constructed
|
|
145
|
-
*
|
|
146
|
-
* that port here, exclusively, a line before the fork.
|
|
138
|
+
* Written when the application is constructed, not when it listens: an entry that listens on its
|
|
139
|
+
* TLS port first would take that port here, exclusively, one line before the fork.
|
|
147
140
|
*
|
|
148
141
|
* @returns {void}
|
|
149
142
|
*/
|
|
@@ -154,10 +147,8 @@ function becomeSupervisor() {
|
|
|
154
147
|
/**
|
|
155
148
|
* Forks the workers and keeps that many of them alive.
|
|
156
149
|
*
|
|
157
|
-
* A worker
|
|
158
|
-
*
|
|
159
|
-
* primary alone, which is what a container sends, is passed on rather than leaving the workers
|
|
160
|
-
* running with nobody watching them.
|
|
150
|
+
* A dead worker is replaced and has nothing to rebuild, it binds the shared port again. A signal
|
|
151
|
+
* that reaches only the primary, which is what a container sends, is passed on to the workers.
|
|
161
152
|
*
|
|
162
153
|
* @param {number} count
|
|
163
154
|
* @returns {{stop: () => void}}
|
|
@@ -165,7 +156,7 @@ function becomeSupervisor() {
|
|
|
165
156
|
function forkWorkers(count) {
|
|
166
157
|
let stopping = false;
|
|
167
158
|
supervising = true;
|
|
168
|
-
/** @param {
|
|
159
|
+
/** @param {import("cluster").Worker} worker @param {number} code @param {string} signal */
|
|
169
160
|
const onExit = (worker, code, signal) => {
|
|
170
161
|
if (!stopping) {
|
|
171
162
|
console.error(`worker ${worker.process.pid} exited (${signal || code}), starting another`);
|
|
@@ -181,14 +172,14 @@ function forkWorkers(count) {
|
|
|
181
172
|
supervising = false;
|
|
182
173
|
cluster.off("exit", onExit);
|
|
183
174
|
for (const id of Object.keys(cluster.workers ?? {})) {
|
|
184
|
-
/** @type {
|
|
175
|
+
/** @type {NodeJS.Dict<import("cluster").Worker>} */ (cluster.workers)[id]?.kill();
|
|
185
176
|
}
|
|
186
177
|
};
|
|
187
178
|
for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
188
179
|
process.on(signal, () => {
|
|
189
180
|
stop();
|
|
190
|
-
// the primary exits on its own once the last IPC channel closes
|
|
191
|
-
// it does
|
|
181
|
+
// the primary exits on its own once the last IPC channel closes, this only makes sure
|
|
182
|
+
// it does. Unref'd, so it never keeps the process up by itself
|
|
192
183
|
const done = setInterval(() => {
|
|
193
184
|
if (Object.keys(cluster.workers ?? {}).length === 0) {
|
|
194
185
|
clearInterval(done);
|
package/src/compression.js
CHANGED
|
@@ -14,25 +14,29 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
/** @typedef {import("./request.js")} Request */
|
|
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
|
+
*/
|
|
23
|
+
|
|
17
24
|
// express.compression(), which answers with a compressed body when the client asked for one.
|
|
18
25
|
//
|
|
19
26
|
// The options, the defaults and the order the decision is taken in are the compression module's,
|
|
20
|
-
// so a front that already uses it can drop the require and change nothing else.
|
|
21
|
-
// different, and
|
|
27
|
+
// so a front that already uses it can drop the require and change nothing else. Three things are
|
|
28
|
+
// different, and each one only turns a worse answer into a better one:
|
|
22
29
|
//
|
|
23
30
|
// - a response that arrives whole, which is every res.send() and res.json(), is compressed in
|
|
24
|
-
// one call instead of through a transform stream, and goes out with a Content-Length.
|
|
25
|
-
// bytes
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// so a client that takes both is answered exactly as it was before, and above gzip, which it
|
|
31
|
-
// beats on ratio and on time. A Node whose zlib has no zstd never offers it.
|
|
31
|
+
// one call instead of through a transform stream, and goes out with a Content-Length. Same
|
|
32
|
+
// bytes: zlib.gzipSync and a createGzip fed the same body in one write agree.
|
|
33
|
+
// - partial content is left alone. The compression module compresses a 206 too, and the result
|
|
34
|
+
// is a byte range of the file described as gzip, which no client can decode.
|
|
35
|
+
// - zstd is on offer, which the compression module cannot do. Ranked below brotli and above
|
|
36
|
+
// gzip, so a client that takes both is answered as before. A Node with no zstd never offers it.
|
|
32
37
|
//
|
|
33
|
-
// The streaming half is the module's own design
|
|
34
|
-
//
|
|
35
|
-
// hears from the compressor rather than from a socket that is no longer what it is waiting for.
|
|
38
|
+
// The streaming half is the module's own design: a transform stream, its output written as it
|
|
39
|
+
// comes, and the drain listeners moved onto it so a pipe that fills up hears from the compressor.
|
|
36
40
|
|
|
37
41
|
"use strict";
|
|
38
42
|
|
|
@@ -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
|
|
@@ -73,7 +78,7 @@ const NO_TRANSFORM = /(?:^|,)\s*?no-transform\s*?(?:,|$)/;
|
|
|
73
78
|
* Says the answer depends on Accept-Encoding. res.vary() parses what is there and merges, which on
|
|
74
79
|
* the usual response is parsing an absent header: only a response that already varies pays for it.
|
|
75
80
|
*
|
|
76
|
-
* @param {
|
|
81
|
+
* @param {Response} res
|
|
77
82
|
*/
|
|
78
83
|
function addVary(res) {
|
|
79
84
|
if (res.getHeader("Vary") === undefined) {
|
|
@@ -95,30 +100,27 @@ if (HAS_ZSTD) {
|
|
|
95
100
|
ENFORCEABLE.add("zstd");
|
|
96
101
|
}
|
|
97
102
|
|
|
98
|
-
// Up to this many bytes a whole body is compressed on this thread,
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// threads and the loop has everyone else to serve: measured with gzip at the default level, sync
|
|
102
|
-
// wins by 43% at 1.4KB and by 22% at 16KB, and loses by 32% at 32KB and by 90% at 78KB.
|
|
103
|
+
// Up to this many bytes a whole body is compressed on this thread, above it on the libuv pool. One
|
|
104
|
+
// call either way, what changes is who waits. Measured with gzip at the default level: sync wins by
|
|
105
|
+
// 43% at 1.4KB and by 22% at 16KB, and loses by 32% at 32KB and by 90% at 78KB.
|
|
103
106
|
const SYNC_LIMIT = 24 * 1024;
|
|
104
107
|
|
|
105
108
|
const noop = () => {};
|
|
106
109
|
|
|
107
110
|
/**
|
|
108
111
|
* A whole-body compressor that keeps one stream instead of letting zlib build and throw one away
|
|
109
|
-
* per call, which
|
|
110
|
-
*
|
|
112
|
+
* per call, which under the sync limit costs more than the compression does. Same bytes, a third
|
|
113
|
+
* of the time.
|
|
111
114
|
*
|
|
112
|
-
* It is private node, and two things have to be held in place
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* of this differently gets the public API back and loses nothing but the speed.
|
|
115
|
+
* It is private node, and two things have to be held in place: close, because the FINISH that ends
|
|
116
|
+
* the member would take the binding with it, and the handle, which the same FINISH drops off the
|
|
117
|
+
* stream. The probe compresses each body twice and gives up unless every answer matches `oneShot`,
|
|
118
|
+
* so a node that does this differently gets the public API back.
|
|
117
119
|
*
|
|
118
|
-
* Only for the deflate formats. A brotli stream carries context across a reset
|
|
119
|
-
* second body with bytes that depend on the first.
|
|
120
|
+
* Only for the deflate formats. A brotli stream carries context across a reset.
|
|
120
121
|
*
|
|
121
|
-
* @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
|
|
122
124
|
* @param {number} finishFlag
|
|
123
125
|
* @param {(body: Buffer) => Buffer} oneShot
|
|
124
126
|
* @returns {(body: Buffer) => Buffer}
|
|
@@ -200,8 +202,8 @@ function reusableCompressor(create, finishFlag, oneShot) {
|
|
|
200
202
|
* The default filter: whether the content type is worth compressing at all. A response with no
|
|
201
203
|
* type is left alone, since nothing says what its bytes are.
|
|
202
204
|
*
|
|
203
|
-
* @param {
|
|
204
|
-
* @param {
|
|
205
|
+
* @param {Request} req
|
|
206
|
+
* @param {Response} res
|
|
205
207
|
* @returns {boolean}
|
|
206
208
|
*/
|
|
207
209
|
function shouldCompress(req, res) {
|
|
@@ -219,7 +221,7 @@ const isCompressible = memoizeByString((type) => compressible(type) === true);
|
|
|
219
221
|
/**
|
|
220
222
|
* How many bytes a chunk is, which is what the threshold is compared against.
|
|
221
223
|
*
|
|
222
|
-
* @param {any} chunk
|
|
224
|
+
* @param {any} chunk a body piece, in whatever shape the caller wrote it
|
|
223
225
|
* @param {BufferEncoding} [encoding]
|
|
224
226
|
* @returns {number}
|
|
225
227
|
*/
|
|
@@ -233,7 +235,7 @@ function chunkLength(chunk, encoding) {
|
|
|
233
235
|
/**
|
|
234
236
|
* The bytes of a chunk, whatever it arrived as.
|
|
235
237
|
*
|
|
236
|
-
* @param {any} chunk
|
|
238
|
+
* @param {any} chunk a body piece, in whatever shape the caller wrote it
|
|
237
239
|
* @param {BufferEncoding} [encoding]
|
|
238
240
|
* @returns {Buffer}
|
|
239
241
|
*/
|
|
@@ -255,7 +257,7 @@ function toBuffer(chunk, encoding) {
|
|
|
255
257
|
* @param {object} [options]
|
|
256
258
|
* @param {number|string} [options.threshold] the smallest body worth compressing, bytes or "1kb".
|
|
257
259
|
* Default 1024. A response whose size is not known in advance is compressed whatever its size.
|
|
258
|
-
* @param {(req:
|
|
260
|
+
* @param {(req: Request, res: Response) => boolean} [options.filter] whether this response should be
|
|
259
261
|
* compressed at all. The default says yes to any compressible content type.
|
|
260
262
|
* @param {string} [options.enforceEncoding] what to use when the request carries no
|
|
261
263
|
* Accept-Encoding at all. Default "identity", which is to say nothing is compressed.
|
|
@@ -263,26 +265,26 @@ function toBuffer(chunk, encoding) {
|
|
|
263
265
|
* @param {object} [options.zstd] zstd options, `params` included, node's own defaults otherwise.
|
|
264
266
|
* @param {string[]} [options.encodings] the encodings this middleware may answer with, out of
|
|
265
267
|
* "br", "zstd", "gzip" and "deflate". What is not named is never used, however the client ranks
|
|
266
|
-
* it
|
|
267
|
-
*
|
|
268
|
-
* offer, and enforceEncoding stays its own explicit choice, outside this list. This option is
|
|
269
|
-
* fulmine's own, the compression module has no equivalent.
|
|
268
|
+
* it. An uncompressed answer is always on offer, and enforceEncoding is outside this list. This
|
|
269
|
+
* option is fulmine's own, the compression module has no equivalent.
|
|
270
270
|
* @param {number} [options.level] zlib compression level, for gzip and deflate.
|
|
271
271
|
* @param {number} [options.chunkSize] zlib chunk size.
|
|
272
272
|
* @param {number} [options.memLevel] zlib memory level.
|
|
273
273
|
* @param {number} [options.strategy] zlib strategy.
|
|
274
274
|
* @param {number} [options.windowBits] zlib window size.
|
|
275
|
-
* @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
|
|
276
278
|
*/
|
|
277
279
|
function compression(options) {
|
|
278
280
|
const opts = options || {};
|
|
279
281
|
// the whole bag goes to zlib, as the compression module does: level, memLevel, strategy,
|
|
280
282
|
// windowBits and chunkSize arrive under their own names and zlib ignores the rest
|
|
281
|
-
const zlibOptions = /** @type {
|
|
283
|
+
const zlibOptions = /** @type {import("zlib").ZlibOptions} */ (opts);
|
|
282
284
|
const brotliOptions = { ...opts.brotli };
|
|
283
285
|
brotliOptions.params = {
|
|
284
286
|
[zlib.constants.BROTLI_PARAM_QUALITY]: 4,
|
|
285
|
-
...(opts.brotli && /** @type {
|
|
287
|
+
...(opts.brotli && /** @type {import("zlib").BrotliOptions} */ (opts.brotli).params)
|
|
286
288
|
};
|
|
287
289
|
// node's default level, unlike brotli above: zstd at its default is already in the band where
|
|
288
290
|
// this middleware wants to be, and dropping it further buys nothing worth the ratio
|
|
@@ -291,7 +293,7 @@ function compression(options) {
|
|
|
291
293
|
const enforceEncoding = opts.enforceEncoding || "identity";
|
|
292
294
|
// bytes.parse reads "1kb" and hands back null for anything it cannot, an absent option
|
|
293
295
|
// included, which is where the default comes in
|
|
294
|
-
const threshold = bytes.parse(/** @type {
|
|
296
|
+
const threshold = bytes.parse(/** @type {string|number} */ (opts.threshold)) ?? 1024;
|
|
295
297
|
// the mask handed to the negotiation, built once here: a name nobody knows is a config
|
|
296
298
|
// mistake and throws now rather than serving the wrong bytes later
|
|
297
299
|
let allowed = ENCODING_DEFAULT;
|
|
@@ -368,7 +370,8 @@ function compression(options) {
|
|
|
368
370
|
|
|
369
371
|
/**
|
|
370
372
|
* @param {string} method
|
|
371
|
-
* @returns {
|
|
373
|
+
* @returns {import("stream").Transform & import("zlib").Zlib} the transform stream for a body that
|
|
374
|
+
* arrives in pieces
|
|
372
375
|
*/
|
|
373
376
|
function compressStream(method) {
|
|
374
377
|
if (method === "gzip") {
|
|
@@ -384,14 +387,10 @@ function compression(options) {
|
|
|
384
387
|
}
|
|
385
388
|
|
|
386
389
|
return function compression(req, res, next) {
|
|
387
|
-
// Negotiated here rather than when the body arrives
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
// answer still depends on the header even when this particular client did not ask.
|
|
392
|
-
// straight from the raw entries where this request keeps them: reading req.headers here
|
|
393
|
-
// built the whole object for one name. Folded, so a repeated Accept-Encoding still reads
|
|
394
|
-
// as the joined list the headers object would have shown
|
|
390
|
+
// Negotiated here rather than when the body arrives: whether this request could take a
|
|
391
|
+
// compressed body at all decides how much of this middleware the response has to carry.
|
|
392
|
+
// Most requests cannot, and those get the Vary and nothing else. Read straight from the raw
|
|
393
|
+
// entries, folded: reading req.headers here built the whole object for one name
|
|
395
394
|
const accept =
|
|
396
395
|
typeof req._foldedHeader === "function"
|
|
397
396
|
? req._foldedHeader("accept-encoding")
|
|
@@ -403,19 +402,32 @@ function compression(options) {
|
|
|
403
402
|
if (!chosen || chosen === "identity" || req.method === "HEAD") {
|
|
404
403
|
res.flush = noFlush;
|
|
405
404
|
const _plainEnd = res.end;
|
|
405
|
+
const _plainWriteHead = res.writeHead;
|
|
406
406
|
let varied = false;
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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);
|
|
418
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();
|
|
419
431
|
return _plainEnd.call(this, chunk, encoding, callback);
|
|
420
432
|
};
|
|
421
433
|
return next();
|
|
@@ -424,15 +436,18 @@ function compression(options) {
|
|
|
424
436
|
const _write = res.write;
|
|
425
437
|
const _end = res.end;
|
|
426
438
|
const _on = res.on;
|
|
439
|
+
const _writeHead = res.writeHead;
|
|
427
440
|
|
|
428
441
|
/** drain listeners parked until there is a compressor to hang them on, see res.on below */
|
|
429
|
-
let listeners = /** @type {
|
|
430
|
-
/** @type {
|
|
442
|
+
let listeners = /** @type {OnArgs[]|null} */ ([]);
|
|
443
|
+
/** @type {(import("stream").Transform & import("zlib").Zlib)|null} */
|
|
431
444
|
let stream = null;
|
|
432
445
|
let decided = false;
|
|
446
|
+
// the encoding decided on, "" for none; the compressor itself starts with the first byte
|
|
447
|
+
let method = "";
|
|
433
448
|
let ended = false;
|
|
434
449
|
/** what end() was given to call back, held until the compressor has finished */
|
|
435
|
-
let endCallback = /** @type {
|
|
450
|
+
let endCallback = /** @type {(() => void)|undefined} */ (undefined);
|
|
436
451
|
|
|
437
452
|
// the compression module adds this, and code written against it calls it: an SSE feed
|
|
438
453
|
// pushes its event out with res.flush(). Nothing to flush before there is a compressor
|
|
@@ -458,10 +473,9 @@ function compression(options) {
|
|
|
458
473
|
}
|
|
459
474
|
|
|
460
475
|
/**
|
|
461
|
-
* Whether this response is compressed, and how. Taken once, when the first byte of
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
* when the answer goes out uncompressed because the answer still depends on the header.
|
|
476
|
+
* Whether this response is compressed, and how. Taken once, when the first byte of body
|
|
477
|
+
* arrives, which is also when the headers are decided. The order is the compression
|
|
478
|
+
* module's, and so is the Vary, added even when the answer goes out uncompressed.
|
|
465
479
|
*
|
|
466
480
|
* @param {number} [length] the size of the body, when end() already has all of it
|
|
467
481
|
* @returns {string} the encoding chosen, "" to send the body as it is
|
|
@@ -508,28 +522,39 @@ function compression(options) {
|
|
|
508
522
|
* @param {string} method
|
|
509
523
|
*/
|
|
510
524
|
function startStream(method) {
|
|
511
|
-
|
|
525
|
+
// the closures below read the local: inside them the checker forgets the field is set
|
|
526
|
+
const compressor = (stream = compressStream(method));
|
|
512
527
|
// The parked listeners, and the list itself stays rather than being emptied: res.on
|
|
513
|
-
// reads it to know
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
stream.on(listener[0], listener[1]);
|
|
528
|
+
// reads it to know a drain listener belongs on the compressor from here on. A pipe
|
|
529
|
+
// registers its own the first time write() says to slow down, which is after this
|
|
530
|
+
for (const listener of /** @type {OnArgs[]} */ (listeners)) {
|
|
531
|
+
compressor.on(listener[0], listener[1]);
|
|
518
532
|
}
|
|
519
|
-
|
|
533
|
+
compressor.on("data", (chunk) => {
|
|
520
534
|
if (_write.call(res, chunk) === false) {
|
|
521
|
-
|
|
535
|
+
compressor.pause();
|
|
522
536
|
}
|
|
523
537
|
});
|
|
524
|
-
|
|
538
|
+
compressor.on("end", () => {
|
|
525
539
|
_end.call(res, endCallback);
|
|
526
540
|
});
|
|
527
|
-
_on.call(res, "drain", () =>
|
|
541
|
+
_on.call(res, "drain", () => compressor.resume());
|
|
528
542
|
// an aborted response never reaches the end of the stream, and the zlib context behind
|
|
529
543
|
// it is native memory that a garbage collector is in no hurry to reach
|
|
530
|
-
_on.call(res, "close", () =>
|
|
544
|
+
_on.call(res, "close", () => compressor.destroy());
|
|
531
545
|
}
|
|
532
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
|
+
|
|
533
558
|
res.write = function write(chunk, encoding, callback) {
|
|
534
559
|
if (typeof encoding === "function") {
|
|
535
560
|
callback = encoding;
|
|
@@ -539,10 +564,10 @@ function compression(options) {
|
|
|
539
564
|
return false;
|
|
540
565
|
}
|
|
541
566
|
if (!decided) {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
567
|
+
method = decide();
|
|
568
|
+
}
|
|
569
|
+
if (method && !stream) {
|
|
570
|
+
startStream(method);
|
|
546
571
|
}
|
|
547
572
|
if (stream) {
|
|
548
573
|
return stream.write(toBuffer(chunk, encoding), callback);
|
|
@@ -551,8 +576,7 @@ function compression(options) {
|
|
|
551
576
|
};
|
|
552
577
|
|
|
553
578
|
res.end = function end(chunk, encoding, callback) {
|
|
554
|
-
// node's shapes
|
|
555
|
-
// argument only arrives from code written against node's ServerResponse
|
|
579
|
+
// node's shapes: the callback may sit in either position
|
|
556
580
|
if (typeof chunk === "function") {
|
|
557
581
|
callback = chunk;
|
|
558
582
|
chunk = undefined;
|
|
@@ -564,6 +588,14 @@ function compression(options) {
|
|
|
564
588
|
if (ended) {
|
|
565
589
|
return this;
|
|
566
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
|
+
}
|
|
567
599
|
if (stream) {
|
|
568
600
|
ended = true;
|
|
569
601
|
endCallback = callback;
|
|
@@ -574,32 +606,29 @@ function compression(options) {
|
|
|
574
606
|
}
|
|
575
607
|
return this;
|
|
576
608
|
}
|
|
577
|
-
if (
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
res.setHeader("Content-Length", String(body.length));
|
|
587
|
-
return _end.call(this, body, callback);
|
|
588
|
-
}
|
|
589
|
-
compressWholeAsync(method, input, (err, body) => {
|
|
590
|
-
// the client can leave while the pool is working, and writing to a
|
|
591
|
-
// response that is already gone is not something uWS survives
|
|
592
|
-
if (res.aborted || res.finished) {
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
if (err) {
|
|
596
|
-
return res.destroy(err);
|
|
597
|
-
}
|
|
598
|
-
res.setHeader("Content-Length", String(body.length));
|
|
599
|
-
_end.call(res, body, callback);
|
|
600
|
-
});
|
|
601
|
-
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);
|
|
602
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;
|
|
603
632
|
}
|
|
604
633
|
ended = true;
|
|
605
634
|
return _end.call(this, chunk, callback);
|