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/NOTICE +29 -2
- package/README.md +176 -63
- package/package.json +11 -2
- package/src/application.js +67 -34
- package/src/cli.js +302 -5
- package/src/declarative.js +28 -0
- package/src/index.js +2 -0
- package/src/middlewares.js +67 -6
- package/src/node-shim.js +16 -3
- package/src/options.d.ts +16 -0
- package/src/parse-query.js +19 -2
- package/src/request.js +365 -60
- package/src/response.js +172 -12
- package/src/router.js +767 -177
- package/src/types.d.ts +16 -0
- package/src/usage.js +16 -0
- package/src/utils.js +272 -37
- package/src/view.js +2 -0
- package/src/websocket.js +16 -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
|
|
@@ -119,7 +121,84 @@ function statusLine(code, text) {
|
|
|
119
121
|
return `${code} ${text ?? statuses.message[code] ?? "unknown"}`.trim();
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
1488
|
+
object[key](this.req, this, next);
|
|
1329
1489
|
} else if (object.default) {
|
|
1330
|
-
object.default(this.req, this,
|
|
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
|
-
|
|
1496
|
+
next(err);
|
|
1337
1497
|
}
|
|
1338
1498
|
|
|
1339
1499
|
return this;
|