svelte-adapter-uws 0.3.4 → 0.3.5

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
@@ -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 via uWebSockets.js worker thread distribution. A primary thread creates an acceptor app that distributes incoming connections across worker threads, each running their own uWS instance. This works on **all platforms** (Linux, macOS, Windows).
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.
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: accept connections, distribute to worker threads ──
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
- // Acceptor app must match worker SSL mode
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
- const acceptorApp = is_tls
34
- ? uWS.SSLApp({ cert_file_name: ssl_cert, key_file_name: ssl_key })
35
- : uWS.App();
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
- const descriptor = workers.get(worker);
91
- if (descriptor) {
92
- try { acceptorApp.removeChildAppDescriptor(descriptor); } catch {}
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
- // If no workers have descriptors, stop accepting connections so
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
- const has_live_worker = [...workers.values()].some(d => d !== null);
99
- if (!has_live_worker && listen_socket) {
100
- uWS.us_listen_socket_close(listen_socket);
101
- listen_socket = null;
102
- listening = false;
103
- console.log('All workers down, acceptor paused until a replacement is ready');
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 - register with acceptor, don't listen
172
- parentPort.postMessage({ type: 'descriptor', descriptor: getDescriptor() });
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
- * Uses uWebSockets.js worker thread distribution (acceptor app + child app descriptors).
35
- * Works on **all platforms** (Linux, macOS, Windows). Workers auto-restart on crash.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-adapter-uws",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "SvelteKit adapter for uWebSockets.js - high-performance C++ HTTP server with built-in WebSocket support",
5
5
  "author": "Kevin Radziszewski",
6
6
  "license": "MIT",