fulmine.js 5.4.1 → 5.5.1
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/README.md +29 -0
- package/package.json +1 -1
- package/src/request.js +67 -0
- package/src/response.js +343 -14
- package/src/types.d.ts +32 -3
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ shared machine, so the number would describe the machine rather than the framewo
|
|
|
46
46
|
- [Attribution](#attribution)
|
|
47
47
|
- [Difference from similar projects](#difference-from-similar-projects)
|
|
48
48
|
- [Migrating](#migrating)
|
|
49
|
+
- [Angular SSR](#angular-ssr)
|
|
49
50
|
- [When Express is somebody else's dependency](#when-express-is-somebody-elses-dependency)
|
|
50
51
|
- [Docker](#docker)
|
|
51
52
|
- [Differences from Express](#differences-from-express)
|
|
@@ -130,6 +131,33 @@ npx fulmine differences # print the list below and change nothing
|
|
|
130
131
|
The command is installed under both `fulmine` and `fulmine.js`. Use `fulmine`: `npx` cannot run a
|
|
131
132
|
command whose name ends in `.js` on Windows, where it exits without a word.
|
|
132
133
|
|
|
134
|
+
### Angular SSR
|
|
135
|
+
|
|
136
|
+
The `server.ts` that `ng add @angular/ssr` generates is an ordinary Express application, so the same
|
|
137
|
+
one-line change applies, and `@angular/ssr`'s own `AngularNodeAppEngine` and
|
|
138
|
+
`writeResponseToNodeResponse` work against Fulmine's request and response unchanged. One extra step
|
|
139
|
+
is needed, and it is Angular's build rather than this library: the server bundle is built with
|
|
140
|
+
esbuild, which tries to inline every dependency and cannot load µWS's native binary. Declare the two
|
|
141
|
+
as external in `angular.json`:
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
"architect": { "build": { "options": {
|
|
145
|
+
"externalDependencies": ["fulmine.js", "uWebSockets.js"]
|
|
146
|
+
} } }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
What it is worth, measured on an Angular 22 application with each server reporting its own CPU per
|
|
150
|
+
request, nine alternating rounds: **static assets 3.29x**, and **a page served from a cache 1.50x**.
|
|
151
|
+
The render itself is the same JavaScript on both sides and measures the same, so on a cache miss the
|
|
152
|
+
framework is not what your page is waiting for. Which is the useful way round: an SSR application
|
|
153
|
+
spends most of its traffic outside the render, and that is where the difference is.
|
|
154
|
+
|
|
155
|
+
Caching those pages is [`ng-ssr-caching`](https://www.npmjs.com/package/ng-ssr-caching), a middleware
|
|
156
|
+
that runs on Express and here alike, and the same measurement says a page costs 17.2ms to render and
|
|
157
|
+
1.9ms to serve from it. It is worth knowing why it keeps the ETag beside the bytes: a cache that
|
|
158
|
+
stores only the body makes the server hash the whole document again on every hit, and measures level
|
|
159
|
+
with no cache at all on the serving side.
|
|
160
|
+
|
|
133
161
|
### When Express is somebody else's dependency
|
|
134
162
|
|
|
135
163
|
A framework built on Express does not `require("express")` in your code, it requires it in its own,
|
|
@@ -627,6 +655,7 @@ Fulmine adds three of its own:
|
|
|
627
655
|
- ✅ res.removeHeader()
|
|
628
656
|
- ✅ res.write()
|
|
629
657
|
- ✅ res.writeHead()
|
|
658
|
+
- ✅ res.flushHeaders()
|
|
630
659
|
|
|
631
660
|
### Router
|
|
632
661
|
|
package/package.json
CHANGED
package/src/request.js
CHANGED
|
@@ -700,6 +700,73 @@ module.exports = class Request extends LazyReadable {
|
|
|
700
700
|
return this.res?.finished || this.res?.aborted;
|
|
701
701
|
}
|
|
702
702
|
|
|
703
|
+
/** @type {AbortController|undefined} */
|
|
704
|
+
#abortController;
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* node's `req.signal`, an AbortSignal that fires when the request is over, so work started for
|
|
708
|
+
* a visitor who has gone away can be stopped. `@angular/ssr` reads it when it builds a web
|
|
709
|
+
* Request out of this one, which is how an SSR render learns to give up.
|
|
710
|
+
*
|
|
711
|
+
* Made on the first ask rather than for every request: most requests never look at it, and an
|
|
712
|
+
* AbortController each would be an allocation nobody reads.
|
|
713
|
+
*
|
|
714
|
+
* @returns {AbortSignal}
|
|
715
|
+
*/
|
|
716
|
+
get signal() {
|
|
717
|
+
if (!this.#abortController) {
|
|
718
|
+
const controller = new AbortController();
|
|
719
|
+
this.#abortController = controller;
|
|
720
|
+
const stop = () => controller.abort();
|
|
721
|
+
if (this.res?.aborted || this.res?.finished) {
|
|
722
|
+
stop();
|
|
723
|
+
} else {
|
|
724
|
+
// the request, not the response: a client abort is reported by destroying this
|
|
725
|
+
// stream and emitting "aborted" on it, and the response is never told to close
|
|
726
|
+
this.once("aborted", stop);
|
|
727
|
+
this.once("close", stop);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return this.#abortController.signal;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* The trailing headers of a chunked request. µWebSockets.js does not surface them, so these
|
|
736
|
+
* stay empty, which is also what node hands back until the request has ended.
|
|
737
|
+
*
|
|
738
|
+
* @returns {Record<string, string>}
|
|
739
|
+
*/
|
|
740
|
+
get trailers() {
|
|
741
|
+
return {};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* @returns {Record<string, string[]>}
|
|
746
|
+
*/
|
|
747
|
+
get trailersDistinct() {
|
|
748
|
+
return {};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* node's per-request socket timeout. µWS runs its own idle timeout, set through
|
|
753
|
+
* `uwsOptions.idleTimeout`, and this cannot change it. The listener is registered as node's is.
|
|
754
|
+
*
|
|
755
|
+
* @param {number} msecs
|
|
756
|
+
* @param {() => void} [callback]
|
|
757
|
+
* @returns {this}
|
|
758
|
+
*/
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
*
|
|
762
|
+
*/
|
|
763
|
+
setTimeout(msecs, callback) {
|
|
764
|
+
if (typeof callback === "function") {
|
|
765
|
+
this.once("timeout", callback);
|
|
766
|
+
}
|
|
767
|
+
return this;
|
|
768
|
+
}
|
|
769
|
+
|
|
703
770
|
/**
|
|
704
771
|
* Readable's pull. uWS pushes the body rather than being pulled from, so all this does is
|
|
705
772
|
* lift the backpressure that a full queue put on it.
|
package/src/response.js
CHANGED
|
@@ -51,6 +51,18 @@ const { EventEmitter } = require("events");
|
|
|
51
51
|
const http = require("http");
|
|
52
52
|
const ms = require("ms");
|
|
53
53
|
|
|
54
|
+
// How much a chunked response may gather before it is handed to uWS. Measured on uWS alone with
|
|
55
|
+
// 33 KB written in 500 pieces: 16.2ms handed over one piece at a time, 0.79ms in 4 KB blocks,
|
|
56
|
+
// 0.43ms in 8 KB, 0.45ms in 16 KB. Past 8 KB the curve is flat, so this is the smallest size that
|
|
57
|
+
// buys the whole saving, and it bounds what one response can hold back.
|
|
58
|
+
const COALESCE_LIMIT = 16 * 1024;
|
|
59
|
+
|
|
60
|
+
// Below which a chunk is worth gathering. Merging costs a copy, and a chunk that is already
|
|
61
|
+
// substantial does not earn it back: measured at 33 KB in eight pieces, gathering them made the
|
|
62
|
+
// response 28% dearer, while the same bytes in sixty-six pieces got 3.4x cheaper. Anything from
|
|
63
|
+
// here up goes straight through.
|
|
64
|
+
const COALESCE_BELOW = 4 * 1024;
|
|
65
|
+
|
|
54
66
|
const outgoingMessage = new http.OutgoingMessage();
|
|
55
67
|
const symbols = Object.getOwnPropertySymbols(outgoingMessage);
|
|
56
68
|
// if a future node renames it, fall back to a private symbol rather than writing a property
|
|
@@ -208,6 +220,24 @@ module.exports = class Response extends LazyWritable {
|
|
|
208
220
|
/** @type {((err?: Error|null) => void)|null} */
|
|
209
221
|
#pendingCallback = null;
|
|
210
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Chunks written but not yet handed to uWS, and their total size.
|
|
225
|
+
*
|
|
226
|
+
* uWS charges for a write against everything already buffered behind it, so a chunked response
|
|
227
|
+
* written in many small pieces costs quadratically once it passes the socket's own buffer:
|
|
228
|
+
* measured on uWS alone, 500 writes of 66 bytes take 13ms against 0.06ms for 100 of them.
|
|
229
|
+
* Handing it the same bytes in blocks costs 0.4ms. Nothing here changes what goes on the wire,
|
|
230
|
+
* only how many calls it takes to put it there.
|
|
231
|
+
* @type {Buffer[]}
|
|
232
|
+
*/
|
|
233
|
+
#queued = [];
|
|
234
|
+
|
|
235
|
+
/** How many bytes {@link #queued} holds, kept alongside so the flush does not add them up. */
|
|
236
|
+
#queuedBytes = 0;
|
|
237
|
+
|
|
238
|
+
/** Whether a flush is already booked for the end of this turn. */
|
|
239
|
+
#flushBooked = false;
|
|
240
|
+
|
|
211
241
|
/** @type {any} */
|
|
212
242
|
#outHeaders = null;
|
|
213
243
|
|
|
@@ -400,10 +430,55 @@ module.exports = class Response extends LazyWritable {
|
|
|
400
430
|
return this.#socket;
|
|
401
431
|
}
|
|
402
432
|
|
|
433
|
+
/**
|
|
434
|
+
* Hands everything queued to uWS as one write, which is where the saving is, and keeps the
|
|
435
|
+
* backpressure the single write used to do.
|
|
436
|
+
*
|
|
437
|
+
* @param {((err?: Error|null) => void)|null} callback the stream's, when there is one waiting
|
|
438
|
+
*/
|
|
439
|
+
#flushQueued(callback) {
|
|
440
|
+
if (this.#queuedBytes === 0) {
|
|
441
|
+
if (callback) callback(null);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const body = this.#queued.length === 1 ? this.#queued[0] : Buffer.concat(this.#queued, this.#queuedBytes);
|
|
445
|
+
this.#queued = [];
|
|
446
|
+
this.#queuedBytes = 0;
|
|
447
|
+
|
|
448
|
+
const ok = this._res.write(body);
|
|
449
|
+
if (ok) {
|
|
450
|
+
this.writingChunk = false;
|
|
451
|
+
if (callback) callback(null);
|
|
452
|
+
else this.emit("drain");
|
|
453
|
+
} else if (callback) {
|
|
454
|
+
this.#pendingCallback = callback;
|
|
455
|
+
this._res.onWritable(() => {
|
|
456
|
+
if (this.aborted || this.finished) return true;
|
|
457
|
+
const cb = this.#pendingCallback;
|
|
458
|
+
this.#pendingCallback = null;
|
|
459
|
+
this.writingChunk = false;
|
|
460
|
+
if (cb) cb(null);
|
|
461
|
+
return true;
|
|
462
|
+
});
|
|
463
|
+
} else {
|
|
464
|
+
// nothing is waiting on this one: uWS drains it and the next write finds out
|
|
465
|
+
this.writingChunk = false;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* The booked flush. An arrow so it can be handed to nextTick without binding it every write.
|
|
471
|
+
*/
|
|
472
|
+
#flushOnTick = () => {
|
|
473
|
+
this.#flushBooked = false;
|
|
474
|
+
if (this.aborted || this.finished || this.#queuedBytes === 0) return;
|
|
475
|
+
this._res.cork(() => this.#flushQueued(null));
|
|
476
|
+
};
|
|
477
|
+
|
|
403
478
|
/**
|
|
404
479
|
* Writable's sink. Sends the headers if they have not gone yet, then hands the chunk to uWS,
|
|
405
|
-
* either
|
|
406
|
-
* much there would be. Backpressure comes back as onWritable, which is what defers the
|
|
480
|
+
* either through the queue above for a chunked response or through tryEnd when a Content-Length
|
|
481
|
+
* said how much there would be. Backpressure comes back as onWritable, which is what defers the
|
|
407
482
|
* callback rather than dropping the chunk.
|
|
408
483
|
*
|
|
409
484
|
* @param {any} chunk
|
|
@@ -441,20 +516,23 @@ module.exports = class Response extends LazyWritable {
|
|
|
441
516
|
}
|
|
442
517
|
|
|
443
518
|
if (this.chunkedTransfer) {
|
|
444
|
-
|
|
445
|
-
|
|
519
|
+
// Held back rather than written, and handed over in one piece at the end of this
|
|
520
|
+
// turn or once it is big enough to be worth a call. A stream that writes once per
|
|
521
|
+
// turn, an SSE feed for instance, still leaves on its own turn: the queue only ever
|
|
522
|
+
// gathers what was written without yielding in between.
|
|
523
|
+
this.#queued.push(/** @type {Buffer} */ (chunk));
|
|
524
|
+
this.#queuedBytes += /** @type {Buffer} */ (chunk).byteLength;
|
|
525
|
+
// a chunk that is already big enough to be worth its own call leaves with whatever
|
|
526
|
+
// was waiting in front of it, rather than paying for a copy it does not need
|
|
527
|
+
if (this.#queuedBytes >= COALESCE_LIMIT || /** @type {Buffer} */ (chunk).byteLength >= COALESCE_BELOW) {
|
|
528
|
+
this.#flushQueued(callback);
|
|
529
|
+
} else {
|
|
530
|
+
if (!this.#flushBooked) {
|
|
531
|
+
this.#flushBooked = true;
|
|
532
|
+
process.nextTick(this.#flushOnTick);
|
|
533
|
+
}
|
|
446
534
|
this.writingChunk = false;
|
|
447
535
|
callback(null);
|
|
448
|
-
} else {
|
|
449
|
-
this.#pendingCallback = callback;
|
|
450
|
-
this._res.onWritable(() => {
|
|
451
|
-
if (this.aborted || this.finished) return true;
|
|
452
|
-
const cb = this.#pendingCallback;
|
|
453
|
-
this.#pendingCallback = null;
|
|
454
|
-
this.writingChunk = false;
|
|
455
|
-
if (cb) cb(null);
|
|
456
|
-
return true;
|
|
457
|
-
});
|
|
458
536
|
}
|
|
459
537
|
} else {
|
|
460
538
|
const lastOffset = this._res.getWriteOffset();
|
|
@@ -658,6 +736,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
658
736
|
* @param {any} cb
|
|
659
737
|
*/
|
|
660
738
|
_finish(data, cb) {
|
|
739
|
+
// read before the head is written below, which is what sets the flag: what matters further
|
|
740
|
+
// down is whether something had already committed the framing, a flushHeaders() or a first
|
|
741
|
+
// res.write(), not whether this call is about to write the head itself
|
|
742
|
+
const headWasAlreadyOut = this.headersSent;
|
|
661
743
|
if (!this.headersSent) {
|
|
662
744
|
// freshness is not decided here. node's end() knows nothing about conditional
|
|
663
745
|
// requests, and Express answers 304 from send() and from sendFile(), each of
|
|
@@ -679,6 +761,20 @@ module.exports = class Response extends LazyWritable {
|
|
|
679
761
|
this._res.endWithoutBody();
|
|
680
762
|
} else if (!data && contentLength) {
|
|
681
763
|
this._res.endWithoutBody(contentLength.toString());
|
|
764
|
+
} else if (headWasAlreadyOut && this.chunkedTransfer) {
|
|
765
|
+
// whatever is still queued goes first: end() must not overtake the body written before it
|
|
766
|
+
this.#flushQueued(null);
|
|
767
|
+
// The head has already gone out without a length, which is what flushHeaders() and the
|
|
768
|
+
// first res.write() both do, so this response is committed to chunked framing and a
|
|
769
|
+
// length can no longer describe it. node is committed the same way: after a flush,
|
|
770
|
+
// res.end("body") sends a chunk, not a Content-Length. Handing the body to uWS's end()
|
|
771
|
+
// here would have it append a length to a head that already said otherwise, which is
|
|
772
|
+
// what the comparison test caught.
|
|
773
|
+
if (data) {
|
|
774
|
+
this._res.write(data);
|
|
775
|
+
this._sentBody = data;
|
|
776
|
+
}
|
|
777
|
+
this._res.endWithoutBody();
|
|
682
778
|
} else {
|
|
683
779
|
// a Buffer goes to uWS as the view it is: copying it into a fresh ArrayBuffer was
|
|
684
780
|
// an allocation per body, and uWS reads the view's own offset and length
|
|
@@ -1232,6 +1328,239 @@ module.exports = class Response extends LazyWritable {
|
|
|
1232
1328
|
return this;
|
|
1233
1329
|
}
|
|
1234
1330
|
|
|
1331
|
+
/**
|
|
1332
|
+
* Sends the status line and the headers now, without waiting for a body, which is node's
|
|
1333
|
+
* flushHeaders(). Callers use it to let the client start on the head while the body is still
|
|
1334
|
+
* being produced, and one of them is `@angular/ssr`'s writeResponseToNodeResponse, which calls
|
|
1335
|
+
* it before streaming a rendered page.
|
|
1336
|
+
*
|
|
1337
|
+
* A second call does nothing, as node's does. Nothing is written for a response already
|
|
1338
|
+
* finished or aborted: uWS has let go of it by then.
|
|
1339
|
+
*
|
|
1340
|
+
* @returns {void}
|
|
1341
|
+
*/
|
|
1342
|
+
flushHeaders() {
|
|
1343
|
+
if (this.headersSent || this.finished || this.aborted) {
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
this._res.cork(() => {
|
|
1347
|
+
this.writeHead(this.statusCode);
|
|
1348
|
+
// the same rule the chunked write path follows: uWS emits the 200 head itself, byte for
|
|
1349
|
+
// byte, so writing it again would only cost a crossing
|
|
1350
|
+
if (this.statusCode !== 200 || this.statusText !== undefined) {
|
|
1351
|
+
this._res.writeStatus(statusLine(this.statusCode, this.statusText));
|
|
1352
|
+
}
|
|
1353
|
+
// true, as the chunked path passes for a string chunk: what follows a flush is a body
|
|
1354
|
+
// written in pieces, and the framing has to be the one that allows them
|
|
1355
|
+
this.writeHeaders(true);
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* node's `writeEarlyHints`, which sends a `103` carrying the resources the page will want, so a
|
|
1361
|
+
* browser can start fetching them while the server is still rendering.
|
|
1362
|
+
*
|
|
1363
|
+
* **Nothing is sent here.** µWebSockets.js has no API for an informational response, so the
|
|
1364
|
+
* hints cannot reach the wire, and this exists so that code written for Express keeps running
|
|
1365
|
+
* rather than dying on "res.writeEarlyHints is not a function". The callback is still called,
|
|
1366
|
+
* because node calls it once the hints are out and a caller may be waiting on it.
|
|
1367
|
+
*
|
|
1368
|
+
* `writeContinue` and `writeProcessing` below are the same story with `100` and `102`.
|
|
1369
|
+
*
|
|
1370
|
+
* @param {Record<string, string|string[]>} [hints]
|
|
1371
|
+
* @param {() => void} [callback]
|
|
1372
|
+
* @returns {void}
|
|
1373
|
+
*/
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
*
|
|
1377
|
+
*/
|
|
1378
|
+
writeEarlyHints(hints, callback) {
|
|
1379
|
+
this.#refuseInformationAfterHead();
|
|
1380
|
+
// node writes the hints first and calls back after, so a caller that sequences work on it
|
|
1381
|
+
// gets the same order here
|
|
1382
|
+
if (typeof callback === "function") {
|
|
1383
|
+
process.nextTick(callback);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* node's `writeContinue`, the `100` that answers an `Expect: 100-continue`. Nothing is sent:
|
|
1389
|
+
* see {@link Response#writeEarlyHints}.
|
|
1390
|
+
*
|
|
1391
|
+
* @returns {void}
|
|
1392
|
+
*/
|
|
1393
|
+
writeContinue() {
|
|
1394
|
+
this.#refuseInformationAfterHead();
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/**
|
|
1398
|
+
* node's `writeProcessing`, the `102`. Nothing is sent: see {@link Response#writeEarlyHints}.
|
|
1399
|
+
*
|
|
1400
|
+
* @returns {void}
|
|
1401
|
+
*/
|
|
1402
|
+
writeProcessing() {
|
|
1403
|
+
this.#refuseInformationAfterHead();
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* node throws from all three of the above once the head has gone out, since an informational
|
|
1408
|
+
* response can only come before it, and an application may well be relying on that throw.
|
|
1409
|
+
*
|
|
1410
|
+
* @returns {void}
|
|
1411
|
+
*/
|
|
1412
|
+
#refuseInformationAfterHead() {
|
|
1413
|
+
if (this.headersSent) {
|
|
1414
|
+
/** @type {NodeJS.ErrnoException} */
|
|
1415
|
+
const err = new Error("Cannot write headers after they are sent to the client");
|
|
1416
|
+
err.code = "ERR_HTTP_HEADERS_SENT";
|
|
1417
|
+
throw err;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* node's `addTrailers`, the headers that follow a chunked body. µWebSockets.js cannot send
|
|
1423
|
+
* them, so nothing is written and the response is otherwise unaffected.
|
|
1424
|
+
*
|
|
1425
|
+
* @param {Record<string, string>|[string, string][]} [headers]
|
|
1426
|
+
* @returns {void}
|
|
1427
|
+
*/
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
*
|
|
1431
|
+
*/
|
|
1432
|
+
addTrailers(headers) {}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* node's per-response socket timeout. µWS runs its own idle timeout, set through
|
|
1436
|
+
* `uwsOptions.idleTimeout`, and this cannot change it. The callback is registered on "timeout"
|
|
1437
|
+
* as node's does, so nothing is lost by calling it, and nothing happens either.
|
|
1438
|
+
*
|
|
1439
|
+
* @param {number} msecs
|
|
1440
|
+
* @param {() => void} [callback]
|
|
1441
|
+
* @returns {this}
|
|
1442
|
+
*/
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
*
|
|
1446
|
+
*/
|
|
1447
|
+
setTimeout(msecs, callback) {
|
|
1448
|
+
if (typeof callback === "function") {
|
|
1449
|
+
this.once("timeout", callback);
|
|
1450
|
+
}
|
|
1451
|
+
return this;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* node's `assignSocket` and `detachSocket`, which the http server uses when a response is
|
|
1456
|
+
* handed a raw socket. There is no such socket here.
|
|
1457
|
+
*
|
|
1458
|
+
* @param {any} [socket]
|
|
1459
|
+
* @returns {void}
|
|
1460
|
+
*/
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
*
|
|
1464
|
+
*/
|
|
1465
|
+
assignSocket(socket) {}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* @param {any} [socket]
|
|
1469
|
+
* @returns {void}
|
|
1470
|
+
*/
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
*
|
|
1474
|
+
*/
|
|
1475
|
+
detachSocket(socket) {}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* node's `statusMessage`, the reason phrase. It is held as `statusText` here, and the two are
|
|
1479
|
+
* the same thing: this is the name node and Express use, so code that sets it keeps working.
|
|
1480
|
+
*
|
|
1481
|
+
* @returns {string|undefined}
|
|
1482
|
+
*/
|
|
1483
|
+
get statusMessage() {
|
|
1484
|
+
return this.statusText;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
set statusMessage(value) {
|
|
1488
|
+
this.statusText = value;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* Whether a header has been set on this response, which is node's `hasHeader`. Names are
|
|
1493
|
+
* compared lowercased, as node compares them.
|
|
1494
|
+
*
|
|
1495
|
+
* @param {string} name
|
|
1496
|
+
* @returns {boolean}
|
|
1497
|
+
*/
|
|
1498
|
+
hasHeader(name) {
|
|
1499
|
+
return this.headers[name.toLowerCase()] !== undefined;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* The names of the headers set so far, lowercased, which is node's `getHeaderNames`.
|
|
1504
|
+
*
|
|
1505
|
+
* @returns {string[]}
|
|
1506
|
+
*/
|
|
1507
|
+
getHeaderNames() {
|
|
1508
|
+
return Object.keys(this.headers);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* node's `getRawHeaderNames`, which returns the names in the case they were set in. Header
|
|
1513
|
+
* names are held lowercased here, so this returns what {@link Response#getHeaderNames} does.
|
|
1514
|
+
*
|
|
1515
|
+
* @returns {string[]}
|
|
1516
|
+
*/
|
|
1517
|
+
getRawHeaderNames() {
|
|
1518
|
+
return Object.keys(this.headers);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* Adds a value to a header without replacing what is there, which is node's `appendHeader`.
|
|
1523
|
+
* A header that already has one value becomes a list, as node makes it.
|
|
1524
|
+
*
|
|
1525
|
+
* @param {string} name
|
|
1526
|
+
* @param {string|readonly string[]} value
|
|
1527
|
+
* @returns {this}
|
|
1528
|
+
*/
|
|
1529
|
+
appendHeader(name, value) {
|
|
1530
|
+
const key = name.toLowerCase();
|
|
1531
|
+
const current = this.headers[key];
|
|
1532
|
+
if (current === undefined) {
|
|
1533
|
+
return this.setHeader(name, /** @type {any} */ (value));
|
|
1534
|
+
}
|
|
1535
|
+
const merged = [].concat(/** @type {any} */ (current), /** @type {any} */ (value));
|
|
1536
|
+
return this.setHeader(name, /** @type {any} */ (merged));
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* Sets several headers at once from a Headers or a Map, which is node's `setHeaders`. A
|
|
1541
|
+
* `Headers` gives `set-cookie` back through getSetCookie, so those stay separate values rather
|
|
1542
|
+
* than one folded string.
|
|
1543
|
+
*
|
|
1544
|
+
* @param {Headers|Map<string, string|readonly string[]>} headers
|
|
1545
|
+
* @returns {this}
|
|
1546
|
+
*/
|
|
1547
|
+
setHeaders(headers) {
|
|
1548
|
+
if (typeof Headers === "function" && headers instanceof Headers) {
|
|
1549
|
+
for (const name of new Set([...headers.keys()])) {
|
|
1550
|
+
if (name === "set-cookie") {
|
|
1551
|
+
this.setHeader(name, /** @type {any} */ (headers.getSetCookie()));
|
|
1552
|
+
} else {
|
|
1553
|
+
this.setHeader(name, /** @type {any} */ (headers.get(name)));
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return this;
|
|
1557
|
+
}
|
|
1558
|
+
for (const [name, value] of headers) {
|
|
1559
|
+
this.setHeader(name, /** @type {any} */ (value));
|
|
1560
|
+
}
|
|
1561
|
+
return this;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1235
1564
|
/**
|
|
1236
1565
|
* Node asks this before validating a header value, and answering true keeps it permissive.
|
|
1237
1566
|
* Only reached through code that goes down node's own header path.
|
package/src/types.d.ts
CHANGED
|
@@ -96,10 +96,39 @@ declare module "fulmine.js" {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
interface Fulmine extends Omit<e.Express, "listen"> {
|
|
99
|
+
/**
|
|
100
|
+
* The app is a node request handler, so it can be handed to anything that takes one. That
|
|
101
|
+
* is what `http.createServer(app)` does, what supertest does, and what `@angular/ssr`'s
|
|
102
|
+
* createNodeRequestHandler(app) does in the server.ts Angular generates.
|
|
103
|
+
*
|
|
104
|
+
* Express declares this through its own RequestHandler on the Express interface; here it
|
|
105
|
+
* has to be written out, because the request and response an application sees are this
|
|
106
|
+
* project's own and node's shapes only arrive through the shim. It has always worked at
|
|
107
|
+
* runtime and the type did not say so, which compiled fine in JavaScript and stopped a
|
|
108
|
+
* TypeScript consumer at the first line that passed the app anywhere.
|
|
109
|
+
*/
|
|
110
|
+
(
|
|
111
|
+
req: import("http").IncomingMessage,
|
|
112
|
+
res: import("http").ServerResponse,
|
|
113
|
+
next?: (err?: unknown) => void
|
|
114
|
+
): void | Promise<void>;
|
|
115
|
+
|
|
99
116
|
readonly uwsApp: uWS.TemplatedApp;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Binds, and calls back the way Express 5 does: with nothing when the socket is listening,
|
|
120
|
+
* and with the error when the bind failed, since Express registers the listen callback on
|
|
121
|
+
* 'error' as well as on 'listening'. `this` inside it is the app, which is what listen
|
|
122
|
+
* returns here and what Express's http.Server is there.
|
|
123
|
+
*
|
|
124
|
+
* A string port is accepted and is what `process.env.PORT` gives you: numeric strings are
|
|
125
|
+
* bound as ports, anything else is taken as a unix socket path and bound through µWS's
|
|
126
|
+
* listen_unix. The four shapes below are node's own.
|
|
127
|
+
*/
|
|
128
|
+
listen(callback?: (error?: Error) => void): FulmineServer;
|
|
129
|
+
listen(port: number | string, callback?: (error?: Error) => void): FulmineServer;
|
|
130
|
+
listen(port: number | string, host: string, callback?: (error?: Error) => void): FulmineServer;
|
|
131
|
+
listen(port: number | string, host: string, backlog: number, callback?: (error?: Error) => void): FulmineServer;
|
|
103
132
|
ws(path: string, behavior: WebSocketBehavior): this;
|
|
104
133
|
publish(topic: string, message: string | ArrayBuffer | Buffer, isBinary?: boolean, compress?: boolean): boolean;
|
|
105
134
|
numSubscribers(topic: string): number;
|