svelte-adapter-uws 0.3.4 → 0.3.6
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 +46 -1
- package/files/index.js +69 -24
- package/index.d.ts +12 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -419,6 +419,7 @@ If you set `envPrefix: 'MY_APP_'` in the adapter config, all variables are prefi
|
|
|
419
419
|
| `BODY_SIZE_LIMIT` | `512K` | Max request body size (supports `K`, `M`, `G` suffixes) |
|
|
420
420
|
| `SHUTDOWN_TIMEOUT` | `30` | Seconds to wait during graceful shutdown |
|
|
421
421
|
| `CLUSTER_WORKERS` | - | Number of worker threads (or `auto` for CPU count) |
|
|
422
|
+
| `CLUSTER_MODE` | *(auto)* | `reuseport` (Linux default) or `acceptor` (other platforms) |
|
|
422
423
|
|
|
423
424
|
### Graceful shutdown
|
|
424
425
|
|
|
@@ -2130,7 +2131,7 @@ docker run -p 3000:3000 \
|
|
|
2130
2131
|
|
|
2131
2132
|
## Clustering
|
|
2132
2133
|
|
|
2133
|
-
The adapter supports multi-core scaling
|
|
2134
|
+
The adapter supports multi-core scaling with two modes, auto-selected based on platform.
|
|
2134
2135
|
|
|
2135
2136
|
Set the `CLUSTER_WORKERS` environment variable to enable it:
|
|
2136
2137
|
|
|
@@ -2147,6 +2148,21 @@ CLUSTER_WORKERS=auto PORT=8080 ORIGIN=https://example.com node build
|
|
|
2147
2148
|
|
|
2148
2149
|
If a worker crashes, it is automatically restarted with exponential backoff. On `SIGTERM`/`SIGINT`, the primary tells all workers to drain in-flight requests and shut down gracefully.
|
|
2149
2150
|
|
|
2151
|
+
### Clustering modes
|
|
2152
|
+
|
|
2153
|
+
**`reuseport`** (Linux default) -- each worker binds to the same port via `SO_REUSEPORT`. The kernel distributes incoming connections across all listening workers. There is no single-threaded acceptor bottleneck and no single point of failure -- one worker crashing does not affect the others.
|
|
2154
|
+
|
|
2155
|
+
**`acceptor`** (macOS/Windows default) -- a primary thread creates an acceptor app that receives all connections and distributes them to worker threads via uWS child app descriptors. Works on all platforms.
|
|
2156
|
+
|
|
2157
|
+
The mode is auto-detected. Override it explicitly if needed:
|
|
2158
|
+
|
|
2159
|
+
```bash
|
|
2160
|
+
# Force acceptor mode on Linux (e.g. for debugging)
|
|
2161
|
+
CLUSTER_MODE=acceptor CLUSTER_WORKERS=auto node build
|
|
2162
|
+
```
|
|
2163
|
+
|
|
2164
|
+
Setting `CLUSTER_MODE=reuseport` on non-Linux platforms is an error (SO_REUSEPORT is not reliable outside Linux).
|
|
2165
|
+
|
|
2150
2166
|
### WebSocket + clustering
|
|
2151
2167
|
|
|
2152
2168
|
`platform.publish()` is automatically relayed across all workers via the primary thread, so subscribers on any worker receive the message. This is built in -- no external pub/sub needed.
|
|
@@ -2158,6 +2174,35 @@ Per-worker limitations (acceptable for most apps):
|
|
|
2158
2174
|
- `platform.subscribers(topic)` - returns the count for the local worker only
|
|
2159
2175
|
- `platform.sendTo(filter, ...)` - only reaches connections on the local worker
|
|
2160
2176
|
|
|
2177
|
+
### Docker / multi-process deployments (Linux)
|
|
2178
|
+
|
|
2179
|
+
On Linux, `SO_REUSEPORT` is set on every `app.listen()` call -- including single-process mode. This means multiple independent `node build` processes can bind to the same port without any adapter-level clustering. The kernel distributes connections across them.
|
|
2180
|
+
|
|
2181
|
+
If you already have external pub/sub (Redis, Postgres LISTEN/NOTIFY) handling cross-process messaging, you do not need `CLUSTER_WORKERS` at all. Just run multiple replicas and let your infrastructure handle the rest:
|
|
2182
|
+
|
|
2183
|
+
```yaml
|
|
2184
|
+
# docker-compose.yml
|
|
2185
|
+
services:
|
|
2186
|
+
app:
|
|
2187
|
+
build: .
|
|
2188
|
+
command: node build
|
|
2189
|
+
network_mode: host
|
|
2190
|
+
environment:
|
|
2191
|
+
- PORT=443
|
|
2192
|
+
- SSL_CERT=/certs/cert.pem
|
|
2193
|
+
- SSL_KEY=/certs/key.pem
|
|
2194
|
+
deploy:
|
|
2195
|
+
replicas: 4
|
|
2196
|
+
```
|
|
2197
|
+
|
|
2198
|
+
Each replica is a plain single-process `node build`. No coordinator thread, no built-in relay. Docker handles restarts, Redis handles cross-process messaging, the kernel handles port sharing.
|
|
2199
|
+
|
|
2200
|
+
With `network_mode: host`, containers share the host network stack directly -- no port mapping needed, and services like Postgres and Redis are reachable via `127.0.0.1`. This avoids Docker bridge DNS and gives the best network performance.
|
|
2201
|
+
|
|
2202
|
+
**When to use what:**
|
|
2203
|
+
- **`CLUSTER_WORKERS`** -- single-machine deployments without Docker/k8s/systemd managing processes for you
|
|
2204
|
+
- **Docker replicas** -- production deployments where your infrastructure already handles process management and you have external pub/sub for cross-process messaging
|
|
2205
|
+
|
|
2161
2206
|
---
|
|
2162
2207
|
|
|
2163
2208
|
## Performance
|
package/files/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
-
import { isMainThread, parentPort, threadId, Worker } from 'node:worker_threads';
|
|
2
|
+
import { isMainThread, parentPort, threadId, Worker, workerData } from 'node:worker_threads';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { env } from 'ENV';
|
|
5
5
|
|
|
@@ -11,10 +11,9 @@ const cluster_workers = env('CLUSTER_WORKERS', '');
|
|
|
11
11
|
const is_primary = cluster_workers && isMainThread;
|
|
12
12
|
|
|
13
13
|
if (is_primary) {
|
|
14
|
-
// ── Primary thread:
|
|
14
|
+
// ── Primary thread: spawn workers, coordinate shutdown ──
|
|
15
15
|
|
|
16
16
|
const { availableParallelism } = await import('node:os');
|
|
17
|
-
const uWS = (await import('uWebSockets.js')).default;
|
|
18
17
|
|
|
19
18
|
const num = cluster_workers === 'auto'
|
|
20
19
|
? availableParallelism()
|
|
@@ -25,16 +24,40 @@ if (is_primary) {
|
|
|
25
24
|
process.exit(1);
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
//
|
|
27
|
+
// On Linux, uWS sets SO_REUSEPORT by default so each worker can bind
|
|
28
|
+
// to the same port independently and the kernel distributes connections.
|
|
29
|
+
// No single-threaded acceptor bottleneck, no single point of failure.
|
|
30
|
+
// On other platforms, fall back to the acceptor model (main thread
|
|
31
|
+
// accepts connections and distributes them to workers via descriptors).
|
|
32
|
+
const cluster_mode = env('CLUSTER_MODE', process.platform === 'linux' ? 'reuseport' : 'acceptor');
|
|
33
|
+
|
|
34
|
+
if (cluster_mode === 'reuseport' && process.platform !== 'linux') {
|
|
35
|
+
console.error(
|
|
36
|
+
`CLUSTER_MODE=reuseport requires Linux (SO_REUSEPORT is not reliable on ${process.platform}). ` +
|
|
37
|
+
'Remove CLUSTER_MODE to use the default acceptor mode.'
|
|
38
|
+
);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (cluster_mode !== 'reuseport' && cluster_mode !== 'acceptor') {
|
|
43
|
+
console.error(`Invalid CLUSTER_MODE: '${cluster_mode}'. Use 'reuseport' or 'acceptor'.`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Acceptor mode needs a uWS app to receive and distribute connections
|
|
29
48
|
const ssl_cert = env('SSL_CERT', '');
|
|
30
49
|
const ssl_key = env('SSL_KEY', '');
|
|
31
50
|
const is_tls = !!(ssl_cert && ssl_key);
|
|
32
51
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
52
|
+
let uWS, acceptorApp;
|
|
53
|
+
if (cluster_mode === 'acceptor') {
|
|
54
|
+
uWS = (await import('uWebSockets.js')).default;
|
|
55
|
+
acceptorApp = is_tls
|
|
56
|
+
? uWS.SSLApp({ cert_file_name: ssl_cert, key_file_name: ssl_key })
|
|
57
|
+
: uWS.App();
|
|
58
|
+
}
|
|
36
59
|
|
|
37
|
-
console.log(`Primary thread starting ${num} workers...`);
|
|
60
|
+
console.log(`Primary thread starting ${num} workers (${cluster_mode} mode)...`);
|
|
38
61
|
|
|
39
62
|
/** @type {Map<import('node:worker_threads').Worker, any>} */
|
|
40
63
|
const workers = new Map();
|
|
@@ -51,11 +74,13 @@ if (is_primary) {
|
|
|
51
74
|
const restart_timers = new Set();
|
|
52
75
|
|
|
53
76
|
function spawn_worker() {
|
|
54
|
-
const worker = new Worker(fileURLToPath(import.meta.url)
|
|
77
|
+
const worker = new Worker(fileURLToPath(import.meta.url), {
|
|
78
|
+
workerData: { mode: cluster_mode }
|
|
79
|
+
});
|
|
55
80
|
workers.set(worker, null);
|
|
56
81
|
|
|
57
82
|
worker.on('message', (msg) => {
|
|
58
|
-
if (msg.type === 'descriptor') {
|
|
83
|
+
if (msg.type === 'descriptor' && cluster_mode === 'acceptor') {
|
|
59
84
|
workers.set(worker, msg.descriptor);
|
|
60
85
|
acceptorApp.addChildAppDescriptor(msg.descriptor);
|
|
61
86
|
console.log(`Worker thread ${worker.threadId} registered`);
|
|
@@ -78,6 +103,12 @@ if (is_primary) {
|
|
|
78
103
|
}
|
|
79
104
|
});
|
|
80
105
|
}
|
|
106
|
+
} else if (msg.type === 'ready' && cluster_mode === 'reuseport') {
|
|
107
|
+
console.log(`Worker thread ${worker.threadId} listening on :${port}`);
|
|
108
|
+
restart_delay = 0;
|
|
109
|
+
restart_attempts = 0;
|
|
110
|
+
for (const t of restart_timers) clearTimeout(t);
|
|
111
|
+
restart_timers.clear();
|
|
81
112
|
} else if (msg.type === 'publish') {
|
|
82
113
|
// Relay pub/sub to all OTHER workers
|
|
83
114
|
for (const [w] of workers) {
|
|
@@ -87,20 +118,26 @@ if (is_primary) {
|
|
|
87
118
|
});
|
|
88
119
|
|
|
89
120
|
worker.on('exit', (code) => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
121
|
+
if (cluster_mode === 'acceptor') {
|
|
122
|
+
const descriptor = workers.get(worker);
|
|
123
|
+
if (descriptor) {
|
|
124
|
+
try { acceptorApp.removeChildAppDescriptor(descriptor); } catch {}
|
|
125
|
+
}
|
|
93
126
|
}
|
|
94
127
|
workers.delete(worker);
|
|
95
128
|
if (!shutting_down) {
|
|
96
|
-
//
|
|
129
|
+
// In acceptor mode, stop accepting when all workers are down so
|
|
97
130
|
// clients get a clean connection-refused instead of an empty app.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
131
|
+
// In reuseport mode, each worker owns its listen socket -- when
|
|
132
|
+
// it dies, the kernel stops routing to it automatically.
|
|
133
|
+
if (cluster_mode === 'acceptor') {
|
|
134
|
+
const has_live_worker = [...workers.values()].some(d => d !== null);
|
|
135
|
+
if (!has_live_worker && listen_socket) {
|
|
136
|
+
uWS.us_listen_socket_close(listen_socket);
|
|
137
|
+
listen_socket = null;
|
|
138
|
+
listening = false;
|
|
139
|
+
console.log('All workers down, acceptor paused until a replacement is ready');
|
|
140
|
+
}
|
|
104
141
|
}
|
|
105
142
|
restart_attempts++;
|
|
106
143
|
if (restart_attempts > RESTART_MAX_ATTEMPTS) {
|
|
@@ -139,8 +176,8 @@ if (is_primary) {
|
|
|
139
176
|
for (const t of restart_timers) clearTimeout(t);
|
|
140
177
|
restart_timers.clear();
|
|
141
178
|
|
|
142
|
-
// Stop accepting new connections
|
|
143
|
-
if (listen_socket) {
|
|
179
|
+
// Stop accepting new connections (acceptor mode only)
|
|
180
|
+
if (cluster_mode === 'acceptor' && listen_socket) {
|
|
144
181
|
uWS.us_listen_socket_close(listen_socket);
|
|
145
182
|
listen_socket = null;
|
|
146
183
|
}
|
|
@@ -168,8 +205,16 @@ if (is_primary) {
|
|
|
168
205
|
// Single-process mode (no clustering)
|
|
169
206
|
start(host, parseInt(port, 10));
|
|
170
207
|
} else {
|
|
171
|
-
// Worker thread
|
|
172
|
-
|
|
208
|
+
// Worker thread startup depends on clustering mode
|
|
209
|
+
if (workerData?.mode === 'reuseport') {
|
|
210
|
+
// Reuseport: each worker listens on the shared port directly.
|
|
211
|
+
// The kernel distributes incoming connections via SO_REUSEPORT.
|
|
212
|
+
start(host, parseInt(port, 10));
|
|
213
|
+
parentPort.postMessage({ type: 'ready' });
|
|
214
|
+
} else {
|
|
215
|
+
// Acceptor: register with the main thread's acceptor app
|
|
216
|
+
parentPort.postMessage({ type: 'descriptor', descriptor: getDescriptor() });
|
|
217
|
+
}
|
|
173
218
|
|
|
174
219
|
parentPort.on('message', (msg) => {
|
|
175
220
|
if (msg.type === 'shutdown') {
|
package/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { WebSocket } from 'uWebSockets.js';
|
|
|
21
21
|
* | `BODY_SIZE_LIMIT` | `512K` | Max request body size (`K`, `M`, `G` suffixes) |
|
|
22
22
|
* | `SHUTDOWN_TIMEOUT` | `30` | Seconds to wait during graceful shutdown |
|
|
23
23
|
* | `CLUSTER_WORKERS` | - | Number of worker threads (`'auto'` for CPU count) |
|
|
24
|
+
* | `CLUSTER_MODE` | *(auto)* | `'reuseport'` (Linux default) or `'acceptor'` (other platforms) |
|
|
24
25
|
*
|
|
25
26
|
* All variables respect the `envPrefix` option (e.g. `MY_APP_PORT` if `envPrefix: 'MY_APP_'`).
|
|
26
27
|
*
|
|
@@ -31,8 +32,17 @@ import type { WebSocket } from 'uWebSockets.js';
|
|
|
31
32
|
* CLUSTER_WORKERS=4 node build # fixed 4 workers
|
|
32
33
|
* ```
|
|
33
34
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
35
|
+
* Two clustering modes are available:
|
|
36
|
+
*
|
|
37
|
+
* - **`reuseport`** (Linux default) - each worker binds to the same port via `SO_REUSEPORT`.
|
|
38
|
+
* The kernel distributes incoming connections across workers. No single-threaded acceptor
|
|
39
|
+
* bottleneck, no single point of failure. One worker crashing does not affect others.
|
|
40
|
+
*
|
|
41
|
+
* - **`acceptor`** (macOS/Windows default) - a primary thread accepts all connections and
|
|
42
|
+
* distributes them to workers via uWS child app descriptors. Works on all platforms.
|
|
43
|
+
*
|
|
44
|
+
* The mode is auto-detected from the platform. Override with `CLUSTER_MODE=acceptor` or
|
|
45
|
+
* `CLUSTER_MODE=reuseport` (reuseport requires Linux). Workers auto-restart on crash.
|
|
36
46
|
*
|
|
37
47
|
* **WebSocket + clustering:** `publish()` is automatically relayed across all workers.
|
|
38
48
|
* `sendTo()`, `connections`, and `subscribers()` operate on the local worker only.
|
package/package.json
CHANGED