fulmine.js 5.2.0 → 5.4.0

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/response.js CHANGED
@@ -2,6 +2,8 @@
2
2
  Copyright 2024 dimden.dev
3
3
  Copyright 2026 Nigro Simone
4
4
 
5
+ This file is derived from Ultimate Express and has been modified.
6
+
5
7
  Licensed under the Apache License, Version 2.0 (the "License");
6
8
  you may not use this file except in compliance with the License.
7
9
  You may obtain a copy of the License at
@@ -119,7 +121,84 @@ function statusLine(code, text) {
119
121
  return `${code} ${text ?? statuses.message[code] ?? "unknown"}`.trim();
120
122
  }
121
123
 
122
- module.exports = class Response extends Writable {
124
+ /**
125
+ * A Writable that has not built its state yet, the mirror of LazyReadable in request.js and there
126
+ * for the same reason: a response is a Writable because middleware expects one, and the ordinary
127
+ * one never uses it. `send()` reaches `end()`, which is overridden here and goes straight to
128
+ * _finish, so the WritableState is allocated for every response and read by nobody. It is needed
129
+ * only by res.write(), by a stream piped into the response, by cork and by the writableX getters.
130
+ *
131
+ * `Response extends LazyWritable`, whose prototype is Writable's, so `res instanceof Writable`
132
+ * stays true and every Writable method is reachable; what is missing is `_writableState`, built on
133
+ * the first touch. Measured at 45 nanoseconds a response on the machine this was written on.
134
+ *
135
+ * As on the Readable side the wrapping is generated rather than written out: every own member of
136
+ * Writable's prototype gets a version that materialises first, so there is no list to keep in step.
137
+ * Missing one would not be a slow path, it would be a TypeError on `undefined._writableState`.
138
+ */
139
+ class LazyWritableBase {}
140
+ Object.setPrototypeOf(LazyWritableBase.prototype, Writable.prototype);
141
+ Object.setPrototypeOf(LazyWritableBase, Writable);
142
+
143
+ // what the chain says at runtime, said again for the type checker, which cannot see a prototype
144
+ // being reassigned
145
+ const LazyWritable = /** @type {typeof Writable} */ (/** @type {unknown} */ (LazyWritableBase));
146
+
147
+ /**
148
+ * Builds the stream this object has been pretending to be. Idempotent: everything reachable from
149
+ * outside goes through it, so it is called far more often than it does anything.
150
+ *
151
+ * EventEmitter's init keeps an _events that is already there, so both the shape the constructor
152
+ * wrote and any listener added before this survive it.
153
+ *
154
+ * @param {any} stream
155
+ */
156
+ function materialiseWritable(stream) {
157
+ if (stream._writableState === undefined) {
158
+ Writable.call(stream);
159
+ }
160
+ }
161
+
162
+ for (const member of [
163
+ ...Object.getOwnPropertyNames(Writable.prototype),
164
+ ...Object.getOwnPropertySymbols(Writable.prototype)
165
+ ]) {
166
+ if (member === "constructor") {
167
+ continue;
168
+ }
169
+ const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(Writable.prototype, member));
170
+ if (typeof descriptor.value === "function") {
171
+ const inner = descriptor.value;
172
+ Object.defineProperty(LazyWritableBase.prototype, member, {
173
+ ...descriptor,
174
+ /** @this {any} @param {...any} args */
175
+ value: function (...args) {
176
+ materialiseWritable(this);
177
+ return inner.apply(this, args);
178
+ }
179
+ });
180
+ } else if (descriptor.get || descriptor.set) {
181
+ const innerGet = descriptor.get;
182
+ const innerSet = descriptor.set;
183
+ Object.defineProperty(LazyWritableBase.prototype, member, {
184
+ ...descriptor,
185
+ get: innerGet
186
+ ? /** @this {any} */ function () {
187
+ materialiseWritable(this);
188
+ return innerGet.call(this);
189
+ }
190
+ : undefined,
191
+ set: innerSet
192
+ ? /** @this {any} @param {any} value */ function (value) {
193
+ materialiseWritable(this);
194
+ innerSet.call(this, value);
195
+ }
196
+ : undefined
197
+ });
198
+ }
199
+ }
200
+
201
+ module.exports = class Response extends LazyWritable {
123
202
  /** @type {Socket|null} */
124
203
  #socket = null;
125
204
 
@@ -149,6 +228,19 @@ module.exports = class Response extends Writable {
149
228
  */
150
229
  constructor(res, req, app) {
151
230
  super();
231
+ // the EventEmitter half stays eager, since the stream half is what LazyWritable defers and
232
+ // the two listeners below are written straight into this map. These are the five keys and
233
+ // the order node's own Writable constructor lays down, so the hidden class is the one every
234
+ // other stream in the process has, and node's init keeps this object when the state is
235
+ // finally built
236
+ this._events = {
237
+ close: undefined,
238
+ error: undefined,
239
+ prefinish: undefined,
240
+ finish: undefined,
241
+ drain: undefined
242
+ };
243
+ this._eventsCount = 0;
152
244
  this._req = req;
153
245
  // linked here rather than by the caller: the pair is built together, and a field the
154
246
  // constructor leaves unset is a shape change on whoever assigns it first
@@ -183,13 +275,42 @@ module.exports = class Response extends Writable {
183
275
  }
184
276
 
185
277
  this.body = undefined;
278
+ // what was handed to uWS, kept so a caller asking for content-length after the fact can be
279
+ // answered, see get(). Undefined until the response ends, and for one that sends no body
280
+ /** @type {string|Buffer|undefined} */
281
+ this._sentBody = undefined;
186
282
  // false while the uWS route handler is still in its synchronous window, where uWS holds
187
283
  // the socket corked itself; the two uWS entry points flip it once that window closes
188
284
  this._corkNeeded = false;
189
285
  // shared methods, not arrows: two closures and a once() wrapper here were four
190
286
  // allocations per request. EventEmitter calls listeners with this = the emitter.
191
- this.on("error", this._onAbortError);
192
- this.on("close", this._onCloseCleanup);
287
+ //
288
+ // Written into the map rather than through on(). A stream arrives with its _events already
289
+ // shaped, "close" and "error" among the keys and every value undefined, so filling two of
290
+ // them is the same hidden class on() would have produced and none of its work: the argument
291
+ // check, the newListener emission, the is-there-one-already branch and the max listener
292
+ // count. Two calls per response, and the profile put them at 6% of a hello-world.
293
+ //
294
+ // The condition is the whole safety of it: a fresh response has no listeners, so both slots
295
+ // are free, and anything else falls back to on(). Adding one later goes through on() as
296
+ // usual and finds what this wrote, because this wrote what on() writes.
297
+ // cast because _events and _eventsCount are EventEmitter's own bookkeeping and carry no
298
+ // type: they are what on() writes, and this writes the same two entries
299
+ const self = /** @type {any} */ (this);
300
+ const events = self._events;
301
+ if (
302
+ self._eventsCount === 0 &&
303
+ events !== undefined &&
304
+ events.error === undefined &&
305
+ events.close === undefined
306
+ ) {
307
+ events.error = this._onAbortError;
308
+ events.close = this._onCloseCleanup;
309
+ self._eventsCount = 2;
310
+ } else {
311
+ this.on("error", this._onAbortError);
312
+ this.on("close", this._onCloseCleanup);
313
+ }
193
314
  }
194
315
 
195
316
  /** @param {Error} err */
@@ -421,6 +542,12 @@ module.exports = class Response extends Writable {
421
542
  // the value here is nearly always the 10-char "keep-alive", which paid a scan per response
422
543
  const closing =
423
544
  typeof connection === "string" && connection.length === 5 && connection.toLowerCase() === "close";
545
+ // for..in over an object some responses delete from, which is the shape the request side
546
+ // was taken off for #rawHeadersEntries. It stays here, and the difference is where the
547
+ // deletes are: a 200 with a body performs none. Only 204, 304, 205, the freshness branch of
548
+ // sendFile and removeHeader do, this object is built fresh per response, so a dictionary one
549
+ // of them made costs that response and nothing after it. The request side deleted on every
550
+ // request, which is what made it worth a different structure
424
551
  for (const header in headers) {
425
552
  if (closing && header === "keep-alive") {
426
553
  continue;
@@ -557,8 +684,12 @@ module.exports = class Response extends Writable {
557
684
  // an allocation per body, and uWS reads the view's own offset and length
558
685
  if (this.req.method === "HEAD") {
559
686
  const length = Buffer.byteLength(data ?? "");
687
+ this.headers["content-length"] = String(length);
560
688
  this._res.endWithoutBody(length.toString());
561
689
  } else {
690
+ // remembered rather than measured: only a caller that asks for content-length pays
691
+ // for it, and uWS is measuring the same bytes for the wire anyway
692
+ this._sentBody = data ?? "";
562
693
  this._res.end(data);
563
694
  }
564
695
  }
@@ -703,7 +834,10 @@ module.exports = class Response extends Writable {
703
834
  // Express's completion handler, exactly: a callback hears everything and the response is
704
835
  // left alone, so it can still answer 200 after a 404 error. Without one, a directory
705
836
  // falls through as a plain next(), and aborts and write errors go nowhere.
706
- const next = this.req.next;
837
+ // the router's next and not the route's: express reports a file it could not serve past
838
+ // the rest of the route, so a four argument handler written inside the route never sees
839
+ // it. _leaveRoute is that; req.next is kept for everything else, see Walk#stepOutOfRoute
840
+ const next = this.req._leaveRoute ?? this.req.next;
707
841
  const done = /** @type {(err?: any) => void} */ (
708
842
  (err) => {
709
843
  if (callback) return callback(err);
@@ -857,9 +991,6 @@ module.exports = class Response extends Writable {
857
991
  if (options.etag && !this.headers["etag"]) {
858
992
  this.headers["etag"] = statTag(stat, true);
859
993
  }
860
- if (!options.etag) {
861
- this.req.noEtag = true;
862
- }
863
994
 
864
995
  // announced before the conditional checks, because those can return early and the header
865
996
  // still belongs on the response. send does it in the same order, so a 412 or a 416 still
@@ -928,6 +1059,14 @@ module.exports = class Response extends Writable {
928
1059
  }
929
1060
  }
930
1061
 
1062
+ // Turning the etag off means this file goes out without one, and only this file: the
1063
+ // exits above hand an error to the application, and the body its handler sends computes
1064
+ // its own etag, as it does on express. Suppressing it before those exits made a 416 or a
1065
+ // 412 answer without one.
1066
+ if (!options.etag) {
1067
+ this.req.noEtag = true;
1068
+ }
1069
+
931
1070
  // anything but the whole file, whether from a Range header or the start/end options,
932
1071
  // has to go through the read stream with explicit bounds
933
1072
  const partial = offset > 0 || len < stat.size;
@@ -1150,7 +1289,19 @@ module.exports = class Response extends Writable {
1150
1289
  * @returns {string|string[]|undefined}
1151
1290
  */
1152
1291
  get(field) {
1153
- return this.headers[field.toLowerCase()];
1292
+ const name = field.toLowerCase();
1293
+ const value = this.headers[name];
1294
+ // Content-Length is on the wire but not in here: uWS measures the body it is handed and
1295
+ // writes the header itself, which saves measuring it twice. Express sets it in send(), so
1296
+ // anything reading it back finds it there, and morgan's common and combined formats do
1297
+ // exactly that on every line they write. Worked out here rather than in send() so a
1298
+ // response nobody asks pays nothing, and kept once worked out.
1299
+ if (value === undefined && name === "content-length" && this._sentBody !== undefined) {
1300
+ const length = Buffer.byteLength(this._sentBody);
1301
+ this.headers["content-length"] = String(length);
1302
+ return String(length);
1303
+ }
1304
+ return value;
1154
1305
  }
1155
1306
 
1156
1307
  /**
@@ -1226,7 +1377,11 @@ module.exports = class Response extends Writable {
1226
1377
  const done =
1227
1378
  callback ||
1228
1379
  ((err, str) => {
1229
- if (err) return this.req.next(err);
1380
+ // the router's next and not the route's, the same as sendFile: express reports a
1381
+ // view it could not render to req.next, which the router owns, so the rest of the
1382
+ // route is skipped and a four argument handler written inside it never sees the
1383
+ // error. A callback, when there is one, hears everything instead
1384
+ if (err) return (this.req._leaveRoute ?? this.req.next)(err);
1230
1385
  this.send(str);
1231
1386
  });
1232
1387
 
@@ -1323,17 +1478,22 @@ module.exports = class Response extends Writable {
1323
1478
 
1324
1479
  this.vary("Accept");
1325
1480
 
1481
+ // the router next, as express hands over: inside a route with a four argument handler of
1482
+ // its own, a 406 leaves the route rather than being caught by that handler. Where the two
1483
+ // are the same step, _leaveRoute is the very object the surrounding layer received, which
1484
+ // is what express's own test asserts, see Walk#runRoute
1485
+ const next = this.req._leaveRoute ?? this.req.next;
1326
1486
  if (key) {
1327
1487
  this.set("Content-Type", normalizeType(key).value);
1328
- object[key](this.req, this, this.req.next);
1488
+ object[key](this.req, this, next);
1329
1489
  } else if (object.default) {
1330
- object.default(this.req, this, this.req.next);
1490
+ object.default(this.req, this, next);
1331
1491
  } else {
1332
1492
  // an error and not an answer: express hands the error handler the types it could have
1333
1493
  // sent, which is how an application says what it supports
1334
1494
  const err = httpError(406);
1335
1495
  err.types = keys.map((type) => normalizeType(type).value);
1336
- this.req.next(err);
1496
+ next(err);
1337
1497
  }
1338
1498
 
1339
1499
  return this;