@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 +242 -1
- package/SKILL.md +297 -0
- package/dist/SKILL.md +297 -0
- package/dist/sveltekit/handler.server.d.ts +9 -3
- package/dist/sveltekit/handler.server.js +16 -3
- package/dist/sveltekit/index.d.ts +2 -1
- package/dist/sveltekit/index.js +2 -1
- package/dist/sveltekit/transport.svelte.d.ts +95 -0
- package/dist/sveltekit/transport.svelte.js +183 -0
- package/package.json +4 -3
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
|
-
|
|
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 (
|
|
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
|
|
1
|
+
export * from "./handler.server";
|
|
2
|
+
export { decodeRivetLoad, encodeRivetLoad, type RivetLoadOptions, RivetLoadResult, type RivetQueryResult, rivetLoad, } from "./transport.svelte";
|
package/dist/sveltekit/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from
|
|
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
|
+
"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"
|