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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fulmine.js",
3
- "version": "5.19.3",
3
+ "version": "5.19.4",
4
4
  "description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -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 {any} req uWS request
167
- * @param {any} res uWS response
168
- * @param {any} app the application this request arrived at
169
- * @param {any} [preset] a literal registration's constants
170
- * @param {any} [skipHolder] where a granted header skip lives
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 {any} res uWS response
181
- * @param {any} req the Request, already built
182
- * @param {any} app the application this request arrived at
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: any }} */ ({ head: null });
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 {any} */ (this.response)._pendingIn = this._pending;
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: any) => void} resolve
289
- * @param {(err: any) => void} reject
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 {any} res uWS response
497
- * @param {any} req uWS request, readable only during this call
498
- * @param {any} [preset] a literal registration's constants, see nativePreset in the router
499
- * @param {any} [skipHolder] where a granted header skip lives, forwarded whole: dropping
500
- * it here silently turned every skip off, since the native closures call this override
501
- * @returns {any} the request, with the response reachable as request.res
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 {any} res the uWS response
536
- * @param {any} req the uWS request
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>} [options] locals for the view
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 {any} */ (options);
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 {any} ts the compiler
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 {any} node a string literal naming a module */
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 {any} */
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 {any} */ (sourceType),
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 {any} node a string literal naming a module */
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 AST node. acorn ships no useful node types, and every shape here is checked by hand
365
- * @param {(node: any) => void} visit
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 {any[]} the prototypes to stub, this command's copy first
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: any[], entry: string}|null} null once the reason has been printed
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 {any} */ (e);
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 {any[]} apps
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 {any[]} routes
766
- * @param {any[]} native
767
- * @param {any[]} declarative
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 {any} app the application the entry file built
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 {any} worker @param {number} code @param {string} signal */
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 {any} */ (cluster.workers)[id]?.kill();
175
+ /** @type {NodeJS.Dict<import("cluster").Worker>} */ (cluster.workers)[id]?.kill();
176
176
  }
177
177
  };
178
178
  for (const signal of ["SIGTERM", "SIGINT"]) {
@@ -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: 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
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?: 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
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 {any} */ (opts);
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 {any} */ (opts.brotli).params)
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 {any} */ (opts.threshold)) ?? 1024;
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 {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
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
- res.end = function end(chunk, encoding, callback) {
398
- if (!varied) {
399
- varied = true;
400
- const cacheControl = res.headersSent ? undefined : res.getHeader("Cache-Control");
401
- if (
402
- !res.headersSent &&
403
- filter(req, res) &&
404
- !(cacheControl && NO_TRANSFORM.test(String(cacheControl)))
405
- ) {
406
- addVary(res);
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 {any[][]|null} */ ([]);
420
- /** @type {any} */
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 {any} */ (undefined);
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
- 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));
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 {any[][]} */ (listeners)) {
505
- stream.on(listener[0], listener[1]);
530
+ for (const listener of /** @type {OnArgs[]} */ (listeners)) {
531
+ compressor.on(listener[0], listener[1]);
506
532
  }
507
- stream.on("data", (chunk) => {
533
+ compressor.on("data", (chunk) => {
508
534
  if (_write.call(res, chunk) === false) {
509
- stream.pause();
535
+ compressor.pause();
510
536
  }
511
537
  });
512
- stream.on("end", () => {
538
+ compressor.on("end", () => {
513
539
  _end.call(res, endCallback);
514
540
  });
515
- _on.call(res, "drain", () => stream.resume());
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", () => stream.destroy());
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
- const method = decide();
531
- if (method) {
532
- startStream(method);
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, of which this project's own end() takes (data, cb): the third
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 (!decided) {
566
- const method = decide(chunkLength(chunk, encoding));
567
- if (method) {
568
- // the whole answer is here, so it is compressed in one call rather than
569
- // through a stream, and goes out with the length it ended up being
570
- ended = true;
571
- const input = toBuffer(chunk, encoding);
572
- if (input.length <= SYNC_LIMIT) {
573
- const body = compressWhole(method, input);
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);