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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fulmine.js",
3
- "version": "5.4.1",
3
+ "version": "5.5.0",
4
4
  "description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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
- listen(port: number, callback?: (token: any) => void): FulmineServer;
101
- listen(port: number, host: string, callback?: (token: any) => void): FulmineServer;
102
- listen(callback: (token: any) => void): FulmineServer;
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;