@blujosi/rivetkit-svelte 2.2.0 → 2.3.2

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
@@ -24,6 +24,7 @@ npm i @blujosi/rivetkit-svelte rivetkit
24
24
  - **Svelte 5 Runes** — Built for `$state`, `$effect`, and `$derived`
25
25
  - **Real-time Actor Connections** — Connect to RivetKit actors with automatic state sync
26
26
  - **Event Handling** — `useEvent` with automatic cleanup
27
+ - **CRUD Transforms** — Pre-built transform factories for managing lists via create/update/delete events
27
28
  - **Type Safety** — Full TypeScript support with registry type inference
28
29
  - **SSR Compatible** — Browser guard for SvelteKit SSR
29
30
  - **SvelteKit Handler** — Run RivetKit serverless inside your SvelteKit app
@@ -470,6 +471,139 @@ count?.refetch();
470
471
  | **Best for** | Most use-cases | High-frequency events where refetch would be wasteful |
471
472
  | **Refetch** | `.refetch()` available | Not available |
472
473
 
474
+ ---
475
+
476
+ ### CRUD Transforms (`@blujosi/rivetkit-svelte`)
477
+
478
+ Pre-built `transform` factories for managing lists of items via create/update/delete events. Use with `useQuery` or `rivetLoad`.
479
+
480
+ ```typescript
481
+ import {
482
+ crudTransform,
483
+ createTransform,
484
+ updateTransform,
485
+ deleteTransform,
486
+ } from "@blujosi/rivetkit-svelte";
487
+ ```
488
+
489
+ #### `crudTransform<T>(opts?)`
490
+
491
+ A unified transform that handles all three CRUD operations in a single function. The actor broadcasts events wrapped in a `CrudEvent<T>`:
492
+
493
+ ```typescript
494
+ // In the actor:
495
+ c.broadcast("taskChanged", { type: "created", data: newTask });
496
+ c.broadcast("taskChanged", { type: "updated", data: updatedTask });
497
+ c.broadcast("taskChanged", { type: "deleted", data: deletedTask });
498
+ ```
499
+
500
+ ```svelte
501
+ <script lang="ts">
502
+ import { crudTransform } from "@blujosi/rivetkit-svelte";
503
+
504
+ interface Task { id: string; title: string; done: boolean }
505
+
506
+ // With useQuery (client-side)
507
+ const tasks = actor?.useQuery({
508
+ action: "getTasks",
509
+ event: "taskChanged",
510
+ initialValue: [] as Task[],
511
+ transform: crudTransform<Task>({ key: "id" }),
512
+ });
513
+ </script>
514
+ ```
515
+
516
+ ```typescript
517
+ // With rivetLoad (SSR + live)
518
+ const tasks = await rivetLoad(client, {
519
+ actor: "taskList",
520
+ key: ["my-list"],
521
+ action: "getTasks",
522
+ event: "taskChanged",
523
+ transform: crudTransform<Task>({ key: "id" }),
524
+ });
525
+ ```
526
+
527
+ If the incoming payload is **not** wrapped in `{ type, data }`, `crudTransform` falls back to an **upsert** — it updates the item if it exists, or appends it otherwise.
528
+
529
+ **Options (`CrudTransformOptions<T>`):**
530
+
531
+ | Option | Type | Default | Description |
532
+ |---|---|---|---|
533
+ | `key` | `keyof T \| (item: T) => unknown` | `"id"` | Property name or accessor used to uniquely identify items |
534
+
535
+ **`CrudEvent<T>` shape:**
536
+
537
+ | Field | Type | Description |
538
+ |---|---|---|
539
+ | `type` | `"created" \| "updated" \| "deleted"` | The operation type |
540
+ | `data` | `T` | The item (or key value for deletes) |
541
+
542
+ #### Individual Transforms
543
+
544
+ Use these when you have **separate events** for each operation:
545
+
546
+ ##### `createTransform<T>(opts?)`
547
+
548
+ Appends the incoming item to the list. Duplicates (same key) are ignored.
549
+
550
+ ```typescript
551
+ const tasks = actor?.useQuery({
552
+ action: "getTasks",
553
+ event: "taskCreated",
554
+ initialValue: [],
555
+ transform: createTransform<Task>({ key: "id" }),
556
+ });
557
+ ```
558
+
559
+ ##### `updateTransform<T>(opts?)`
560
+
561
+ Replaces the matching item in-place. If no match is found, the list is returned unchanged.
562
+
563
+ ```typescript
564
+ const tasks = actor?.useQuery({
565
+ action: "getTasks",
566
+ event: "taskUpdated",
567
+ initialValue: [],
568
+ transform: updateTransform<Task>({ key: "id" }),
569
+ });
570
+ ```
571
+
572
+ ##### `deleteTransform<T>(opts?)`
573
+
574
+ Removes the matching item. Accepts either a full object or a raw key value.
575
+
576
+ ```typescript
577
+ const tasks = actor?.useQuery({
578
+ action: "getTasks",
579
+ event: "taskDeleted",
580
+ initialValue: [],
581
+ transform: deleteTransform<Task>({ key: "id" }),
582
+ });
583
+
584
+ // The actor can broadcast just the key:
585
+ c.broadcast("taskDeleted", taskId);
586
+ // Or the full object:
587
+ c.broadcast("taskDeleted", task);
588
+ ```
589
+
590
+ #### Custom Key Functions
591
+
592
+ For items keyed by something other than a single property:
593
+
594
+ ```typescript
595
+ const items = actor?.useQuery({
596
+ action: "getItems",
597
+ event: "itemChanged",
598
+ initialValue: [],
599
+ transform: crudTransform<Item>({
600
+ key: (item) => `${item.type}:${item.slug}`,
601
+ }),
602
+ });
603
+ ```
604
+
605
+ ---
606
+
473
607
  ### SvelteKit Exports (`@blujosi/rivetkit-svelte/sveltekit`)
474
608
 
475
609
  #### `createRivetKitHandler(opts)`
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
+ ```
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Generic CRUD transform factories for use with `useQuery` and `rivetLoad`.
3
+ *
4
+ * These produce `transform` functions that handle incoming create/update/delete
5
+ * events against a list of items, keyed by an identifier field.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const tasks = await rivetLoad(client, {
10
+ * actor: "taskList",
11
+ * key: ["my-list"],
12
+ * action: "getTasks",
13
+ * event: "taskChanged",
14
+ * transform: crudTransform<Task>({ key: "id" }),
15
+ * });
16
+ * ```
17
+ */
18
+ /**
19
+ * An incoming event payload that carries a CRUD operation type.
20
+ *
21
+ * Actors should broadcast events in this shape:
22
+ * ```ts
23
+ * c.broadcast("todoListUpdate", { data: todo, type: "created" })
24
+ * ```
25
+ */
26
+ export interface CrudEvent<T> {
27
+ data: T;
28
+ type: "created" | "updated" | "deleted";
29
+ }
30
+ /** Options shared by all CRUD transform factories. */
31
+ export interface CrudTransformOptions<T> {
32
+ /**
33
+ * Property name (or accessor) used to uniquely identify items.
34
+ * Defaults to `"id"`.
35
+ */
36
+ key?: keyof T | ((item: T) => unknown);
37
+ }
38
+ /**
39
+ * Transform for a **create** event.
40
+ * - **Array:** appends the incoming item; duplicates (same key) are ignored.
41
+ * - **Single item:** replaces the current value with the incoming item.
42
+ */
43
+ export declare function createTransform<T>(opts?: CrudTransformOptions<T>): <C extends T[] | T>(current: C, incoming: unknown) => C;
44
+ /**
45
+ * Transform for an **update** event.
46
+ * - **Array:** replaces the matching item in-place; returns unchanged if no match.
47
+ * - **Single item:** replaces the current value with the incoming item.
48
+ */
49
+ export declare function updateTransform<T>(opts?: CrudTransformOptions<T>): <C extends T[] | T>(current: C, incoming: unknown) => C;
50
+ /**
51
+ * Transform for a **delete** event.
52
+ * - **Array:** removes the matching item. `incoming` can be the full item or just the key value.
53
+ * - **Single item:** returns the current value unchanged (cannot delete a scalar).
54
+ */
55
+ export declare function deleteTransform<T>(opts?: CrudTransformOptions<T>): <C extends T[] | T>(current: C, incoming: unknown) => C;
56
+ /**
57
+ * A single transform that handles create, update, and delete events.
58
+ *
59
+ * Incoming payloads must be a `CrudEvent<T>` with `{ data, type }`:
60
+ * ```ts
61
+ * { data: item, type: "created" }
62
+ * { data: item, type: "updated" }
63
+ * { data: item, type: "deleted" }
64
+ * ```
65
+ *
66
+ * Actors should broadcast in this shape:
67
+ * ```ts
68
+ * c.broadcast("todoListUpdate", { data: todo, type: "created" })
69
+ * ```
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const users = todoActor?.useQuery({
74
+ * action: "getUsers",
75
+ * event: "userListUpdate",
76
+ * initialValue: [],
77
+ * transform: crudTransform<User>({ key: "id" }),
78
+ * });
79
+ * ```
80
+ */
81
+ export declare function crudTransform<T>(opts?: CrudTransformOptions<T>): <C extends T[] | T>(current: C, incoming: CrudEvent<T>) => C;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Generic CRUD transform factories for use with `useQuery` and `rivetLoad`.
3
+ *
4
+ * These produce `transform` functions that handle incoming create/update/delete
5
+ * events against a list of items, keyed by an identifier field.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const tasks = await rivetLoad(client, {
10
+ * actor: "taskList",
11
+ * key: ["my-list"],
12
+ * action: "getTasks",
13
+ * event: "taskChanged",
14
+ * transform: crudTransform<Task>({ key: "id" }),
15
+ * });
16
+ * ```
17
+ */
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers
20
+ // ---------------------------------------------------------------------------
21
+ function getKey(item, key) {
22
+ return typeof key === "function" ? key(item) : item[key];
23
+ }
24
+ // ---------------------------------------------------------------------------
25
+ // Individual transforms
26
+ // ---------------------------------------------------------------------------
27
+ /**
28
+ * Transform for a **create** event.
29
+ * - **Array:** appends the incoming item; duplicates (same key) are ignored.
30
+ * - **Single item:** replaces the current value with the incoming item.
31
+ */
32
+ export function createTransform(opts = {}) {
33
+ const keyProp = opts.key ?? "id";
34
+ return ((current, incoming) => {
35
+ const item = incoming;
36
+ if (Array.isArray(current)) {
37
+ const id = getKey(item, keyProp);
38
+ if (current.some((c) => getKey(c, keyProp) === id))
39
+ return current;
40
+ return [...current, item];
41
+ }
42
+ return item;
43
+ });
44
+ }
45
+ /**
46
+ * Transform for an **update** event.
47
+ * - **Array:** replaces the matching item in-place; returns unchanged if no match.
48
+ * - **Single item:** replaces the current value with the incoming item.
49
+ */
50
+ export function updateTransform(opts = {}) {
51
+ const keyProp = opts.key ?? "id";
52
+ return ((current, incoming) => {
53
+ const item = incoming;
54
+ if (Array.isArray(current)) {
55
+ const id = getKey(item, keyProp);
56
+ const idx = current.findIndex((c) => getKey(c, keyProp) === id);
57
+ if (idx === -1)
58
+ return current;
59
+ const next = [...current];
60
+ next[idx] = item;
61
+ return next;
62
+ }
63
+ return item;
64
+ });
65
+ }
66
+ /**
67
+ * Transform for a **delete** event.
68
+ * - **Array:** removes the matching item. `incoming` can be the full item or just the key value.
69
+ * - **Single item:** returns the current value unchanged (cannot delete a scalar).
70
+ */
71
+ export function deleteTransform(opts = {}) {
72
+ const keyProp = opts.key ?? "id";
73
+ return ((current, incoming) => {
74
+ if (Array.isArray(current)) {
75
+ const id = typeof incoming === "object" && incoming !== null
76
+ ? getKey(incoming, keyProp)
77
+ : incoming;
78
+ return current.filter((c) => getKey(c, keyProp) !== id);
79
+ }
80
+ return current;
81
+ });
82
+ }
83
+ // ---------------------------------------------------------------------------
84
+ // Unified CRUD transform
85
+ // ---------------------------------------------------------------------------
86
+ /**
87
+ * A single transform that handles create, update, and delete events.
88
+ *
89
+ * Incoming payloads must be a `CrudEvent<T>` with `{ data, type }`:
90
+ * ```ts
91
+ * { data: item, type: "created" }
92
+ * { data: item, type: "updated" }
93
+ * { data: item, type: "deleted" }
94
+ * ```
95
+ *
96
+ * Actors should broadcast in this shape:
97
+ * ```ts
98
+ * c.broadcast("todoListUpdate", { data: todo, type: "created" })
99
+ * ```
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * const users = todoActor?.useQuery({
104
+ * action: "getUsers",
105
+ * event: "userListUpdate",
106
+ * initialValue: [],
107
+ * transform: crudTransform<User>({ key: "id" }),
108
+ * });
109
+ * ```
110
+ */
111
+ export function crudTransform(opts = {}) {
112
+ const create = createTransform(opts);
113
+ const update = updateTransform(opts);
114
+ const del = deleteTransform(opts);
115
+ return ((current, incoming) => {
116
+ switch (incoming.type) {
117
+ case "created":
118
+ return create(current, incoming.data);
119
+ case "updated":
120
+ return update(current, incoming.data);
121
+ case "deleted":
122
+ return del(current, incoming.data);
123
+ default:
124
+ return current;
125
+ }
126
+ });
127
+ }
@@ -1 +1,2 @@
1
+ export * from "./crud-transforms";
1
2
  export * from "./rivet.svelte.js";
@@ -1 +1,2 @@
1
+ export * from "./crud-transforms";
1
2
  export * from "./rivet.svelte.js";
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "@blujosi/rivetkit-svelte",
3
- "version": "2.2.0",
3
+ "version": "2.3.2",
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"