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/NOTICE +29 -2
- package/README.md +148 -62
- package/package.json +6 -2
- package/src/application.js +97 -34
- package/src/cli.js +302 -5
- package/src/declarative.js +19 -0
- package/src/index.js +2 -0
- package/src/middlewares.js +97 -25
- package/src/node-shim.js +5 -3
- package/src/options.d.ts +110 -0
- package/src/parse-query.js +19 -2
- package/src/request.js +357 -38
- package/src/response.js +79 -15
- package/src/router.js +686 -160
- package/src/types.d.ts +52 -3
- package/src/usage.js +16 -0
- package/src/utils.js +267 -36
- package/src/view.js +2 -0
- package/src/websocket.js +239 -0
- package/src/worker.js +2 -0
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
|
-
|
|
184
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
897
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
/**
|