@blujosi/rivetkit-svelte 2.3.10 → 2.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -315,7 +315,7 @@ const count = counterActor?.useQuery({
315
315
  | `args` | `any[]?` | Optional arguments passed to the action |
316
316
  | `event` | `string` | The event name to subscribe to for real-time updates |
317
317
  | `initialValue` | `T` | The value to use before the action resolves |
318
- | `transform` | `(current: T, incoming: any) => T?` | Optional function to merge incoming event data with the current value |
318
+ | `transform` | `(current: T, incoming: any) => T?` | Optional function to merge incoming event data with the current value. For CRUD lists, use `crudTransform<T>()` |
319
319
 
320
320
  **Default transform behavior:**
321
321
  - **Plain objects** — shallow merge: `{ ...current, ...incoming }`
@@ -533,8 +533,8 @@ c.broadcast("taskChanged", { type: "deleted", data: deletedTask });
533
533
 
534
534
  interface Task { id: string; title: string; done: boolean }
535
535
 
536
- // With useQuery (client-side)
537
- const tasks = actor?.useQuery({
536
+ // With useQuery (client-side) — T is the full state type (Task[])
537
+ const tasks = actor?.useQuery<Task[]>({
538
538
  action: "getTasks",
539
539
  event: "taskChanged",
540
540
  initialValue: [] as Task[],
@@ -544,8 +544,8 @@ c.broadcast("taskChanged", { type: "deleted", data: deletedTask });
544
544
  ```
545
545
 
546
546
  ```typescript
547
- // With rivetLoad (SSR + live)
548
- const tasks = await rivetLoad(client, {
547
+ // With rivetLoad (SSR + live) — T is the item type, data returns Task[]
548
+ const tasks = await rivetLoad<Registry, Task>(client, {
549
549
  actor: "taskList",
550
550
  key: ["my-list"],
551
551
  action: "getTasks",
@@ -622,7 +622,7 @@ c.broadcast("taskDeleted", task);
622
622
  For items keyed by something other than a single property:
623
623
 
624
624
  ```typescript
625
- const items = actor?.useQuery({
625
+ const items = actor?.useQuery<Item[]>({
626
626
  action: "getItems",
627
627
  event: "itemChanged",
628
628
  initialValue: [],
@@ -743,17 +743,22 @@ export const transport = {
743
743
 
744
744
  #### 2. Use `rivetLoad()` in your load function
745
745
 
746
+ `rivetLoad<T>` is designed for **collections** — `T` is the item type and `data` returns `T[]`. The default transform is `crudTransform<T>()`, so standard CRUD events work out of the box.
747
+
746
748
  ```typescript
747
- // src/routes/+page.ts
749
+ // src/routes/todos/+page.ts
748
750
  import { rivetLoad } from "@blujosi/rivetkit-svelte/sveltekit"
749
751
  import { rivetClient } from "$lib/actor.client"
752
+ import type { Registry } from "$backend/registry"
753
+
754
+ interface Todo { id: string; text: string; done: boolean }
750
755
 
751
756
  export const load = async () => ({
752
- count: await rivetLoad(rivetClient, {
753
- actor: 'counter',
754
- key: ['test-counter'],
755
- action: 'getCount',
756
- event: 'newCount',
757
+ todos: await rivetLoad<Registry, Todo>(rivetClient, {
758
+ actor: 'todoList',
759
+ key: ['my-list'],
760
+ action: 'getTodos',
761
+ event: 'todoChanged',
757
762
  })
758
763
  })
759
764
  ```
@@ -761,19 +766,23 @@ export const load = async () => ({
761
766
  #### 3. Use the data in your component
762
767
 
763
768
  ```svelte
764
- <!-- src/routes/+page.svelte -->
769
+ <!-- src/routes/todos/+page.svelte -->
765
770
  <script lang="ts">
766
771
  let { data } = $props()
767
772
 
768
- // data.count is already a reactive RivetQueryResult
773
+ // data.todos is already a reactive RivetQueryResult
769
774
  // It has SSR data immediately, then upgrades to live updates
770
- const count = $derived(data.count.data)
775
+ const todos = $derived(data.todos.data)
771
776
  </script>
772
777
 
773
- {#if data.count.isLoading}
778
+ {#if data.todos.isLoading}
774
779
  <p>Loading...</p>
775
780
  {:else}
776
- <h1>Counter: {count}</h1>
781
+ <ul>
782
+ {#each todos ?? [] as todo}
783
+ <li>{todo.text}</li>
784
+ {/each}
785
+ </ul>
777
786
  {/if}
778
787
  ```
779
788
 
@@ -786,15 +795,20 @@ Fetch actor data for use in SvelteKit load functions. Dual-mode:
786
795
  - **Server (SSR):** calls the action via stateless HTTP, wraps result for transport
787
796
  - **Client (navigation):** calls action for initial data, then creates a live subscription immediately
788
797
 
798
+ `rivetLoad<T>` is designed for **collections** — `T` is the item type and `data` returns `T[]`. The default transform is `crudTransform<T>()`, so standard CRUD events work without specifying a transform.
799
+
789
800
  ```typescript
790
- const result = await rivetLoad(rivetClient, {
791
- actor: 'counter',
792
- key: ['test-counter'],
793
- action: 'getCount',
794
- event: 'newCount',
801
+ interface Todo { id: string; text: string; done: boolean }
802
+
803
+ const result = await rivetLoad<Registry, Todo>(rivetClient, {
804
+ actor: 'todoList',
805
+ key: ['my-list'],
806
+ action: 'getTodos',
807
+ event: 'todoChanged',
795
808
  args: [], // optional action arguments
796
809
  params: { authToken: 'jwt-...' }, // optional connection params
797
- transform: (current, incoming) => incoming, // optional transform
810
+ // transform defaults to crudTransform<T>() override if needed:
811
+ // transform: (current, incoming) => [...current, incoming.data],
798
812
  })
799
813
  ```
800
814
 
@@ -810,13 +824,13 @@ const result = await rivetLoad(rivetClient, {
810
824
  | `params` | `Record<string, string>?` | Optional connection parameters |
811
825
  | `createInRegion` | `string?` | Region to create the actor in |
812
826
  | `createWithInput` | `unknown?` | Input data for actor creation |
813
- | `transform` | `(current: T, incoming: unknown) => T?` | Transform incoming event data. Default: full replacement |
827
+ | `transform` | `(current: T[], incoming: CrudEvent<T>) => T[]?` | Transform incoming event data. Default: `crudTransform<T>()` |
814
828
 
815
829
  **Returns:** `RivetQueryResult<T>` — a reactive object with:
816
830
 
817
831
  | Property | Type | Description |
818
832
  |---|---|---|
819
- | `data` | `T \| undefined` | The current value |
833
+ | `data` | `T[] \| undefined` | The current value (collection of items) |
820
834
  | `isLoading` | `boolean` | `true` while loading |
821
835
  | `error` | `Error \| undefined` | Error, if any |
822
836
  | `isConnected` | `boolean` | Whether the live connection is active |
@@ -838,24 +852,29 @@ export const transport = {
838
852
  }
839
853
  ```
840
854
 
841
- > **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.
855
+ > **Note:** `decodeRivetLoad` accepts an optional third argument `transform` if you need to customize how event data is applied. By default, the transform is `crudTransform<T>()`, which handles `CrudEvent<T>` payloads (`{ type: "created" | "updated" | "deleted", data: T }`).
842
856
 
843
857
  ### Multiple queries in one load
844
858
 
845
859
  ```typescript
846
860
  // src/routes/+page.ts
861
+ import type { Registry } from "$backend/registry"
862
+
863
+ interface Todo { id: string; text: string; done: boolean }
864
+ interface Comment { id: string; todoId: string; body: string }
865
+
847
866
  export const load = async () => ({
848
- count: await rivetLoad(rivetClient, {
849
- actor: 'counter',
850
- key: ['test-counter'],
851
- action: 'getCount',
852
- event: 'newCount',
867
+ todos: await rivetLoad<Registry, Todo>(rivetClient, {
868
+ actor: 'todoList',
869
+ key: ['my-list'],
870
+ action: 'getTodos',
871
+ event: 'todoChanged',
853
872
  }),
854
- countDouble: await rivetLoad(rivetClient, {
855
- actor: 'counter',
856
- key: ['test-counter'],
857
- action: 'getCountDouble',
858
- event: 'newDoubleCount',
873
+ comments: await rivetLoad<Registry, Comment>(rivetClient, {
874
+ actor: 'todoList',
875
+ key: ['my-list'],
876
+ action: 'getComments',
877
+ event: 'commentChanged',
859
878
  }),
860
879
  })
861
880
  ```
@@ -870,32 +889,26 @@ SSR data gives you read access. For mutations (calling actions that change state
870
889
 
871
890
  let { data } = $props();
872
891
 
873
- // Read: SSR data with live updates
874
- const count = $derived(data.count.data);
892
+ // Read: SSR data with live updates (T[] collection)
893
+ const todos = $derived(data.todos.data);
875
894
 
876
895
  // Write: useActor for action calls
877
- const counterActor = useActor?.({
878
- name: "counter",
879
- key: ["test-counter"],
896
+ const todoActor = useActor?.({
897
+ name: "todoList",
898
+ key: ["my-list"],
880
899
  });
881
900
 
882
- const increment = async () => {
883
- await counterActor?.current?.connection?.increment(1);
901
+ const addTodo = async () => {
902
+ await todoActor?.current?.connection?.addTodo({ text: "New task" });
884
903
  };
885
904
  </script>
886
905
 
887
- <h1>Counter: {count}</h1>
888
- <button onclick={increment}>Increment</button>
889
- ```
890
-
891
- ---
892
-
893
- ## Common Pitfalls
894
-
895
- ### Don't call `useActor` inside `onMount`
896
-
897
- `useActor` uses `$effect` runes internally. Runes must be initialized during synchronous component setup, not in deferred callbacks.
898
-
906
+ <ul>
907
+ {#each todos ?? [] as todo}
908
+ <li>{todo.text}</li>
909
+ {/each}
910
+ </ul>
911
+ <button onclick={addTodo}>Add Todo</button>
899
912
  ```typescript
900
913
  // BAD
901
914
  onMount(() => {
@@ -2,7 +2,8 @@
2
2
  * Generic CRUD transform factories for use with `useQuery` and `rivetLoad`.
3
3
  *
4
4
  * These produce `transform` functions that handle incoming create/update/delete
5
- * events against a list of items, keyed by an identifier field.
5
+ * events against a collection of items (`T[]`), keyed by an identifier field.
6
+ * `T` is always the **item** type; the state is always `T[]`.
6
7
  *
7
8
  * @example
8
9
  * ```ts
@@ -37,22 +38,19 @@ export interface CrudTransformOptions<T> {
37
38
  }
38
39
  /**
39
40
  * 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.
41
+ * Appends the incoming item to the collection; duplicates (same key) are ignored.
42
42
  */
43
- export declare function createTransform<T>(opts?: CrudTransformOptions<T>): <C extends T[] | T>(current: C, incoming: CrudEvent<T>) => C;
43
+ export declare function createTransform<T>(opts?: CrudTransformOptions<T>): (current: T[], incoming: CrudEvent<T>) => T[];
44
44
  /**
45
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.
46
+ * Replaces the matching item in the collection; returns unchanged if no match.
48
47
  */
49
- export declare function updateTransform<T>(opts?: CrudTransformOptions<T>): <C extends T[] | T>(current: C, incoming: CrudEvent<T>) => C;
48
+ export declare function updateTransform<T>(opts?: CrudTransformOptions<T>): (current: T[], incoming: CrudEvent<T>) => T[];
50
49
  /**
51
50
  * 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).
51
+ * Removes the matching item from the collection. `incoming.data` can be the full item or just the key value.
54
52
  */
55
- export declare function deleteTransform<T>(opts?: CrudTransformOptions<T>): <C extends T[] | T>(current: C, incoming: CrudEvent<T>) => C;
53
+ export declare function deleteTransform<T>(opts?: CrudTransformOptions<T>): (current: T[], incoming: CrudEvent<T>) => T[];
56
54
  /**
57
55
  * A single transform that handles create, update, and delete events.
58
56
  *
@@ -70,7 +68,7 @@ export declare function deleteTransform<T>(opts?: CrudTransformOptions<T>): <C e
70
68
  *
71
69
  * @example
72
70
  * ```ts
73
- * const users = todoActor?.useQuery({
71
+ * const users = todoActor?.useQuery<User>({
74
72
  * action: "getUsers",
75
73
  * event: "userListUpdate",
76
74
  * initialValue: [],
@@ -78,4 +76,4 @@ export declare function deleteTransform<T>(opts?: CrudTransformOptions<T>): <C e
78
76
  * });
79
77
  * ```
80
78
  */
81
- export declare function crudTransform<T>(opts?: CrudTransformOptions<T>): <C extends T[] | T>(current: C, incoming: CrudEvent<T>) => C;
79
+ export declare function crudTransform<T>(opts?: CrudTransformOptions<T>): (current: T[], incoming: CrudEvent<T>) => T[];
@@ -2,7 +2,8 @@
2
2
  * Generic CRUD transform factories for use with `useQuery` and `rivetLoad`.
3
3
  *
4
4
  * These produce `transform` functions that handle incoming create/update/delete
5
- * events against a list of items, keyed by an identifier field.
5
+ * events against a collection of items (`T[]`), keyed by an identifier field.
6
+ * `T` is always the **item** type; the state is always `T[]`.
6
7
  *
7
8
  * @example
8
9
  * ```ts
@@ -29,64 +30,55 @@ function getKey(item, key) {
29
30
  return item[key];
30
31
  }
31
32
  }
33
+ function resolveId(item, key) {
34
+ return key != null ? getKey(item, key) : item.id;
35
+ }
32
36
  // ---------------------------------------------------------------------------
33
37
  // Individual transforms
34
38
  // ---------------------------------------------------------------------------
35
39
  /**
36
40
  * Transform for a **create** event.
37
- * - **Array:** appends the incoming item; duplicates (same key) are ignored.
38
- * - **Single item:** replaces the current value with the incoming item.
41
+ * Appends the incoming item to the collection; duplicates (same key) are ignored.
39
42
  */
40
43
  export function createTransform(opts = {}) {
41
- const keyProp = opts.key ?? "id";
42
- return ((current, incoming) => {
44
+ const keyProp = opts.key;
45
+ return (current, incoming) => {
43
46
  const item = incoming.data;
44
- if (Array.isArray(current)) {
45
- const id = getKey(item, keyProp);
46
- if (current.some((c) => getKey(c, keyProp) === id))
47
- return current;
48
- return [...current, item];
49
- }
50
- return item;
51
- });
47
+ const id = resolveId(item, keyProp);
48
+ if (current.some((c) => resolveId(c, keyProp) === id))
49
+ return current;
50
+ return [...current, item];
51
+ };
52
52
  }
53
53
  /**
54
54
  * Transform for an **update** event.
55
- * - **Array:** replaces the matching item in-place; returns unchanged if no match.
56
- * - **Single item:** replaces the current value with the incoming item.
55
+ * Replaces the matching item in the collection; returns unchanged if no match.
57
56
  */
58
57
  export function updateTransform(opts = {}) {
59
- const keyProp = opts.key ?? "id";
60
- return ((current, incoming) => {
58
+ const keyProp = opts.key;
59
+ return (current, incoming) => {
61
60
  const item = incoming.data;
62
- if (Array.isArray(current)) {
63
- const id = getKey(item, keyProp);
64
- const idx = current.findIndex((c) => getKey(c, keyProp) === id);
65
- if (idx === -1)
66
- return current;
67
- const next = [...current];
68
- next[idx] = item;
69
- return next;
70
- }
71
- return item;
72
- });
61
+ const id = resolveId(item, keyProp);
62
+ const idx = current.findIndex((c) => resolveId(c, keyProp) === id);
63
+ if (idx === -1)
64
+ return current;
65
+ const next = [...current];
66
+ next[idx] = item;
67
+ return next;
68
+ };
73
69
  }
74
70
  /**
75
71
  * Transform for a **delete** event.
76
- * - **Array:** removes the matching item. `incoming` can be the full item or just the key value.
77
- * - **Single item:** returns the current value unchanged (cannot delete a scalar).
72
+ * Removes the matching item from the collection. `incoming.data` can be the full item or just the key value.
78
73
  */
79
74
  export function deleteTransform(opts = {}) {
80
- const keyProp = opts.key ?? "id";
81
- return ((current, incoming) => {
82
- if (Array.isArray(current)) {
83
- const id = typeof incoming.data === "object" && incoming.data !== null
84
- ? getKey(incoming.data, keyProp)
85
- : incoming.data;
86
- return current.filter((c) => getKey(c, keyProp) !== id);
87
- }
88
- return current;
89
- });
75
+ const keyProp = opts.key;
76
+ return (current, incoming) => {
77
+ const id = typeof incoming.data === "object" && incoming.data !== null
78
+ ? resolveId(incoming.data, keyProp)
79
+ : incoming.data;
80
+ return current.filter((c) => resolveId(c, keyProp) !== id);
81
+ };
90
82
  }
91
83
  // ---------------------------------------------------------------------------
92
84
  // Unified CRUD transform
@@ -108,7 +100,7 @@ export function deleteTransform(opts = {}) {
108
100
  *
109
101
  * @example
110
102
  * ```ts
111
- * const users = todoActor?.useQuery({
103
+ * const users = todoActor?.useQuery<User>({
112
104
  * action: "getUsers",
113
105
  * event: "userListUpdate",
114
106
  * initialValue: [],
@@ -120,7 +112,7 @@ export function crudTransform(opts = {}) {
120
112
  const create = createTransform(opts);
121
113
  const update = updateTransform(opts);
122
114
  const del = deleteTransform(opts);
123
- return ((current, incoming) => {
115
+ return (current, incoming) => {
124
116
  switch (incoming.type) {
125
117
  case "created":
126
118
  return create(current, incoming);
@@ -131,5 +123,5 @@ export function crudTransform(opts = {}) {
131
123
  default:
132
124
  return current;
133
125
  }
134
- });
126
+ };
135
127
  }
@@ -64,7 +64,7 @@ export interface ActorStateReference<AD extends AnyActorDefinition> {
64
64
  import { createClient } from "rivetkit/client";
65
65
  export { createClient } from "rivetkit/client";
66
66
  export declare function createRivetKit<Registry extends AnyActorRegistry>(clientInput?: Parameters<typeof createClient>[0], opts?: CreateRivetKitOptions<Registry>): {
67
- useActor: <ActorName extends keyof ExtractActorsFromRegistry<Registry>>(opts: ActorOptions<Registry, ActorName>) => {
67
+ useActor: <ActorName extends keyof ExtractActorsFromRegistry<Registry> & string>(opts: ActorOptions<Registry, ActorName>) => {
68
68
  current: {
69
69
  connect(): void;
70
70
  readonly connection: any;
@@ -102,7 +102,7 @@ export declare function createRivetKit<Registry extends AnyActorRegistry>(client
102
102
  };
103
103
  };
104
104
  export declare function createRivetKitWithClient<Registry extends AnyActorRegistry>(client: Client<Registry>, opts?: CreateRivetKitOptions<Registry>): {
105
- useActor: <ActorName extends keyof ExtractActorsFromRegistry<Registry>>(opts: ActorOptions<Registry, ActorName>) => {
105
+ useActor: <ActorName extends keyof ExtractActorsFromRegistry<Registry> & string>(opts: ActorOptions<Registry, ActorName>) => {
106
106
  current: {
107
107
  connect(): void;
108
108
  readonly connection: any;
@@ -7,14 +7,15 @@
7
7
  */
8
8
  import type { AnyActorRegistry } from "@rivetkit/framework-base";
9
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;
10
+ import type { CrudEvent } from "../svelte/crud-transforms";
11
+ /** Reactive query result returned by rivetLoad (after decode) and on client nav. `T` is the item type; data is always `T[]`. */
12
+ export interface RivetQueryResult<T> {
13
+ readonly data: T[] | undefined;
13
14
  readonly isLoading: boolean;
14
15
  readonly error: Error | undefined;
15
16
  readonly isConnected: boolean;
16
17
  }
17
- export interface RivetLoadOptions<T = unknown> {
18
+ export interface RivetLoadOptions<T> {
18
19
  /** Actor name from the registry (e.g. 'counter'). */
19
20
  actor: string;
20
21
  /** Unique key for the actor instance. */
@@ -31,8 +32,8 @@ export interface RivetLoadOptions<T = unknown> {
31
32
  createInRegion?: string;
32
33
  /** Optional input data for actor creation. */
33
34
  createWithInput?: unknown;
34
- /** Transform incoming event data into the new value. Default: full replacement. */
35
- transform?: (current: T, incoming: any) => T;
35
+ /** Transform incoming event data into the new collection. Default: `crudTransform<T>()`. */
36
+ transform?: (current: T[], incoming: CrudEvent<T>) => T[];
36
37
  }
37
38
  /** Marker class for transport.encode to recognize. */
38
39
  export declare class RivetLoadResult<T = unknown> {
@@ -91,5 +92,5 @@ export declare function encodeRivetLoad(value: unknown): false | RivetLoadEncode
91
92
  * Decode a serialized RivetLoadResult into a live actor subscription.
92
93
  * Uses createDetachedActorQuery — works outside component context.
93
94
  */
94
- export declare function decodeRivetLoad<Registry extends AnyActorRegistry>(encoded: RivetLoadEncoded, client: Client<Registry>, transform?: (current: unknown, incoming: unknown) => unknown): RivetQueryResult<unknown>;
95
+ export declare function decodeRivetLoad<Registry extends AnyActorRegistry>(encoded: RivetLoadEncoded, client: Client<Registry>, transform?: (current: unknown[], incoming: CrudEvent<unknown>) => unknown[]): RivetQueryResult<unknown>;
95
96
  export {};
@@ -5,6 +5,7 @@
5
5
  * On the client, transport.decode upgrades it to a live actor subscription.
6
6
  * On client-side navigation, rivetLoad creates a live subscription directly.
7
7
  */
8
+ import { crudTransform } from "../svelte/crud-transforms";
8
9
  const IS_BROWSER = typeof globalThis.document !== "undefined";
9
10
  // ============================================================================
10
11
  // RivetLoadResult — the serializable container
@@ -132,7 +133,7 @@ export function decodeRivetLoad(encoded, client, transform) {
132
133
  * Connects to the actor, subscribes to event(s), and keeps data reactive.
133
134
  */
134
135
  function createDetachedActorQuery(client, opts, initialData) {
135
- const { actor: actorName, key, event, params, createInRegion, createWithInput, transform = (_current, incoming) => incoming, } = opts;
136
+ const { actor: actorName, key, event, params, createInRegion, createWithInput, transform = crudTransform(), } = opts;
136
137
  let data = $state(initialData);
137
138
  let isLoading = $state(false);
138
139
  let error = $state(undefined);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blujosi/rivetkit-svelte",
3
- "version": "2.3.10",
3
+ "version": "2.3.12",
4
4
  "scripts": {
5
5
  "build": "vite build && npm run prepack",
6
6
  "preview": "vite preview",