fulmine.js 5.1.9 → 5.3.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
@@ -123,6 +125,7 @@ module.exports = class Response extends Writable {
123
125
  /** @type {Socket|null} */
124
126
  #socket = null;
125
127
 
128
+ /** Whether end() has run, which is what makes a second one a no-op rather than a throw. */
126
129
  #ended = false;
127
130
 
128
131
  /** @type {((err?: Error|null) => void)|null} */
@@ -131,6 +134,10 @@ module.exports = class Response extends Writable {
131
134
  /** @type {any} */
132
135
  #outHeaders = null;
133
136
 
137
+ /**
138
+ * The request this response answers, linked so either reaches the other.
139
+ * @type {InstanceType<typeof import("./request.js")>}
140
+ */
134
141
  req;
135
142
 
136
143
  /**
@@ -145,6 +152,9 @@ module.exports = class Response extends Writable {
145
152
  constructor(res, req, app) {
146
153
  super();
147
154
  this._req = req;
155
+ // linked here rather than by the caller: the pair is built together, and a field the
156
+ // constructor leaves unset is a shape change on whoever assigns it first
157
+ this.req = req;
148
158
  this._res = res;
149
159
  this.headersSent = false;
150
160
  this.app = app;
@@ -180,8 +190,33 @@ module.exports = class Response extends Writable {
180
190
  this._corkNeeded = false;
181
191
  // shared methods, not arrows: two closures and a once() wrapper here were four
182
192
  // allocations per request. EventEmitter calls listeners with this = the emitter.
183
- this.on("error", this._onAbortError);
184
- this.on("close", this._onCloseCleanup);
193
+ //
194
+ // Written into the map rather than through on(). A stream arrives with its _events already
195
+ // shaped, "close" and "error" among the keys and every value undefined, so filling two of
196
+ // them is the same hidden class on() would have produced and none of its work: the argument
197
+ // check, the newListener emission, the is-there-one-already branch and the max listener
198
+ // count. Two calls per response, and the profile put them at 6% of a hello-world.
199
+ //
200
+ // The condition is the whole safety of it: a fresh response has no listeners, so both slots
201
+ // are free, and anything else falls back to on(). Adding one later goes through on() as
202
+ // usual and finds what this wrote, because this wrote what on() writes.
203
+ // cast because _events and _eventsCount are EventEmitter's own bookkeeping and carry no
204
+ // type: they are what on() writes, and this writes the same two entries
205
+ const self = /** @type {any} */ (this);
206
+ const events = self._events;
207
+ if (
208
+ self._eventsCount === 0 &&
209
+ events !== undefined &&
210
+ events.error === undefined &&
211
+ events.close === undefined
212
+ ) {
213
+ events.error = this._onAbortError;
214
+ events.close = this._onCloseCleanup;
215
+ self._eventsCount = 2;
216
+ } else {
217
+ this.on("error", this._onAbortError);
218
+ this.on("close", this._onCloseCleanup);
219
+ }
185
220
  }
186
221
 
187
222
  /** @param {Error} err */
@@ -673,7 +708,7 @@ module.exports = class Response extends Writable {
673
708
  * "ignore"), `acceptRanges`, `cacheControl`, `immutable`, `etag` and `setHeaders`.
674
709
  *
675
710
  * @param {string} path
676
- * @param {Record<string, any>} [options]
711
+ * @param {import("./options").SendFileOptions} [options]
677
712
  * @param {(err?: Error) => void} [callback] called once sent, or with the error
678
713
  */
679
714
  sendFile(path, options = new NullObject(), callback) {
@@ -695,7 +730,10 @@ module.exports = class Response extends Writable {
695
730
  // Express's completion handler, exactly: a callback hears everything and the response is
696
731
  // left alone, so it can still answer 200 after a 404 error. Without one, a directory
697
732
  // falls through as a plain next(), and aborts and write errors go nowhere.
698
- const next = this.req.next;
733
+ // the router's next and not the route's: express reports a file it could not serve past
734
+ // the rest of the route, so a four argument handler written inside the route never sees
735
+ // it. _leaveRoute is that; req.next is kept for everything else, see Walk#stepOutOfRoute
736
+ const next = this.req._leaveRoute ?? this.req.next;
699
737
  const done = /** @type {(err?: any) => void} */ (
700
738
  (err) => {
701
739
  if (callback) return callback(err);
@@ -849,9 +887,6 @@ module.exports = class Response extends Writable {
849
887
  if (options.etag && !this.headers["etag"]) {
850
888
  this.headers["etag"] = statTag(stat, true);
851
889
  }
852
- if (!options.etag) {
853
- this.req.noEtag = true;
854
- }
855
890
 
856
891
  // announced before the conditional checks, because those can return early and the header
857
892
  // still belongs on the response. send does it in the same order, so a 412 or a 416 still
@@ -893,9 +928,11 @@ module.exports = class Response extends Writable {
893
928
  // range requests
894
929
  if (options.acceptRanges) {
895
930
  if (this.req.headers.range) {
896
- let ranges = this.req.range(len, {
897
- combine: true
898
- });
931
+ // the branch above established the header is there, so range() cannot answer
932
+ // the undefined it uses to mean "no Range header"
933
+ let ranges = /** @type {ReturnType<typeof import("range-parser")>} */ (
934
+ this.req.range(len, { combine: true })
935
+ );
899
936
 
900
937
  // if-range
901
938
  if (!isRangeFresh(this.req, this)) {
@@ -918,6 +955,14 @@ module.exports = class Response extends Writable {
918
955
  }
919
956
  }
920
957
 
958
+ // Turning the etag off means this file goes out without one, and only this file: the
959
+ // exits above hand an error to the application, and the body its handler sends computes
960
+ // its own etag, as it does on express. Suppressing it before those exits made a 416 or a
961
+ // 412 answer without one.
962
+ if (!options.etag) {
963
+ this.req.noEtag = true;
964
+ }
965
+
921
966
  // anything but the whole file, whether from a Range header or the start/end options,
922
967
  // has to go through the read stream with explicit bounds
923
968
  const partial = offset > 0 || len < stat.size;
@@ -1003,7 +1048,7 @@ module.exports = class Response extends Writable {
1003
1048
  *
1004
1049
  * @param {string} path
1005
1050
  * @param {string} [filename] name offered to the user, defaults to the basename of the path
1006
- * @param {Record<string, any>} [options] passed through to sendFile
1051
+ * @param {import("./options").SendFileOptions} [options] passed through to sendFile
1007
1052
  * @param {(err?: Error) => void} [callback]
1008
1053
  */
1009
1054
  download(path, filename, options, callback) {
@@ -1216,7 +1261,11 @@ module.exports = class Response extends Writable {
1216
1261
  const done =
1217
1262
  callback ||
1218
1263
  ((err, str) => {
1219
- if (err) return this.req.next(err);
1264
+ // the router's next and not the route's, the same as sendFile: express reports a
1265
+ // view it could not render to req.next, which the router owns, so the rest of the
1266
+ // route is skipped and a four argument handler written inside it never sees the
1267
+ // error. A callback, when there is one, hears everything instead
1268
+ if (err) return (this.req._leaveRoute ?? this.req.next)(err);
1220
1269
  this.send(str);
1221
1270
  });
1222
1271
 
@@ -1236,7 +1285,10 @@ module.exports = class Response extends Writable {
1236
1285
  */
1237
1286
  cookie(name, value, options) {
1238
1287
  const opt = { ...(options ?? {}) }; // create a new ref because we change original object (https://github.com/dimdenGD/ultimate-express/issues/68)
1239
- if (opt.signed && !this.req.secret) {
1288
+ // cookie-parser hangs the secret on the request, so it is read off it rather than
1289
+ // declared here: without that middleware there is none, which is what this checks
1290
+ const req = /** @type {any} */ (this.req);
1291
+ if (opt.signed && !req.secret) {
1240
1292
  // the message has to read like this: it is the one Express throws, and it names the
1241
1293
  // thing that is actually missing rather than the library that noticed
1242
1294
  throw new Error('cookieParser("secret") required for signed cookies');
@@ -1254,7 +1306,7 @@ module.exports = class Response extends Writable {
1254
1306
  delete opt.maxAge;
1255
1307
  }
1256
1308
  if (opt.signed) {
1257
- val = "s:" + sign(val, this.req.secret);
1309
+ val = "s:" + sign(val, req.secret);
1258
1310
  }
1259
1311
 
1260
1312
  if (opt.path == null) {
@@ -1304,10 +1356,18 @@ module.exports = class Response extends Writable {
1304
1356
  */
1305
1357
  format(object) {
1306
1358
  const keys = Object.keys(object).filter((v) => v !== "default");
1307
- const key = keys.length > 0 ? this.req.accepts(keys) : false;
1359
+ // accepts answers the whole list only when asked with no arguments; given types it
1360
+ // answers the best of them, or false
1361
+ const key = keys.length > 0 ? /** @type {string|false} */ (this.req.accepts(keys)) : false;
1308
1362
 
1309
1363
  this.vary("Accept");
1310
1364
 
1365
+ // req.next, not the router next sendFile and render use. Express's is the router's here
1366
+ // too, so inside a route with a four argument handler of its own this hands the error to
1367
+ // that handler where express would have left the route. Passing the router next instead
1368
+ // fails express's own res.format test, which asserts the handler is given the very same
1369
+ // function the surrounding middleware received: outside a route the two are equivalent but
1370
+ // not identical. The whole thing is the open req.next question at Walk's constructor
1311
1371
  if (key) {
1312
1372
  this.set("Content-Type", normalizeType(key).value);
1313
1373
  object[key](this.req, this, this.req.next);
@@ -1501,6 +1561,10 @@ module.exports = class Response extends Writable {
1501
1561
  return this.set("content-type", ct);
1502
1562
  }
1503
1563
 
1564
+ /**
1565
+ * express carries both names for the same method, and middleware reaches for either.
1566
+ * @type {(type: string) => any}
1567
+ */
1504
1568
  contentType = this.type;
1505
1569
 
1506
1570
  /**