fulmine.js 5.4.1 → 5.5.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/README.md +24 -0
- package/package.json +1 -1
- package/src/response.js +44 -0
- 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,28 @@ 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 an in-process
|
|
151
|
+
cache 1.90x**, but only when the cache keeps the body's ETag and length beside the bytes. A cache
|
|
152
|
+
that stores the bytes alone measures level with Express, because both then hash the document again
|
|
153
|
+
on every hit. The render itself is the same JavaScript on both sides and measures the same: on a
|
|
154
|
+
cache miss the framework is not what your page is waiting for.
|
|
155
|
+
|
|
133
156
|
### When Express is somebody else's dependency
|
|
134
157
|
|
|
135
158
|
A framework built on Express does not `require("express")` in your code, it requires it in its own,
|
|
@@ -627,6 +650,7 @@ Fulmine adds three of its own:
|
|
|
627
650
|
- ✅ res.removeHeader()
|
|
628
651
|
- ✅ res.write()
|
|
629
652
|
- ✅ res.writeHead()
|
|
653
|
+
- ✅ res.flushHeaders()
|
|
630
654
|
|
|
631
655
|
### Router
|
|
632
656
|
|
package/package.json
CHANGED
package/src/response.js
CHANGED
|
@@ -658,6 +658,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
658
658
|
* @param {any} cb
|
|
659
659
|
*/
|
|
660
660
|
_finish(data, cb) {
|
|
661
|
+
// read before the head is written below, which is what sets the flag: what matters further
|
|
662
|
+
// down is whether something had already committed the framing, a flushHeaders() or a first
|
|
663
|
+
// res.write(), not whether this call is about to write the head itself
|
|
664
|
+
const headWasAlreadyOut = this.headersSent;
|
|
661
665
|
if (!this.headersSent) {
|
|
662
666
|
// freshness is not decided here. node's end() knows nothing about conditional
|
|
663
667
|
// requests, and Express answers 304 from send() and from sendFile(), each of
|
|
@@ -679,6 +683,18 @@ module.exports = class Response extends LazyWritable {
|
|
|
679
683
|
this._res.endWithoutBody();
|
|
680
684
|
} else if (!data && contentLength) {
|
|
681
685
|
this._res.endWithoutBody(contentLength.toString());
|
|
686
|
+
} else if (headWasAlreadyOut && this.chunkedTransfer) {
|
|
687
|
+
// The head has already gone out without a length, which is what flushHeaders() and the
|
|
688
|
+
// first res.write() both do, so this response is committed to chunked framing and a
|
|
689
|
+
// length can no longer describe it. node is committed the same way: after a flush,
|
|
690
|
+
// res.end("body") sends a chunk, not a Content-Length. Handing the body to uWS's end()
|
|
691
|
+
// here would have it append a length to a head that already said otherwise, which is
|
|
692
|
+
// what the comparison test caught.
|
|
693
|
+
if (data) {
|
|
694
|
+
this._res.write(data);
|
|
695
|
+
this._sentBody = data;
|
|
696
|
+
}
|
|
697
|
+
this._res.endWithoutBody();
|
|
682
698
|
} else {
|
|
683
699
|
// a Buffer goes to uWS as the view it is: copying it into a fresh ArrayBuffer was
|
|
684
700
|
// an allocation per body, and uWS reads the view's own offset and length
|
|
@@ -1232,6 +1248,34 @@ module.exports = class Response extends LazyWritable {
|
|
|
1232
1248
|
return this;
|
|
1233
1249
|
}
|
|
1234
1250
|
|
|
1251
|
+
/**
|
|
1252
|
+
* Sends the status line and the headers now, without waiting for a body, which is node's
|
|
1253
|
+
* flushHeaders(). Callers use it to let the client start on the head while the body is still
|
|
1254
|
+
* being produced, and one of them is `@angular/ssr`'s writeResponseToNodeResponse, which calls
|
|
1255
|
+
* it before streaming a rendered page.
|
|
1256
|
+
*
|
|
1257
|
+
* A second call does nothing, as node's does. Nothing is written for a response already
|
|
1258
|
+
* finished or aborted: uWS has let go of it by then.
|
|
1259
|
+
*
|
|
1260
|
+
* @returns {void}
|
|
1261
|
+
*/
|
|
1262
|
+
flushHeaders() {
|
|
1263
|
+
if (this.headersSent || this.finished || this.aborted) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
this._res.cork(() => {
|
|
1267
|
+
this.writeHead(this.statusCode);
|
|
1268
|
+
// the same rule the chunked write path follows: uWS emits the 200 head itself, byte for
|
|
1269
|
+
// byte, so writing it again would only cost a crossing
|
|
1270
|
+
if (this.statusCode !== 200 || this.statusText !== undefined) {
|
|
1271
|
+
this._res.writeStatus(statusLine(this.statusCode, this.statusText));
|
|
1272
|
+
}
|
|
1273
|
+
// true, as the chunked path passes for a string chunk: what follows a flush is a body
|
|
1274
|
+
// written in pieces, and the framing has to be the one that allows them
|
|
1275
|
+
this.writeHeaders(true);
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1235
1279
|
/**
|
|
1236
1280
|
* Node asks this before validating a header value, and answering true keeps it permissive.
|
|
1237
1281
|
* 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;
|