@blujosi/rivetkit-svelte 2.1.3 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -474,7 +474,7 @@ count?.refetch();
474
474
 
475
475
  #### `createRivetKitHandler(opts)`
476
476
 
477
- Creates SvelteKit request handlers for all HTTP methods.
477
+ Creates SvelteKit request handlers for all HTTP methods. Every incoming request to the catch-all route is forwarded to the RivetKit registry handler.
478
478
 
479
479
  ```typescript
480
480
  import { createRivetKitHandler } from "@blujosi/rivetkit-svelte/sveltekit";
@@ -483,6 +483,247 @@ export const { GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS } =
483
483
  createRivetKitHandler({ isDev: true, registry, rivetSiteUrl: "http://localhost:5173" });
484
484
  ```
485
485
 
486
+ **Options:**
487
+
488
+ | Option | Type | Description |
489
+ |---|---|---|
490
+ | `registry` | `Registry` | Your RivetKit registry instance |
491
+ | `isDev` | `boolean` | Enables auto-engine spawn and runner pool config |
492
+ | `rivetSiteUrl` | `string?` | Base URL for the site. Falls back to `PUBLIC_RIVET_ENDPOINT` env var |
493
+ | `headers` | `Record<string, string>?` | Static headers added to **every** request sent to the registry handler |
494
+ | `getHeaders` | `(event: RequestEvent) => Record<string, string>?` | Dynamic per-request headers. Receives the full SvelteKit `RequestEvent` |
495
+
496
+ #### Passing Custom Headers (Authentication, JWT Tokens, etc.)
497
+
498
+ The `headers` and `getHeaders` options let you inject headers into every request forwarded to your RivetKit actors. This is essential for passing JWT tokens, session IDs, or any other authentication data from your SvelteKit application to your actors so they can verify and authorize requests.
499
+
500
+ Since `getHeaders` receives the full SvelteKit `RequestEvent`, you have access to `locals`, `cookies`, `url`, `params`, and anything else set by your hooks or middleware — making it the ideal place to forward auth context.
501
+
502
+ **Pass a JWT token from `locals` (set by your auth hook):**
503
+
504
+ ```typescript
505
+ // src/routes/api/rivet/[...rest]/+server.ts
506
+ import { createRivetKitHandler } from "@blujosi/rivetkit-svelte/sveltekit";
507
+ import { dev } from "$app/environment";
508
+ import { registry } from "$backend/registry";
509
+
510
+ export const { GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS } =
511
+ createRivetKitHandler({
512
+ isDev: !!dev,
513
+ registry,
514
+ rivetSiteUrl: "http://localhost:5173",
515
+ // Forward auth token from locals (populated in hooks.server.ts)
516
+ getHeaders: (event) => ({
517
+ "x-app-token": event.locals.token ?? "",
518
+ }),
519
+ });
520
+ ```
521
+
522
+ Your actors can then read `x-app-token` from the incoming request headers to authenticate and authorize the caller.
523
+
524
+ **Combine static and dynamic headers:**
525
+
526
+ ```typescript
527
+ createRivetKitHandler({
528
+ isDev: !!dev,
529
+ registry,
530
+ rivetSiteUrl: "http://localhost:5173",
531
+ // Static headers — same on every request
532
+ headers: {
533
+ "x-api-version": "2",
534
+ "x-app-name": "my-app",
535
+ },
536
+ // Dynamic headers — per-request, from SvelteKit locals/cookies
537
+ getHeaders: (event) => ({
538
+ "x-app-token": event.locals.token ?? "",
539
+ "x-user-id": event.locals.user?.id ?? "",
540
+ "x-session-id": event.cookies.get("session_id") ?? "",
541
+ }),
542
+ });
543
+ ```
544
+
545
+ Static `headers` are applied first, then `getHeaders` — so dynamic headers can override static ones if they share the same key. Both are set on **every** request that passes through the handler.
546
+
547
+ ---
548
+
549
+ ## SSR → Live Query Transport
550
+
551
+ `@blujosi/rivetkit-svelte` supports server-side rendering of actor data that seamlessly upgrades to live WebSocket subscriptions on the client — **zero loading flash, instant first paint, then real-time forever.**
552
+
553
+ This uses SvelteKit's built-in [`transport` hook](https://svelte.dev/docs/kit/hooks#Universal-hooks-transport) to serialize actor query results across the SSR boundary.
554
+
555
+ ### How it works
556
+
557
+ 1. **`rivetLoad()`** in your `+page.ts` load function calls an actor action via stateless HTTP on the server
558
+ 2. SvelteKit's `transport` hook serializes the result across the SSR boundary
559
+ 3. On the client, `transport.decode` creates a live WebSocket subscription with the SSR data as initial state
560
+ 4. On client-side navigation, `rivetLoad()` detects the browser and creates the subscription directly
561
+ 5. Events from the actor push updates to the reactive query — no manual refresh needed
562
+
563
+ ### Setup
564
+
565
+ #### 1. Add the transport hook
566
+
567
+ ```typescript
568
+ // src/hooks.ts
569
+ import { encodeRivetLoad, decodeRivetLoad } from "@blujosi/rivetkit-svelte/sveltekit"
570
+ import { rivetClient } from "$lib/actor.client"
571
+
572
+ export const transport = {
573
+ RivetLoadResult: {
574
+ encode: (value) => encodeRivetLoad(value),
575
+ decode: (encoded) => decodeRivetLoad(encoded, rivetClient),
576
+ },
577
+ }
578
+ ```
579
+
580
+ #### 2. Use `rivetLoad()` in your load function
581
+
582
+ ```typescript
583
+ // src/routes/+page.ts
584
+ import { rivetLoad } from "@blujosi/rivetkit-svelte/sveltekit"
585
+ import { rivetClient } from "$lib/actor.client"
586
+
587
+ export const load = async () => ({
588
+ count: await rivetLoad(rivetClient, {
589
+ actor: 'counter',
590
+ key: ['test-counter'],
591
+ action: 'getCount',
592
+ event: 'newCount',
593
+ })
594
+ })
595
+ ```
596
+
597
+ #### 3. Use the data in your component
598
+
599
+ ```svelte
600
+ <!-- src/routes/+page.svelte -->
601
+ <script lang="ts">
602
+ let { data } = $props()
603
+
604
+ // data.count is already a reactive RivetQueryResult
605
+ // It has SSR data immediately, then upgrades to live updates
606
+ const count = $derived(data.count.data)
607
+ </script>
608
+
609
+ {#if data.count.isLoading}
610
+ <p>Loading...</p>
611
+ {:else}
612
+ <h1>Counter: {count}</h1>
613
+ {/if}
614
+ ```
615
+
616
+ That's it. The page renders with SSR data on first paint, SvelteKit preloads on link hover, and then the actor connection keeps data live in real-time.
617
+
618
+ ### `rivetLoad(client, options)`
619
+
620
+ Fetch actor data for use in SvelteKit load functions. Dual-mode:
621
+
622
+ - **Server (SSR):** calls the action via stateless HTTP, wraps result for transport
623
+ - **Client (navigation):** calls action for initial data, then creates a live subscription immediately
624
+
625
+ ```typescript
626
+ const result = await rivetLoad(rivetClient, {
627
+ actor: 'counter',
628
+ key: ['test-counter'],
629
+ action: 'getCount',
630
+ event: 'newCount',
631
+ args: [], // optional action arguments
632
+ params: { authToken: 'jwt-...' }, // optional connection params
633
+ transform: (current, incoming) => incoming, // optional transform
634
+ })
635
+ ```
636
+
637
+ **Options:**
638
+
639
+ | Option | Type | Description |
640
+ |---|---|---|
641
+ | `actor` | `string` | Actor name from your registry |
642
+ | `key` | `string \| string[]` | Unique key for the actor instance |
643
+ | `action` | `string` | Action name to call for the initial value |
644
+ | `event` | `string \| string[]` | Event name(s) to subscribe to for live updates |
645
+ | `args` | `unknown[]?` | Optional arguments passed to the action |
646
+ | `params` | `Record<string, string>?` | Optional connection parameters |
647
+ | `createInRegion` | `string?` | Region to create the actor in |
648
+ | `createWithInput` | `unknown?` | Input data for actor creation |
649
+ | `transform` | `(current: T, incoming: unknown) => T?` | Transform incoming event data. Default: full replacement |
650
+
651
+ **Returns:** `RivetQueryResult<T>` — a reactive object with:
652
+
653
+ | Property | Type | Description |
654
+ |---|---|---|
655
+ | `data` | `T \| undefined` | The current value |
656
+ | `isLoading` | `boolean` | `true` while loading |
657
+ | `error` | `Error \| undefined` | Error, if any |
658
+ | `isConnected` | `boolean` | Whether the live connection is active |
659
+
660
+ ### `encodeRivetLoad(value)` / `decodeRivetLoad(encoded, client, transform?)`
661
+
662
+ Transport encode/decode functions for `src/hooks.ts`. Wire them into SvelteKit's `transport` hook to enable SSR → live query upgrade.
663
+
664
+ ```typescript
665
+ // src/hooks.ts
666
+ import { encodeRivetLoad, decodeRivetLoad } from "@blujosi/rivetkit-svelte/sveltekit"
667
+ import { rivetClient } from "$lib/actor.client"
668
+
669
+ export const transport = {
670
+ RivetLoadResult: {
671
+ encode: (value) => encodeRivetLoad(value),
672
+ decode: (encoded) => decodeRivetLoad(encoded, rivetClient),
673
+ },
674
+ }
675
+ ```
676
+
677
+ > **Note:** `decodeRivetLoad` accepts an optional third argument `transform` if you need to customize how event data is applied. By default, incoming event data fully replaces the current value.
678
+
679
+ ### Multiple queries in one load
680
+
681
+ ```typescript
682
+ // src/routes/+page.ts
683
+ export const load = async () => ({
684
+ count: await rivetLoad(rivetClient, {
685
+ actor: 'counter',
686
+ key: ['test-counter'],
687
+ action: 'getCount',
688
+ event: 'newCount',
689
+ }),
690
+ countDouble: await rivetLoad(rivetClient, {
691
+ actor: 'counter',
692
+ key: ['test-counter'],
693
+ action: 'getCountDouble',
694
+ event: 'newDoubleCount',
695
+ }),
696
+ })
697
+ ```
698
+
699
+ ### Using with `useActor` for mutations
700
+
701
+ SSR data gives you read access. For mutations (calling actions that change state), combine with `useActor`:
702
+
703
+ ```svelte
704
+ <script lang="ts">
705
+ import { useActor } from "$lib";
706
+
707
+ let { data } = $props();
708
+
709
+ // Read: SSR data with live updates
710
+ const count = $derived(data.count.data);
711
+
712
+ // Write: useActor for action calls
713
+ const counterActor = useActor?.({
714
+ name: "counter",
715
+ key: ["test-counter"],
716
+ });
717
+
718
+ const increment = async () => {
719
+ await counterActor?.current?.connection?.increment(1);
720
+ };
721
+ </script>
722
+
723
+ <h1>Counter: {count}</h1>
724
+ <button onclick={increment}>Increment</button>
725
+ ```
726
+
486
727
  ---
487
728
 
488
729
  ## Common Pitfalls
package/SKILL.md ADDED
@@ -0,0 +1,297 @@
1
+ # @blujosi/rivetkit-svelte — Agent Skill
2
+
3
+ > Svelte 5 integration for [RivetKit](https://rivet.dev). Reactive actor connections via runes, SvelteKit handler for serverless deployment, and SSR → live query transport.
4
+
5
+ ## Package entry points
6
+
7
+ | Import path | Purpose |
8
+ |---|---|
9
+ | `@blujosi/rivetkit-svelte` | Client-side: `createClient`, `createRivetKit`, `createRivetKitWithClient`, `useActor` |
10
+ | `@blujosi/rivetkit-svelte/sveltekit` | Server + SSR: `createRivetKitHandler`, `rivetLoad`, `encodeRivetLoad`, `decodeRivetLoad` |
11
+
12
+ ## Core concepts
13
+
14
+ ### Actor model
15
+ RivetKit actors are persistent, stateful server-side entities. Each actor has:
16
+ - **Actions** — stateless HTTP calls (request/response)
17
+ - **Events** — real-time WebSocket broadcasts to subscribed clients
18
+ - **State** — persisted per-instance, keyed by `name` + `key`
19
+
20
+ ### Registry
21
+ A `registry` defines all available actors. Created via `setup({ use: { counter, chat, ... } })` from `rivetkit`. The `Registry` type flows through the entire system for type safety.
22
+
23
+ ### Client
24
+ `Client<Registry>` is the typed RivetKit client. Created via `createClient<Registry>(endpoint)`. Works on both server and browser.
25
+
26
+ ---
27
+
28
+ ## API reference
29
+
30
+ ### `createClient<Registry>(url)`
31
+ Creates a typed RivetKit client.
32
+
33
+ ```ts
34
+ import { createClient } from "@blujosi/rivetkit-svelte";
35
+ import type { Registry } from "$backend/registry";
36
+
37
+ const client = createClient<Registry>("http://localhost:5173/api/rivet");
38
+ ```
39
+
40
+ ### `createRivetKitWithClient<Registry>(client)`
41
+ Creates the `useActor` hook from an existing client.
42
+
43
+ ```ts
44
+ import { createClient, createRivetKitWithClient } from "@blujosi/rivetkit-svelte";
45
+ const client = createClient<Registry>(url);
46
+ export const { useActor } = createRivetKitWithClient(client);
47
+ ```
48
+
49
+ ### `createRivetKit<Registry>(url)`
50
+ Shorthand — creates both client and `useActor` in one call.
51
+
52
+ ```ts
53
+ import { createRivetKit } from "@blujosi/rivetkit-svelte";
54
+ export const { useActor } = createRivetKit<Registry>("http://localhost:5173/api/rivet");
55
+ ```
56
+
57
+ ### `useActor(options)`
58
+ Connects to a RivetKit actor. Returns reactive state + event/query helpers. **Must be called at top-level of component script** (not inside `onMount`, `$effect`, or callbacks).
59
+
60
+ ```ts
61
+ const actor = useActor({
62
+ name: "counter",
63
+ key: ["test-counter"],
64
+ params: {}, // optional connection params
65
+ createInRegion: "us-east-1", // optional
66
+ createWithInput: {}, // optional
67
+ enabled: true, // optional, default true
68
+ });
69
+ ```
70
+
71
+ **Returned object:**
72
+
73
+ | Property/Method | Description |
74
+ |---|---|
75
+ | `actor.current.connection` | Call actions: `actor.current.connection?.increment(1)` |
76
+ | `actor.current.handle` | Low-level actor handle |
77
+ | `actor.current.isConnected` | `boolean` — connection status |
78
+ | `actor.current.isConnecting` | `boolean` — connecting in progress |
79
+ | `actor.current.isError` | `boolean` — error state |
80
+ | `actor.current.error` | `Error \| null` |
81
+ | `actor.useEvent(name, handler)` | Subscribe to actor events (auto-cleanup) |
82
+ | `actor.useQuery(opts)` | Live query: initial fetch + event-driven updates |
83
+ | `actor.useActionQuery(opts)` | Action query: refetches on event (event = invalidation signal) |
84
+
85
+ ### `useEvent(eventName, handler)`
86
+ Registers an event listener. **Call at top-level, not inside `$effect`.**
87
+
88
+ ```ts
89
+ actor?.useEvent("newCount", (value: number) => {
90
+ count = value;
91
+ });
92
+ ```
93
+
94
+ ### `useQuery(opts)`
95
+ Reactive query — fetches initial value via action, then subscribes to event for live updates.
96
+
97
+ ```ts
98
+ const count = actor?.useQuery({
99
+ action: "getCount",
100
+ event: "newCount",
101
+ initialValue: 0,
102
+ args: [], // optional
103
+ transform: (current, incoming) => incoming, // optional
104
+ });
105
+ // count?.value, count?.isLoading, count?.error
106
+ ```
107
+
108
+ **Default transform:** shallow merge for objects, full replacement for primitives/arrays.
109
+
110
+ ### `useActionQuery(opts)`
111
+ Reactive query — fetches via action, re-fetches when event fires. Event data is **ignored** (pure invalidation signal). **Recommended for most use cases.**
112
+
113
+ ```ts
114
+ const count = actor?.useActionQuery({
115
+ action: "getCount",
116
+ event: "newCount", // or ["newCount", "countReset"]
117
+ initialValue: 0,
118
+ args: () => [filter, limit], // optional reactive args
119
+ });
120
+ // count?.value, count?.isLoading, count?.error, count?.refetch()
121
+ ```
122
+
123
+ **`useActionQuery` vs `useQuery`:**
124
+ - `useActionQuery` — event triggers action re-call, simpler, always consistent with server state
125
+ - `useQuery` — event data is used directly via transform, better for high-frequency updates
126
+
127
+ ---
128
+
129
+ ## SvelteKit handler
130
+
131
+ ### `createRivetKitHandler(opts)`
132
+ Creates SvelteKit request handlers for a catch-all API route.
133
+
134
+ ```ts
135
+ // src/routes/api/rivet/[...rest]/+server.ts
136
+ import { createRivetKitHandler } from "@blujosi/rivetkit-svelte/sveltekit";
137
+ import { dev } from "$app/environment";
138
+ import { registry } from "$backend/registry";
139
+
140
+ export const { GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS } =
141
+ createRivetKitHandler({
142
+ isDev: !!dev,
143
+ registry,
144
+ rivetSiteUrl: "http://localhost:5173",
145
+ headers: { "x-api-version": "2" }, // optional static headers
146
+ getHeaders: (event) => ({ // optional dynamic headers
147
+ "x-app-token": event.locals.token ?? "",
148
+ }),
149
+ });
150
+ ```
151
+
152
+ **Options:**
153
+
154
+ | Option | Type | Description |
155
+ |---|---|---|
156
+ | `registry` | `Registry<any>` | Your RivetKit registry |
157
+ | `isDev` | `boolean` | Auto-starts engine, configures runner pool |
158
+ | `rivetSiteUrl` | `string?` | Base URL of the site |
159
+ | `headers` | `Record<string, string>?` | Static headers on every request |
160
+ | `getHeaders` | `(event: RequestEvent) => Record<string, string>?` | Dynamic per-request headers |
161
+
162
+ ---
163
+
164
+ ## SSR → Live query transport
165
+
166
+ Server-render actor data that upgrades to live WebSocket on the client. Zero loading flash.
167
+
168
+ ### How it works
169
+ 1. `rivetLoad()` in `+page.ts` calls actor action via HTTP during SSR
170
+ 2. SvelteKit `transport` hook serializes the result across the SSR boundary
171
+ 3. On client, `transport.decode` upgrades to a live WebSocket subscription
172
+ 4. On client-side navigation, `rivetLoad()` creates the subscription directly
173
+ 5. Events push live updates — no polling
174
+
175
+ ### Setup
176
+
177
+ **1. Transport hook (`src/hooks.ts`):**
178
+
179
+ ```ts
180
+ import { encodeRivetLoad, decodeRivetLoad } from "@blujosi/rivetkit-svelte/sveltekit";
181
+ import { rivetClient } from "$lib/actor.client";
182
+
183
+ export const transport = {
184
+ RivetLoadResult: {
185
+ encode: (value) => encodeRivetLoad(value),
186
+ decode: (encoded) => decodeRivetLoad(encoded, rivetClient),
187
+ },
188
+ };
189
+ ```
190
+
191
+ **2. Load function (`+page.ts`):**
192
+
193
+ ```ts
194
+ import { rivetLoad } from "@blujosi/rivetkit-svelte/sveltekit";
195
+ import { rivetClient } from "$lib/actor.client";
196
+
197
+ export const load = async () => ({
198
+ count: await rivetLoad(rivetClient, {
199
+ actor: "counter",
200
+ key: ["test-counter"],
201
+ action: "getCount",
202
+ event: "newCount",
203
+ }),
204
+ });
205
+ ```
206
+
207
+ **3. Component (`+page.svelte`):**
208
+
209
+ ```svelte
210
+ <script lang="ts">
211
+ let { data } = $props();
212
+ const count = $derived(data.count.data);
213
+ </script>
214
+
215
+ {#if data.count.isLoading}
216
+ <p>Loading...</p>
217
+ {:else}
218
+ <h1>Counter: {count}</h1>
219
+ {/if}
220
+ ```
221
+
222
+ ### `rivetLoad(client, options)`
223
+
224
+ | Option | Type | Description |
225
+ |---|---|---|
226
+ | `actor` | `string` | Actor name from registry |
227
+ | `key` | `string \| string[]` | Actor instance key |
228
+ | `action` | `string` | Action to call for initial data |
229
+ | `event` | `string \| string[]` | Event(s) for live updates |
230
+ | `args` | `unknown[]?` | Action arguments |
231
+ | `params` | `Record<string, string>?` | Connection params |
232
+ | `transform` | `(current, incoming) => T?` | Custom update transform |
233
+
234
+ **Returns:** `RivetQueryResult<T>` — `{ data, isLoading, error, isConnected }`
235
+
236
+ ### `encodeRivetLoad(value)` / `decodeRivetLoad(encoded, client, transform?)`
237
+ Transport encode/decode for `src/hooks.ts`. Decode accepts optional `transform` third argument.
238
+
239
+ ---
240
+
241
+ ## Client setup pattern
242
+
243
+ ```ts
244
+ // src/lib/actor.client.ts
245
+ import { createClient, createRivetKitWithClient } from "@blujosi/rivetkit-svelte";
246
+ import type { Client } from "rivetkit/client";
247
+ import { browser } from "$app/environment";
248
+ import type { Registry } from "$backend/registry";
249
+ import { PUBLIC_APP_URL } from "$env/static/public";
250
+
251
+ const endpoint = browser
252
+ ? `${location.origin}/api/rivet`
253
+ : `${PUBLIC_APP_URL ?? "http://localhost:5173"}/api/rivet`;
254
+
255
+ export const rivetClient: Client<Registry> = createClient<Registry>(endpoint);
256
+ const { useActor } = createRivetKitWithClient(rivetClient);
257
+ export { useActor };
258
+ ```
259
+
260
+ > **Important:** `useActor` must only be called in the browser. Guard with `browser` check in components.
261
+
262
+ ---
263
+
264
+ ## Common pitfalls
265
+
266
+ 1. **Don't call `useActor` inside `onMount`** — runes need synchronous component setup:
267
+ ```ts
268
+ // BAD: onMount(() => { const a = useActor(...) })
269
+ // GOOD: const a = browser ? useActor(...) : undefined
270
+ ```
271
+
272
+ 2. **Don't call `useEvent` inside `$effect`** — it manages its own effects internally. Wrapping it causes duplicate listeners.
273
+
274
+ 3. **Don't call `.connect()` on the connection** — connections are auto-managed. `.connect()` would send an RPC action named "connect" to the actor.
275
+
276
+ 4. **`useQuery` vs `useActionQuery`** — prefer `useActionQuery` for most cases. Use `useQuery` only when you need to use event payloads directly (high-frequency updates).
277
+
278
+ 5. **SSR client endpoint** — the client must resolve to a valid URL on both server and browser. Use `PUBLIC_APP_URL` env var for the server-side fallback.
279
+
280
+ 6. **Transport hook required for SSR** — `rivetLoad()` results must pass through the `transport` hook in `src/hooks.ts` to upgrade to live subscriptions on the client.
281
+
282
+ ---
283
+
284
+ ## File structure (typical SvelteKit app)
285
+
286
+ ```
287
+ src/
288
+ hooks.ts # transport hook for SSR
289
+ lib/
290
+ actor.client.ts # client + useActor setup
291
+ routes/
292
+ api/rivet/[...rest]/+server.ts # SvelteKit handler
293
+ +page.ts # load function with rivetLoad()
294
+ +page.svelte # component using reactive data
295
+ backend/
296
+ registry.ts # actor definitions + registry
297
+ ```
package/dist/SKILL.md ADDED
@@ -0,0 +1,297 @@
1
+ # @blujosi/rivetkit-svelte — Agent Skill
2
+
3
+ > Svelte 5 integration for [RivetKit](https://rivet.dev). Reactive actor connections via runes, SvelteKit handler for serverless deployment, and SSR → live query transport.
4
+
5
+ ## Package entry points
6
+
7
+ | Import path | Purpose |
8
+ |---|---|
9
+ | `@blujosi/rivetkit-svelte` | Client-side: `createClient`, `createRivetKit`, `createRivetKitWithClient`, `useActor` |
10
+ | `@blujosi/rivetkit-svelte/sveltekit` | Server + SSR: `createRivetKitHandler`, `rivetLoad`, `encodeRivetLoad`, `decodeRivetLoad` |
11
+
12
+ ## Core concepts
13
+
14
+ ### Actor model
15
+ RivetKit actors are persistent, stateful server-side entities. Each actor has:
16
+ - **Actions** — stateless HTTP calls (request/response)
17
+ - **Events** — real-time WebSocket broadcasts to subscribed clients
18
+ - **State** — persisted per-instance, keyed by `name` + `key`
19
+
20
+ ### Registry
21
+ A `registry` defines all available actors. Created via `setup({ use: { counter, chat, ... } })` from `rivetkit`. The `Registry` type flows through the entire system for type safety.
22
+
23
+ ### Client
24
+ `Client<Registry>` is the typed RivetKit client. Created via `createClient<Registry>(endpoint)`. Works on both server and browser.
25
+
26
+ ---
27
+
28
+ ## API reference
29
+
30
+ ### `createClient<Registry>(url)`
31
+ Creates a typed RivetKit client.
32
+
33
+ ```ts
34
+ import { createClient } from "@blujosi/rivetkit-svelte";
35
+ import type { Registry } from "$backend/registry";
36
+
37
+ const client = createClient<Registry>("http://localhost:5173/api/rivet");
38
+ ```
39
+
40
+ ### `createRivetKitWithClient<Registry>(client)`
41
+ Creates the `useActor` hook from an existing client.
42
+
43
+ ```ts
44
+ import { createClient, createRivetKitWithClient } from "@blujosi/rivetkit-svelte";
45
+ const client = createClient<Registry>(url);
46
+ export const { useActor } = createRivetKitWithClient(client);
47
+ ```
48
+
49
+ ### `createRivetKit<Registry>(url)`
50
+ Shorthand — creates both client and `useActor` in one call.
51
+
52
+ ```ts
53
+ import { createRivetKit } from "@blujosi/rivetkit-svelte";
54
+ export const { useActor } = createRivetKit<Registry>("http://localhost:5173/api/rivet");
55
+ ```
56
+
57
+ ### `useActor(options)`
58
+ Connects to a RivetKit actor. Returns reactive state + event/query helpers. **Must be called at top-level of component script** (not inside `onMount`, `$effect`, or callbacks).
59
+
60
+ ```ts
61
+ const actor = useActor({
62
+ name: "counter",
63
+ key: ["test-counter"],
64
+ params: {}, // optional connection params
65
+ createInRegion: "us-east-1", // optional
66
+ createWithInput: {}, // optional
67
+ enabled: true, // optional, default true
68
+ });
69
+ ```
70
+
71
+ **Returned object:**
72
+
73
+ | Property/Method | Description |
74
+ |---|---|
75
+ | `actor.current.connection` | Call actions: `actor.current.connection?.increment(1)` |
76
+ | `actor.current.handle` | Low-level actor handle |
77
+ | `actor.current.isConnected` | `boolean` — connection status |
78
+ | `actor.current.isConnecting` | `boolean` — connecting in progress |
79
+ | `actor.current.isError` | `boolean` — error state |
80
+ | `actor.current.error` | `Error \| null` |
81
+ | `actor.useEvent(name, handler)` | Subscribe to actor events (auto-cleanup) |
82
+ | `actor.useQuery(opts)` | Live query: initial fetch + event-driven updates |
83
+ | `actor.useActionQuery(opts)` | Action query: refetches on event (event = invalidation signal) |
84
+
85
+ ### `useEvent(eventName, handler)`
86
+ Registers an event listener. **Call at top-level, not inside `$effect`.**
87
+
88
+ ```ts
89
+ actor?.useEvent("newCount", (value: number) => {
90
+ count = value;
91
+ });
92
+ ```
93
+
94
+ ### `useQuery(opts)`
95
+ Reactive query — fetches initial value via action, then subscribes to event for live updates.
96
+
97
+ ```ts
98
+ const count = actor?.useQuery({
99
+ action: "getCount",
100
+ event: "newCount",
101
+ initialValue: 0,
102
+ args: [], // optional
103
+ transform: (current, incoming) => incoming, // optional
104
+ });
105
+ // count?.value, count?.isLoading, count?.error
106
+ ```
107
+
108
+ **Default transform:** shallow merge for objects, full replacement for primitives/arrays.
109
+
110
+ ### `useActionQuery(opts)`
111
+ Reactive query — fetches via action, re-fetches when event fires. Event data is **ignored** (pure invalidation signal). **Recommended for most use cases.**
112
+
113
+ ```ts
114
+ const count = actor?.useActionQuery({
115
+ action: "getCount",
116
+ event: "newCount", // or ["newCount", "countReset"]
117
+ initialValue: 0,
118
+ args: () => [filter, limit], // optional reactive args
119
+ });
120
+ // count?.value, count?.isLoading, count?.error, count?.refetch()
121
+ ```
122
+
123
+ **`useActionQuery` vs `useQuery`:**
124
+ - `useActionQuery` — event triggers action re-call, simpler, always consistent with server state
125
+ - `useQuery` — event data is used directly via transform, better for high-frequency updates
126
+
127
+ ---
128
+
129
+ ## SvelteKit handler
130
+
131
+ ### `createRivetKitHandler(opts)`
132
+ Creates SvelteKit request handlers for a catch-all API route.
133
+
134
+ ```ts
135
+ // src/routes/api/rivet/[...rest]/+server.ts
136
+ import { createRivetKitHandler } from "@blujosi/rivetkit-svelte/sveltekit";
137
+ import { dev } from "$app/environment";
138
+ import { registry } from "$backend/registry";
139
+
140
+ export const { GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS } =
141
+ createRivetKitHandler({
142
+ isDev: !!dev,
143
+ registry,
144
+ rivetSiteUrl: "http://localhost:5173",
145
+ headers: { "x-api-version": "2" }, // optional static headers
146
+ getHeaders: (event) => ({ // optional dynamic headers
147
+ "x-app-token": event.locals.token ?? "",
148
+ }),
149
+ });
150
+ ```
151
+
152
+ **Options:**
153
+
154
+ | Option | Type | Description |
155
+ |---|---|---|
156
+ | `registry` | `Registry<any>` | Your RivetKit registry |
157
+ | `isDev` | `boolean` | Auto-starts engine, configures runner pool |
158
+ | `rivetSiteUrl` | `string?` | Base URL of the site |
159
+ | `headers` | `Record<string, string>?` | Static headers on every request |
160
+ | `getHeaders` | `(event: RequestEvent) => Record<string, string>?` | Dynamic per-request headers |
161
+
162
+ ---
163
+
164
+ ## SSR → Live query transport
165
+
166
+ Server-render actor data that upgrades to live WebSocket on the client. Zero loading flash.
167
+
168
+ ### How it works
169
+ 1. `rivetLoad()` in `+page.ts` calls actor action via HTTP during SSR
170
+ 2. SvelteKit `transport` hook serializes the result across the SSR boundary
171
+ 3. On client, `transport.decode` upgrades to a live WebSocket subscription
172
+ 4. On client-side navigation, `rivetLoad()` creates the subscription directly
173
+ 5. Events push live updates — no polling
174
+
175
+ ### Setup
176
+
177
+ **1. Transport hook (`src/hooks.ts`):**
178
+
179
+ ```ts
180
+ import { encodeRivetLoad, decodeRivetLoad } from "@blujosi/rivetkit-svelte/sveltekit";
181
+ import { rivetClient } from "$lib/actor.client";
182
+
183
+ export const transport = {
184
+ RivetLoadResult: {
185
+ encode: (value) => encodeRivetLoad(value),
186
+ decode: (encoded) => decodeRivetLoad(encoded, rivetClient),
187
+ },
188
+ };
189
+ ```
190
+
191
+ **2. Load function (`+page.ts`):**
192
+
193
+ ```ts
194
+ import { rivetLoad } from "@blujosi/rivetkit-svelte/sveltekit";
195
+ import { rivetClient } from "$lib/actor.client";
196
+
197
+ export const load = async () => ({
198
+ count: await rivetLoad(rivetClient, {
199
+ actor: "counter",
200
+ key: ["test-counter"],
201
+ action: "getCount",
202
+ event: "newCount",
203
+ }),
204
+ });
205
+ ```
206
+
207
+ **3. Component (`+page.svelte`):**
208
+
209
+ ```svelte
210
+ <script lang="ts">
211
+ let { data } = $props();
212
+ const count = $derived(data.count.data);
213
+ </script>
214
+
215
+ {#if data.count.isLoading}
216
+ <p>Loading...</p>
217
+ {:else}
218
+ <h1>Counter: {count}</h1>
219
+ {/if}
220
+ ```
221
+
222
+ ### `rivetLoad(client, options)`
223
+
224
+ | Option | Type | Description |
225
+ |---|---|---|
226
+ | `actor` | `string` | Actor name from registry |
227
+ | `key` | `string \| string[]` | Actor instance key |
228
+ | `action` | `string` | Action to call for initial data |
229
+ | `event` | `string \| string[]` | Event(s) for live updates |
230
+ | `args` | `unknown[]?` | Action arguments |
231
+ | `params` | `Record<string, string>?` | Connection params |
232
+ | `transform` | `(current, incoming) => T?` | Custom update transform |
233
+
234
+ **Returns:** `RivetQueryResult<T>` — `{ data, isLoading, error, isConnected }`
235
+
236
+ ### `encodeRivetLoad(value)` / `decodeRivetLoad(encoded, client, transform?)`
237
+ Transport encode/decode for `src/hooks.ts`. Decode accepts optional `transform` third argument.
238
+
239
+ ---
240
+
241
+ ## Client setup pattern
242
+
243
+ ```ts
244
+ // src/lib/actor.client.ts
245
+ import { createClient, createRivetKitWithClient } from "@blujosi/rivetkit-svelte";
246
+ import type { Client } from "rivetkit/client";
247
+ import { browser } from "$app/environment";
248
+ import type { Registry } from "$backend/registry";
249
+ import { PUBLIC_APP_URL } from "$env/static/public";
250
+
251
+ const endpoint = browser
252
+ ? `${location.origin}/api/rivet`
253
+ : `${PUBLIC_APP_URL ?? "http://localhost:5173"}/api/rivet`;
254
+
255
+ export const rivetClient: Client<Registry> = createClient<Registry>(endpoint);
256
+ const { useActor } = createRivetKitWithClient(rivetClient);
257
+ export { useActor };
258
+ ```
259
+
260
+ > **Important:** `useActor` must only be called in the browser. Guard with `browser` check in components.
261
+
262
+ ---
263
+
264
+ ## Common pitfalls
265
+
266
+ 1. **Don't call `useActor` inside `onMount`** — runes need synchronous component setup:
267
+ ```ts
268
+ // BAD: onMount(() => { const a = useActor(...) })
269
+ // GOOD: const a = browser ? useActor(...) : undefined
270
+ ```
271
+
272
+ 2. **Don't call `useEvent` inside `$effect`** — it manages its own effects internally. Wrapping it causes duplicate listeners.
273
+
274
+ 3. **Don't call `.connect()` on the connection** — connections are auto-managed. `.connect()` would send an RPC action named "connect" to the actor.
275
+
276
+ 4. **`useQuery` vs `useActionQuery`** — prefer `useActionQuery` for most cases. Use `useQuery` only when you need to use event payloads directly (high-frequency updates).
277
+
278
+ 5. **SSR client endpoint** — the client must resolve to a valid URL on both server and browser. Use `PUBLIC_APP_URL` env var for the server-side fallback.
279
+
280
+ 6. **Transport hook required for SSR** — `rivetLoad()` results must pass through the `transport` hook in `src/hooks.ts` to upgrade to live subscriptions on the client.
281
+
282
+ ---
283
+
284
+ ## File structure (typical SvelteKit app)
285
+
286
+ ```
287
+ src/
288
+ hooks.ts # transport hook for SSR
289
+ lib/
290
+ actor.client.ts # client + useActor setup
291
+ routes/
292
+ api/rivet/[...rest]/+server.ts # SvelteKit handler
293
+ +page.ts # load function with rivetLoad()
294
+ +page.svelte # component using reactive data
295
+ backend/
296
+ registry.ts # actor definitions + registry
297
+ ```
@@ -1,10 +1,15 @@
1
- import type { RequestHandler } from "@sveltejs/kit";
1
+ import type { RequestEvent, RequestHandler } from "@sveltejs/kit";
2
2
  import type { Registry } from "rivetkit";
3
- export declare const createRivetKitHandler: (opts?: {
3
+ interface RivetKitHandlerOpts {
4
4
  registry: Registry<any>;
5
5
  isDev: boolean;
6
6
  rivetSiteUrl?: string;
7
- }) => {
7
+ /** Static headers added to every request sent to the registry handler */
8
+ headers?: Record<string, string>;
9
+ /** Dynamic headers resolved per-request. Receives the full SvelteKit RequestEvent. */
10
+ getHeaders?: (event: RequestEvent) => Record<string, string> | Promise<Record<string, string>>;
11
+ }
12
+ export declare const createRivetKitHandler: (opts?: RivetKitHandlerOpts) => {
8
13
  GET: RequestHandler;
9
14
  POST: RequestHandler;
10
15
  PUT: RequestHandler;
@@ -13,3 +18,4 @@ export declare const createRivetKitHandler: (opts?: {
13
18
  HEAD: RequestHandler;
14
19
  OPTIONS: RequestHandler;
15
20
  };
21
+ export {};
@@ -1,7 +1,7 @@
1
1
  import { getLogger } from "rivetkit/log";
2
2
  const _devRunnerVersion = Math.floor(Date.now() / 1000);
3
3
  const _logger = getLogger("driver-sveltekit");
4
- const handler = async (request, opts) => {
4
+ const handler = async (request, event, opts) => {
5
5
  const _requestUrl = new URL(request.url);
6
6
  const rivetSiteUrl = opts?.rivetSiteUrl;
7
7
  if (!rivetSiteUrl) {
@@ -42,12 +42,25 @@ const handler = async (request, opts) => {
42
42
  const newRequest = new Request(newUrl, request);
43
43
  newRequest.headers.set("host", new URL(newUrl).host);
44
44
  newRequest.headers.set("accept-encoding", "application/json");
45
+ // Apply static headers
46
+ if (opts?.headers) {
47
+ for (const [key, value] of Object.entries(opts.headers)) {
48
+ newRequest.headers.set(key, value);
49
+ }
50
+ }
51
+ // Apply dynamic per-request headers
52
+ if (opts?.getHeaders) {
53
+ const dynamicHeaders = await opts.getHeaders(event);
54
+ for (const [key, value] of Object.entries(dynamicHeaders)) {
55
+ newRequest.headers.set(key, value);
56
+ }
57
+ }
45
58
  return await registry.handler(newRequest);
46
59
  // return fetch(newRequest, { method: request.method, redirect: "manual" })
47
60
  };
48
61
  export const createRivetKitHandler = (opts) => {
49
- const requestHandler = async ({ request }) => {
50
- return handler(request, opts);
62
+ const requestHandler = async (event) => {
63
+ return handler(event.request, event, opts);
51
64
  };
52
65
  return {
53
66
  GET: requestHandler,
@@ -1 +1,2 @@
1
- export * from './handler.server';
1
+ export * from "./handler.server";
2
+ export { decodeRivetLoad, encodeRivetLoad, type RivetLoadOptions, RivetLoadResult, type RivetQueryResult, rivetLoad, } from "./transport.svelte";
@@ -1 +1,2 @@
1
- export * from './handler.server';
1
+ export * from "./handler.server";
2
+ export { decodeRivetLoad, encodeRivetLoad, RivetLoadResult, rivetLoad, } from "./transport.svelte";
@@ -0,0 +1,95 @@
1
+ /**
2
+ * SSR bridge — rivetLoad() + transport encode/decode.
3
+ *
4
+ * On the server, rivetLoad fetches via a stateless RivetKit action call.
5
+ * On the client, transport.decode upgrades it to a live actor subscription.
6
+ * On client-side navigation, rivetLoad creates a live subscription directly.
7
+ */
8
+ import type { AnyActorRegistry } from "@rivetkit/framework-base";
9
+ import type { Client } from "rivetkit/client";
10
+ /** Reactive query result returned by rivetLoad (after decode) and on client nav. */
11
+ export interface RivetQueryResult<T = unknown> {
12
+ readonly data: T | undefined;
13
+ readonly isLoading: boolean;
14
+ readonly error: Error | undefined;
15
+ readonly isConnected: boolean;
16
+ }
17
+ export interface RivetLoadOptions<T = unknown> {
18
+ /** Actor name from the registry (e.g. 'counter'). */
19
+ actor: string;
20
+ /** Unique key for the actor instance. */
21
+ key: string | string[];
22
+ /** Action name to call for the initial value. */
23
+ action: string;
24
+ /** Arguments to pass to the action. */
25
+ args?: unknown[];
26
+ /** Event name(s) to subscribe to for live updates. */
27
+ event: string | string[];
28
+ /** Optional connection params (e.g. auth tokens). */
29
+ params?: Record<string, string>;
30
+ /** Optional region to create the actor in. */
31
+ createInRegion?: string;
32
+ /** Optional input data for actor creation. */
33
+ createWithInput?: unknown;
34
+ /** Transform incoming event data into the new value. Default: full replacement. */
35
+ transform?: (current: T, incoming: unknown) => T;
36
+ }
37
+ /** Marker class for transport.encode to recognize. */
38
+ export declare class RivetLoadResult<T = unknown> {
39
+ readonly actorName: string;
40
+ readonly key: string | string[];
41
+ readonly action: string;
42
+ readonly args: unknown[];
43
+ readonly event: string | string[];
44
+ readonly data: T;
45
+ readonly params?: Record<string, string> | undefined;
46
+ readonly createInRegion?: string | undefined;
47
+ readonly createWithInput?: unknown | undefined;
48
+ readonly __rivetLoad = true;
49
+ constructor(actorName: string, key: string | string[], action: string, args: unknown[], event: string | string[], data: T, params?: Record<string, string> | undefined, createInRegion?: string | undefined, createWithInput?: unknown | undefined);
50
+ }
51
+ interface RivetLoadEncoded {
52
+ actorName: string;
53
+ key: string | string[];
54
+ action: string;
55
+ args: unknown[];
56
+ event: string | string[];
57
+ data: unknown;
58
+ params?: Record<string, string>;
59
+ createInRegion?: string;
60
+ createWithInput?: unknown;
61
+ }
62
+ /**
63
+ * Fetch actor data for use in SvelteKit load functions.
64
+ *
65
+ * - **Server (SSR):** calls the action via stateless HTTP, wraps the result in
66
+ * a RivetLoadResult. transport.decode upgrades it to a live subscription on
67
+ * the client.
68
+ * - **Client (navigation):** calls the action for initial data, then immediately
69
+ * creates a live subscription via createDetachedActorQuery().
70
+ *
71
+ * ```ts
72
+ * // +page.ts
73
+ * export const load = async () => ({
74
+ * count: await rivetLoad(rivetClient, {
75
+ * actor: 'counter',
76
+ * key: ['test-counter'],
77
+ * action: 'getCount',
78
+ * event: 'newCount',
79
+ * })
80
+ * })
81
+ * ```
82
+ */
83
+ export declare function rivetLoad<Registry extends AnyActorRegistry, T = unknown>(client: Client<Registry>, opts: RivetLoadOptions<T>): Promise<RivetQueryResult<T>>;
84
+ /**
85
+ * Encode a RivetLoadResult for serialization across the SSR boundary.
86
+ * Uses duck-type check (`__rivetLoad`) instead of `instanceof` because
87
+ * Vite HMR can create separate class identities for the same module.
88
+ */
89
+ export declare function encodeRivetLoad(value: unknown): false | RivetLoadEncoded;
90
+ /**
91
+ * Decode a serialized RivetLoadResult into a live actor subscription.
92
+ * Uses createDetachedActorQuery — works outside component context.
93
+ */
94
+ export declare function decodeRivetLoad<Registry extends AnyActorRegistry>(encoded: RivetLoadEncoded, client: Client<Registry>, transform?: (current: unknown, incoming: unknown) => unknown): RivetQueryResult<unknown>;
95
+ export {};
@@ -0,0 +1,183 @@
1
+ /**
2
+ * SSR bridge — rivetLoad() + transport encode/decode.
3
+ *
4
+ * On the server, rivetLoad fetches via a stateless RivetKit action call.
5
+ * On the client, transport.decode upgrades it to a live actor subscription.
6
+ * On client-side navigation, rivetLoad creates a live subscription directly.
7
+ */
8
+ const IS_BROWSER = typeof globalThis.document !== "undefined";
9
+ // ============================================================================
10
+ // RivetLoadResult — the serializable container
11
+ // ============================================================================
12
+ /** Marker class for transport.encode to recognize. */
13
+ export class RivetLoadResult {
14
+ actorName;
15
+ key;
16
+ action;
17
+ args;
18
+ event;
19
+ data;
20
+ params;
21
+ createInRegion;
22
+ createWithInput;
23
+ __rivetLoad = true;
24
+ constructor(actorName, key, action, args, event, data, params, createInRegion, createWithInput) {
25
+ this.actorName = actorName;
26
+ this.key = key;
27
+ this.action = action;
28
+ this.args = args;
29
+ this.event = event;
30
+ this.data = data;
31
+ this.params = params;
32
+ this.createInRegion = createInRegion;
33
+ this.createWithInput = createWithInput;
34
+ }
35
+ }
36
+ // ============================================================================
37
+ // rivetLoad — for load functions
38
+ // ============================================================================
39
+ /**
40
+ * Fetch actor data for use in SvelteKit load functions.
41
+ *
42
+ * - **Server (SSR):** calls the action via stateless HTTP, wraps the result in
43
+ * a RivetLoadResult. transport.decode upgrades it to a live subscription on
44
+ * the client.
45
+ * - **Client (navigation):** calls the action for initial data, then immediately
46
+ * creates a live subscription via createDetachedActorQuery().
47
+ *
48
+ * ```ts
49
+ * // +page.ts
50
+ * export const load = async () => ({
51
+ * count: await rivetLoad(rivetClient, {
52
+ * actor: 'counter',
53
+ * key: ['test-counter'],
54
+ * action: 'getCount',
55
+ * event: 'newCount',
56
+ * })
57
+ * })
58
+ * ```
59
+ */
60
+ export async function rivetLoad(client, opts) {
61
+ const { actor: actorName, key, action, args = [], event, params, createInRegion, createWithInput, } = opts;
62
+ const normalizedKey = Array.isArray(key) ? key : [key];
63
+ // Get a stateless handle via ClientRaw.getOrCreate (dynamic name access)
64
+ const handle = client.getOrCreate(actorName, normalizedKey, {
65
+ params,
66
+ createInRegion,
67
+ createWithInput,
68
+ });
69
+ // Call the action by name via ActorHandleRaw.action
70
+ const data = await handle.action({
71
+ name: action,
72
+ args,
73
+ });
74
+ if (IS_BROWSER) {
75
+ // Client-side navigation: create live subscription immediately
76
+ return createDetachedActorQuery(client, opts, data);
77
+ }
78
+ // Server-side: wrap in RivetLoadResult for transport serialization
79
+ return new RivetLoadResult(actorName, key, action, args, event, data, params, createInRegion, createWithInput);
80
+ }
81
+ // ============================================================================
82
+ // Transport encode/decode — for hooks.ts
83
+ // ============================================================================
84
+ /**
85
+ * Encode a RivetLoadResult for serialization across the SSR boundary.
86
+ * Uses duck-type check (`__rivetLoad`) instead of `instanceof` because
87
+ * Vite HMR can create separate class identities for the same module.
88
+ */
89
+ export function encodeRivetLoad(value) {
90
+ if (value instanceof RivetLoadResult ||
91
+ (value != null && typeof value === "object" && "__rivetLoad" in value)) {
92
+ const v = value;
93
+ return {
94
+ actorName: v.actorName,
95
+ key: v.key,
96
+ action: v.action,
97
+ args: v.args,
98
+ event: v.event,
99
+ data: v.data,
100
+ params: v.params,
101
+ createInRegion: v.createInRegion,
102
+ createWithInput: v.createWithInput,
103
+ };
104
+ }
105
+ return false;
106
+ }
107
+ /**
108
+ * Decode a serialized RivetLoadResult into a live actor subscription.
109
+ * Uses createDetachedActorQuery — works outside component context.
110
+ */
111
+ export function decodeRivetLoad(encoded, client, transform) {
112
+ return createDetachedActorQuery(client, {
113
+ actor: encoded.actorName,
114
+ key: encoded.key,
115
+ action: encoded.action,
116
+ args: encoded.args,
117
+ event: encoded.event,
118
+ params: encoded.params,
119
+ createInRegion: encoded.createInRegion,
120
+ createWithInput: encoded.createWithInput,
121
+ transform,
122
+ }, encoded.data);
123
+ }
124
+ // ============================================================================
125
+ // createDetachedActorQuery — live subscription outside component context
126
+ // ============================================================================
127
+ /**
128
+ * Create a live actor subscription outside of Svelte component context.
129
+ * Used by transport.decode (SSR hydration) and rivetLoad (client navigation).
130
+ *
131
+ * Uses raw $state signals — no $effect needed for lifecycle.
132
+ * Connects to the actor, subscribes to event(s), and keeps data reactive.
133
+ */
134
+ function createDetachedActorQuery(client, opts, initialData) {
135
+ const { actor: actorName, key, event, params, createInRegion, createWithInput, transform = (_current, incoming) => incoming, } = opts;
136
+ let data = $state(initialData);
137
+ let isLoading = $state(false);
138
+ let error = $state(undefined);
139
+ let isConnected = $state(false);
140
+ const normalizedKey = Array.isArray(key) ? key : [key];
141
+ // Get a handle via ClientRaw.getOrCreate (dynamic name access)
142
+ const handle = client.getOrCreate(actorName, normalizedKey, {
143
+ params,
144
+ createInRegion,
145
+ createWithInput,
146
+ });
147
+ // Connect for event subscriptions
148
+ const conn = handle.connect();
149
+ // Track connection status
150
+ conn.onOpen(() => {
151
+ isConnected = true;
152
+ });
153
+ conn.onClose(() => {
154
+ isConnected = false;
155
+ });
156
+ conn.onError((err) => {
157
+ error = err instanceof Error ? err : new Error(String(err));
158
+ });
159
+ // Subscribe to event(s) for live updates
160
+ const events = Array.isArray(event) ? event : [event];
161
+ for (const evt of events) {
162
+ conn.on(evt, (...args) => {
163
+ const incoming = args.length === 1 ? args[0] : args;
164
+ data = transform(data, incoming);
165
+ isLoading = false;
166
+ error = undefined;
167
+ });
168
+ }
169
+ return {
170
+ get data() {
171
+ return data;
172
+ },
173
+ get isLoading() {
174
+ return isLoading;
175
+ },
176
+ get error() {
177
+ return error;
178
+ },
179
+ get isConnected() {
180
+ return isConnected;
181
+ },
182
+ };
183
+ }
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "@blujosi/rivetkit-svelte",
3
- "version": "2.1.3",
3
+ "version": "2.2.1",
4
4
  "scripts": {
5
5
  "build": "vite build && npm run prepack",
6
6
  "preview": "vite preview",
7
7
  "prepare": "svelte-kit sync || echo ''",
8
- "prepack": "svelte-kit sync && svelte-package && publint",
8
+ "prepack": "svelte-kit sync && svelte-package && cp SKILL.md dist/SKILL.md && publint",
9
9
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
10
10
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
11
11
  },
12
12
  "files": [
13
13
  "dist",
14
14
  "!dist/**/*.test.*",
15
- "!dist/**/*.spec.*"
15
+ "!dist/**/*.spec.*",
16
+ "SKILL.md"
16
17
  ],
17
18
  "sideEffects": [
18
19
  "**/*.css"