@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 +134 -0
- package/SKILL.md +297 -0
- package/dist/SKILL.md +297 -0
- package/dist/svelte/crud-transforms.d.ts +81 -0
- package/dist/svelte/crud-transforms.js +127 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.js +1 -0
- package/package.json +4 -3
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
|
+
}
|
package/dist/svelte/index.d.ts
CHANGED
package/dist/svelte/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blujosi/rivetkit-svelte",
|
|
3
|
-
"version": "2.2
|
|
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"
|