svelte-realtime 0.5.0-next.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/MIGRATION.md ADDED
@@ -0,0 +1,462 @@
1
+ # Migration guide: svelte-realtime 0.4.x to 0.5.x
2
+
3
+ This guide is organized by **tier**. Most apps only need to read the first two sections.
4
+
5
+ - **[Critical](#critical-read-first)** - runtime behavior changed; audit your code or production may break silently.
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, deprecations, dead code removed.
10
+
11
+ `svelte-realtime` 0.5 raises its peerDep on `svelte-adapter-uws` to `^0.5.0`. See the adapter's MIGRATION.md for breaking changes on that side.
12
+
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).
14
+
15
+ ---
16
+
17
+ ## Critical (read first)
18
+
19
+ These four changes close real security bugs that were sitting in idiomatic 0.4 code paths. Audit your code for the patterns described; production deploys may surface new `FORBIDDEN` / `UNAUTHENTICATED` errors and that is the framework now correctly doing its job.
20
+
21
+ ### Async `access` / `filter` predicates and `live.gate` predicates no longer fail open
22
+
23
+ **What changed.** Pre-0.5, the wire-RPC stream path read `if (!streamFilter(ctx, ...))` and the SSR mirror read `if (!predicate(ctx, ...))` synchronously. An async predicate returns a Promise, which is truthy, which made the deny branch unreachable: async-deny became async-allow. Every stream guarded by `access: async (ctx) => ...` (the idiomatic shape for predicates that consult a DB / session store) silently bypassed the developer's intended gate.
24
+
25
+ The fix awaits the predicate before the truthiness check. The matching adapter-side fix in `svelte-adapter-uws@^0.5.0` makes `subscribe` / `subscribeBatch` async-safe and `platform.checkSubscribe` returns `Promise<string|null>`.
26
+
27
+ `live.access.any(...)` and `live.access.all(...)` had the same bug at the composition layer: both used `Array.prototype.some` / `every`, which read a `Promise<false>` as truthy and either short-circuited to allow (`any`) or fell through to allow (`all`). Apps composing async sub-predicates inside `any` / `all` silently bypassed every gate. The helpers now return `Promise<boolean>` and `await` each sub-predicate in order, preserving correct short-circuit semantics. The leaf helpers (`live.access.org`, `live.access.user`, `live.access.role`, `live.access.owner`, `live.access.team`) remain sync.
28
+
29
+ **How to migrate.**
30
+
31
+ - Upgrade `svelte-adapter-uws` to `^0.5.0` (already required by 0.5).
32
+ - Audit every async `access` / `filter` predicate, every async `live.gate(...)`, every async `subscribe` / `subscribeBatch` hook, and every `live.access.any(...)` / `live.access.all(...)` whose sub-predicates were async. Predicates that were silently letting requests through will now correctly deny. Expect to see new `FORBIDDEN` / `UNAUTHENTICATED` errors land on streams that were previously open.
33
+ - Sync predicates and sync `checkSubscribe` returns are unaffected (await unwraps non-Promise values transparently).
34
+ - Callers that invoked the result of `live.access.any(...)` or `live.access.all(...)` manually (rare; the typical pattern is to pass the returned predicate to `live.stream({ access })` where the runtime awaits it) must `await` the result.
35
+
36
+ ### `live.idempotent` cache key is now namespaced by RPC path
37
+
38
+ **In-flight cache entries become invisible after deploy.**
39
+
40
+ **What changed.** Pre-0.5, the wrapper used the raw client-supplied `idempotencyKey` as the cache slot regardless of which RPC path was registered with it. A privileged `privateRpc.with({ idempotencyKey: 'abc' })` could be replayed by a public `publicRpc.with({ idempotencyKey: 'abc' })` and read the cached private result without invoking the public handler. The cache key sent to `store.acquire(...)` is now `'rpc:' + path + ':' + userKey`.
41
+
42
+ **This is NOT backward compatible.** In-flight cache entries from before this release become invisible after deploy: the namespaced key does not match the old un-namespaced key.
43
+
44
+ **How to migrate.**
45
+
46
+ - For Redis-backed idempotency stores: schedule the upgrade for a low-traffic window or accept that any in-flight retry that lands on the new build re-runs the handler. Old keys eventually TTL out (default 48 hours).
47
+ - For in-memory stores: entries clear on process restart, no action needed.
48
+ - Custom `keyFrom` callbacks: still encode tenant scope explicitly. The framework cannot guess the app's tenant shape; the new namespacing only closes the cross-RPC class.
49
+ - New cap: `idempotencyKey` longer than 256 characters now throws `LiveError('INVALID_REQUEST', ...)`. Pre-fix, stores accepted attacker-supplied 200KB keys.
50
+
51
+ ### `ctx.publish('__*', ...)` now throws `LiveError('INVALID_TOPIC')`
52
+
53
+ **What changed.** Pre-fix, app code could call `ctx.publish('__signal:victim', ...)` or `ctx.publish('__rpc', { id: 'guess', ok: true, data })` to spoof framework-internal frames. Combined with the wire-side `__`-subscribe block, only the framework should publish system channels. `ctx.publish()` now throws `LiveError('INVALID_TOPIC', ...)` when the topic begins with `__`.
54
+
55
+ **How to migrate.**
56
+
57
+ - Server-side `live.signal()` and the plugin-side broadcasts (`__presence:*`, `__group:*`, `__replay:*`) are unaffected - they go through `platform.publish()`, not `ctx.publish()`.
58
+ - Apps that genuinely need to broadcast on a `__`-prefixed topic should reach for `platform.publish(...)` directly so the intent is explicit at the call site.
59
+ - Otherwise, rename the topic so it does not start with `__`.
60
+
61
+ ### Stream-RPC subscribes consult the adapter's wire-level subscribe gate
62
+
63
+ **What changed.** Pre-0.5, a `live.stream`/`live.room` subscribe ran the loader, delivered its initial data, and (for rooms) published the presence `'join'` event BEFORE the adapter's `subscribe` / `subscribeBatch` hook chain ever fired. Apps gating private rooms via `subscribeBatch` (rather than via `live.stream({ access })` / `live.room({ guard })`) saw their loader output reach denied users. `_executeStreamRpc` now calls `platform.checkSubscribe?.(ws, topic)` after `__streamFilter` and admission checks but before `ws.subscribe(topic)` / `__onSubscribe` / loader.
64
+
65
+ **How to migrate.**
66
+
67
+ - Upgrade `svelte-adapter-uws` to `^0.5.0`.
68
+ - If your `subscribe` / `subscribeBatch` hook denies access, expect denials to land on the client's `stream.error` store as `RpcError` with the canonical denial code (`UNAUTHENTICATED` / `FORBIDDEN` / `INVALID_TOPIC` / `RATE_LIMITED`, or whatever string your hook returns). Loader output and presence joins for denied users no longer leak.
69
+ - If you were relying on the leak (you should not be), gate via `live.stream({ access })` or `live.room({ guard })` instead so the deny path runs synchronously at the realtime layer.
70
+
71
+ ---
72
+
73
+ ## Required source changes
74
+
75
+ These won't run cleanly until you make the change.
76
+
77
+ ### Runtime: Node.js 22+ required (was Node 20+)
78
+
79
+ **What changed.** `package.json#engines.node` moved from `>=20.0.0` to `>=22.0.0`. Tracks the adapter's bump, which in turn tracks `uWebSockets.js` v20.67.0 dropping Node 20 support upstream.
80
+
81
+ **How to migrate.** See `svelte-adapter-uws/MIGRATION.md` for the full uWS-side rationale. No realtime-specific action beyond bumping your runtime to Node 22+.
82
+
83
+ ### Bump `svelte-adapter-uws` peerDep
84
+
85
+ **What changed.** The peer-dep floor moved from `^0.4.0` to `^0.5.0`. Most apps will not work with an adapter older than 0.5 after upgrading - new primitives consumed by this package (`platform.checkSubscribe`, `platform.request` / `onRequest`, `platform.publishBatched`, `init` / `shutdown` lifecycle, `platform.maxPayloadLength`, `platform.bufferedAmount`, async-safe subscribe gates) are version-gated.
86
+
87
+ **How to migrate.** Bump both packages together:
88
+
89
+ ```diff
90
+ - "svelte-adapter-uws": "^0.4.x"
91
+ - "svelte-realtime": "^0.4.x"
92
+ + "svelte-adapter-uws": "^0.5.0"
93
+ + "svelte-realtime": "^0.5.0"
94
+ ```
95
+
96
+ Read the adapter's MIGRATION.md for adapter-side changes (subscribe gate semantics, `init`/`shutdown` hook contract, `maxPayloadLength` default raise from 16KB to 1MB).
97
+
98
+ ### `hooks.ws.js` must export `unsubscribe` (and `close` signature changed)
99
+
100
+ **What changed.** The 0.4.0 release introduced an `unsubscribe` hook that fires in real time when a client drops a topic. The `close` hook signature changed from `({ platform })` to `({ platform, subscriptions })` and only fires `onUnsubscribe` for topics still active at disconnect time.
101
+
102
+ **How to migrate.** Re-export all three hooks from `hooks.ws.js`:
103
+
104
+ ```diff
105
+ - export { message, close } from 'svelte-realtime/server';
106
+ + export { message, close, unsubscribe } from 'svelte-realtime/server';
107
+ ```
108
+
109
+ Existing destructures of `{ platform }` keep working; `subscriptions` is additive.
110
+
111
+ ### `live.room()` actions require `topicArgs`
112
+
113
+ **What changed (0.4.0).** `live.room()` with actions previously inferred the argument count from `topicFn.length`. The 0.4.0 release made `topicArgs` explicit.
114
+
115
+ **How to migrate.**
116
+
117
+ ```diff
118
+ export const board = live.room(
119
+ (ctx, boardId) => `board:${boardId}`,
120
+ + { topicArgs: 1 },
121
+ {
122
+ init: async (ctx, boardId) => loadBoard(boardId),
123
+ actions: { addCard: live(...) }
124
+ }
125
+ );
126
+ ```
127
+
128
+ Rooms without `actions` are unaffected.
129
+
130
+ ### `live.room()` `onLeave` and `onJoin` semantics
131
+
132
+ **What changed (0.4.0).** `onLeave` callback signature changed from `(ctx)` to `(ctx, topic)`. `onJoin` now runs after `initFn` succeeds; if `initFn` throws, `onJoin` is not called.
133
+
134
+ **How to migrate.**
135
+
136
+ ```diff
137
+ - onLeave: (ctx) => { /* ... */ }
138
+ + onLeave: (ctx, topic) => { /* ... */ }
139
+ ```
140
+
141
+ If you relied on `onJoin` running before `initFn`, restructure: side effects that should fire regardless go in `init`'s try/catch; side effects that should only fire on a successful init stay in `onJoin`.
142
+
143
+ ### `live.validated()` rejects unrecognized schema types
144
+
145
+ **What changed (0.4.0).** Previously passed input through with a dev warning. Now returns an error. Standard Schema (Zod, ArkType, Valibot v1+) is the canonical surface as of 0.4.19; legacy `.safeParse` / `._run` paths remain.
146
+
147
+ **How to migrate.** Ensure every schema is Zod, Valibot, ArkType, or another Standard Schema-compatible validator. Async schemas are rejected with a clear error.
148
+
149
+ ### Reserved topic prefix `__` rejected at definition time
150
+
151
+ **What changed (0.4.0).** `live.stream()` and `live.channel()` now reject topics with the `__` prefix at definition time. Async topic functions are also rejected at definition time. Binary RPC headers >65535 bytes return `PAYLOAD_TOO_LARGE`.
152
+
153
+ **How to migrate.** Rename any `__`-prefixed topic in your stream/channel definitions. Use `platform.publish(...)` directly if you have a legitimate framework-level use case (which is rare and typically wrong).
154
+
155
+ ---
156
+
157
+ ## Notable defaults and behaviors
158
+
159
+ These change observable runtime behavior. Most apps are unaffected; a few will notice.
160
+
161
+ ### Stream store errors no longer replace the data value (`.error` is a separate Readable)
162
+
163
+ **What changed.** This change shipped in 0.4.21 and remains the public contract for 0.5. Pre-0.4.21, connection failures, timeouts, and rejected fetches set the store value to `{ error: RpcError }`, replacing whatever data was there. Patterns like `($store ?? []).filter(...)` crashed with `TypeError` because the error object is truthy but not an array.
164
+
165
+ The store value now always holds your data type (or `undefined` while loading). Errors are surfaced on a separate `.error` `Readable<RpcError | null>`, and `.status` is a `Readable<'loading' | 'connected' | 'reconnecting' | 'error'>`.
166
+
167
+ **How to migrate.**
168
+
169
+ ```diff
170
+ - {#if $messages?.error}
171
+ - <p>{$messages.error.message}</p>
172
+ + const err = messages.error;
173
+ + {#if $err}
174
+ + <p>{$err.message}</p>
175
+ ```
176
+
177
+ Code that uses `$store === undefined` for loading and otherwise treats the value as data needs no changes.
178
+
179
+ ### Auto-replay routing for `live.stream({ replay: true })` -- bespoke `wrapWithReplay` proxies must opt out via `WRAPPED_FOR_REPLAY` or be dropped
180
+
181
+ **What changed.** Pre-fix, the user was responsible for wrapping the platform with a `wrapWithReplay` proxy at every seam (`createMessage({ platform: wrapWithReplay })` AND `setCronPlatform(wrapWithReplay(platform))`). The docs showed the wrap only on the RPC seam, so cron-published events to a `replay: true` topic silently bypassed the buffer; reconnecting clients never saw missed cron ticks even when the documented three-tier reconnect (`replay -> delta.fromSeq -> rehydrate`) was correctly declared on the stream.
182
+
183
+ The fix moves replay routing into the framework. `live.stream(topic, loader, { replay: true })` registers the topic at declaration time (or at first-subscribe time for dynamic topic factories). When `platform.replay` is exposed by the adapter, the framework auto-routes every publish to a registered topic through `platform.replay.publish(...)` -- regardless of which seam the publisher sits on. Cron auto-publish, `ctx.publish` from RPC handlers, `ctx.publish` from cron handlers, all flow through the same routing helper. The buffer is populated end-to-end with no user wiring beyond `replay: true`.
184
+
185
+ **This is observable for two user shapes:**
186
+
187
+ 1. **Apps without a custom `wrapWithReplay` proxy:** the framework now Just Works. Cron-published events to replay-eligible topics get buffered automatically; reconnecting clients see missed events on resume. No action required; this fixes the silent bug.
188
+
189
+ 2. **Apps with a custom `wrapWithReplay` proxy:** the framework's auto-routing runs alongside the user proxy's routing and DOUBLE-WRITES to Redis. To preserve old behavior, mark the proxy with `[WRAPPED_FOR_REPLAY] = true` so the framework defers entirely:
190
+
191
+ ```js
192
+ import { WRAPPED_FOR_REPLAY } from 'svelte-realtime/server';
193
+
194
+ function wrapWithReplay(p) {
195
+ const wrapped = new Proxy(p, { /* ...your intercepts... */ });
196
+ wrapped[WRAPPED_FOR_REPLAY] = true; // explicit opt-out
197
+ return wrapped;
198
+ }
199
+ ```
200
+
201
+ Or drop the wrap entirely (recommended -- the framework now owns the same job). Most bespoke `wrapWithReplay` proxies were doing exactly what the framework now does built-in: matching topics against a regex and routing to `replay.publish`. The framework's registry-from-declaration approach is more precise (topics are sourced from `live.stream({ replay: true })`, not regex patterns) and removes the asymmetry between seams.
202
+
203
+ **Dev-mode misconfiguration warning.** If `replay: true` is declared on a stream but `platform.replay` is undefined when the first publish to that topic happens, the framework logs a one-time `console.warn` per topic with the install pointer for the replay extension. Catches the "I declared `replay: true` but never installed the extension" footgun loudly. Production runs silently.
204
+
205
+ **How to migrate.**
206
+
207
+ - Most apps: do nothing. Cron + RPC + derived publishes to `replay: true` topics now reach the buffer automatically.
208
+ - Apps with a custom `wrapWithReplay` proxy: add `[WRAPPED_FOR_REPLAY] = true` to keep the proxy authoritative, OR drop the proxy and let the framework own routing.
209
+ - Apps that wired cron-side replay separately via `setCronPlatform(wrapWithReplay(platform))`: drop the cron-side wrap. The framework now routes cron auto-publishes through replay automatically. Bus wrapping still needs `configureCron({ bus })` for cluster fan-out (orthogonal concern).
210
+
211
+ ### `live.upload` aggregate pre-handler buffer cap raised; chunk-0 frames may be rejected with `OVERLOADED`
212
+
213
+ **What changed.** Pre-fix, every concurrent upload stream got its own 16 MB pre-handler-resolution buffer with no aggregate cap. N concurrent connections opening streamId 0 with a 16 MB chunk-0 payload each could allocate `16 * N` MB of worker memory before any handler-side cap could fire. The default aggregate cap is now 64 MB across all in-flight pending uploads; chunk-0 frames that would exceed are rejected with `OVERLOADED` and the streamId is never registered.
214
+
215
+ **How to migrate.** No action required for normal traffic. If you orchestrate many concurrent uploads (batch import, parallel media transcode), surface the `OVERLOADED` code to the client and either retry with backoff or queue uploads. The cap is tunable via `_setCapsForTest({ uploadPendingMaxAggregate: bytes })` for tests; a runtime option is a follow-up.
216
+
217
+ ### Global middleware `next()` is single-call-guarded
218
+
219
+ **What changed.** Pre-fix, a buggy middleware written as `next().then(() => next())` re-entered the chain and executed the downstream handler twice. Side-effecting handlers (charge customer, send email, increment counter) silently doubled. Each middleware frame in `_runWithMiddleware` now creates its own one-shot `next()`. The second call throws `Error('middleware: next() called more than once. ...')`.
220
+
221
+ **How to migrate.** If your middleware was buggy (calling `next()` twice), the throw lands inside your middleware function (typically caught by the surrounding RPC error handler). Fix the bug; the call now fails loud rather than silently doubling.
222
+
223
+ ### `live.push` rejects with structured `LiveError` codes (not message-substring sniffing)
224
+
225
+ **What changed.** Pre-fix, callers had to sniff `err.message.includes('timed out')` to distinguish deadline expiry from other failures. Deadline expiry from the adapter primitive (`Error('request timed out')`) is now translated to `LiveError('TIMEOUT', ...)`; argument-validation throws lift from plain `Error` to `LiveError('VALIDATION', ...)`. Message text is preserved verbatim on `.message` (so existing substring checks keep working) and the original error rides on `.cause`.
226
+
227
+ The full `live.push` failure surface now discriminates via `err.code`: `VALIDATION` / `NOT_FOUND` / `TIMEOUT` / `CONNECTION_CLOSED` / caller-defined.
228
+
229
+ **How to migrate.** Replace `err.message.includes('timed out')` with `err.code === 'TIMEOUT'`. The substring check still works during the transition.
230
+
231
+ ### `configureCron` accepts a partial config (validation message changed)
232
+
233
+ **What changed.** Previous rule was "leader is required". Now: at least one of `leader` or `bus` must be present. The validation error message changed from `"config must include a leader field"` to `"config must include at least one of leader or bus"`.
234
+
235
+ **How to migrate.** No action required for existing `{ leader: ... }` call sites. If you parse the error message in tests, update the substring.
236
+
237
+ ### Vite plugin classifies single-arity `(ctx) => topic` as static streams
238
+
239
+ **What changed.** `_isDynamicExport` previously returned true for any function-form first argument regardless of arity, so `live.stream((ctx) => 'events:' + ctx.user.id, ...)` produced a factory-shaped client stub. The stub is now arity-aware: 0-param topic-fns and 1-param ctx-only topic-fns (named `ctx` / `context` / `_ctx`, or typed as `Ctx` / `Context` / `RequestContext` / `ServerContext` / `LiveContext`) are static; everything else stays dynamic.
240
+
241
+ **How to migrate.** If your client code has `myStream(ctx.user.id).subscribe(...)` for what is now correctly classified as static, drop the call:
242
+
243
+ ```diff
244
+ - $: items = $myStream(orgId);
245
+ + $: items = $myStream;
246
+ ```
247
+
248
+ Existing 1-arg-non-ctx and 2+arg topic-fns are unaffected.
249
+
250
+ ### Stream reconnect backoff timing changed
251
+
252
+ **What changed (0.4.0).** Old: fixed 50-200ms random delay. New: first two attempts use 20-100ms, then exponential backoff up to 5 minutes with jitter. Observable timing difference, not an API change.
253
+
254
+ **How to migrate.** If you have tests asserting reconnect timings, update them. Production code is unaffected.
255
+
256
+ ### `live.upload` chunk pacing uses `conn.bufferedAmount`
257
+
258
+ **What changed.** The client pump now checks `conn.bufferedAmount` after every chunk and pauses sends when the WS send queue exceeds a high-water mark, resuming when it drops below a low-water mark. Defaults: 4MB high-water, 1MB low-water, 50ms drain-poll interval. Configurable via `configure({ upload: { highWaterMark, lowWaterMark } })`.
259
+
260
+ **How to migrate.** No code change required. Apps that explicitly relied on unbounded queue growth (you should not be) will see paced sends.
261
+
262
+ ### `MAX_PRESENCE_REF` is now an exported tunable cap (default 1,000,000)
263
+
264
+ **What changed.** The in-memory presence-ref map (`_presenceRef`) cap was an unexported `10_000` internal that bounded only refcount bookkeeping. It now backs the zero-config presence-roster fallback (presence stream init reconstructs the roster from `_presenceRef` when `platform.presence.list` isn't wired) and is exported alongside the other documented capacity caps. Default raised to 1,000,000.
265
+
266
+ Saturation behavior: entries with a pending leave timer are evicted first; if still full, the new join is dropped silently and a one-shot warning surfaces.
267
+
268
+ **How to migrate.** If you have a custom build that referenced the unexported `10_000` constant, switch to the exported `MAX_PRESENCE_REF` import. Apps that wire `platform.presence` (Redis-backed) bypass the fallback and are unaffected.
269
+
270
+ ### `live.publishRateWarning` and `live.silentTopicWarning` validation throws at registration
271
+
272
+ **What changed.** Invalid `threshold` / `intervalMs` / `thresholdMs` (non-positive, non-finite, wrong type) now throw `[svelte-realtime]`-prefixed errors at registration time so misconfiguration shows up at boot rather than mid-traffic.
273
+
274
+ **How to migrate.** No action required for valid configs. Audit any dynamic config wiring that could pass `0`, `-1`, `'30s'` (string), etc.
275
+
276
+ ### `live.cron` 6-field expressions and 1Hz tick
277
+
278
+ **What changed.** A leading seconds field unlocks sub-minute granularity. Once any 6-field schedule is registered, the cron tick adapts from 60s to 1Hz (sticky for the process lifetime; cleared by `_clearCron` for HMR). 5-field schedules running under the 1Hz tick fire only at second `:00` of any matching minute, so they keep their once-per-matching-minute semantics.
279
+
280
+ `live.cron` no longer fires concurrently with itself: the tick now skips a path whose previous invocation is still in flight and increments `cronCount{status: 'skipped'}`.
281
+
282
+ **How to migrate.** No action required. If you had a long-running cron job that intentionally relied on parallel invocations (you should not), the next tick is now skipped instead of running in parallel; restructure to either let the previous invocation finish or split the work into independent jobs with different paths.
283
+
284
+ ---
285
+
286
+ ## Recommended new patterns
287
+
288
+ Not required. Adopting these gets you the full 0.5 experience.
289
+
290
+ ### Move `setCronPlatform` and `live.configurePush({ remoteRegistry })` to `init({ platform })`
291
+
292
+ **What changed.** Both functions used to be wired from `open(ws, platform)`. The recommended call site is now the adapter's `init({ platform })` lifecycle hook, which fires once per worker after the listen socket is bound and before any upgrade / open / message hook runs. This eliminates the boot-to-first-connect window where cron ticks were no-ops and `live.push` could not reach cross-instance users.
293
+
294
+ **How to migrate.** Move the calls from `open` to `init`:
295
+
296
+ ```js
297
+ // hooks.ws.js
298
+ import { setCronPlatform, live, message, close, unsubscribe } from 'svelte-realtime/server';
299
+
300
+ export function init({ platform }) {
301
+ setCronPlatform(platform);
302
+ live.configurePush({ remoteRegistry: registry });
303
+ }
304
+
305
+ export { message, close, unsubscribe };
306
+ ```
307
+
308
+ Requires `svelte-adapter-uws@^0.5.0`. The legacy `open(ws, platform)` call site continues to work as a fallback.
309
+
310
+ ### `live.upload({ reauthEvery })` mid-stream re-auth
311
+
312
+ **What changed.** Pre-fix, `_startUpload` ran the module guard once at chunk-0 arrival; if the user's session was revoked mid-upload (token expiry, explicit logout, role downgrade) the upload kept running with the original auth grant. Passing `live.upload(handler, { reauthEvery: <bytes> })` re-runs the same guard against the live `ctx` every N bytes received past the last re-auth. If the guard rejects, the upload aborts with `UNAUTHENTICATED` / `FORBIDDEN` and the consumer observes the abort signal.
313
+
314
+ Default is unset (legacy behavior: guard runs once at chunk-0). The option must be opted into per upload because not every upload has a meaningful re-auth boundary.
315
+
316
+ **How to migrate.** No action required for write-once short uploads. For long-tail user uploads (multi-minute, multi-GB) where session revocation matters:
317
+
318
+ ```js
319
+ export const avatar = live.upload(
320
+ async (ctx, name, mime) => { /* ... */ },
321
+ { reauthEvery: 16 * 1024 * 1024 } // re-auth every 16 MB
322
+ );
323
+ ```
324
+
325
+ ### Auto-discovery of adapter `maxPayloadLength` for upload frame sizing
326
+
327
+ **What changed.** The server piggybacks `platform.maxPayloadLength` on the first upload-response envelope per WS via a `__cap` field; the client caches the value globally and uses it as the wire frame size for subsequent uploads, subtracting envelope overhead (10 bytes on chunks 1+, `12 + argsLen` on chunk 0) per chunk internally. The first upload uses the conservative 12KB default; the second upload onwards uses the full discovered cap.
328
+
329
+ User-configured `configure({ upload: { frameSize } })` always wins over discovery, but is silently clamped down to the discovered cap with a one-time dev warn if it exceeds the adapter's limit. The framework guarantees no wire frame ever exceeds `platform.maxPayloadLength`; without this clamp the adapter would close the connection with code 1009.
330
+
331
+ **How to migrate.** No action required. If you pinned `chunkSize` to 12KB by hand, drop the override and let auto-discovery upgrade frames on adapters with `maxPayloadLength: 1MB` (the 0.5.x default).
332
+
333
+ ### `configure({ upload: { chunkSize } })` renamed to `frameSize`; `chunkSize` accepted as deprecated alias
334
+
335
+ **Why the rename.** Pre-rename, the `chunkSize` knob was raw payload bytes per chunk with no clamp and no warn. A user reading "the adapter cap is 1MB" and setting `chunkSize: 1024 * 1024` was correctly following the docs and silently built frames slightly over the cap (the envelope overhead added `12 + argsLen` bytes); uWS evaluated frame size on receive and closed the connection with code 1009. The failure was silent: the client error path never fired because the connection close beat the chunk send-ack.
336
+
337
+ **What changed.**
338
+
339
+ - The knob is renamed to `frameSize` and means "max wire frame bytes," matching `platform.maxPayloadLength`'s semantic 1:1. The framework subtracts envelope overhead per chunk internally; user code does no envelope arithmetic.
340
+ - A hard ceiling clamps user input to the discovered adapter cap, with a one-time dev warn when clamping kicks in. Overflow is now structurally impossible.
341
+ - The auto path drops the old 0.9 safety factor: frame size auto-defaults to the FULL discovered cap (the safety factor was a workaround for the missing per-chunk overhead subtraction; that's now done correctly).
342
+ - `chunkSize` is accepted as a deprecated alias for `frameSize`. Existing config still works; a one-time dev warn points at the rename. Both fields take the same numeric value.
343
+ - When both `frameSize` and `chunkSize` are set, `frameSize` wins and no deprecation warn fires.
344
+
345
+ **How to migrate.**
346
+
347
+ - Existing apps with `configure({ upload: { chunkSize: N } })` keep working. To silence the deprecation warn, rename the field to `frameSize`. The numeric value passes through unchanged.
348
+ - Existing apps that set `chunkSize` to the adapter's `maxPayloadLength` (the silent-overflow case) are now correctly clamped down by the envelope overhead. Effective payload-per-chunk drops by ~12 + `argsLen` bytes (typically ~30-50 bytes); throughput change is invisible (~0.003% on a 1MB cap).
349
+ - New apps should use `frameSize`. The mental model is: "this is the wire frame budget; the framework slices payload to fit."
350
+
351
+ ```js
352
+ // Before
353
+ configure({ upload: { chunkSize: 1024 * 1024 } }); // silently overflows on 1MB cap
354
+
355
+ // After
356
+ configure({ upload: { frameSize: 1024 * 1024 } }); // safe; framework subtracts envelope per chunk
357
+ ```
358
+
359
+ ### `RpcError` and `LiveError` SvelteKit transport (opt-in)
360
+
361
+ **What changed.** Typed errors thrown during `+page.server.js` `load()` previously arrived at `+error.svelte` as plain `Error` instances. The new `realtimeTransport()` SvelteKit transport hook (from `svelte-realtime/hooks`) auto-registers serialization for `RpcError` and `LiveError` across the SSR / client boundary so the original class with `code` intact is preserved.
362
+
363
+ This is opt-in (additive), but apps that catch `LiveError` in their error boundary by `instanceof` or `err.code` will see the previous behavior (plain Error) until they wire the transport.
364
+
365
+ **How to migrate.** Wire the transport hook from `src/hooks.js` (NOT `hooks.server.js`):
366
+
367
+ ```js
368
+ // src/hooks.js
369
+ import { realtimeTransport } from 'svelte-realtime/hooks';
370
+ export const transport = realtimeTransport();
371
+ ```
372
+
373
+ ---
374
+
375
+ ## Cosmetic
376
+
377
+ Type-only changes, deprecations, dead code removed. No action required for most apps.
378
+
379
+ ### `pushHooks.close` now drains stream-subscription bookkeeping when called with `ctx`
380
+
381
+ **What changed.** Pre-0.5, `pushHooks.close` was push-only. Apps following the JSDoc-ordained `export const close = pushHooks.close;` left stream-subscription bookkeeping (`_topicWsCounts`, silent-topic watchdogs, `__onUnsubscribe` callbacks) un-drained. A 30s flurry of `silent topic` warnings fired after every page closed.
382
+
383
+ `pushHooks.close(ws, ctx)` now routes through the realtime `close` when the adapter passes `ctx` (production), so the existing JSDoc-style re-export gets the same full cleanup with no doc-ordained migration. Direct one-arg `pushHooks.close(ws)` calls (tests, custom flows) still work as push-only via a fallback branch.
384
+
385
+ **How to migrate.** Nothing required if you wired `pushHooks.close` per the JSDoc. If you wired both `pushHooks.close` AND `realtime.close` manually (composed), the calls are idempotent across both registries - no double-drain bug.
386
+
387
+ ### `onCronError()` is deprecated; use `onError()`
388
+
389
+ **What changed (0.4.0).** `onError(handler)` is the global error handler for cron, effects, and derived stream errors. `onCronError` still works but delegates.
390
+
391
+ **How to migrate.**
392
+
393
+ ```diff
394
+ - onCronError((err, jobName) => log.error({ err, jobName }))
395
+ + onError((err) => log.error({ err }))
396
+ ```
397
+
398
+ ### Snapshot hydration for `live.aggregate` skips `__proto__` / `constructor` / `prototype`
399
+
400
+ **What changed.** Pre-fix, `Object.assign(entry.state, snapshotState)` accepted any keys present in the snapshot payload. A snapshot returned from a backend (Redis cache, JSON payload from another service) is a hostile-input boundary - a payload like `JSON.parse('{"__proto__":{"polluted":1}}')` would have stamped `polluted` on `Object.prototype`. Snapshot hydration now copies own enumerable keys via a helper that skips `__proto__`, `constructor`, and `prototype`.
401
+
402
+ **How to migrate.** No action required unless you stored values under literal keys named `__proto__` / `constructor` / `prototype` in your aggregate state (don't). Both the single-snapshot path (`live.aggregate(..., { snapshot })`) and the per-window snapshot path (`live.aggregate(..., { snapshots: { ... } })`) go through the helper.
403
+
404
+ ### Realtime rate-limit identity probes `id`, `user_id`, and `userId`
405
+
406
+ **What changed.** Pre-fix, the default per-handler rate-limit identity key read only `ctx.user.id`. Apps with sessions whose shape uses Postgres-convention `user_id` or camelCase `userId` fell back to the per-connection guest bucket - defeating per-user rate limits exactly when they mattered most. `_getIdentityKey` now reads `ctx.user.id ?? ctx.user.user_id ?? ctx.user.userId`.
407
+
408
+ **How to migrate.** No action required if your session shape exposes any of the three. If your session uses a different field, override:
409
+
410
+ ```js
411
+ live.rateLimit({ identity: ctx => ctx.user?.tenant_user_id, /* ... */ })
412
+ ```
413
+
414
+ ### Vite codegen path interpolation now JSON-quoted (RCE fix)
415
+
416
+ **What changed.** Pre-fix, the Vite plugin emitted client stubs and server-bundle registrations as `__rpc('${modulePath}/${name}')`. A filename containing a `'` could break out of the generated string literal and inject code. Every interpolation now routes through `JSON.stringify(...)`.
417
+
418
+ **How to migrate.** No action required. Regenerate the build (`vite build`); old generated bundles on disk should be discarded.
419
+
420
+ ### SSR stub generation now uses the same classifier as client stubs
421
+
422
+ **What changed.** An earlier 0.5 prerelease taught the client-side classifier that single-arity ctx-only topic functions are static, but the SSR stub generator (`_generateSsrStubs`) was missed in that pass. Result: client said "static StreamStore", SSR said "factory function", and `$inbox` during SSR rendering called `factory.subscribe(...)` and crashed every affected page with `TypeError: store.subscribe is not a function`. Both paths now go through the canonical `_isDynamicExport` classifier.
423
+
424
+ **How to migrate.** No action required after upgrading to 0.5 final.
425
+
426
+ ### `live.configurePush()` typings: `remoteRegistry` is now accepted, both fields are optional
427
+
428
+ **What changed.** `server.d.ts` typed `identify` as required and omitted `remoteRegistry`. The runtime accepted either field individually and rejected only when neither was present. The typed surface now mirrors that contract via a discriminated union.
429
+
430
+ **How to migrate.** TypeScript users: if you pinned to the broken types via assertions, drop them. `live.configurePush({ remoteRegistry })` and `live.configurePush({ identify })` typecheck cleanly. `live.configurePush({})` is now a compile-time error.
431
+
432
+ ### Removed: dead `pipe.filter().transformEvent` field
433
+
434
+ **What changed.** `pipe.filter()` returned `{ transformInit, transformEvent }` but `pipe()` only ever consumed `transformInit`, so `transformEvent` had no runtime effect. The field is removed from `server.js` and `server.d.ts`. The README pointer for per-event projection now correctly directs to the `transform` option on `live.stream({ transform })`.
435
+
436
+ **How to migrate.** If you set `pipe.filter({ transformEvent: ... })`, the value was being silently discarded - move to `live.stream({ transform })`:
437
+
438
+ ```diff
439
+ - live.stream(topic, loader, pipe(pipe.filter({ transformEvent: project })))
440
+ + live.stream(topic, loader, { transform: project })
441
+ ```
442
+
443
+ ### Default in-process lock backing `live.lock` rewritten as a per-key FIFO waiter queue
444
+
445
+ **What changed.** The prior `Map<string, Promise>` chain was replaced so that `maxWaitMs` cancellations skip cancelled waiters cleanly without breaking FIFO. Behavior for existing call sites without `maxWaitMs` is identical: same FIFO, same parallelism across keys, same handler-error propagation that unblocks the next waiter.
446
+
447
+ The adapter's `lock.clear()` now rejects pending waiters with a typed `LOCK_CLEARED` error instead of leaving them hanging.
448
+
449
+ **How to migrate.** If your code calls the adapter's lock plugin directly via `lock.clear()` and ignored `clear()`-driven rejections, add a `catch`:
450
+
451
+ ```diff
452
+ await someLockedCall().catch(err => {
453
+ + if (err.code === 'LOCK_CLEARED') return;
454
+ throw err;
455
+ });
456
+ ```
457
+
458
+ ### `__binaryRpc` accepts `ArrayBuffer | ArrayBufferView`
459
+
460
+ **What changed (0.4.0).** The TypeScript signature widened from `ArrayBuffer` to `ArrayBuffer | ArrayBufferView`. Not breaking for callers passing `ArrayBuffer`.
461
+
462
+ **How to migrate.** None required for runtime; TypeScript users may need to relax overly-narrow argument types if they were re-typing the function.