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/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
- // A node process runs the application on one core, and the other fifteen sit there. The usual
20
- // answer is the cluster module, where the primary holds the listening socket and hands each
21
- // accepted connection to a worker over an IPC channel. µWS does not need that: it can bind with
22
- // the port marked shared, which is SO_REUSEPORT, and then every worker has its own listening
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
- // The flag has been passed for a while, see Application#listen: a worker binds shared and a lone
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
- * not the share of it the orchestrator gave away, so a 2-core pod on a 64-core node would fork 64
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 is a throw rather than a quiet zero: `cluster: "atuo"` running on one
107
- * core in production, with nothing said about it, is the failure this whole thing is against.
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 has forked workers, and so serves nothing itself. An application carries
128
- // the setting, but the answer is about the process: an entry with a second app on a TLS port would
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 and not when it listens, because the order is the
145
- * application's to choose: an entry that listens on its TLS port first would otherwise have taken
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 that dies is replaced, and there is nothing to rebuild when it comes back: it binds the
158
- * shared port again and the kernel starts handing it connections. A signal that reaches the
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 {any} worker @param {number} code @param {string} signal */
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 {any} */ (cluster.workers)[id]?.kill();
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; this only makes sure
191
- // it does, and being unref'd it never keeps the process up by itself
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);
@@ -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. Two things are
21
- // different, and both only ever turn a worse answer into a better one:
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. The
25
- // bytes are the same bytes: zlib.gzipSync and a createGzip that receives the same body in one
26
- // write produce the same deflate output.
27
- // - partial content is left alone. The compression module compresses a 206 as well, and the
28
- // result is a byte range of the file described as gzip, which no client can decode.
29
- // - zstd is on offer, which the compression module cannot do at all. It is ranked below brotli,
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, because it is the right one: a transform stream,
34
- // its output written as it comes, and the drain listeners moved onto it so a pipe that fills up
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 {any} res
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, and above it on the libuv pool.
99
- // One call either way; what changes is who waits. A small body pays more for the hop onto the pool
100
- // than the compression costs, and a large one is worth handing over, since the pool has four
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 on a body under the sync limit costs more than the compression does. Same bytes,
110
- * a third of the time.
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 for it: close, because the FINISH
113
- * that ends the member would otherwise take the binding with it, and the handle, which that same
114
- * FINISH drops off the stream before returning. The probe then compresses each body twice and
115
- * gives the whole thing up unless every answer matches `oneShot` exactly, so a node that does any
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 and answers the
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 {any} req
204
- * @param {any} res
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: any, res: any) => boolean} [options.filter] whether this response should be
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: a server that prefers cheap gzip over brotli passes ["gzip", "deflate"], one that would
267
- * rather answer zstd than brotli passes ["zstd", "gzip"]. An uncompressed answer is always on
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?: any) => void) => void} the middleware
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 {any} */ (opts);
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 {any} */ (opts.brotli).params)
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 {any} */ (opts.threshold)) ?? 1024;
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 {any} the transform stream for a body that arrives in pieces
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, because the answer to "could this
388
- // request take a compressed body at all" decides how much of this middleware the response
389
- // has to carry. Most requests to most routes cannot: a client that sent no Accept-Encoding,
390
- // one that refused everything, a HEAD. Those get the Vary and nothing else, since the
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
- res.end = function end(chunk, encoding, callback) {
408
- if (!varied) {
409
- varied = true;
410
- const cacheControl = res.headersSent ? undefined : res.getHeader("Cache-Control");
411
- if (
412
- !res.headersSent &&
413
- filter(req, res) &&
414
- !(cacheControl && NO_TRANSFORM.test(String(cacheControl)))
415
- ) {
416
- addVary(res);
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 {any[][]|null} */ ([]);
430
- /** @type {any} */
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 {any} */ (undefined);
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 the
462
- * body arrives, which is also when the headers are decided: everything read here is set
463
- * by then. The order is the compression module's, and so is the Vary, which is added even
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
- stream = compressStream(method);
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 that a drain listener belongs on the compressor from here on. That
514
- // matters because a pipe registers its own the first time write() tells it to slow
515
- // down, which is after this, and from here on the compressor is what fills up.
516
- for (const listener of /** @type {any[][]} */ (listeners)) {
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
- stream.on("data", (chunk) => {
533
+ compressor.on("data", (chunk) => {
520
534
  if (_write.call(res, chunk) === false) {
521
- stream.pause();
535
+ compressor.pause();
522
536
  }
523
537
  });
524
- stream.on("end", () => {
538
+ compressor.on("end", () => {
525
539
  _end.call(res, endCallback);
526
540
  });
527
- _on.call(res, "drain", () => stream.resume());
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", () => stream.destroy());
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
- const method = decide();
543
- if (method) {
544
- startStream(method);
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, of which this project's own end() takes (data, cb): the third
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 (!decided) {
578
- const method = decide(chunkLength(chunk, encoding));
579
- if (method) {
580
- // the whole answer is here, so it is compressed in one call rather than
581
- // through a stream, and goes out with the length it ended up being
582
- ended = true;
583
- const input = toBuffer(chunk, encoding);
584
- if (input.length <= SYNC_LIMIT) {
585
- const body = compressWhole(method, input);
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);