svelte-adapter-uws 0.2.16 → 0.2.18

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
@@ -40,22 +40,27 @@ I've been loving Svelte and SvelteKit for a long time. I always wanted to expand
40
40
  - [Seeding initial state](#seeding-initial-state)
41
41
 
42
42
  **Plugins**
43
- - [Replay (SSR gap)](#replay-plugin-ssr-gap)
44
- - [Presence](#presence-plugin)
45
- - [Typed channels](#typed-channels-plugin)
46
- - [Throttle/debounce](#throttledebounce-plugin)
43
+ - [Replay (SSR gap)](#replay-ssr-gap)
44
+ - [Presence](#presence)
45
+ - [Typed channels](#typed-channels)
46
+ - [Throttle/debounce](#throttledebounce)
47
47
 
48
48
  **Deployment & scaling**
49
49
  - [Deploying with Docker](#deploying-with-docker)
50
50
  - [Clustering](#clustering)
51
51
  - [Performance](#performance)
52
52
 
53
+ **Examples**
54
+ - [Full example: real-time todo list](#full-example-real-time-todo-list)
55
+
53
56
  **Help**
54
57
  - [Troubleshooting](#troubleshooting)
55
58
  - [License](#license)
56
59
 
57
60
  ---
58
61
 
62
+ **Getting started**
63
+
59
64
  ## Installation
60
65
 
61
66
  ### Starting from scratch
@@ -281,6 +286,8 @@ The Vite plugin is required for WebSocket support in both dev and production (se
281
286
 
282
287
  Changes to your `hooks.ws` file are picked up automatically — the plugin reloads the handler on save, no dev server restart needed.
283
288
 
289
+ **Note:** The dev server does not enforce `allowedOrigins`. Origin checks only run in production. A warning is logged at startup as a reminder.
290
+
284
291
  **vite.config.js**
285
292
  ```js
286
293
  import { sveltekit } from '@sveltejs/kit/vite';
@@ -318,6 +325,8 @@ SSL_CERT=./cert.pem SSL_KEY=./key.pem PORT=443 node build
318
325
 
319
326
  ---
320
327
 
328
+ **Configuration**
329
+
321
330
  ## Adapter options
322
331
 
323
332
  ```js
@@ -443,6 +452,56 @@ SSL_CERT=./cert.pem SSL_KEY=./key.pem PORT=443 HOST=0.0.0.0 BODY_SIZE_LIMIT=10M
443
452
 
444
453
  ---
445
454
 
455
+ ## TypeScript setup
456
+
457
+ Add the platform type to your `src/app.d.ts`:
458
+
459
+ ```ts
460
+ import type { Platform as AdapterPlatform } from 'svelte-adapter-uws';
461
+
462
+ declare global {
463
+ namespace App {
464
+ interface Platform extends AdapterPlatform {}
465
+ }
466
+ }
467
+
468
+ export {};
469
+ ```
470
+
471
+ Now `event.platform.publish()`, `event.platform.topic()`, etc. are fully typed.
472
+
473
+ ---
474
+
475
+ ## Svelte 4 support
476
+
477
+ This adapter supports both Svelte 4 and Svelte 5. All examples in this README use Svelte 5 syntax (`$props()`, runes). If you're on Svelte 4, here's how to translate:
478
+
479
+ **Svelte 5 (used in examples)**
480
+ ```svelte
481
+ <script>
482
+ import { crud } from 'svelte-adapter-uws/client';
483
+
484
+ let { data } = $props();
485
+ const todos = crud('todos', data.todos);
486
+ </script>
487
+ ```
488
+
489
+ **Svelte 4 equivalent**
490
+ ```svelte
491
+ <script>
492
+ import { crud } from 'svelte-adapter-uws/client';
493
+
494
+ export let data;
495
+ const todos = crud('todos', data.todos);
496
+ </script>
497
+ ```
498
+
499
+ The only difference is how you receive props. The client store API (`on`, `crud`, `lookup`, `latest`, `count`, `once`, `status`, `connect`) works identically in both versions - it uses `svelte/store` which hasn't changed.
500
+
501
+ ---
502
+
503
+ **WebSocket deep dive**
504
+
446
505
  ## WebSocket handler (`hooks.ws`)
447
506
 
448
507
  ### No handler needed (simplest)
@@ -1184,7 +1243,79 @@ export async function subscribe(ws, topic, { platform }) {
1184
1243
 
1185
1244
  Opt-in modules that build on top of the adapter's public API. They don't change any core behavior -- if you don't import them, they don't exist. Each plugin ships in its own subdirectory under `plugins/` with separate server and client entry points.
1186
1245
 
1187
- ### Replay plugin (SSR gap)
1246
+ ### Middleware
1247
+
1248
+ Composable message processing pipeline. Chain functions that run on inbound messages before your handler logic. Each middleware receives a context and a `next` function -- call `next()` to continue, skip it to stop the chain.
1249
+
1250
+ #### Setup
1251
+
1252
+ ```js
1253
+ // src/lib/server/pipeline.js
1254
+ import { createMiddleware } from 'svelte-adapter-uws/plugins/middleware';
1255
+
1256
+ export const pipeline = createMiddleware(
1257
+ // logging
1258
+ async (ctx, next) => {
1259
+ console.log(`[${ctx.topic}] ${ctx.event}`);
1260
+ await next();
1261
+ },
1262
+ // auth check
1263
+ async (ctx, next) => {
1264
+ const userId = ctx.ws.getUserData()?.userId;
1265
+ if (!userId) return; // stop chain -- unauthenticated
1266
+ ctx.locals.userId = userId;
1267
+ await next();
1268
+ },
1269
+ // data enrichment
1270
+ async (ctx, next) => {
1271
+ ctx.data = { ...ctx.data, processedAt: Date.now() };
1272
+ await next();
1273
+ }
1274
+ );
1275
+ ```
1276
+
1277
+ #### Usage
1278
+
1279
+ ```js
1280
+ // src/hooks.ws.js
1281
+ import { pipeline } from '$lib/server/pipeline';
1282
+
1283
+ export async function message(ws, data, { platform }) {
1284
+ const msg = JSON.parse(Buffer.from(data).toString());
1285
+ const ctx = await pipeline.run(ws, msg, platform);
1286
+ if (!ctx) return; // chain was stopped (e.g. auth failed)
1287
+
1288
+ // ctx.locals.userId is available here
1289
+ // ctx.data has the enriched data
1290
+ }
1291
+ ```
1292
+
1293
+ #### API
1294
+
1295
+ | Method | Description |
1296
+ |---|---|
1297
+ | `pipeline.run(ws, message, platform)` | Execute the chain. Returns context or `null` if stopped |
1298
+ | `pipeline.use(fn)` | Append a middleware at runtime |
1299
+
1300
+ The context object:
1301
+
1302
+ | Field | Description |
1303
+ |---|---|
1304
+ | `ctx.ws` | The WebSocket connection |
1305
+ | `ctx.message` | Original parsed message |
1306
+ | `ctx.topic` | Message topic (mutable) |
1307
+ | `ctx.event` | Message event (mutable) |
1308
+ | `ctx.data` | Message data (mutable) |
1309
+ | `ctx.platform` | Platform reference |
1310
+ | `ctx.locals` | Scratch space for middleware to share data |
1311
+
1312
+ #### Limitations
1313
+
1314
+ - **Server-side only.** No client component.
1315
+ - **No state.** The middleware itself is stateless -- it's a pure pipeline. Use `ctx.locals` to pass data between middlewares within a single message.
1316
+ - **Double `next()` guard.** Calling `next()` twice in the same middleware is a no-op (the second call does nothing).
1317
+
1318
+ ### Replay (SSR gap)
1188
1319
 
1189
1320
  When you combine SSR with WebSocket live updates, there's a gap between server-side data loading and the moment the client's WebSocket connects. Messages published during that window are lost.
1190
1321
 
@@ -1302,12 +1433,10 @@ const messages = onReplay('chat', { since: data.seq }).scan([], reducer);
1302
1433
 
1303
1434
  ---
1304
1435
 
1305
- ### Presence plugin
1436
+ ### Presence
1306
1437
 
1307
1438
  Track who's connected to a topic in real time. Handles multi-tab dedup (same user with two tabs open = one presence entry), broadcasts join/leave events, and provides a live store on the client.
1308
1439
 
1309
- Like the replay plugin, this is opt-in and has zero impact on the adapter core.
1310
-
1311
1440
  #### Setup
1312
1441
 
1313
1442
  Create a shared presence instance:
@@ -1413,7 +1542,7 @@ If no `key` field is found in the selected data (e.g. no auth), each connection
1413
1542
  - **Single-worker only.** Each worker tracks its own presence. In clustered mode, the list reflects only the local worker's connections.
1414
1543
  - **Requires subscription.** The client must subscribe to the topic (via `on()`, `crud()`, etc.) for the server's `subscribe` hook to fire. `presence('room')` alone shows you the list but doesn't register you as present unless you're also subscribed to `room`.
1415
1544
 
1416
- ### Typed channels plugin
1545
+ ### Typed channels
1417
1546
 
1418
1547
  Define message schemas per topic so event names and data shapes are validated at publish time. Catches typos and shape mismatches before they reach the wire -- instead of silently sending garbage that the client ignores.
1419
1548
 
@@ -1497,7 +1626,7 @@ You can still use `crud()`, `lookup()`, `latest()`, etc. directly with the topic
1497
1626
  - **Runtime only.** The validation happens at publish/send time, not at compile time. TypeScript generics give you autocomplete for event names, but data shape checking is runtime.
1498
1627
  - **No dependency on Zod.** The plugin accepts any validator function or any object with a `.parse()` method. You bring your own validation library (or use plain functions).
1499
1628
 
1500
- ### Throttle/debounce plugin
1629
+ ### Throttle/debounce
1501
1630
 
1502
1631
  Per-topic publish rate limiting. Wraps `platform.publish()` to coalesce rapid-fire updates (mouse position, typing indicators, live metrics). Sends the latest value at most once per interval. No timers to manage yourself.
1503
1632
 
@@ -1581,56 +1710,308 @@ t=260 [timer fires, 100ms] --> sends {q:"hel"}
1581
1710
  - **Latest value only.** Intermediate values within an interval are discarded, not queued. If you need every message delivered, don't throttle.
1582
1711
  - **Timer-based.** Uses `setTimeout` internally. Precision depends on Node.js event loop load (typically < 1ms drift).
1583
1712
 
1584
- ---
1713
+ ### Rate limiting
1585
1714
 
1586
- ## TypeScript setup
1715
+ Token-bucket rate limiter for inbound WebSocket messages. Protects against spam, abuse, and runaway clients. Supports per-IP, per-connection, or custom key extraction, with optional auto-ban when a bucket is exhausted.
1587
1716
 
1588
- Add the platform type to your `src/app.d.ts`:
1717
+ Different from throttle -- throttle shapes **outbound** publish rate, rate limiting protects **inbound** against abuse.
1589
1718
 
1590
- ```ts
1591
- import type { Platform as AdapterPlatform } from 'svelte-adapter-uws';
1719
+ #### Setup
1592
1720
 
1593
- declare global {
1594
- namespace App {
1595
- interface Platform extends AdapterPlatform {}
1596
- }
1721
+ ```js
1722
+ // src/lib/server/ratelimit.js
1723
+ import { createRateLimit } from 'svelte-adapter-uws/plugins/ratelimit';
1724
+
1725
+ export const limiter = createRateLimit({
1726
+ points: 10, // 10 messages
1727
+ interval: 1000, // per second
1728
+ blockDuration: 30000 // auto-ban for 30s when exhausted
1729
+ });
1730
+ ```
1731
+
1732
+ #### Usage
1733
+
1734
+ ```js
1735
+ // src/hooks.ws.js
1736
+ import { limiter } from '$lib/server/ratelimit';
1737
+
1738
+ export function message(ws, data, { platform }) {
1739
+ const { allowed, remaining, resetMs } = limiter.consume(ws);
1740
+ if (!allowed) return; // drop the message
1741
+
1742
+ // ... handle message normally
1597
1743
  }
1744
+ ```
1598
1745
 
1599
- export {};
1746
+ #### API
1747
+
1748
+ | Method | Description |
1749
+ |---|---|
1750
+ | `limiter.consume(ws, cost?)` | Deduct tokens (cost must be >= 0, defaults to 1), returns `{ allowed, remaining, resetMs }` |
1751
+ | `limiter.reset(key)` | Clear the bucket for a key |
1752
+ | `limiter.ban(key, duration?)` | Manually ban a key |
1753
+ | `limiter.unban(key)` | Remove a ban |
1754
+ | `limiter.clear()` | Reset all state |
1755
+
1756
+ #### Options
1757
+
1758
+ | Option | Default | Description |
1759
+ |---|---|---|
1760
+ | `points` | *required* | Tokens per interval (positive integer) |
1761
+ | `interval` | *required* | Refill interval in ms |
1762
+ | `blockDuration` | `0` | Auto-ban duration in ms when exhausted (0 = no auto-ban) |
1763
+ | `keyBy` | `'ip'` | `'ip'`, `'connection'`, or `(ws) => string` |
1764
+
1765
+ With `keyBy: 'ip'` (default), the limiter reads `userData.remoteAddress`, `.ip`, or `.address`. With `keyBy: 'connection'`, each WebSocket gets its own bucket. Pass a function for custom grouping (e.g. by user ID or room).
1766
+
1767
+ #### Limitations
1768
+
1769
+ - **Server-side only.** No client component needed.
1770
+ - **In-memory.** Buckets live in the process. In cluster mode, each worker has independent rate limits (acceptable for most apps -- abusers hit the same worker via the acceptor).
1771
+ - **Lazy cleanup.** Expired buckets are swept when the internal map exceeds 1000 entries.
1772
+
1773
+ ### Cursor (ephemeral state)
1774
+
1775
+ Lightweight fire-and-forget broadcasting for transient state -- mouse cursors, text selections, drag positions, drawing strokes. Built-in throttle with trailing edge ensures the final position always arrives. Auto-cleanup on disconnect.
1776
+
1777
+ #### Setup
1778
+
1779
+ ```js
1780
+ // src/lib/server/cursors.js
1781
+ import { createCursor } from 'svelte-adapter-uws/plugins/cursor';
1782
+
1783
+ export const cursors = createCursor({
1784
+ throttle: 50, // at most one broadcast per 50ms per user per topic
1785
+ select: (userData) => ({ id: userData.id, name: userData.name, color: userData.color })
1786
+ });
1600
1787
  ```
1601
1788
 
1602
- Now `event.platform.publish()`, `event.platform.topic()`, etc. are fully typed.
1789
+ #### Server usage
1603
1790
 
1604
- ---
1791
+ ```js
1792
+ // src/hooks.ws.js
1793
+ import { cursors } from '$lib/server/cursors';
1605
1794
 
1606
- ## Svelte 4 support
1795
+ export function message(ws, data, { platform }) {
1796
+ const msg = JSON.parse(Buffer.from(data).toString());
1797
+ if (msg.type === 'cursor') {
1798
+ cursors.update(ws, msg.topic, { x: msg.x, y: msg.y }, platform);
1799
+ }
1800
+ }
1607
1801
 
1608
- This adapter supports both Svelte 4 and Svelte 5. All examples in this README use Svelte 5 syntax (`$props()`, runes). If you're on Svelte 4, here's how to translate:
1802
+ export function close(ws, { platform }) {
1803
+ cursors.remove(ws, platform);
1804
+ }
1805
+ ```
1806
+
1807
+ #### Client usage
1609
1808
 
1610
- **Svelte 5 (used in examples)**
1611
1809
  ```svelte
1612
1810
  <script>
1613
- import { crud } from 'svelte-adapter-uws/client';
1811
+ import { cursor } from 'svelte-adapter-uws/plugins/cursor/client';
1614
1812
 
1615
- let { data } = $props();
1616
- const todos = crud('todos', data.todos);
1813
+ const positions = cursor('canvas');
1617
1814
  </script>
1815
+
1816
+ {#each [...$positions] as [key, { user, data }] (key)}
1817
+ <div
1818
+ class="cursor-dot"
1819
+ style="left: {data.x}px; top: {data.y}px; background: {user.color}"
1820
+ >
1821
+ {user.name}
1822
+ </div>
1823
+ {/each}
1618
1824
  ```
1619
1825
 
1620
- **Svelte 4 equivalent**
1826
+ The client store is a `Readable<Map<string, { user, data }>>`. The Map updates when cursors move or disconnect.
1827
+
1828
+ #### Server API
1829
+
1830
+ | Method | Description |
1831
+ |---|---|
1832
+ | `cursors.update(ws, topic, data, platform)` | Broadcast position (throttled) |
1833
+ | `cursors.remove(ws, platform)` | Remove from all topics, broadcast removal |
1834
+ | `cursors.list(topic)` | Current positions (for SSR) |
1835
+ | `cursors.clear()` | Reset all state and timers |
1836
+
1837
+ #### How throttle works
1838
+
1839
+ The cursor plugin uses leading edge + trailing edge throttle internally:
1840
+
1841
+ ```
1842
+ t=0 update({x:0}) --> broadcasts immediately (leading edge)
1843
+ t=20 update({x:5}) --> stored (within 50ms window)
1844
+ t=40 update({x:9}) --> stored (overwrites x:5)
1845
+ t=50 [timer fires] --> broadcasts {x:9} (trailing edge)
1846
+ ```
1847
+
1848
+ The trailing edge ensures you always see where the cursor stopped, even if the user stops moving mid-window.
1849
+
1850
+ #### Limitations
1851
+
1852
+ - **In-memory.** Cursor positions live in the process. In cluster mode, each worker tracks its own connections.
1853
+ - **No persistence.** Positions are lost on restart. This is intentional -- cursors are ephemeral.
1854
+
1855
+ ### Queue (ordered delivery)
1856
+
1857
+ Per-key async task queue with configurable concurrency and backpressure. Guarantees in-order processing per key -- useful for sequential operations like collaborative editing, turn-based games, or transaction sequences.
1858
+
1859
+ #### Setup
1860
+
1861
+ ```js
1862
+ // src/lib/server/queue.js
1863
+ import { createQueue } from 'svelte-adapter-uws/plugins/queue';
1864
+
1865
+ // Sequential processing per key (default concurrency: 1)
1866
+ export const queue = createQueue({ maxSize: 100 });
1867
+ ```
1868
+
1869
+ #### Usage
1870
+
1871
+ ```js
1872
+ // src/hooks.ws.js
1873
+ import { queue } from '$lib/server/queue';
1874
+
1875
+ export async function message(ws, data, { platform }) {
1876
+ const msg = JSON.parse(Buffer.from(data).toString());
1877
+
1878
+ // Messages for the same topic are processed one at a time
1879
+ const result = await queue.push(msg.topic, async () => {
1880
+ const record = await db.update(msg.data);
1881
+ platform.publish(msg.topic, 'updated', record);
1882
+ return record;
1883
+ });
1884
+ }
1885
+ ```
1886
+
1887
+ #### API
1888
+
1889
+ | Method | Description |
1890
+ |---|---|
1891
+ | `queue.push(key, task)` | Enqueue a task, returns promise with the task's return value |
1892
+ | `queue.size(key?)` | Waiting + running count for a key, or total |
1893
+ | `queue.clear(key?)` | Cancel waiting tasks (running tasks continue) |
1894
+ | `queue.drain(key?)` | Wait for all tasks to complete |
1895
+
1896
+ #### Options
1897
+
1898
+ | Option | Default | Description |
1899
+ |---|---|---|
1900
+ | `concurrency` | `1` | Max concurrent tasks per key |
1901
+ | `maxSize` | `Infinity` | Max waiting tasks per key (rejects when exceeded) |
1902
+ | `onDrop` | `null` | Called with `{ key, task }` when a task is rejected |
1903
+
1904
+ Different keys are independent -- `push('room-a', ...)` and `push('room-b', ...)` run concurrently. Only tasks with the same key are queued.
1905
+
1906
+ #### Limitations
1907
+
1908
+ - **Server-side only.** No client component.
1909
+ - **In-memory.** Queue state lives in the process. Not durable across restarts.
1910
+ - **No cancellation.** Running tasks cannot be aborted. `clear()` only rejects waiting tasks.
1911
+
1912
+ ### Broadcast groups
1913
+
1914
+ Named groups with explicit membership, roles, metadata, and lifecycle hooks. Like topics but with access control -- you decide who can join, what role they have, and what happens when the group fills up or closes.
1915
+
1916
+ #### Setup
1917
+
1918
+ ```js
1919
+ // src/lib/server/lobby.js
1920
+ import { createGroup } from 'svelte-adapter-uws/plugins/groups';
1921
+
1922
+ export const lobby = createGroup('lobby', {
1923
+ maxMembers: 50,
1924
+ meta: { game: 'chess' },
1925
+ onJoin: (ws, role) => console.log('joined as', role),
1926
+ onFull: (ws, role) => {
1927
+ // optionally notify the rejected client
1928
+ }
1929
+ });
1930
+ ```
1931
+
1932
+ #### Server usage
1933
+
1934
+ ```js
1935
+ // src/hooks.ws.js
1936
+ import { lobby } from '$lib/server/lobby';
1937
+
1938
+ export function subscribe(ws, topic, { platform }) {
1939
+ if (topic === 'lobby') {
1940
+ const joined = lobby.join(ws, platform, 'member');
1941
+ if (!joined) {
1942
+ platform.send(ws, 'system', 'error', 'Lobby is full');
1943
+ }
1944
+ }
1945
+ }
1946
+
1947
+ export function close(ws, { platform }) {
1948
+ lobby.leave(ws, platform);
1949
+ }
1950
+ ```
1951
+
1952
+ Publish to group members:
1953
+
1954
+ ```js
1955
+ // Broadcast to everyone
1956
+ lobby.publish(platform, 'chat', { text: 'hello' });
1957
+
1958
+ // Broadcast only to admins
1959
+ lobby.publish(platform, 'admin-alert', { msg: 'new report' }, 'admin');
1960
+ ```
1961
+
1962
+ #### Client usage
1963
+
1621
1964
  ```svelte
1622
1965
  <script>
1623
- import { crud } from 'svelte-adapter-uws/client';
1966
+ import { group } from 'svelte-adapter-uws/plugins/groups/client';
1624
1967
 
1625
- export let data;
1626
- const todos = crud('todos', data.todos);
1968
+ const lobby = group('lobby');
1969
+ const members = lobby.members;
1627
1970
  </script>
1971
+
1972
+ <p>{$members.length} members</p>
1628
1973
  ```
1629
1974
 
1630
- The only difference is how you receive props. The client store API (`on`, `crud`, `lookup`, `latest`, `count`, `once`, `status`, `connect`) works identically in both versions - it uses `svelte/store` which hasn't changed.
1975
+ The client store exposes two reactive values: the main store for events (`$lobby` -- latest message) and `.members` for the live member list. The member list updates automatically on join, leave, and close events -- no polling needed.
1976
+
1977
+ #### Server API
1978
+
1979
+ | Method | Description |
1980
+ |---|---|
1981
+ | `group.join(ws, platform, role?)` | Add member. Returns `true` or `false` if full/closed |
1982
+ | `group.leave(ws, platform)` | Remove member |
1983
+ | `group.publish(platform, event, data, role?)` | Broadcast (optionally filtered by role) |
1984
+ | `group.send(platform, ws, event, data)` | Send to one member (throws if not a member) |
1985
+ | `group.members()` | Array of `{ ws, role }` |
1986
+ | `group.count()` | Member count |
1987
+ | `group.has(ws)` | Check membership |
1988
+ | `group.close(platform)` | Dissolve group, notify everyone |
1989
+ | `group.name` | Group name (read-only) |
1990
+ | `group.meta` | Metadata (get/set) |
1991
+
1992
+ Roles: `'member'` (default), `'admin'`, `'viewer'`.
1993
+
1994
+ #### Options
1995
+
1996
+ | Option | Default | Description |
1997
+ |---|---|---|
1998
+ | `maxMembers` | `Infinity` | Maximum members |
1999
+ | `meta` | `{}` | Initial metadata (shallow-copied) |
2000
+ | `onJoin` | -- | `(ws, role) => void` |
2001
+ | `onLeave` | -- | `(ws, role) => void` |
2002
+ | `onFull` | -- | `(ws, role) => void` |
2003
+ | `onClose` | -- | `() => void` |
2004
+
2005
+ #### Limitations
2006
+
2007
+ - **In-memory.** Group state lives in the process. In cluster mode, each worker manages its own groups independently.
2008
+ - **No persistence.** Groups are lost on restart. If you need durable rooms, store membership in a database and rebuild on start.
2009
+ - **Role-filtered publish uses `send()`.** When filtering by role, the plugin iterates members and sends individually instead of using the topic broadcast. Fine for typical group sizes, but O(n) with member count.
1631
2010
 
1632
2011
  ---
1633
2012
 
2013
+ **Deployment & scaling**
2014
+
1634
2015
  ## Deploying with Docker
1635
2016
 
1636
2017
  uWebSockets.js is a native C++ addon, so your Docker image needs to match the platform it was compiled for. Build inside the container to be safe.
@@ -1784,6 +2165,8 @@ node bench/run-compare.mjs # full comparison vs adapter-node + socket.io
1784
2165
 
1785
2166
  ---
1786
2167
 
2168
+ **Examples**
2169
+
1787
2170
  ## Full example: real-time todo list
1788
2171
 
1789
2172
  Here's a complete example tying everything together.
@@ -1879,6 +2262,8 @@ Open the page in two browser tabs. Create, toggle, or delete a todo in one tab -
1879
2262
 
1880
2263
  ---
1881
2264
 
2265
+ **Help**
2266
+
1882
2267
  ## Troubleshooting
1883
2268
 
1884
2269
  ### "WebSocket works in production but not in dev"
package/files/handler.js CHANGED
@@ -831,6 +831,10 @@ if (WS_ENABLED) {
831
831
  if (requestPort) {
832
832
  expectedHost = requestHost.replace(/:\d+$/, '') + ':' + requestPort;
833
833
  }
834
+ // Strip default ports so "example.com" matches "example.com:443"
835
+ // (URL.host omits the port when it is the default for the scheme)
836
+ const defaultPort = requestScheme === 'https' ? '443' : '80';
837
+ expectedHost = expectedHost.replace(':' + defaultPort, '');
834
838
  allowed = parsed.host === expectedHost && parsed.protocol === requestScheme + ':';
835
839
  }
836
840
  } catch {
package/files/index.js CHANGED
@@ -45,8 +45,8 @@ if (is_primary) {
45
45
  // Exponential backoff for crash-looping workers
46
46
  let restart_delay = 0;
47
47
  const RESTART_DELAY_MAX = 5000;
48
- /** @type {ReturnType<typeof setTimeout> | null} */
49
- let backoff_reset_timer = null;
48
+ /** @type {Set<ReturnType<typeof setTimeout>>} */
49
+ const restart_timers = new Set();
50
50
 
51
51
  function spawn_worker() {
52
52
  const worker = new Worker(fileURLToPath(import.meta.url));
@@ -59,7 +59,8 @@ if (is_primary) {
59
59
  console.log(`Worker thread ${worker.threadId} registered`);
60
60
  // Worker started successfully - reset backoff
61
61
  restart_delay = 0;
62
- if (backoff_reset_timer) { clearTimeout(backoff_reset_timer); backoff_reset_timer = null; }
62
+ for (const t of restart_timers) clearTimeout(t);
63
+ restart_timers.clear();
63
64
  // Start (or resume) listening once a worker is ready to handle requests
64
65
  if (!listening) {
65
66
  listening = true;
@@ -100,7 +101,12 @@ if (is_primary) {
100
101
  }
101
102
  restart_delay = restart_delay ? Math.min(restart_delay * 2, RESTART_DELAY_MAX) : 100;
102
103
  console.log(`Worker thread ${worker.threadId} exited with code ${code}, restarting in ${restart_delay}ms...`);
103
- backoff_reset_timer = setTimeout(() => { spawn_worker(); }, restart_delay);
104
+ const timer = setTimeout(() => {
105
+ restart_timers.delete(timer);
106
+ if (shutting_down) return;
107
+ spawn_worker();
108
+ }, restart_delay);
109
+ restart_timers.add(timer);
104
110
  }
105
111
  // If shutting down and all workers have exited, exit immediately
106
112
  if (shutting_down && workers.size === 0) {
@@ -121,11 +127,9 @@ if (is_primary) {
121
127
  shutting_down = true;
122
128
  console.log(`Primary received ${reason}, shutting down ${workers.size} workers...`);
123
129
 
124
- // Cancel any pending worker restart so we don't spawn during shutdown
125
- if (backoff_reset_timer) {
126
- clearTimeout(backoff_reset_timer);
127
- backoff_reset_timer = null;
128
- }
130
+ // Cancel all pending worker restarts so we don't spawn during shutdown
131
+ for (const t of restart_timers) clearTimeout(t);
132
+ restart_timers.clear();
129
133
 
130
134
  // Stop accepting new connections
131
135
  if (listen_socket) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-adapter-uws",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
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",
@@ -53,6 +53,34 @@
53
53
  "./plugins/throttle": {
54
54
  "types": "./plugins/throttle/server.d.ts",
55
55
  "default": "./plugins/throttle/server.js"
56
+ },
57
+ "./plugins/ratelimit": {
58
+ "types": "./plugins/ratelimit/server.d.ts",
59
+ "default": "./plugins/ratelimit/server.js"
60
+ },
61
+ "./plugins/cursor": {
62
+ "types": "./plugins/cursor/server.d.ts",
63
+ "default": "./plugins/cursor/server.js"
64
+ },
65
+ "./plugins/cursor/client": {
66
+ "types": "./plugins/cursor/client.d.ts",
67
+ "default": "./plugins/cursor/client.js"
68
+ },
69
+ "./plugins/middleware": {
70
+ "types": "./plugins/middleware/server.d.ts",
71
+ "default": "./plugins/middleware/server.js"
72
+ },
73
+ "./plugins/queue": {
74
+ "types": "./plugins/queue/server.d.ts",
75
+ "default": "./plugins/queue/server.js"
76
+ },
77
+ "./plugins/groups": {
78
+ "types": "./plugins/groups/server.d.ts",
79
+ "default": "./plugins/groups/server.js"
80
+ },
81
+ "./plugins/groups/client": {
82
+ "types": "./plugins/groups/client.d.ts",
83
+ "default": "./plugins/groups/client.js"
56
84
  }
57
85
  },
58
86
  "types": "./index.d.ts",