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 CHANGED
@@ -13,8 +13,35 @@ Fulmine is not affiliated with, endorsed by, or maintained by the authors of
13
13
  Ultimate Express.
14
14
 
15
15
  This product includes code derived from fast-querystring
16
- (https://github.com/anonrig/fast-querystring), Copyright (c) Yagiz Nizipli,
17
- licensed under the MIT License, vendored in src/parse-query.js.
16
+ (https://github.com/anonrig/fast-querystring), vendored in src/parse-query.js
17
+ and licensed under the MIT License, which requires the following to travel with
18
+ every copy:
19
+
20
+ Copyright (c) 2022 Yagiz Nizipli
21
+
22
+ Permission is hereby granted, free of charge, to any
23
+ person obtaining a copy of this software and associated
24
+ documentation files (the "Software"), to deal in the
25
+ Software without restriction, including without
26
+ limitation the rights to use, copy, modify, merge,
27
+ publish, distribute, sublicense, and/or sell copies of
28
+ the Software, and to permit persons to whom the Software
29
+ is furnished to do so, subject to the following
30
+ conditions:
31
+
32
+ The above copyright notice and this permission notice
33
+ shall be included in all copies or substantial portions
34
+ of the Software.
35
+
36
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
37
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
38
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
39
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
40
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
41
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
42
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
43
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
44
+ DEALINGS IN THE SOFTWARE.
18
45
 
19
46
  As required by section 4(b) of the Apache License, the following are the
20
47
  significant changes made to the original work:
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <img src="./assets/logo-mark.svg" alt="" width="88" align="right">
2
+
1
3
  # Fulmine
2
4
 
3
5
  A drop-in replacement for Express 5, running on [µWebSockets.js](https://github.com/uNetworking/uWebSockets.js) instead of `node:http`. Your existing middleware keeps working.
@@ -19,15 +21,51 @@ There is a command that does that replacing for you, across a whole project, and
19
21
  npx fulmine migrate --dry-run # say what it would change, change nothing
20
22
  npx fulmine migrate # do it
21
23
  npx fulmine differences # just the list of what to check by hand
24
+ npx fulmine profile # what listen() decided about each route
22
25
  ```
23
26
 
24
27
  See [Migrating](#migrating) for what it handles and what it deliberately does not.
25
28
 
29
+ There is a **[live demo](https://fulmine-demo.fly.dev)**, which is an ordinary Express application:
30
+ real routes, `helmet`, `cors`, `compression`, `express-session` and `morgan` unmodified, and a
31
+ WebSocket chat served by `app.ws()`. It links to [its own source](https://fulmine-demo.fly.dev/source),
32
+ which is [in this repository](./demo). It shows no throughput figure on purpose: it runs on a small
33
+ shared machine, so the number would describe the machine rather than the framework.
34
+
26
35
  [![npm version](https://img.shields.io/npm/v/fulmine.js)](https://www.npmjs.com/package/fulmine.js)
27
36
  [![Node.js >= 22.0.0](https://img.shields.io/badge/Node.js-%3E=22.0.0-green)](https://nodejs.org)
28
37
  [![Coverage Status](https://coveralls.io/repos/github/nigrosimone/fulmine.js/badge.svg?branch=main)](https://coveralls.io/github/nigrosimone/fulmine.js?branch=main)
38
+ [![CodeQL](https://github.com/nigrosimone/fulmine.js/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/nigrosimone/fulmine.js/actions/workflows/codeql.yml)
29
39
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
30
40
 
41
+ ## Table of contents
42
+
43
+ - [Why this exists](#why-this-exists)
44
+ - [Performance](#performance)
45
+ - [Public benchmarks](#public-benchmarks)
46
+ - [Attribution](#attribution)
47
+ - [Difference from similar projects](#difference-from-similar-projects)
48
+ - [Migrating](#migrating)
49
+ - [When Express is somebody else's dependency](#when-express-is-somebody-elses-dependency)
50
+ - [Docker](#docker)
51
+ - [Differences from Express](#differences-from-express)
52
+ - [Performance tips](#performance-tips)
53
+ - [WebSockets](#websockets)
54
+ - [socket.io](#socketio)
55
+ - [HTTP/3](#http3)
56
+ - [Versioning](#versioning)
57
+ - [Compatibility](#compatibility)
58
+ - [express](#express)
59
+ - [Application](#application)
60
+ - [Application settings](#application-settings)
61
+ - [Request](#request)
62
+ - [Response](#response)
63
+ - [Router](#router)
64
+ - [Tested middlewares](#tested-middlewares)
65
+ - [Tested view engines](#tested-view-engines)
66
+ - [The demo](https://fulmine-demo.fly.dev)
67
+ - [Working on Fulmine](./CONTRIBUTING.md)
68
+
31
69
  ## Why this exists
32
70
 
33
71
  There are several fast HTTP servers for Node built on [µWebSockets.js](https://github.com/uNetworking/uWebSockets.js). What is scarce is one you can actually drop into an existing Express application without rewriting it.
@@ -91,22 +129,64 @@ npx fulmine differences # print the list below and change nothing
91
129
  The command is installed under both `fulmine` and `fulmine.js`. Use `fulmine`: `npx` cannot run a
92
130
  command whose name ends in `.js` on Windows, where it exits without a word.
93
131
 
132
+ ### When Express is somebody else's dependency
133
+
134
+ A framework built on Express does not `require("express")` in your code, it requires it in its own,
135
+ so there is nothing for `migrate` to rewrite. Every package manager can answer `express` with this
136
+ package instead, for your project and everything under it:
137
+
138
+ ```jsonc
139
+ // npm and its lockfile, in package.json
140
+ {
141
+ "overrides": {
142
+ "express": "npm:fulmine.js@^5"
143
+ }
144
+ }
145
+
146
+ // pnpm, in package.json
147
+ {
148
+ "pnpm": {
149
+ "overrides": {
150
+ "express": "npm:fulmine.js@^5"
151
+ }
152
+ }
153
+ }
154
+
155
+ // yarn 1 and berry, in package.json
156
+ {
157
+ "resolutions": {
158
+ "express": "npm:fulmine.js@^5"
159
+ }
160
+ }
161
+ ```
162
+
163
+ Then reinstall, so the lockfile is rewritten: `rm -rf node_modules` and `npm install`, or the
164
+ equivalent for your manager. `npm ls express` should answer `express@npm:fulmine.js`.
165
+
166
+ Two things to know before you do it. The substitution reaches **every** dependency that asks for
167
+ Express, including ones you have never looked at, so run your own tests afterwards and read
168
+ [the differences](#differences-from-express): what a framework does with Express is usually more
169
+ than what an application does. And a package that reaches into `express/lib/...` rather than its
170
+ public surface will not find what it expects, since the files there are ours.
171
+
172
+ Bun is not an option: µWebSockets.js is a native Node addon, and Bun does not load it.
173
+
94
174
  ## Docker
95
175
 
96
176
  Two things about µWebSockets.js make a Dockerfile that works for Express fail here, and both have easy answers:
97
177
 
98
- - **No Alpine, and no Debian bookworm either.** µWebSockets.js ships prebuilt binaries linked against glibc 2.38 or newer. Alpine images use musl, so the binary does not load at all; `node:22` and `node:22-slim` are Debian bookworm, whose glibc 2.36 fails at startup with `GLIBC_2.38' not found`. Use the trixie variants: `node:22-trixie-slim` and up.
99
- - **`git` must be there when `npm install` runs.** µWebSockets.js is not on npm; it is installed straight from GitHub (`github:uNetworking/uWebSockets.js`), and npm uses git to fetch it. Full images like `node:22-trixie` have git; `-slim` ones do not.
178
+ - **No Alpine, and no Debian bookworm either.** µWebSockets.js ships prebuilt binaries linked against glibc 2.38 or newer. Alpine images use musl, so the binary does not load at all; `node:26` and `node:26-slim` are Debian bookworm, whose glibc 2.36 fails at startup with `GLIBC_2.38' not found`. Use the trixie variants: `node:26-trixie-slim` and up.
179
+ - **`git` must be there when `npm install` runs.** µWebSockets.js is not on npm; it is installed straight from GitHub (`github:uNetworking/uWebSockets.js`), and npm uses git to fetch it. Full images like `node:26-trixie` have git; `-slim` ones do not.
100
180
 
101
181
  The clean way to satisfy both is a multi-stage build: install with the full image, run with the slim one.
102
182
 
103
183
  ```dockerfile
104
- FROM node:22-trixie AS build
184
+ FROM node:26-trixie AS build
105
185
  WORKDIR /app
106
186
  COPY package*.json ./
107
187
  RUN npm ci --omit=dev
108
188
 
109
- FROM node:22-trixie-slim
189
+ FROM node:26-trixie-slim
110
190
  WORKDIR /app
111
191
  COPY --from=build /app/node_modules ./node_modules
112
192
  COPY . .
@@ -114,7 +194,7 @@ EXPOSE 3000
114
194
  CMD ["node", "server.js"]
115
195
  ```
116
196
 
117
- A single-stage `node:22-trixie-slim` image works too if you `apt-get install -y git ca-certificates` before `npm ci`. Prebuilt binaries exist for x64 and arm64 on Linux, macOS and Windows, so nothing is compiled at install time either way.
197
+ A single-stage `node:26-trixie-slim` image works too if you `apt-get install -y git ca-certificates` before `npm ci`. Prebuilt binaries exist for x64 and arm64 on Linux, macOS and Windows, so nothing is compiled at install time either way.
118
198
 
119
199
  ## Differences from Express
120
200
 
@@ -181,6 +261,37 @@ On top of that, a handler simple enough to be read at registration time is compi
181
261
 
182
262
  `app.set("declarative responses", false)` turns the whole thing off if you would rather have Express's exact framing than the speed.
183
263
 
264
+ None of that is guesswork you have to do from the outside. `listen()` decides it all, and `npx fulmine profile` prints what it decided:
265
+
266
+ ```sh
267
+ npx fulmine profile # the file package.json's "main" points at
268
+ npx fulmine profile server.js # or name it
269
+ ```
270
+
271
+ ```text
272
+ 7 route(s), 4 answered by µWS itself
273
+
274
+ GET /api/health µWS /api/health (2 in front of it in its chain)
275
+ GET /hello µWS /hello (compiled to a response, reads no query)
276
+ GET /:anything router: something before it in the same router overlaps its paths
277
+ GET /after-the-param router: the parameter route /:anything is written before it
278
+ SEARCH /odd router: µWS does not serve SEARCH
279
+
280
+ What this adds up to
281
+
282
+ 4 of 7 route(s) matched by µWS in C++
283
+ 1 answered from a response written at startup, running no javascript
284
+ layers in front of a compiled handler: 1 at least, 2 at most, 1.8 on average
285
+
286
+ Worth changing, if these are routes that carry traffic
287
+
288
+ GET /after-the-param
289
+ write it above /:anything. Express answers whichever matches first, so the order is
290
+ already what decides, and with the literal first µWS can match it in C++ as well.
291
+ ```
292
+
293
+ It loads the application with `listen()` replaced by the half that compiles the routes, so nothing binds a port and the listen callback does not run: profiling a running service does not start a second copy of it. There is no score, on purpose. A percentage of routes is not a percentage of traffic, and an application with a thousand cold routes and one hot one that fell back would score well and serve badly.
294
+
184
295
  2. Do not use external `serve-static` module. Instead use built-in `express.static()` middleware, which is optimized for Fulmine.
185
296
 
186
297
  3. Do not use `body-parser` module. Instead use built-in `express.text()`, `express.json()` etc.
@@ -195,10 +306,37 @@ On top of that, a handler simple enough to be read at registration time is compi
195
306
 
196
307
  ## WebSockets
197
308
 
198
- Since you don't create http server manually, you can't properly use http.on("upgrade") to handle WebSockets. To solve this, there's currently 2 options:
309
+ `app.ws()` registers a WebSocket route, served by µWS itself. There is no `http.Server` underneath, so `http.on("upgrade")` and the libraries built on it do not apply; this is the replacement.
310
+
311
+ ```js
312
+ app.ws("/room/:id", {
313
+ upgrade(req, res) {
314
+ // runs before the handshake, with a real request and response.
315
+ // Answering the response declines the socket:
316
+ if (!req.query.token) return res.sendStatus(401);
317
+ // and anything left on the request is there for the socket's whole life:
318
+ req.room = req.params.id;
319
+ },
320
+ open(ws) {
321
+ ws.subscribe(ws.req.room);
322
+ },
323
+ message(ws, message, isBinary) {
324
+ ws.publish(ws.req.room, message, isBinary);
325
+ },
326
+ close(ws, code, message) {}
327
+ });
328
+ ```
199
329
 
200
- - [Ultimate WS](https://github.com/dimdenGD/ultimate-ws) implements a `ws` compatible API on the same idea: a drop-in replacement for the `ws` module. It was written against Ultimate Express and hooks into the same upgrade mechanism, which Fulmine still exposes, but that combination is not covered by this project's tests. There's a guide for how to upgrade http requests in the documentation.
201
- - You can simply use `app.uwsApp` to access uWebSockets.js `App` instance and call its `ws()` method directly.
330
+ - **The behavior object is µWS's**, settings included: `maxPayloadLength`, `idleTimeout`, `compression`, `maxBackpressure`, `sendPingsAutomatically` and the rest are passed through untouched, as are the `open`, `message`, `drain`, `close`, `ping`, `pong`, `dropped` and `subscription` handlers. The socket is µWS's too, so `send`, `subscribe`, `publish`, `cork` and `getBufferedAmount` behave exactly as its documentation describes.
331
+ - **`upgrade(req, res)` is this project's addition.** It runs before the handshake with the same `Request` and `Response` your routes get, so a session, a token or a header decides whether the socket opens. Answering the response, with `res.sendStatus(401)` or any other write, declines the upgrade. Returning a promise holds the handshake until it settles, which is what an authentication lookup needs.
332
+ - **`ws.req` is that request**, and it outlives the response: the client's address, headers, query and params are readable from any handler for as long as the socket is open. Hanging your own values on it in `upgrade` is how per-connection state gets to `message`.
333
+ - **Routers work.** `router.ws("/lobby", …)` mounted with `app.use("/chat", router)` serves `/chat/lobby`.
334
+ - **Paths are the ones µWS matches**: literal, or with parameters that are a whole segment such as `/room/:id`. Anything else throws where it is written rather than failing to match later.
335
+ - **Broadcasting from outside a socket**: `app.publish(topic, message)` and `app.numSubscribers(topic)`.
336
+
337
+ A WebSocket route and an ordinary route can share a path: the upgrade goes to the WebSocket route, a plain GET goes through normal routing.
338
+
339
+ If you would rather use the `ws` module's API, [Ultimate WS](https://github.com/dimdenGD/ultimate-ws) is a drop-in replacement for it written against Ultimate Express, and Fulmine still exposes the mechanism it hooks into, but that combination is not covered by this project's tests. `app.uwsApp` also remains available for anything µWS offers that this does not.
202
340
 
203
341
  ### socket.io
204
342
 
@@ -467,57 +605,5 @@ Any Express view engine should work. Here's list of engines we include in our te
467
605
 
468
606
  ## Working on Fulmine
469
607
 
470
- ```sh
471
- npm test # the comparison suite: every test runs against Express, then against
472
- # Fulmine, and the two outputs have to match byte for byte
473
- npm test middlewares # one category
474
- npm test tests/tests/res/res-send.js # one file
475
-
476
- npm run test:unit # the pure functions, which the comparison cannot reach
477
- npm run test:types # the TypeScript declarations, through tsd
478
- npm run typecheck # checkJs over src, which is where the JSDoc types are checked
479
-
480
- npm run lint # eslint, including the rule that every function in src carries a JSDoc block
481
- npm run format # prettier
482
- npm run cover # the comparison suite under nyc, then an HTML report
483
-
484
- npm run benchmark:compare -- --duration 20 # against Express, scenario by scenario
485
- npm run benchmark:ab -- --against main # this working tree against another revision
486
-
487
- npm run test:express # Express's own test suite, run against this
488
- npm run test:express -- res.sendFile --verbose # one area of it, with mocha's output
489
- ```
490
-
491
- The comparison suite is the load-bearing one. A test is a file that prints; the runner executes it
492
- twice, once with `express` and once with this, and fails on any difference. That is why adding a
493
- test means writing something that prints what you want compared, and why a test that prints from
494
- both the server and the client at once is a bug: the two orderings are a race.
495
-
496
- ### Writing a comparison test
497
-
498
- A test file is an ordinary script. The first line is its description, the second may carry a marker,
499
- and the rest sets up an app, makes requests and prints. `tests/helpers.js` has what to print with:
500
-
501
- | | |
502
- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
503
- | `fetchTest(url, init)` | `fetch`, plus a line with the status and the headers worth comparing. Returns the response untouched, so the test goes on to read the body as it would have. Lines come out in call order, never in arrival order. |
504
- | `sequential([() => …])` | Runs requests one at a time. `Promise.all` starts them together and the two servers then answer in whatever order they scheduled, which is a difference the runner would report as a failure. |
505
- | `// INSPECT` | On the second line. The runner then mounts `inspectRequest` in front of every app the file makes, and each request prints its `method`, `url`, `originalUrl`, `baseUrl`, `path`, `protocol`, `secure`, `hostname`, `host`, `xhr`, `subdomains` and `query`. |
506
- | `// OFF: reason` | Skips the file. |
507
-
508
- `// INSPECT` is not free everywhere, which is why it is asked for rather than always on. It is a
509
- middleware, so a route behind it stops being compiled into a declarative response and is served by
510
- the ordinary path instead: a file whose routes do compile would quietly stop covering the compiled
511
- one. And Express builds its router at the first `use()`, freezing `strict routing` and
512
- `case sensitive routing` as they are at that moment, so a file that sets either one afterwards must
513
- not ask for it. Everywhere else it is worth having: it is what caught a pathless mount dropping the
514
- middleware in front of it.
515
-
516
- `npm run test:express` is the other kind of test: it clones Express at the version in
517
- `devDependencies`, points its entry at this source and runs its suite against it. It is a bug mine
518
- rather than a gate, and its exit status says nothing. Read the header of `tools/express-suite.js`
519
- before reading its numbers: some of what it reports is Express testing its own internals, which the
520
- clone still has, and some is internals used as public API.
521
-
522
- `benchmark/README.md` covers measuring, including why the A/B runs pipelined by default and why a
523
- null control matters.
608
+ How to run the suites, what each of them is for, and how to write a comparison test:
609
+ [`CONTRIBUTING.md`](./CONTRIBUTING.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fulmine.js",
3
- "version": "5.1.9",
3
+ "version": "5.3.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": {
@@ -12,6 +12,7 @@
12
12
  "test:unit": "node --test \"tests/unit/*.test.js\"",
13
13
  "test:types": "tsd --files tests/types/*.test-d.ts",
14
14
  "test:express": "node tools/express-suite.js",
15
+ "fuzz": "node tools/fuzz.js",
15
16
  "benchmark:compare": "node benchmark/run.js",
16
17
  "cover": "npm run cover:full && npm run cover:report",
17
18
  "cover:unit": "nyc --silent npm run test",
@@ -27,7 +28,10 @@
27
28
  "benchmark:profile": "node benchmark/profile.js",
28
29
  "release:local": "node tools/release-local.js",
29
30
  "cover:full": "nyc --silent npm run test && nyc --silent --no-clean npm run test:unit && nyc --silent --no-clean npm run test:express && nyc report",
30
- "cover:check": "nyc check-coverage --statements 93 --branches 88 --functions 92 --lines 93"
31
+ "cover:check": "nyc check-coverage --statements 93 --branches 88 --functions 92 --lines 93",
32
+ "demo:start": "npm --prefix demo install && npm --prefix demo start",
33
+ "demo:deploy": "fly deploy ./demo --config ./demo/fly.toml",
34
+ "demo:logs": "fly logs --app fulmine-demo"
31
35
  },
32
36
  "engines": {
33
37
  "node": ">=22"
@@ -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
@@ -33,6 +35,7 @@ const path = require("path");
33
35
  const os = require("os");
34
36
  const { Worker } = require("worker_threads");
35
37
  const cluster = require("cluster");
38
+ const { registerWebSocketRoutes } = require("./websocket.js");
36
39
 
37
40
  const cpuCount = os.cpus().length;
38
41
 
@@ -82,6 +85,20 @@ const FILE_CACHE_MAX_ENTRY = 768 * 1024;
82
85
  const FILE_CACHE_BUDGET = 64 * 1024 * 1024;
83
86
 
84
87
  class Application extends Router {
88
+ /**
89
+ * An application reads an unset routing flag from the app it is mounted on, which a plain
90
+ * Router does not: express chains a mounted app's settings onto its parent's.
91
+ *
92
+ * @type {boolean}
93
+ */
94
+ _inheritsSettings = true;
95
+
96
+ /**
97
+ * An application, which a plain Router is not. See Router#_isApplication.
98
+ * @type {boolean}
99
+ */
100
+ _isApplication = true;
101
+
85
102
  /**
86
103
  * @param {object} [settings] the options express() takes. uwsOptions goes to uWS and decides
87
104
  * between an HTTP, an HTTPS and an HTTP/3 server; threads sizes the file-reading pool, and 0
@@ -170,6 +187,12 @@ class Application extends Router {
170
187
  if (parent.response) {
171
188
  Object.setPrototypeOf(this.response, parent.response);
172
189
  }
190
+ // and the engines with them, which is the same chaining express does: a sub-app renders
191
+ // with whatever the parent registered unless it registered its own. Without this a
192
+ // render inside a mounted app looked for a module named after the extension.
193
+ if (parent.engines) {
194
+ Object.setPrototypeOf(this.engines, parent.engines);
195
+ }
173
196
  // a "trust proxy" this app never set is inherited from the parent, as express does:
174
197
  // the defaults are deleted so get() falls through to the parent's value
175
198
  if (
@@ -356,10 +379,6 @@ class Application extends Router {
356
379
  // express's wording, which applications match on
357
380
  throw new TypeError("unknown value for query parser function: " + value);
358
381
  }
359
- } else if (key === "views") {
360
- // a list of directories is searched in order by View.lookup, each resolved here once
361
- this.settings[key] = Array.isArray(value) ? value.map((dir) => path.resolve(dir)) : path.resolve(value);
362
- return this;
363
382
  } else if (key === "etag") {
364
383
  // an etag arriving after listen would make send consult freshness headers the
365
384
  // header-skip routes never copied, so those skips are taken back
@@ -416,13 +435,13 @@ class Application extends Router {
416
435
  }
417
436
 
418
437
  /**
419
- * Whether a setting is truthy. Reads this app's own settings, without falling back to a
420
- * parent app the way get() does.
438
+ * Whether a setting is truthy. Reads through to the app this one is mounted on, as get() does
439
+ * and as express does: mounting chains a sub-app's settings onto its parent's.
421
440
  * @param {string} key setting name
422
441
  * @returns {boolean}
423
442
  */
424
443
  enabled(key) {
425
- return !!this.settings[key];
444
+ return !!this.get(key);
426
445
  }
427
446
 
428
447
  /**
@@ -431,7 +450,7 @@ class Application extends Router {
431
450
  * @returns {boolean}
432
451
  */
433
452
  disabled(key) {
434
- return !this.settings[key];
453
+ return !this.get(key);
435
454
  }
436
455
 
437
456
  /**
@@ -470,32 +489,42 @@ class Application extends Router {
470
489
  * between an error, the automatic OPTIONS reply and a 404.
471
490
  */
472
491
  _createRequestHandler() {
473
- this.uwsApp.any("/*", async (res, req) => {
474
- const request = this.handleRequest(res, req);
475
- const response = request.res;
476
- // armed up front here: this handler awaits, so the response outlives the callback
477
- // on every path through it
478
- this._armAbort(res, response);
479
-
480
- try {
481
- const routed = this._routeRequest(request, response);
482
- // dispatch has run its synchronous stretch inside _routeRequest by now, still
483
- // under the cork uWS holds for this callback; the await below leaves it
484
- response._corkNeeded = true;
485
- const matchedRoute = await routed;
486
- if (!matchedRoute && !response.headersSent && !response.aborted) {
487
- this._endUnmatched(request, response);
488
- }
489
- } catch (err) {
490
- // an internal throw answers 500 as express's final handler would, instead of
491
- // dying as an unhandled rejection
492
- if (response.aborted || response.finished) {
493
- console.error(err);
494
- } else {
495
- this._handleError(err, null, request, response);
496
- }
492
+ this.uwsApp.any("/*", (res, req) => this._serveGeneric(res, req));
493
+ }
494
+
495
+ /**
496
+ * Serves one request by walking this app's chain, with no registration-time shortcut. It is
497
+ * what the catch-all runs, and also what a native registration falls back to when it sees a
498
+ * request it must not answer itself, see the case guard in Router#_registerUwsRoute.
499
+ *
500
+ * @param {any} res the uWS response
501
+ * @param {any} req the uWS request
502
+ */
503
+ async _serveGeneric(res, req) {
504
+ const request = this.handleRequest(res, req);
505
+ const response = request.res;
506
+ // armed up front here: this handler awaits, so the response outlives the callback
507
+ // on every path through it
508
+ this._armAbort(res, response);
509
+
510
+ try {
511
+ const routed = this._routeRequest(request, response);
512
+ // dispatch has run its synchronous stretch inside _routeRequest by now, still
513
+ // under the cork uWS holds for this callback; the await below leaves it
514
+ response._corkNeeded = true;
515
+ const matchedRoute = await routed;
516
+ if (!matchedRoute && !response.headersSent && !response.aborted) {
517
+ this._endUnmatched(request, response);
497
518
  }
498
- });
519
+ } catch (err) {
520
+ // an internal throw answers 500 as express's final handler would, instead of
521
+ // dying as an unhandled rejection
522
+ if (response.aborted || response.finished) {
523
+ console.error(err);
524
+ } else {
525
+ this._handleError(err, null, request, response);
526
+ }
527
+ }
499
528
  }
500
529
 
501
530
  /**
@@ -515,6 +544,9 @@ class Application extends Router {
515
544
  */
516
545
  listen(port, host, backlog, callback) {
517
546
  this._compileOptimizedRoutes();
547
+ // before the catch-all: µWS sends an upgrade to the websocket route even when a
548
+ // catch-all covers the same path, so the two coexist and the order is only tidiness
549
+ registerWebSocketRoutes(this);
518
550
  this._createRequestHandler();
519
551
  // node's shapes: (cb), (port, cb), (port, host, cb) and (port, host, backlog, cb)
520
552
  if (typeof port === "function") {
@@ -595,6 +627,32 @@ class Application extends Router {
595
627
  return this;
596
628
  }
597
629
 
630
+ /**
631
+ * Publishes a message to every socket subscribed to a topic, from outside any of them.
632
+ *
633
+ * The socket's own `publish` reaches the same topics; this one is for the sender that is
634
+ * not a socket, a timer or a route handler broadcasting to a room.
635
+ *
636
+ * @param {string} topic
637
+ * @param {string|ArrayBuffer|Buffer} message
638
+ * @param {boolean} [isBinary]
639
+ * @param {boolean} [compress]
640
+ * @returns {boolean} whether the topic had anyone listening
641
+ */
642
+ publish(topic, message, isBinary, compress) {
643
+ return this.uwsApp.publish(topic, message, isBinary, compress);
644
+ }
645
+
646
+ /**
647
+ * How many sockets are subscribed to a topic.
648
+ *
649
+ * @param {string} topic
650
+ * @returns {number}
651
+ */
652
+ numSubscribers(topic) {
653
+ return this.uwsApp.numSubscribers(topic);
654
+ }
655
+
598
656
  /**
599
657
  * The bound address, or null when not listening.
600
658
  * @returns {{address: string, family: string, port: number}|null}
@@ -703,7 +761,12 @@ class Application extends Router {
703
761
  view = new View(name, {
704
762
  defaultEngine: this.get("view engine"),
705
763
  root: this.get("views"),
706
- engines: { ...this.engines }
764
+ // the object itself, not a copy of it: a mounted app reaches its parent's engines
765
+ // through the prototype chain, and a spread only carries what the app owns, so a
766
+ // sub-app rendering with the parent's engine went off to require() a module named
767
+ // after the extension. Express hands its own object over too, and means to: a view
768
+ // that loads an engine by require caches it back here
769
+ engines: this.engines
707
770
  });
708
771
  if (!view.path) {
709
772
  const dirs =