svelte-adapter-uws 0.5.0-next.21 → 0.5.0-next.22

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/MIGRATION.md CHANGED
@@ -1,107 +1,133 @@
1
1
  # Migration guide: svelte-adapter-uws 0.4.x to 0.5.x
2
2
 
3
- This guide enumerates every breaking change between the latest 0.4.x release and the current 0.5.x line. Apply each step in order; sections are independent unless explicitly chained.
3
+ This guide is organized by **tier**. Most apps only need to read the first two sections.
4
+
5
+ - **[Critical](#critical-read-first)** - security-class behavior changes; audit required.
6
+ - **[Required source changes](#required-source-changes)** - won't run cleanly without these.
7
+ - **[Notable defaults and behaviors](#notable-defaults-and-behaviors)** - probably fine, but you may notice.
8
+ - **[Recommended new patterns](#recommended-new-patterns)** - not required, but better.
9
+ - **[Cosmetic](#cosmetic)** - type-only, internal refactors, niche edge cases.
4
10
 
5
11
  If you are upgrading via `npm install svelte-adapter-uws@latest`, the registry will pull whatever the current `latest` dist-tag points to. To pin a specific 0.5 prerelease, use `svelte-adapter-uws@next`.
6
12
 
7
- ## Breaking changes
13
+ If you have a small app and want the 5-minute version, see the [docs site upgrade quickstart](https://svelte-realtime.dev/docs/upgrade-quickstart).
8
14
 
9
- ### Runtime: Node.js 22+ required (was Node 20+)
15
+ ---
10
16
 
11
- **What changed.** `package.json#engines.node` moved from `>=20.0.0` to `>=22.0.0`. Tracks `uWebSockets.js` v20.67.0, which dropped Node 20 support upstream. Node 22 LTS, Node 24 current, and Node 26 are supported. Picks up real upstream wins from v20.60 to v20.67: backpressure fix (v20.64), Latin-1 string handling (v20.65), faster String args via V8 ValueView (v20.63), zero-cost `getRemoteAddress` / `getRemoteAddressAsText` (v20.66), `getRemotePort` / `getProxiedRemotePort` (v20.61), DeclarativeResponse improvements, and symbol-keyed userData support.
17
+ ## Critical (read first)
12
18
 
13
- **How to migrate.**
19
+ These close real security bugs that affected idiomatic 0.4 code paths. Audit your code; production deploys may surface new denials or new startup errors that previously slipped through silently.
14
20
 
15
- - Confirm your runtime is Node 22+ in CI and prod. Node 22 LTS is the minimum.
16
- - Bump any `node:20-*` Docker base image to `node:22-*` or later.
17
- - Bump CI matrix entries from `node-version: '20'` to `'22'` (and optionally add `'24'`).
18
- - Apps that hand-roll `engines` checks against Node 20 should update accordingly.
21
+ ### Async `subscribe` / `subscribeBatch` hooks now fail closed
19
22
 
20
- ### Peer dep: `@sveltejs/kit ^2.59.0` (was `^2.0.0`)
21
-
22
- **What changed.** The peer-dep floor on `@sveltejs/kit` moved from `^2.0.0` to `^2.59.0`. Apps still pinned to kit 2.0.x to 2.58.x will see a peer-dep warning at install. The previous range allowed pre-2.5 installs that were missing significant cumulative kit fixes; the new floor matches the audit's recommendation.
23
+ **What changed.** Previously, an `async (ws, topic) => false` subscribe hook returned a `Promise<false>` to the runtime; the framework compared the Promise object against `false`, both of which were always falsy-against-truthy mismatches, so the framework treated EVERY async hook return as ALLOWED. Every app using async subscribe hooks (the idiomatic style for hooks that read a session store or DB) silently let every subscribe through, bypassing the developer's intended access control. The runtime now awaits the hook before inspecting its return value.
23
24
 
24
- **How to migrate.** Bump your `@sveltejs/kit` dependency to `^2.59.0` or later in your app's `package.json`. One known transitive advisory persists (`cookie<0.7.0` via kit's own `cookie: ^0.6.0` pin) and is unfixable from this side until the kit team publishes a release that bumps cookie; the advisory is low severity and applies to cookie name/path/domain edge cases.
25
+ **How to migrate.** No source change is required if your hook was already correct in intent. AUDIT every async `subscribe` / `subscribeBatch` export to confirm it returns the values you expect (`false`, `'FORBIDDEN'`, etc) on the deny path; tests that previously passed because every subscribe was allowed will now exercise the real gate. `platform.subscribe(ws, topic)` and `platform.checkSubscribe(ws, topic)` are now async and must be awaited at every call site. If you wrote a `platform.sendTo(filter, ...)` filter as an async function, rewrite it to be sync (resolve any input data into `userData` from the upgrade hook); async filters are now treated as not-matching (fail closed) and log a one-time `console.error`.
25
26
 
26
- ### 1. Async `subscribe` / `subscribeBatch` hooks now fail closed
27
+ ### Wire-level subscribes to `__`-prefixed system topics blocked by default
27
28
 
28
- **What changed.** Previously, an `async (ws, topic) => false` subscribe hook returned a `Promise<false>` to the runtime; the framework compared the Promise object against `false`, both of which were always falsy-against-truthy mismatches, so the framework treated EVERY async hook return as ALLOWED. Every app using async subscribe hooks (the idiomatic style for hooks that read a session store or DB) silently let every subscribe through, bypassing the developer's intended access control. The runtime now awaits the hook before inspecting its return value.
29
+ **What changed.** Previously, any authenticated client could send `{"type":"subscribe","topic":"__signal:victim-userId"}` and intercept every `live.signal()` to that user, plus plugin presence / group / replay broadcasts on `__presence:*`, `__group:*`, `__replay:*`. The wire-level subscribe and subscribe-batch handlers now reject any topic whose first two bytes are `__` with `INVALID_TOPIC`. Server-side `platform.subscribe(ws, '__signal:userId')` (the legitimate framework pattern) still works.
29
30
 
30
- **How to migrate.** No source change is required if your hook was already correct in intent. AUDIT every async `subscribe` / `subscribeBatch` export to confirm it returns the values you expect (`false`, `'FORBIDDEN'`, etc) on the deny path; tests that previously passed because every subscribe was allowed will now exercise the real gate. `platform.subscribe(ws, topic)` and `platform.checkSubscribe(ws, topic)` are now async and must be awaited at every call site. If you wrote a `platform.sendTo(filter, ...)` filter as an async function, rewrite it to be sync (resolve any input data into `userData` from the upgrade hook); async filters are now treated as not-matching (fail closed) and log a one-time `console.error`.
31
+ **How to migrate.** No action needed if you only subscribe to system topics from server code. If your app legitimately routes public topics through the `__` prefix (rare), set `websocket.allowSystemTopicSubscribe: true` in `svelte.config.js`.
31
32
 
32
- ### 2. Default `maxPayloadLength` raised from 16 KB to 1 MB
33
+ ### SSR dedup cache key includes `base_origin` (cross-tenant leak fix)
33
34
 
34
- **What changed.** The default cap on a single inbound WebSocket frame moved from 16 KB to 1 MB. The new default aligns with `socket.io` and Cloudflare Workers' WS message cap. Apps that were chunking large payloads to fit under 16 KB will now accept them in fewer chunks (or in a single frame).
35
+ **What changed.** Previously the dedup key was `method + '\0' + url`. In virtual-hosting deployments (one uWS instance behind multiple Host aliases), two concurrent anonymous GETs to `/` from `tenantA.example` and `tenantB.example` shared one SSR call, producing a cross-tenant leak. The key is now `method + '\0' + base_origin + '\0' + url`.
35
36
 
36
- **How to migrate.** No action needed for most apps. To pin the previous cap, set `websocket.maxPayloadLength: 16 * 1024` in `svelte.config.js`. To pin any other value, set the option to that byte count. DoS protection remains layered: `upgradeAdmission.maxConcurrent` caps connection count, `maxBackpressure` caps per-connection outbound queue size.
37
+ **How to migrate.** No action needed; previously-correct apps see a tighter dedup behavior. Multi-tenant deployments should verify the fix is in place.
37
38
 
38
- ### 3. Wire-level subscribes to `__`-prefixed system topics blocked by default
39
+ ### Replay plugin checks subscribe authorization before reading a topic's buffer
39
40
 
40
- **What changed.** Previously, any authenticated client could send `{"type":"subscribe","topic":"__signal:victim-userId"}` and intercept every `live.signal()` to that user, plus plugin presence / group / replay broadcasts on `__presence:*`, `__group:*`, `__replay:*`. The wire-level subscribe and subscribe-batch handlers now reject any topic whose first two bytes are `__` with `INVALID_TOPIC`. Server-side `platform.subscribe(ws, '__signal:userId')` (the legitimate framework pattern) still works.
41
+ **What changed.** Replay backends now consult `platform.checkSubscribe(ws, topic)` before reading any topic's buffer; topics the wire-subscribe gate would deny emit a `denied` event on `__replay:{topic}` (the client treats this similarly to `truncated`). Pre-fix, an attacker could send a crafted `lastSeenSeqs` map and read history for a topic they could not subscribe to live.
41
42
 
42
- **How to migrate.** No action needed if you only subscribe to system topics from server code. If your app legitimately routes public topics through the `__` prefix (rare), set `websocket.allowSystemTopicSubscribe: true` in `svelte.config.js`.
43
+ **How to migrate.** No action needed if you use the bundled replay client, which handles the new event. Hand-rolled replay consumers should add a branch for `denied` events.
43
44
 
44
- ### 4. `resume` hook now awaited before the `resumed` ack frame
45
+ ### `resume` hook now awaited before the `resumed` ack frame
45
46
 
46
47
  **What changed.** The user's `resume` hook previously fired fire-and-forget; the `{type:'resumed'}` ack went out immediately and the client switched to live mode while replay frames were still in flight, producing out-of-order events. The runtime now awaits the resume hook before sending the ack.
47
48
 
48
49
  **How to migrate.** Confirm any async work you do in the `resume` hook (replay enqueue, DB lookup, etc.) is `await`ed inside the hook body. Long-running synchronous work in the hook will now delay the resume ack; refactor expensive work to fire after the ack via `setImmediate` if needed.
49
50
 
50
- ### 5. `/__ws/auth` POST now requires Origin / `x-requested-with` / `Sec-Fetch-Site`
51
+ ---
51
52
 
52
- **What changed.** The authenticate POST endpoint previously accepted any credentialed cross-origin POST. A request must now satisfy at least one of: `x-requested-with: XMLHttpRequest`, `Sec-Fetch-Site: same-origin`, or an `Origin` header matching `allowedOrigins`. The bundled adapter client always stamps `x-requested-with` on its preflight POST, so browser-side flows are unaffected.
53
+ ## Required source changes
53
54
 
54
- **How to migrate.** No action needed for browser apps using the bundled client. For native (non-browser) clients hitting `/__ws/auth` directly, either stamp `x-requested-with: XMLHttpRequest` on the request, or set `websocket.authPathRequireOrigin: false` in `svelte.config.js` to opt out.
55
+ These won't run cleanly until you make the change. The first one is loud (startup error); the others surface as compile-time type errors or runtime throws on bad input.
55
56
 
56
- ### 6. Dynamic compression skipped for credentialed responses (BREACH defense)
57
+ ### Runtime: Node.js 22+ required (was Node 20+)
57
58
 
58
- **What changed.** The dynamic brotli/gzip branch previously fired on every response above 1 KB. Combined with attacker-influenced reflected input alongside a secret in the page body (CSRF token, session ID, API key), the compressed length leaked the secret one byte at a time via the BREACH attack. Requests carrying `Cookie` or `Authorization` now skip dynamic compression. Anonymous responses still compress; build-time precompressed static files are unaffected.
59
+ **What changed.** `package.json#engines.node` moved from `>=20.0.0` to `>=22.0.0`. Tracks `uWebSockets.js` v20.67.0, which dropped Node 20 support upstream. Node 22 LTS, Node 24 current, and Node 26 are supported. Picks up real upstream wins from v20.60 to v20.67: backpressure fix (v20.64), Latin-1 string handling (v20.65), faster String args via V8 ValueView (v20.63), zero-cost `getRemoteAddress` / `getRemoteAddressAsText` (v20.66), `getRemotePort` / `getProxiedRemotePort` (v20.61), DeclarativeResponse improvements, and symbol-keyed userData support.
59
60
 
60
- **How to migrate.** No action needed; uncompressed credentialed SSR is the safe default. If you have audited every page for BREACH defenses (random per-response masking, prefix randomization, no secrets reflected with attacker input), opt back in via `websocket.compressCredentialedResponses: true`.
61
+ **How to migrate.**
61
62
 
62
- ### 7. Refuse to start on `same-origin` policy without host pin
63
+ - Confirm your runtime is Node 22+ in CI and prod. Node 22 LTS is the minimum.
64
+ - Bump any `node:20-*` Docker base image to `node:22-*` or later.
65
+ - Bump CI matrix entries from `node-version: '20'` to `'22'` (and optionally add `'24'`).
66
+ - Apps that hand-roll `engines` checks against Node 20 should update accordingly.
67
+
68
+ ### Refuse to start on `same-origin` policy without host pin
69
+
70
+ **Your app will throw at startup until you fix this.**
63
71
 
64
72
  **What changed.** A bare `allowedOrigins: 'same-origin'` config running without `ORIGIN` env, `HOST_HEADER` env, native TLS (`SSL_CERT`/`SSL_KEY`), or an `upgrade()` hook previously silently accepted any non-browser scripted client (the same-origin check compares two attacker-controlled headers). The runtime now throws at startup with a human-readable resolution list.
65
73
 
66
74
  **How to migrate.** Pick one of: set the `ORIGIN` env var to your canonical origin, set `HOST_HEADER` env, configure native TLS, export an `upgrade()` hook, or switch `allowedOrigins` to an explicit string-array allowlist. Apps that have audited the deployment can opt out via `websocket.unsafeSameOriginWithoutHostPin: true`.
67
75
 
68
- ### 8. Wire-topic accept set tightened to printable ASCII
76
+ ### `platform.subscribe` and `platform.checkSubscribe` are now async
69
77
 
70
- **What changed.** The wire accept set moved from "anything except control bytes / quote / backslash" to "printable ASCII (0x20-0x7E) except quote / backslash". Pre-fix, hostile clients could subscribe to topics containing line-separator characters, BiDi overrides, byte-order marks, or arbitrary non-ASCII. Server-side `platform.subscribe` and `platform.checkSubscribe` keep their previous looser accept set, so server-side code using non-ASCII topic names is unaffected.
78
+ **What changed.** Both methods previously returned `string | null` synchronously. Returns are now `Promise<string | null>`. Direct `ws.subscribe()` calls intentionally bypass the hook (the bypass we are guarding against).
71
79
 
72
- **How to migrate.** No action needed unless your app legitimately accepts non-ASCII topic names FROM CLIENTS. If it does, set `websocket.allowNonAsciiTopics: true`.
80
+ **How to migrate.** Downstream library / framework code that previously called `ws.subscribe()` directly inside an RPC handler (a real authorization-bypass class of bug) should switch to `await platform.subscribe(ws, topic)` and treat a non-null return value as a denial. Apps using `platform.checkSubscribe` similarly must `await` the return.
73
81
 
74
- ### 9. `isValidWireTopic` rejects `"` and `\\`
82
+ ### Cookie `path` / `domain` attribute injection blocked in `serializeCookie`
75
83
 
76
- **What changed.** The wire-accept set now matches `esc()`'s rejection set. Pre-fix, a client could subscribe to topic `"` (passes wire), and any later `platform.publish('"', ...)` crashed because envelope-build threw on those characters.
84
+ **What changed.** Both attributes are now validated against the same character class as cookie values (no CTLs, no `;`, no `,`, no whitespace, no DEL) before concatenation. Non-strings throw the same way as malformed names/values.
77
85
 
78
- **How to migrate.** No action needed for healthy apps. If you have client code that sent literal `"` or `\\` topic names, rename those topics.
86
+ **How to migrate.** Confirm every `cookies.set(name, value, { path, domain })` call passes a valid path / domain. Calls passing user-influenced strings will now throw at the call site instead of silently producing a malformed `Set-Cookie`.
79
87
 
80
- ### 10. SSR dedup cache key includes `base_origin`
88
+ ### `parse_as_bytes` rejects negative and non-finite values
81
89
 
82
- **What changed.** Previously the dedup key was `method + '\0' + url`. In virtual-hosting deployments (one uWS instance behind multiple Host aliases), two concurrent anonymous GETs to `/` from `tenantA.example` and `tenantB.example` shared one SSR call, producing a cross-tenant leak. The key is now `method + '\0' + base_origin + '\0' + url`.
90
+ **What changed.** `BODY_SIZE_LIMIT=-100K` previously resolved to a negative number that read like "no limit" downstream; `BODY_SIZE_LIMIT=Infinity` similarly bypassed every byte-budget check. Both now resolve to NaN, which the existing `if (isNaN(body_size_limit)) throw` guard routes to a clean startup error.
83
91
 
84
- **How to migrate.** No action needed; previously-correct apps see a tighter dedup behavior.
92
+ **How to migrate.** Audit any `BODY_SIZE_LIMIT` env value you set; values must be strictly positive finite (`512K`, `2M`) or `0` (which means unlimited).
85
93
 
86
- ### 11. Cookie `path` / `domain` attribute injection blocked in `serializeCookie`
94
+ ---
87
95
 
88
- **What changed.** Both attributes are now validated against the same character class as cookie values (no CTLs, no `;`, no `,`, no whitespace, no DEL) before concatenation. Non-strings throw the same way as malformed names/values.
96
+ ## Notable defaults and behaviors
89
97
 
90
- **How to migrate.** Confirm every `cookies.set(name, value, { path, domain })` call passes a valid path / domain. Calls passing user-influenced strings will now throw at the call site instead of silently producing a malformed `Set-Cookie`.
98
+ These change observable runtime behavior. Most apps are unaffected; a few will notice.
91
99
 
92
- ### 12. Wire single-subscribe frames consult `subscribeBatch` when only `subscribeBatch` is exported
100
+ ### Default `maxPayloadLength` raised from 16 KB to 1 MB
93
101
 
94
- **What changed.** Previously, an app exporting only `subscribeBatch` for centralized authorization had its hook fire for batch frames but silently bypassed for single subscribes. Single subscribes are now routed through `subscribeBatch` (treated as a 1-element batch) when only `subscribeBatch` is exported.
102
+ **What changed.** The default cap on a single inbound WebSocket frame moved from 16 KB to 1 MB. uWS itself defaults to 16 MB; 16 KB was excessively conservative and forced chunked-upload frameworks to use ~12 KB chunks (~9000 chunks for a 100 MB file after typical 90% headroom). Apps that were chunking large payloads to fit under 16 KB will now accept them in fewer chunks (or in a single frame).
95
103
 
96
- **How to migrate.** A `subscribeBatch` hook authored before this fix may now receive 1-element `topics` arrays where it previously did not see single frames at all. Confirm your hook handles short arrays correctly (any reasonable hook does).
104
+ **How to migrate.** No action needed for most apps. To pin the previous cap, set `websocket.maxPayloadLength: 16 * 1024` in `svelte.config.js`. To pin any other value, set the option to that byte count. DoS protection remains layered: `upgradeAdmission.maxConcurrent` caps connection count, `maxBackpressure` caps per-connection outbound queue size.
97
105
 
98
- ### 13. Initial-mount client subscribes are microtask-batched
106
+ ### `/__ws/auth` POST requires Origin / `x-requested-with` / `Sec-Fetch-Site`
99
107
 
100
- **What changed.** Multiple `subscribe(topic)` calls landing in the same microtask now coalesce into one `{type:'subscribe-batch', topics, ref}` wire frame instead of N individual `{type:'subscribe', topic, ref}` frames. Triggers `subscribeBatch` on the server once instead of `subscribe` N times.
108
+ **What changed.** The authenticate POST endpoint previously accepted any credentialed cross-origin POST. A request must now satisfy at least one of: `x-requested-with: XMLHttpRequest`, `Sec-Fetch-Site: same-origin`, or an `Origin` header matching `allowedOrigins`. The bundled adapter client always stamps `x-requested-with` on its preflight POST, so browser-side flows are unaffected.
101
109
 
102
- **How to migrate.** Update any test that asserts on the exact wire shape of two same-microtask subscribes seeing two `subscribe` frames; use `.find(m => m.type === 'subscribe-batch' && m.topics.includes(...))` instead. No source change in app code.
110
+ **How to migrate.** No action needed for browser apps using the bundled client. For native (non-browser) clients hitting `/__ws/auth` directly, either stamp `x-requested-with: XMLHttpRequest` on the request, or set `websocket.authPathRequireOrigin: false` in `svelte.config.js` to opt out.
111
+
112
+ ### Dynamic compression skipped for credentialed responses (BREACH defense)
113
+
114
+ **What changed.** The dynamic brotli/gzip branch previously fired on every response above 1 KB. Combined with attacker-influenced reflected input alongside a secret in the page body (CSRF token, session ID, API key), the compressed length leaked the secret one byte at a time via the BREACH attack. Requests carrying `Cookie` or `Authorization` now skip dynamic compression. Anonymous responses still compress; build-time precompressed static files are unaffected.
115
+
116
+ **How to migrate.** No action needed; uncompressed credentialed SSR is the safe default. If you have audited every page for BREACH defenses (random per-response masking, prefix randomization, no secrets reflected with attacker input), opt back in via `websocket.compressCredentialedResponses: true`.
117
+
118
+ ### Wire-topic accept set tightened to printable ASCII
103
119
 
104
- ### 14. Client `status` store expanded to a five-state machine
120
+ **What changed.** The wire accept set moved from "anything except control bytes / quote / backslash" to "printable ASCII (0x20-0x7E) except quote / backslash". Pre-fix, hostile clients could subscribe to topics containing line-separator characters, BiDi overrides, byte-order marks, or arbitrary non-ASCII. Server-side `platform.subscribe` and `platform.checkSubscribe` keep their previous looser accept set, so server-side code using non-ASCII topic names is unaffected.
121
+
122
+ **How to migrate.** No action needed unless your app legitimately accepts non-ASCII topic names FROM CLIENTS. If it does, set `websocket.allowNonAsciiTopics: true`.
123
+
124
+ ### `isValidWireTopic` rejects `"` and `\\`
125
+
126
+ **What changed.** The wire-accept set now matches `esc()`'s rejection set. Pre-fix, a client could subscribe to topic `"` (passes wire), and any later `platform.publish('"', ...)` crashed because envelope-build threw on those characters.
127
+
128
+ **How to migrate.** No action needed for healthy apps. If you have client code that sent literal `"` or `\\` topic names, rename those topics.
129
+
130
+ ### Client `status` store expanded to a five-state machine
105
131
 
106
132
  **What changed.** The `'closed'` state was split into `'disconnected'` (transient, will retry), `'failed'` (terminal: auth denied, max retries, or `close()` called), and `'suspended'` (open but tab is backgrounded). `ready()` now resolves on either `'open'` or `'suspended'`.
107
133
 
@@ -112,89 +138,113 @@ If you are upgrading via `npm install svelte-adapter-uws@latest`, the registry w
112
138
  - `$status === 'failed' || $status === 'disconnected'` for "anything not connected"
113
139
  - Read `_permaClosed` directly if the only relevant case was the terminal one.
114
140
 
115
- ### 15. Presence plugin wire format switched to a compact diff protocol
141
+ ### Presence plugin wire format switched to a compact diff protocol
116
142
 
117
143
  **What changed.** The five-event format (`list` / `join` / `updated` / `leave` / `heartbeat`) collapses to two diff-shaped events plus the existing heartbeat: `presence_state` (full snapshot) and `presence_diff` (joins/leaves). Diffs are microtask-batched. Server and client ship in one bundle, so a single-package upgrade is seamless.
118
144
 
119
145
  **How to migrate.** No action needed for users of the bundled `presence()` Svelte store on the client. Hand-rolled clients that consume the wire directly need to switch decoders to handle `presence_state` and `presence_diff` events. Stale browser tabs from a previous deploy will see a blank presence list until refresh.
120
146
 
121
- ### 16. Replay plugin checks subscribe authorization before reading a topic's buffer
122
-
123
- **What changed.** Replay backends now consult `platform.checkSubscribe(ws, topic)` before reading any topic's buffer; topics the wire-subscribe gate would deny emit a `denied` event on `__replay:{topic}` (the client treats this similarly to `truncated`).
124
-
125
- **How to migrate.** No action needed if you use the bundled replay client, which handles the new event. Hand-rolled replay consumers should add a branch for `denied` events.
126
-
127
- ### 17. `parse_as_bytes` rejects negative and non-finite values
128
-
129
- **What changed.** `BODY_SIZE_LIMIT=-100K` previously resolved to a negative number that read like "no limit" downstream; `BODY_SIZE_LIMIT=Infinity` similarly bypassed every byte-budget check. Both now resolve to NaN, which the existing `if (isNaN(body_size_limit)) throw` guard routes to a clean startup error.
147
+ ### Wire single-subscribe frames consult `subscribeBatch` when only `subscribeBatch` is exported
130
148
 
131
- **How to migrate.** Audit any `BODY_SIZE_LIMIT` env value you set; values must be strictly positive finite (`512K`, `2M`) or `0` (which means unlimited).
132
-
133
- ### 18. `parseCookies` returns a null-prototype object
134
-
135
- **What changed.** The returned bag has no prototype chain; a request with a `__proto__=evil` Cookie cannot leak attacker-controlled values through `cookies.toString` / `cookies.constructor` lookups.
149
+ **What changed.** Previously, an app exporting only `subscribeBatch` for centralized authorization had its hook fire for batch frames but silently bypassed for single subscribes. Single subscribes are now routed through `subscribeBatch` (treated as a 1-element batch) when only `subscribeBatch` is exported.
136
150
 
137
- **How to migrate.** No action needed unless your code reads inherited prototype methods off a `parseCookies` result (rare). Iterate keys with `for (const k in bag)` or `Object.keys(bag)` as before.
151
+ **How to migrate.** A `subscribeBatch` hook authored before this fix may now receive 1-element `topics` arrays where it previously did not see single frames at all. Confirm your hook handles short arrays correctly (any reasonable hook does).
138
152
 
139
- ### 19. `x-no-dedup` header is no longer consulted
153
+ ### Initial-mount client subscribes are microtask-batched
140
154
 
141
- **What changed.** Any anonymous client could previously stamp `x-no-dedup: 1` to defeat SSR shared-leader fan-in and amplify server-side render cost. The header is now ignored.
155
+ **What changed.** Multiple `subscribe(topic)` calls landing in the same microtask now coalesce into one `{type:'subscribe-batch', topics, ref}` wire frame instead of N individual `{type:'subscribe', topic, ref}` frames. Triggers `subscribeBatch` on the server once instead of `subscribe` N times.
142
156
 
143
- **How to migrate.** If you used `x-no-dedup: 1` for per-request tracing during debugging, send a `Cookie` or `Authorization` header instead (legitimate authenticated callers always skip dedup).
157
+ **How to migrate.** Update any test that asserts on the exact wire shape of two same-microtask subscribes seeing two `subscribe` frames; use `.find(m => m.type === 'subscribe-batch' && m.topics.includes(...))` instead. No source change in app code.
144
158
 
145
- ### 20. Dev plugin enforces `allowedOrigins` on the WSS upgrade
159
+ ### Dev plugin enforces `allowedOrigins` on the WSS upgrade
146
160
 
147
161
  **What changed.** The dev plugin previously printed a warning that "Dev mode does not enforce allowedOrigins" and accepted every WS upgrade. Dev now runs the same `isOriginAllowed` check production runs.
148
162
 
149
163
  **How to migrate.** No action needed if your dev `allowedOrigins` matches the dev origin you connect from. To accept dev connections from arbitrary origins (legacy local dev scenarios), pass `devSkipOriginCheck: true` to the Vite plugin.
150
164
 
151
- ### 21. Bounded-by-default capacity caps across the adapter and bundled plugins
165
+ ### Bounded-by-default capacity caps across the adapter and bundled plugins
152
166
 
153
167
  **What changed.** Every internal `Map` / `Set` whose growth is driven by client behaviour or topic cardinality now has an explicit upper bound (default 1,000,000) and a documented saturation behaviour. New subscribes past `MAX_SUBSCRIPTIONS_PER_CONNECTION` respond with `subscribe-denied` reason `'RATE_LIMITED'`. New `platform.request()` past the per-connection cap rejects synchronously. Plugins (`ratelimit`, `throttle`, `debounce`, `cursor`, `presence`, `lock`) gain `maxBuckets` / `maxTopics` / `maxConnections` / `maxKeys` options at the same defaults.
154
168
 
155
169
  **How to migrate.** No action needed for healthy apps; defaults are deliberately generous. Apps that approach 1M of any single resource per connection or per topic registry should investigate the leak rather than raise the cap.
156
170
 
157
- ### 22. `queue` plugin `maxSize` default changed from `Infinity` to `1,000,000`
171
+ ### `queue` plugin `maxSize` default changed from `Infinity` to `1,000,000`
158
172
 
159
173
  **What changed.** The `queue` plugin now drops tasks via `onDrop` once 1M waiting tasks accumulate per key.
160
174
 
161
175
  **How to migrate.** Pass `{ maxSize: Infinity }` explicitly to opt back into the previous behaviour. Any real workload reaching 1M waiting tasks per key likely has a leak.
162
176
 
163
- ### 23. `lock.clear()` rejects pending waiters with `LOCK_CLEARED`
177
+ ### `lock.clear()` rejects pending waiters with `LOCK_CLEARED`
164
178
 
165
179
  **What changed.** Pre-fix, `clear()` only cleared the lookup Map and pending callers continued to resolve as the chain unfolded. The new waiter-queue owns the only reference to pending callers, so `clear()` must explicitly reject them.
166
180
 
167
181
  **How to migrate.** If you relied on pending calls completing across a `clear()` in a teardown path, catch `LOCK_CLEARED` and treat it as success.
168
182
 
169
- ### 24. `start()` is now async; init / shutdown lifecycle hooks supported
183
+ ### `lock.withLock` accepts `maxWaitMs` and rejects with `LOCK_TIMEOUT`
170
184
 
171
- **What changed.** `start(host, port)` (production) and `createTestServer()` (test harness) return promises that resolve only after the new `init` hook completes. A throwing `init` rejects the promise. The dev plugin and test harness await `init` before declaring readiness. Two new optional `hooks.ws` exports: `init({ platform })` and `shutdown({ platform })`.
185
+ **What changed.** Third argument to `withLock(key, fn, { maxWaitMs })` now supports bounded-wait. A rejected waiter receives a typed `LOCK_TIMEOUT` error with `.code`, `.key`, and `.maxWaitMs`. The current holder is not interrupted; subsequent waiters are unaffected.
172
186
 
173
- **How to migrate.** No action needed for the default code paths (the adapter's `index.js` already awaits). If you have custom code calling `start()` directly, await it. To use `init` to capture `platform` at boot (replacing the lazy "first connect" pattern), export `async init({ platform })`. Each worker fires its own `init`; layer leader election on top if you need cluster-wide singleton semantics.
187
+ **How to migrate.** No action needed for existing two-argument callers (no behavior change for them). If you adopt `maxWaitMs`, handle `LOCK_TIMEOUT` rejections at the call site.
174
188
 
175
- ### 25. `lock.withLock` accepts `maxWaitMs` and rejects with `LOCK_TIMEOUT`
189
+ ### `start()` is now async; init / shutdown lifecycle hooks supported
176
190
 
177
- **What changed.** Third argument to `withLock(key, fn, { maxWaitMs })` now supports bounded-wait. A rejected waiter receives a typed `LOCK_TIMEOUT` error with `.code`, `.key`, and `.maxWaitMs`. The current holder is not interrupted; subsequent waiters are unaffected.
191
+ **What changed.** `start(host, port)` (production) and `createTestServer()` (test harness) return promises that resolve only after the new `init` hook completes. A throwing `init` rejects the promise. The dev plugin and test harness await `init` before declaring readiness. Two new optional `hooks.ws` exports: `init({ platform })` and `shutdown({ platform })`.
178
192
 
179
- **How to migrate.** No action needed for existing two-argument callers (no behavior change for them). If you adopt `maxWaitMs`, handle `LOCK_TIMEOUT` rejections at the call site.
193
+ **How to migrate.** No action needed for the default code paths (the adapter's `index.js` already awaits). If you have custom code calling `start()` directly, await it. To use `init` to capture `platform` at boot (replacing the lazy "first connect" pattern), export `async init({ platform })`. Each worker fires its own `init`; layer leader election on top if you need cluster-wide singleton semantics.
180
194
 
181
- ### 26. Per-event `coalesceKey` collapses duplicates in `publishBatched`
195
+ ### Per-event `coalesceKey` collapses duplicates in `publishBatched`
182
196
 
183
197
  **What changed.** Per-event `coalesceKey?: string` collapses same-key duplicates before framing in `publishBatched`. Latest value wins at the latest occurrence's position. Capability-gated: clients opt in via a `{type:'hello', caps:['batch']}` frame after open (the bundled client does this automatically). When the fast path does not apply, `publishBatched` falls back to a per-event `publish()` loop.
184
198
 
185
199
  **How to migrate.** Hand-rolled client code consuming the wire directly should send a `hello` frame advertising the `batch` capability if it wants batched frames; otherwise the server falls back to per-event frames as before. Bundled clients work automatically.
186
200
 
187
- ### 27. Per-connection adapter scratch state moved to Symbol-keyed slots
201
+ ### `x-no-dedup` header is no longer consulted
188
202
 
189
- **What changed.** `__subscriptions` and `__coalesced` previously sat directly on user-visible `getUserData()`. They now live under `WS_SUBSCRIPTIONS` and `WS_COALESCED` symbols exported from `files/utils.js`. The user-facing `CloseContext.subscriptions` shape is unchanged.
203
+ **What changed.** Any anonymous client could previously stamp `x-no-dedup: 1` to defeat SSR shared-leader fan-in and amplify server-side render cost. The header is now ignored.
190
204
 
191
- **How to migrate.** If your `upgrade()` hook returned an object containing a `__subscriptions` or `__coalesced` key, those keys will no longer collide with the adapter; you keep your own values. If your code read the adapter's internals via `getUserData().__subscriptions`, switch to `import { WS_SUBSCRIPTIONS } from 'svelte-adapter-uws/files/utils.js'` and read `getUserData()[WS_SUBSCRIPTIONS]`.
205
+ **How to migrate.** If you used `x-no-dedup: 1` for per-request tracing during debugging, send a `Cookie` or `Authorization` header instead (legitimate authenticated callers always skip dedup).
206
+
207
+ ---
208
+
209
+ ## Recommended new patterns
210
+
211
+ Not required. Adopting these gets you the full 0.5 experience.
212
+
213
+ ### Use `init({ platform })` / `shutdown({ platform })` for once-per-worker setup
214
+
215
+ `init` fires once per worker after the listen socket is bound, before any upgrade / open / message hook. The deterministic place to capture `platform` for cron / push registry / metrics / leader election:
192
216
 
193
- ### 28. `platform.subscribe` and `platform.checkSubscribe` are first-class authorization gates
217
+ ```js
218
+ export async function init({ platform }) {
219
+ // captures platform for use anywhere
220
+ }
194
221
 
195
- **What changed.** `platform.subscribe(ws, topic)` routes a server-initiated subscribe through the user's `hooks.ws.subscribe` authorization hook before the actual `ws.subscribe` runs. Returns `null` on success or a denial reason. `platform.checkSubscribe(ws, topic)` is a pure-gate companion (does not modify subscription state). Direct `ws.subscribe()` calls intentionally bypass the hook (the bypass we are guarding against).
222
+ export async function shutdown() {
223
+ // best-effort teardown before the worker exits
224
+ }
225
+ ```
226
+
227
+ Eliminates the boot-to-first-connect window where state captured "on first open" was unavailable to cron ticks or background work. Per-worker in clustered mode; layer leader election if you need cluster-wide singleton semantics. See the [docs site lifecycle page](https://svelte-realtime.dev/docs/lifecycle).
228
+
229
+ ---
230
+
231
+ ## Cosmetic
232
+
233
+ Type-only changes, internal refactors, niche edge cases. No action required for most apps.
234
+
235
+ ### `parseCookies` returns a null-prototype object
236
+
237
+ **What changed.** The returned bag has no prototype chain; a request with a `__proto__=evil` Cookie cannot leak attacker-controlled values through `cookies.toString` / `cookies.constructor` lookups.
238
+
239
+ **How to migrate.** No action needed unless your code reads inherited prototype methods off a `parseCookies` result (rare). Iterate keys with `for (const k in bag)` or `Object.keys(bag)` as before.
240
+
241
+ ### Per-connection adapter scratch state moved to Symbol-keyed slots
242
+
243
+ **What changed.** `__subscriptions` and `__coalesced` previously sat directly on user-visible `getUserData()`. They now live under `WS_SUBSCRIPTIONS` and `WS_COALESCED` symbols exported from `files/utils.js`. The user-facing `CloseContext.subscriptions` shape is unchanged.
244
+
245
+ **How to migrate.** If your `upgrade()` hook returned an object containing a `__subscriptions` or `__coalesced` key, those keys will no longer collide with the adapter; you keep your own values. If your code read the adapter's internals via `getUserData().__subscriptions`, switch to `import { WS_SUBSCRIPTIONS } from 'svelte-adapter-uws/files/utils.js'` and read `getUserData()[WS_SUBSCRIPTIONS]`.
196
246
 
197
- **How to migrate.** Downstream library / framework code that previously called `ws.subscribe()` directly inside an RPC handler (a real authorization-bypass class of bug) should switch to `await platform.subscribe(ws, topic)` and treat a non-null return value as a denial.
247
+ ---
198
248
 
199
249
  ## After upgrading
200
250
 
package/README.md CHANGED
@@ -75,6 +75,17 @@ I've been loving Svelte and SvelteKit for a long time. I always wanted to expand
75
75
 
76
76
  **Getting started**
77
77
 
78
+ ## Version compatibility
79
+
80
+ The three ecosystem packages move together. Bump them as a group:
81
+
82
+ | `svelte-adapter-uws` | `svelte-realtime` | `svelte-adapter-uws-extensions` | Notes |
83
+ |---|---|---|---|
84
+ | `^0.4.x` | `^0.4.x` | `^0.4.x` | Legacy stable |
85
+ | `^0.5.0` | `^0.5.0` | `^0.5.0` | Current. Node 22+ required. See `MIGRATION.md` if upgrading from 0.4. |
86
+
87
+ Mixed-version installs are rejected at install time with a peer-dep warning.
88
+
78
89
  ## Installation
79
90
 
80
91
  ### Starting from scratch
@@ -415,7 +426,7 @@ adapter({
415
426
 
416
427
  These options control how the server handles misbehaving or slow clients at the WebSocket level:
417
428
 
418
- **`maxPayloadLength`** (default: 1 MB) - the maximum size of a single incoming WebSocket message. If a client sends a message larger than this, uWS closes the connection immediately (not just the message - the entire connection is dropped). Set this based on the largest message your application expects to receive. The 1 MB default aligns with `socket.io`'s default and Cloudflare Workers' WebSocket message cap, keeping apps portable to the edge; uWS itself defaults to 16 MB. For a stricter cap, pin an explicit value (e.g. `16 * 1024` for 16 KB).
429
+ **`maxPayloadLength`** (default: 1 MB) - the maximum size of a single incoming WebSocket message. If a client sends a message larger than this, uWS closes the connection immediately (not just the message - the entire connection is dropped). Set this based on the largest message your application expects to receive. uWS itself defaults to 16 MB; this adapter sets 1 MB as a balanced default that handles typical app payloads in a single frame without forcing chunked-upload frameworks into ~12 KB chunks (which the previous 16 KB default did). For a stricter cap, pin an explicit value (e.g. `16 * 1024` for 16 KB).
419
430
 
420
431
  **`maxBackpressure`** (default: 1 MB) - the per-connection outbound send buffer, AND the threshold above which `publish` / `send` / `publishBatched` silently skip a subscriber. When a specific subscriber's buffer is over this size, uWS drops that frame *for that subscriber only* while continuing to deliver to every non-backpressured subscriber. This makes `publish` / `send` / `publishBatched` volatile-by-default for slow consumers (the right behavior for cursor positions, typing indicators, presence pings - see "Volatile / fire-and-forget delivery" below). The `drain` hook fires per-connection when the buffer empties again. Lower this if you want subscribers shed sooner; raise it if you prefer to keep the connection queued and absorb temporary slowness. uWS's own default is 64 KB; this adapter sets 1 MB to favor keeping the connection alive under pub/sub spikes.
421
432
 
package/client.js CHANGED
@@ -36,7 +36,8 @@ export function connect(options = {}) {
36
36
  console.warn(
37
37
  '[ws] connect() was called with options, but the connection already exists ' +
38
38
  '(created automatically by on(), status, or ready()). ' +
39
- 'Your options are ignored. Call connect() before using other client functions.'
39
+ 'Your options are ignored. Call connect() before using other client functions.\n' +
40
+ ' See: https://svti.me/client-connect'
40
41
  );
41
42
  }
42
43
  return ensureConnection(options, true);
@@ -1088,7 +1089,7 @@ function createConnection(options) {
1088
1089
  return;
1089
1090
  }
1090
1091
  if (msg.type === 'subscribe-denied' && typeof msg.topic === 'string' && typeof msg.reason === 'string') {
1091
- console.warn('[ws] subscribe denied topic=%s reason=%s', msg.topic, msg.reason);
1092
+ console.warn('[ws] subscribe denied topic=%s reason=%s\n See: https://svti.me/subscribe-denied', msg.topic, msg.reason);
1092
1093
  denialsStore.set({ topic: msg.topic, reason: msg.reason, ref: msg.ref });
1093
1094
  return;
1094
1095
  }
@@ -1423,7 +1424,7 @@ function createConnection(options) {
1423
1424
  if (debug) console.log('[ws] send ->', data);
1424
1425
  ws.send(serializeForSend(data));
1425
1426
  } else if (debug) {
1426
- console.warn('[ws] send dropped (not connected) - use sendQueued() to queue messages for reconnect:', data);
1427
+ console.warn('[ws] send dropped (not connected) - use sendQueued() to queue messages for reconnect:', data, '\n See: https://svti.me/send-dropped');
1427
1428
  }
1428
1429
  }
1429
1430
 
@@ -1446,7 +1447,7 @@ function createConnection(options) {
1446
1447
  ws.send(serialized);
1447
1448
  } else {
1448
1449
  if (sendQueue.length >= MAX_QUEUE_SIZE) {
1449
- console.warn('[ws] queue full (' + MAX_QUEUE_SIZE + '), dropping oldest message');
1450
+ console.warn('[ws] queue full (' + MAX_QUEUE_SIZE + '), dropping oldest message\n See: https://svti.me/client-queue');
1450
1451
  sendQueue.shift();
1451
1452
  }
1452
1453
  if (debug) console.log('[ws] queued ->', data);
package/files/handler.js CHANGED
@@ -327,7 +327,8 @@ if (!origin && !host_header && !protocol_header && !is_tls) {
327
327
  'For production, either:\n' +
328
328
  ' SSL_CERT + SSL_KEY for native TLS (no proxy needed)\n' +
329
329
  ' ORIGIN=https://example.com (behind a TLS proxy)\n' +
330
- ' PROTOCOL_HEADER=x-forwarded-proto + HOST_HEADER=x-forwarded-host (flexible proxy)'
330
+ ' PROTOCOL_HEADER=x-forwarded-proto + HOST_HEADER=x-forwarded-host (flexible proxy)\n' +
331
+ ' See: https://svti.me/adapter-origin'
331
332
  );
332
333
  }
333
334
 
@@ -527,7 +528,8 @@ function maybeWarnTopicRegistry() {
527
528
  ' distinct topics. Each entry persists for the process lifetime ' +
528
529
  '(required by the resume protocol). Reduce topic cardinality or ' +
529
530
  'opt out of seq stamping for high-cardinality publishes via ' +
530
- '{ seq: false }. Top recent publishers: ' + JSON.stringify(top)
531
+ '{ seq: false }. Top recent publishers: ' + JSON.stringify(top) +
532
+ '\n See: https://svti.me/topic-cardinality'
531
533
  );
532
534
  }
533
535
 
@@ -602,7 +604,8 @@ function warnLargeBatchFrame(size) {
602
604
  lastBatchOversizeWarnAt = now;
603
605
  console.warn('[ws] publishBatched frame is ' + size + ' bytes (>' + BATCH_FRAME_WARN_BYTES +
604
606
  '). Large frames may trip per-message-deflate and surprise CPU budgets. ' +
605
- 'Consider chunking the batch into multiple publishBatched calls.');
607
+ 'Consider chunking the batch into multiple publishBatched calls.' +
608
+ '\n See: https://svti.me/publish-batched');
606
609
  }
607
610
 
608
611
  /** @type {ReturnType<typeof setInterval> | null} */
@@ -706,7 +709,7 @@ function samplePressure(thresholds) {
706
709
  }
707
710
  lastPublishWarnAt.set(e.topic, now);
708
711
  console.warn(
709
- '[ws] runaway publisher topic=%s msg/s=%d bytes/s=%d',
712
+ '[ws] runaway publisher topic=%s msg/s=%d bytes/s=%d\n See: https://svti.me/pressure',
710
713
  e.topic, Math.round(e.messagesPerSec), Math.round(e.bytesPerSec)
711
714
  );
712
715
  }
@@ -1047,7 +1050,8 @@ const platform = {
1047
1050
  '[ws] platform.sendTo filter returned a Promise; treating as fail-closed.\n' +
1048
1051
  ' Async filters cannot be used here because sendTo iterates every active\n' +
1049
1052
  ' connection synchronously. Resolve the relevant fields into userData from\n' +
1050
- ' your `upgrade` hook so the filter can read them synchronously.'
1053
+ ' your `upgrade` hook so the filter can read them synchronously.\n' +
1054
+ ' See: https://svti.me/sendto-async'
1051
1055
  );
1052
1056
  }
1053
1057
  continue;
@@ -2488,7 +2492,8 @@ if (WS_ENABLED) {
2488
2492
  if (!knownWsExports.has(name)) {
2489
2493
  console.warn(
2490
2494
  `Warning: WebSocket handler exports unknown "${name}". ` +
2491
- `Did you mean one of: ${[...knownWsExports].join(', ')}?`
2495
+ `Did you mean one of: ${[...knownWsExports].join(', ')}?\n` +
2496
+ ' See: https://svti.me/ws-hooks'
2492
2497
  );
2493
2498
  }
2494
2499
  }
@@ -2510,7 +2515,8 @@ if (WS_ENABLED) {
2510
2515
  'Cloudflare Tunnel and some other edge proxies (WebSocket opens, then ' +
2511
2516
  'closes with 1006 TCP FIN). Migrate to the `authenticate` hook to ' +
2512
2517
  'refresh session cookies over a normal HTTP response: ' +
2513
- 'export function authenticate({ cookies }) { cookies.set(...); }'
2518
+ 'export function authenticate({ cookies }) { cookies.set(...); }\n' +
2519
+ ' See: https://svti.me/cf-cookies'
2514
2520
  );
2515
2521
  return;
2516
2522
  }
@@ -2961,7 +2967,8 @@ if (WS_ENABLED) {
2961
2967
  console.warn(
2962
2968
  '[ws] userData key "' + key + '" may contain sensitive data. ' +
2963
2969
  'userData is accessible to all server-side handlers via ws.getUserData(). ' +
2964
- 'Store sensitive data outside userData and reference it by a non-sensitive ID.'
2970
+ 'Store sensitive data outside userData and reference it by a non-sensitive ID.\n' +
2971
+ ' See: https://svti.me/userdata-sensitive'
2965
2972
  );
2966
2973
  }
2967
2974
  }
package/files/index.js CHANGED
@@ -45,7 +45,8 @@ if (is_primary) {
45
45
  if (cluster_mode === 'reuseport' && process.platform !== 'linux') {
46
46
  console.error(
47
47
  `CLUSTER_MODE=reuseport requires Linux (SO_REUSEPORT is not reliable on ${process.platform}). ` +
48
- 'Remove CLUSTER_MODE to use the default acceptor mode.'
48
+ 'Remove CLUSTER_MODE to use the default acceptor mode.\n' +
49
+ ' See: https://svti.me/cluster-mode'
49
50
  );
50
51
  process.exit(1);
51
52
  }
@@ -204,7 +205,7 @@ if (is_primary) {
204
205
  }
205
206
  restart_attempts++;
206
207
  if (restart_attempts > RESTART_MAX_ATTEMPTS) {
207
- console.error(`Worker restart limit reached (${RESTART_MAX_ATTEMPTS}). Exiting.`);
208
+ console.error(`Worker restart limit reached (${RESTART_MAX_ATTEMPTS}). Exiting.\n See: https://svti.me/worker-restart-limit`);
208
209
  process.exit(1);
209
210
  }
210
211
  restart_delay = restart_delay ? Math.min(restart_delay * 2, RESTART_DELAY_MAX) : 100;
package/index.d.ts CHANGED
@@ -143,9 +143,9 @@ export interface WebSocketOptions {
143
143
 
144
144
  /**
145
145
  * Max message size in bytes. Connections sending larger messages are closed.
146
- * The default aligns with `socket.io` and Cloudflare Workers' WebSocket
147
- * message cap; uWS itself defaults to 16 MB. Lower this for stricter caps
148
- * (e.g. `16 * 1024` for 16 KB) when payload-size discipline matters.
146
+ * Default 1 MB is balanced for typical app payloads in a single frame; uWS
147
+ * itself defaults to 16 MB. Lower this for stricter caps (e.g. `16 * 1024`
148
+ * for 16 KB) when payload-size discipline matters.
149
149
  * @default 1048576 (1 MB)
150
150
  */
151
151
  maxPayloadLength?: number;
package/index.js CHANGED
@@ -282,11 +282,12 @@ export default function (opts = {}) {
282
282
  );
283
283
  }
284
284
  const wsOpts = {
285
- // Default raised from 16 KB to 1 MB in next.19. Aligns with
286
- // socket.io's default and Cloudflare Workers' WS message
287
- // cap, both 1 MB. uWS itself defaults to 16 MB; 16 KB was
288
- // excessively conservative and forced chunked-upload
289
- // frameworks to use ~12 KB chunks. DoS exposure is bounded
285
+ // Default raised from 16 KB to 1 MB in next.19. uWS itself
286
+ // defaults to 16 MB; 16 KB was excessively conservative and
287
+ // forced chunked-upload frameworks to use ~12 KB chunks
288
+ // (~9000 chunks for a 100 MB file). 1 MB handles typical app
289
+ // payloads in a single frame without per-app tuning. DoS
290
+ // exposure is bounded
290
291
  // by `upgradeAdmission.maxConcurrent` (connection count)
291
292
  // and `maxBackpressure` (per-conn outbound queue, also
292
293
  // 1 MB), so per-frame cost stays predictable. Apps that
@@ -328,7 +329,7 @@ export default function (opts = {}) {
328
329
  // Defends against an adjacent process injecting forged
329
330
  // messages into the worker_threads relay (typically
330
331
  // reachable only post-compromise, hence opt-in).
331
- workerRelayHmacSecret: websocket?.workerRelayHmacSecret
332
+ workerRelayHmacSecret: websocket?.workerRelayHmacSecret,
332
333
  // CSRF defense for the `/__ws/auth` POST endpoint. By
333
334
  // default, the request must carry one of:
334
335
  // - `x-requested-with: XMLHttpRequest`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-adapter-uws",
3
- "version": "0.5.0-next.21",
3
+ "version": "0.5.0-next.22",
4
4
  "publishConfig": {
5
5
  "tag": "next"
6
6
  },
@@ -133,7 +133,7 @@
133
133
  "node": ">=22.0.0"
134
134
  },
135
135
  "peerDependencies": {
136
- "@sveltejs/kit": "^2.59.0",
136
+ "@sveltejs/kit": "^2.0.0",
137
137
  "svelte": "^4.0.0 || ^5.0.0",
138
138
  "ws": "^8.0.0"
139
139
  },
package/testing.js CHANGED
@@ -232,7 +232,8 @@ export async function createTestServer(options = {}) {
232
232
  console.error(
233
233
  '[adapter-uws/testing] platform.sendTo filter returned a Promise; treating as fail-closed.\n' +
234
234
  ' Resolve filter inputs into userData from your `upgrade` hook so the\n' +
235
- ' filter can read them synchronously.'
235
+ ' filter can read them synchronously.\n' +
236
+ ' See: https://svti.me/sendto-async'
236
237
  );
237
238
  }
238
239
  continue;
package/vite.js CHANGED
@@ -240,7 +240,8 @@ export default function uws(options = {}) {
240
240
  console.error(
241
241
  '[adapter-uws] platform.sendTo filter returned a Promise; treating as fail-closed.\n' +
242
242
  ' Resolve filter inputs into userData from your `upgrade` hook so the\n' +
243
- ' filter can read them synchronously.'
243
+ ' filter can read them synchronously.\n' +
244
+ ' See: https://svti.me/sendto-async'
244
245
  );
245
246
  }
246
247
  continue;
@@ -692,7 +693,7 @@ export default function uws(options = {}) {
692
693
  applyHandlers(mod);
693
694
  }).catch((err) => {
694
695
  handlerFailed = true;
695
- console.error(`[adapter-uws] Failed to load WebSocket handler '${options.handler}':`, err);
696
+ console.error(`[adapter-uws] Failed to load WebSocket handler '${options.handler}':`, err, '\n See: https://svti.me/ws-handler-load');
696
697
  });
697
698
  } else {
698
699
  // Auto-discover src/hooks.ws.{js,ts,mjs}
@@ -928,10 +929,11 @@ export default function uws(options = {}) {
928
929
  '[adapter-uws] upgradeResponse() attaches Set-Cookie to the 101 response. ' +
929
930
  'This fails silently behind Cloudflare Tunnel and some other strict edge proxies ' +
930
931
  '(WebSocket opens, then closes with 1006). Use the `authenticate` hook to ' +
931
- 'refresh session cookies over a normal HTTP response.'
932
+ 'refresh session cookies over a normal HTTP response.\n' +
933
+ ' See: https://svti.me/cf-cookies'
932
934
  );
933
935
  } else {
934
- console.warn('[adapter-uws] upgrade() returned response headers. These are only applied in production (uWS); the ws library used in dev does not support custom 101 headers.');
936
+ console.warn('[adapter-uws] upgrade() returned response headers. These are only applied in production (uWS); the ws library used in dev does not support custom 101 headers.\n See: https://svti.me/dev-101-headers');
935
937
  }
936
938
  }
937
939
  } else {