@datafn/svelte 0.0.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 ADDED
@@ -0,0 +1,290 @@
1
+ # @datafn/svelte
2
+
3
+ Svelte bindings for DataFn. Converts DataFn reactive signals into Svelte readable stores for seamless integration with Svelte 3, 4, and 5 components.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @datafn/svelte @datafn/core
9
+ # Peer dependency
10
+ npm install svelte
11
+ ```
12
+
13
+ ## Features
14
+
15
+ - **Seamless Reactivity** — Mutations and sync changes in DataFn automatically update your Svelte components
16
+ - **Automatic Cleanup** — Store subscriptions are cleaned up when components are destroyed
17
+ - **State Properties** — Stores expose `data`, `loading`, `error`, and `refreshing` for complete UI control
18
+ - **Derived Store Support** — Returns standard Svelte `Readable`, composable with `derived` stores
19
+ - **Svelte 3 / 4 / 5 Compatible** — Works with reactive declarations (`$:`) and runes
20
+
21
+ ---
22
+
23
+ ## Quick Start
24
+
25
+ ```svelte
26
+ <script lang="ts">
27
+ import { createDatafnClient, IndexedDbStorageAdapter } from "@datafn/client";
28
+ import { toSvelteStore } from "@datafn/svelte";
29
+
30
+ const client = createDatafnClient({
31
+ schema: mySchema,
32
+ clientId: "device-1",
33
+ storage: new IndexedDbStorageAdapter("my-db"),
34
+ sync: { remote: "http://localhost:3000/datafn" },
35
+ });
36
+
37
+ // Create a DataFn signal (reactive query)
38
+ const signal = client.table("tasks").signal({
39
+ filters: { completed: false },
40
+ sort: ["-createdAt"],
41
+ });
42
+
43
+ // Convert to Svelte store
44
+ const tasks = toSvelteStore(signal);
45
+ </script>
46
+
47
+ {#if $tasks.loading}
48
+ <p>Loading...</p>
49
+ {:else if $tasks.error}
50
+ <p>Error: {$tasks.error.message}</p>
51
+ {:else}
52
+ {#each $tasks.data || [] as task (task.id)}
53
+ <div>{task.title}</div>
54
+ {/each}
55
+ {/if}
56
+ ```
57
+
58
+ ---
59
+
60
+ ## API
61
+
62
+ ### toSvelteStore\<T\>(signal): DatafnSvelteStore\<T\>
63
+
64
+ Converts a DataFn `Signal<T>` into a Svelte `Readable` store that wraps the signal value with state properties.
65
+
66
+ **Parameters:**
67
+
68
+ | Parameter | Type | Description |
69
+ |-----------|------|-------------|
70
+ | `signal` | `DatafnSignal<T>` | A signal from `table.signal()` or `client.kv.signal()` |
71
+
72
+ **Returns:**
73
+
74
+ A `Readable<{ data: T; loading: boolean; error: DatafnError | null; refreshing: boolean }>`.
75
+
76
+ | Property | Type | Description |
77
+ |----------|------|-------------|
78
+ | `data` | `T` | The current signal value (query results, KV value, etc.) |
79
+ | `loading` | `boolean` | `true` while the initial fetch is in progress |
80
+ | `error` | `DatafnError \| null` | Non-null if the last fetch/refresh failed |
81
+ | `refreshing` | `boolean` | `true` while a background refresh is in progress |
82
+
83
+ **Type:**
84
+
85
+ ```typescript
86
+ type DatafnSvelteStore<T> = Readable<{
87
+ data: T;
88
+ loading: boolean;
89
+ error: DatafnError | null;
90
+ refreshing: boolean;
91
+ }>;
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Usage Patterns
97
+
98
+ ### Table Query Signals
99
+
100
+ ```svelte
101
+ <script lang="ts">
102
+ import { toSvelteStore } from "@datafn/svelte";
103
+ import { client } from "./lib/datafn";
104
+
105
+ // All tasks
106
+ const allTasks = toSvelteStore(
107
+ client.table("tasks").signal({ sort: ["-createdAt"] })
108
+ );
109
+
110
+ // Filtered tasks
111
+ const activeTasks = toSvelteStore(
112
+ client.table("tasks").signal({
113
+ filters: { completed: false },
114
+ sort: ["priority", "-createdAt"],
115
+ })
116
+ );
117
+
118
+ // With select and limit
119
+ const recentTasks = toSvelteStore(
120
+ client.table("tasks").signal({
121
+ select: ["id", "title"],
122
+ sort: ["-createdAt"],
123
+ limit: 5,
124
+ })
125
+ );
126
+ </script>
127
+
128
+ <h2>Active Tasks ({($activeTasks.data || []).length})</h2>
129
+ {#each $activeTasks.data || [] as task (task.id)}
130
+ <div>{task.title}</div>
131
+ {/each}
132
+ ```
133
+
134
+ ### KV Signal Stores
135
+
136
+ ```svelte
137
+ <script lang="ts">
138
+ import { toSvelteStore } from "@datafn/svelte";
139
+ import { client } from "./lib/datafn";
140
+
141
+ // Reactive KV preference
142
+ const theme = toSvelteStore(
143
+ client.kv.signal<string>("pref:theme", { defaultValue: "dark" })
144
+ );
145
+
146
+ async function toggleTheme() {
147
+ const next = $theme.data === "dark" ? "light" : "dark";
148
+ await client.kv.set("pref:theme", next);
149
+ // Store auto-updates — no manual refresh needed
150
+ }
151
+ </script>
152
+
153
+ <button on:click={toggleTheme}>
154
+ Theme: {$theme.data}
155
+ </button>
156
+ ```
157
+
158
+ ### Derived Stores
159
+
160
+ Since `toSvelteStore` returns a standard Svelte `Readable`, you can compose it with `derived`:
161
+
162
+ ```typescript
163
+ import { derived } from "svelte/store";
164
+ import { toSvelteStore } from "@datafn/svelte";
165
+
166
+ const allTasks = toSvelteStore(client.table("tasks").signal({}));
167
+
168
+ // Compute statistics from the live data
169
+ const stats = derived(allTasks, ($tasks) => {
170
+ const data = $tasks.data || [];
171
+ return {
172
+ total: data.length,
173
+ completed: data.filter((t: any) => t.completed).length,
174
+ active: data.filter((t: any) => !t.completed).length,
175
+ };
176
+ });
177
+ ```
178
+
179
+ ```svelte
180
+ <p>{$stats.total} tasks — {$stats.active} active, {$stats.completed} done</p>
181
+ ```
182
+
183
+ ### Reactive Query Parameters (Svelte 3/4)
184
+
185
+ Re-create the store when a reactive variable changes:
186
+
187
+ ```svelte
188
+ <script>
189
+ import { toSvelteStore } from "@datafn/svelte";
190
+ import { client } from "./lib/datafn";
191
+
192
+ export let categoryId;
193
+
194
+ // Re-creates the store whenever categoryId changes
195
+ $: tasks = toSvelteStore(
196
+ client.table("tasks").signal({
197
+ filters: { categoryId },
198
+ sort: ["-createdAt"],
199
+ })
200
+ );
201
+ </script>
202
+
203
+ {#each $tasks.data || [] as task (task.id)}
204
+ <div>{task.title}</div>
205
+ {/each}
206
+ ```
207
+
208
+ ### Loading & Error States
209
+
210
+ Handle all states for a polished UX:
211
+
212
+ ```svelte
213
+ <script>
214
+ import { toSvelteStore } from "@datafn/svelte";
215
+ import { client } from "./lib/datafn";
216
+
217
+ const tasks = toSvelteStore(
218
+ client.table("tasks").signal({ sort: ["-createdAt"] })
219
+ );
220
+ </script>
221
+
222
+ {#if $tasks.loading}
223
+ <div class="skeleton">Loading tasks...</div>
224
+ {:else if $tasks.error}
225
+ <div class="error">
226
+ <p>Failed to load tasks: {$tasks.error.message}</p>
227
+ <code>{$tasks.error.code}</code>
228
+ </div>
229
+ {:else}
230
+ <ul>
231
+ {#each $tasks.data || [] as task (task.id)}
232
+ <li>{task.title}</li>
233
+ {/each}
234
+ </ul>
235
+
236
+ {#if $tasks.refreshing}
237
+ <small>Refreshing...</small>
238
+ {/if}
239
+ {/if}
240
+ ```
241
+
242
+ ### Multiple Stores in a Component
243
+
244
+ ```svelte
245
+ <script lang="ts">
246
+ import { toSvelteStore } from "@datafn/svelte";
247
+ import { client } from "./lib/datafn";
248
+
249
+ const todos = toSvelteStore(client.table("todos").signal({ sort: ["-createdAt"] }));
250
+ const categories = toSvelteStore(client.table("categories").signal({ sort: ["name"] }));
251
+ const theme = toSvelteStore(client.kv.signal("pref:theme", { defaultValue: "dark" }));
252
+ </script>
253
+
254
+ <main class={$theme.data}>
255
+ <h1>Todos ({($todos.data || []).length})</h1>
256
+ <!-- ... -->
257
+ <aside>
258
+ <h2>Categories ({($categories.data || []).length})</h2>
259
+ <!-- ... -->
260
+ </aside>
261
+ </main>
262
+ ```
263
+
264
+ ---
265
+
266
+ ## How It Works
267
+
268
+ 1. `toSvelteStore(signal)` creates a Svelte `readable` store
269
+ 2. On first subscriber, the store subscribes to the DataFn signal
270
+ 3. When the signal value changes (mutation, sync, or refresh), the store updates
271
+ 4. The store reads `signal.loading`, `signal.error`, and `signal.refreshing` on each update
272
+ 5. When the last subscriber unsubscribes (component destroyed), the signal subscription is cleaned up
273
+
274
+ This means:
275
+ - **Lazy**: No data is fetched until a component subscribes
276
+ - **Shared**: Multiple components using the same signal share a single cached query
277
+ - **Auto-updating**: Mutations to the underlying resource trigger automatic signal refresh
278
+
279
+ ---
280
+
281
+ ## Exports
282
+
283
+ ```typescript
284
+ export { toSvelteStore }
285
+ export type { DatafnSvelteStore }
286
+ ```
287
+
288
+ ## License
289
+
290
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ toSvelteStore: () => toSvelteStore
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/toSvelteStore.ts
28
+ var import_store = require("svelte/store");
29
+ function toSvelteStore(signalOrClientRef, signalFactory) {
30
+ if (signalFactory !== void 0) {
31
+ return toSvelteStoreFactory(signalOrClientRef, signalFactory);
32
+ }
33
+ return toSvelteStoreDirect(signalOrClientRef);
34
+ }
35
+ function toSvelteStoreDirect(signal) {
36
+ return (0, import_store.readable)(
37
+ {
38
+ data: signal.get(),
39
+ loading: signal.loading,
40
+ error: signal.error,
41
+ refreshing: signal.refreshing,
42
+ nextCursor: signal.nextCursor ?? null
43
+ },
44
+ (set) => {
45
+ const unsub = signal.subscribe((value) => {
46
+ set({
47
+ data: value,
48
+ loading: signal.loading,
49
+ error: signal.error,
50
+ refreshing: signal.refreshing,
51
+ nextCursor: signal.nextCursor ?? null
52
+ });
53
+ });
54
+ return () => {
55
+ unsub();
56
+ };
57
+ }
58
+ );
59
+ }
60
+ function toSvelteStoreFactory(clientRef, signalFactory) {
61
+ return (0, import_store.readable)(
62
+ { data: void 0, loading: true, error: null, refreshing: false, nextCursor: null },
63
+ (set) => {
64
+ let currentSignal = null;
65
+ let currentUnsubSignal = null;
66
+ const unsubClient = clientRef.subscribe((client) => {
67
+ currentUnsubSignal?.();
68
+ currentSignal?.dispose();
69
+ currentSignal = signalFactory(client);
70
+ set({
71
+ data: currentSignal.get(),
72
+ loading: currentSignal.loading,
73
+ error: currentSignal.error,
74
+ refreshing: currentSignal.refreshing,
75
+ nextCursor: currentSignal.nextCursor ?? null
76
+ });
77
+ currentUnsubSignal = currentSignal.subscribe((value) => {
78
+ set({
79
+ data: value,
80
+ loading: currentSignal.loading,
81
+ error: currentSignal.error,
82
+ refreshing: currentSignal.refreshing,
83
+ nextCursor: currentSignal.nextCursor ?? null
84
+ });
85
+ });
86
+ });
87
+ return () => {
88
+ unsubClient();
89
+ currentUnsubSignal?.();
90
+ currentSignal?.dispose();
91
+ currentSignal = null;
92
+ };
93
+ }
94
+ );
95
+ }
96
+ // Annotate the CommonJS export names for ESM import in node:
97
+ 0 && (module.exports = {
98
+ toSvelteStore
99
+ });
100
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/toSvelteStore.ts"],"sourcesContent":["/**\n * @datafn/svelte public API\n */\n\nexport { toSvelteStore } from \"./toSvelteStore.js\";\nexport type { DatafnSvelteStore, ClientRef } from \"./toSvelteStore.js\";\n","/**\n * Convert DataFn signal to Svelte store\n */\n\nimport type { DatafnSignal, DatafnError } from \"@datafn/core\";\nimport type { Readable } from \"svelte/store\";\nimport { readable } from \"svelte/store\";\n\n/**\n * A client reference that can notify subscribers when the underlying client\n * changes. Compatible with `{ subscribe: client.subscribeClient }` from a\n * unified `createDatafnClient`, or any Svelte writable store holding a client.\n */\nexport type ClientRef<C> = {\n subscribe(fn: (client: C) => void): () => void;\n};\n\n/**\n * Store type that wraps signal value with state properties.\n * Returned by both overloads.\n */\nexport type DatafnSvelteStore<T> = Readable<{\n data: T;\n loading: boolean;\n error: DatafnError | null;\n refreshing: boolean;\n nextCursor?: string | null;\n}>;\n\n/**\n * Convert a lifecycle-aware DataFn signal directly to a Svelte readable store.\n *\n * Signals from `createDatafnClient` are lifecycle-aware — they survive\n * `switchContext()` and rebind automatically. No factory pattern or `clientRef`\n * is needed.\n *\n * ```typescript\n * const todosStore = toSvelteStore(\n * client.todos.signal({ sort: [\"-createdAt\"] })\n * );\n * // Template: {#each $todosStore.data ?? [] as todo}\n * // Also: {#if $todosStore.loading} Loading... {/if}\n * ```\n */\nexport function toSvelteStore<T>(signal: DatafnSignal<T>): DatafnSvelteStore<T>;\n\n/**\n * Convert a DataFn signal factory to a reactive Svelte readable store.\n *\n * Accepts a `clientRef` (anything with a `subscribe(fn)` API) and a\n * `signalFactory` that creates a new signal from the current client. The store\n * automatically disposes the old signal and creates a fresh one whenever the\n * client changes, preventing memory leaks and ensuring signals are always\n * bound to the active client.\n */\nexport function toSvelteStore<T, C>(\n clientRef: ClientRef<C>,\n signalFactory: (client: C) => DatafnSignal<T>,\n): DatafnSvelteStore<T>;\n\nexport function toSvelteStore<T, C = any>(\n signalOrClientRef: DatafnSignal<T> | ClientRef<C>,\n signalFactory?: (client: C) => DatafnSignal<T>,\n): DatafnSvelteStore<T> {\n if (signalFactory !== undefined) {\n return toSvelteStoreFactory(signalOrClientRef as ClientRef<C>, signalFactory);\n }\n return toSvelteStoreDirect(signalOrClientRef as DatafnSignal<T>);\n}\n\n/**\n * Direct signal overload: wraps a DatafnSignal as a Svelte readable store.\n * The store emits `{ data, loading, error, refreshing }` on each update.\n * Does NOT call signal.dispose() on teardown — the signal's lifecycle is\n * managed by the LiveSignalRegistry, allowing multiple stores to share a\n * deduplicated signal safely.\n */\nfunction toSvelteStoreDirect<T>(signal: DatafnSignal<T>): DatafnSvelteStore<T> {\n return readable(\n {\n data: signal.get(),\n loading: signal.loading,\n error: signal.error,\n refreshing: signal.refreshing,\n nextCursor: signal.nextCursor ?? null,\n },\n (set) => {\n const unsub = signal.subscribe((value: T) => {\n set({\n data: value,\n loading: signal.loading,\n error: signal.error,\n refreshing: signal.refreshing,\n nextCursor: signal.nextCursor ?? null,\n });\n });\n return () => {\n unsub();\n };\n },\n );\n}\n\n/**\n * Factory overload: subscribes to clientRef, creates/disposes signal on each\n * client change. Returns a DatafnSvelteStore with { data, loading, error, refreshing }.\n */\nfunction toSvelteStoreFactory<T, C>(\n clientRef: ClientRef<C>,\n signalFactory: (client: C) => DatafnSignal<T>,\n): DatafnSvelteStore<T> {\n return readable(\n { data: undefined as unknown as T, loading: true as boolean, error: null as DatafnError | null, refreshing: false as boolean, nextCursor: null as string | null },\n (set) => {\n let currentSignal: DatafnSignal<T> | null = null;\n let currentUnsubSignal: (() => void) | null = null;\n\n const unsubClient = clientRef.subscribe((client) => {\n // Tear down previous signal\n currentUnsubSignal?.();\n currentSignal?.dispose();\n\n // Create a fresh signal from the new client\n currentSignal = signalFactory(client);\n\n // Emit the initial value immediately\n set({\n data: currentSignal.get(),\n loading: currentSignal.loading,\n error: currentSignal.error,\n refreshing: currentSignal.refreshing,\n nextCursor: currentSignal.nextCursor ?? null,\n });\n\n // Subscribe to future updates\n currentUnsubSignal = currentSignal.subscribe((value: T) => {\n set({\n data: value,\n loading: currentSignal!.loading,\n error: currentSignal!.error,\n refreshing: currentSignal!.refreshing,\n nextCursor: currentSignal!.nextCursor ?? null,\n });\n });\n });\n\n // Cleanup when all Svelte subscribers unsubscribe\n return () => {\n unsubClient();\n currentUnsubSignal?.();\n currentSignal?.dispose();\n currentSignal = null;\n };\n },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACMA,mBAAyB;AAsDlB,SAAS,cACd,mBACA,eACsB;AACtB,MAAI,kBAAkB,QAAW;AAC/B,WAAO,qBAAqB,mBAAmC,aAAa;AAAA,EAC9E;AACA,SAAO,oBAAoB,iBAAoC;AACjE;AASA,SAAS,oBAAuB,QAA+C;AAC7E,aAAO;AAAA,IACL;AAAA,MACE,MAAM,OAAO,IAAI;AAAA,MACjB,SAAS,OAAO;AAAA,MAChB,OAAO,OAAO;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,YAAY,OAAO,cAAc;AAAA,IACnC;AAAA,IACA,CAAC,QAAQ;AACP,YAAM,QAAQ,OAAO,UAAU,CAAC,UAAa;AAC3C,YAAI;AAAA,UACF,MAAM;AAAA,UACN,SAAS,OAAO;AAAA,UAChB,OAAO,OAAO;AAAA,UACd,YAAY,OAAO;AAAA,UACnB,YAAY,OAAO,cAAc;AAAA,QACnC,CAAC;AAAA,MACH,CAAC;AACD,aAAO,MAAM;AACX,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;AAMA,SAAS,qBACP,WACA,eACsB;AACtB,aAAO;AAAA,IACL,EAAE,MAAM,QAA2B,SAAS,MAAiB,OAAO,MAA4B,YAAY,OAAkB,YAAY,KAAsB;AAAA,IAChK,CAAC,QAAQ;AACP,UAAI,gBAAwC;AAC5C,UAAI,qBAA0C;AAE9C,YAAM,cAAc,UAAU,UAAU,CAAC,WAAW;AAElD,6BAAqB;AACrB,uBAAe,QAAQ;AAGvB,wBAAgB,cAAc,MAAM;AAGpC,YAAI;AAAA,UACF,MAAM,cAAc,IAAI;AAAA,UACxB,SAAS,cAAc;AAAA,UACvB,OAAO,cAAc;AAAA,UACrB,YAAY,cAAc;AAAA,UAC1B,YAAY,cAAc,cAAc;AAAA,QAC1C,CAAC;AAGD,6BAAqB,cAAc,UAAU,CAAC,UAAa;AACzD,cAAI;AAAA,YACF,MAAM;AAAA,YACN,SAAS,cAAe;AAAA,YACxB,OAAO,cAAe;AAAA,YACtB,YAAY,cAAe;AAAA,YAC3B,YAAY,cAAe,cAAc;AAAA,UAC3C,CAAC;AAAA,QACH,CAAC;AAAA,MACH,CAAC;AAGD,aAAO,MAAM;AACX,oBAAY;AACZ,6BAAqB;AACrB,uBAAe,QAAQ;AACvB,wBAAgB;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,54 @@
1
+ import { DatafnSignal, DatafnError } from '@datafn/core';
2
+ import { Readable } from 'svelte/store';
3
+
4
+ /**
5
+ * Convert DataFn signal to Svelte store
6
+ */
7
+
8
+ /**
9
+ * A client reference that can notify subscribers when the underlying client
10
+ * changes. Compatible with `{ subscribe: client.subscribeClient }` from a
11
+ * unified `createDatafnClient`, or any Svelte writable store holding a client.
12
+ */
13
+ type ClientRef<C> = {
14
+ subscribe(fn: (client: C) => void): () => void;
15
+ };
16
+ /**
17
+ * Store type that wraps signal value with state properties.
18
+ * Returned by both overloads.
19
+ */
20
+ type DatafnSvelteStore<T> = Readable<{
21
+ data: T;
22
+ loading: boolean;
23
+ error: DatafnError | null;
24
+ refreshing: boolean;
25
+ nextCursor?: string | null;
26
+ }>;
27
+ /**
28
+ * Convert a lifecycle-aware DataFn signal directly to a Svelte readable store.
29
+ *
30
+ * Signals from `createDatafnClient` are lifecycle-aware — they survive
31
+ * `switchContext()` and rebind automatically. No factory pattern or `clientRef`
32
+ * is needed.
33
+ *
34
+ * ```typescript
35
+ * const todosStore = toSvelteStore(
36
+ * client.todos.signal({ sort: ["-createdAt"] })
37
+ * );
38
+ * // Template: {#each $todosStore.data ?? [] as todo}
39
+ * // Also: {#if $todosStore.loading} Loading... {/if}
40
+ * ```
41
+ */
42
+ declare function toSvelteStore<T>(signal: DatafnSignal<T>): DatafnSvelteStore<T>;
43
+ /**
44
+ * Convert a DataFn signal factory to a reactive Svelte readable store.
45
+ *
46
+ * Accepts a `clientRef` (anything with a `subscribe(fn)` API) and a
47
+ * `signalFactory` that creates a new signal from the current client. The store
48
+ * automatically disposes the old signal and creates a fresh one whenever the
49
+ * client changes, preventing memory leaks and ensuring signals are always
50
+ * bound to the active client.
51
+ */
52
+ declare function toSvelteStore<T, C>(clientRef: ClientRef<C>, signalFactory: (client: C) => DatafnSignal<T>): DatafnSvelteStore<T>;
53
+
54
+ export { type ClientRef, type DatafnSvelteStore, toSvelteStore };
@@ -0,0 +1,54 @@
1
+ import { DatafnSignal, DatafnError } from '@datafn/core';
2
+ import { Readable } from 'svelte/store';
3
+
4
+ /**
5
+ * Convert DataFn signal to Svelte store
6
+ */
7
+
8
+ /**
9
+ * A client reference that can notify subscribers when the underlying client
10
+ * changes. Compatible with `{ subscribe: client.subscribeClient }` from a
11
+ * unified `createDatafnClient`, or any Svelte writable store holding a client.
12
+ */
13
+ type ClientRef<C> = {
14
+ subscribe(fn: (client: C) => void): () => void;
15
+ };
16
+ /**
17
+ * Store type that wraps signal value with state properties.
18
+ * Returned by both overloads.
19
+ */
20
+ type DatafnSvelteStore<T> = Readable<{
21
+ data: T;
22
+ loading: boolean;
23
+ error: DatafnError | null;
24
+ refreshing: boolean;
25
+ nextCursor?: string | null;
26
+ }>;
27
+ /**
28
+ * Convert a lifecycle-aware DataFn signal directly to a Svelte readable store.
29
+ *
30
+ * Signals from `createDatafnClient` are lifecycle-aware — they survive
31
+ * `switchContext()` and rebind automatically. No factory pattern or `clientRef`
32
+ * is needed.
33
+ *
34
+ * ```typescript
35
+ * const todosStore = toSvelteStore(
36
+ * client.todos.signal({ sort: ["-createdAt"] })
37
+ * );
38
+ * // Template: {#each $todosStore.data ?? [] as todo}
39
+ * // Also: {#if $todosStore.loading} Loading... {/if}
40
+ * ```
41
+ */
42
+ declare function toSvelteStore<T>(signal: DatafnSignal<T>): DatafnSvelteStore<T>;
43
+ /**
44
+ * Convert a DataFn signal factory to a reactive Svelte readable store.
45
+ *
46
+ * Accepts a `clientRef` (anything with a `subscribe(fn)` API) and a
47
+ * `signalFactory` that creates a new signal from the current client. The store
48
+ * automatically disposes the old signal and creates a fresh one whenever the
49
+ * client changes, preventing memory leaks and ensuring signals are always
50
+ * bound to the active client.
51
+ */
52
+ declare function toSvelteStore<T, C>(clientRef: ClientRef<C>, signalFactory: (client: C) => DatafnSignal<T>): DatafnSvelteStore<T>;
53
+
54
+ export { type ClientRef, type DatafnSvelteStore, toSvelteStore };
package/dist/index.js ADDED
@@ -0,0 +1,73 @@
1
+ // src/toSvelteStore.ts
2
+ import { readable } from "svelte/store";
3
+ function toSvelteStore(signalOrClientRef, signalFactory) {
4
+ if (signalFactory !== void 0) {
5
+ return toSvelteStoreFactory(signalOrClientRef, signalFactory);
6
+ }
7
+ return toSvelteStoreDirect(signalOrClientRef);
8
+ }
9
+ function toSvelteStoreDirect(signal) {
10
+ return readable(
11
+ {
12
+ data: signal.get(),
13
+ loading: signal.loading,
14
+ error: signal.error,
15
+ refreshing: signal.refreshing,
16
+ nextCursor: signal.nextCursor ?? null
17
+ },
18
+ (set) => {
19
+ const unsub = signal.subscribe((value) => {
20
+ set({
21
+ data: value,
22
+ loading: signal.loading,
23
+ error: signal.error,
24
+ refreshing: signal.refreshing,
25
+ nextCursor: signal.nextCursor ?? null
26
+ });
27
+ });
28
+ return () => {
29
+ unsub();
30
+ };
31
+ }
32
+ );
33
+ }
34
+ function toSvelteStoreFactory(clientRef, signalFactory) {
35
+ return readable(
36
+ { data: void 0, loading: true, error: null, refreshing: false, nextCursor: null },
37
+ (set) => {
38
+ let currentSignal = null;
39
+ let currentUnsubSignal = null;
40
+ const unsubClient = clientRef.subscribe((client) => {
41
+ currentUnsubSignal?.();
42
+ currentSignal?.dispose();
43
+ currentSignal = signalFactory(client);
44
+ set({
45
+ data: currentSignal.get(),
46
+ loading: currentSignal.loading,
47
+ error: currentSignal.error,
48
+ refreshing: currentSignal.refreshing,
49
+ nextCursor: currentSignal.nextCursor ?? null
50
+ });
51
+ currentUnsubSignal = currentSignal.subscribe((value) => {
52
+ set({
53
+ data: value,
54
+ loading: currentSignal.loading,
55
+ error: currentSignal.error,
56
+ refreshing: currentSignal.refreshing,
57
+ nextCursor: currentSignal.nextCursor ?? null
58
+ });
59
+ });
60
+ });
61
+ return () => {
62
+ unsubClient();
63
+ currentUnsubSignal?.();
64
+ currentSignal?.dispose();
65
+ currentSignal = null;
66
+ };
67
+ }
68
+ );
69
+ }
70
+ export {
71
+ toSvelteStore
72
+ };
73
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/toSvelteStore.ts"],"sourcesContent":["/**\n * Convert DataFn signal to Svelte store\n */\n\nimport type { DatafnSignal, DatafnError } from \"@datafn/core\";\nimport type { Readable } from \"svelte/store\";\nimport { readable } from \"svelte/store\";\n\n/**\n * A client reference that can notify subscribers when the underlying client\n * changes. Compatible with `{ subscribe: client.subscribeClient }` from a\n * unified `createDatafnClient`, or any Svelte writable store holding a client.\n */\nexport type ClientRef<C> = {\n subscribe(fn: (client: C) => void): () => void;\n};\n\n/**\n * Store type that wraps signal value with state properties.\n * Returned by both overloads.\n */\nexport type DatafnSvelteStore<T> = Readable<{\n data: T;\n loading: boolean;\n error: DatafnError | null;\n refreshing: boolean;\n nextCursor?: string | null;\n}>;\n\n/**\n * Convert a lifecycle-aware DataFn signal directly to a Svelte readable store.\n *\n * Signals from `createDatafnClient` are lifecycle-aware — they survive\n * `switchContext()` and rebind automatically. No factory pattern or `clientRef`\n * is needed.\n *\n * ```typescript\n * const todosStore = toSvelteStore(\n * client.todos.signal({ sort: [\"-createdAt\"] })\n * );\n * // Template: {#each $todosStore.data ?? [] as todo}\n * // Also: {#if $todosStore.loading} Loading... {/if}\n * ```\n */\nexport function toSvelteStore<T>(signal: DatafnSignal<T>): DatafnSvelteStore<T>;\n\n/**\n * Convert a DataFn signal factory to a reactive Svelte readable store.\n *\n * Accepts a `clientRef` (anything with a `subscribe(fn)` API) and a\n * `signalFactory` that creates a new signal from the current client. The store\n * automatically disposes the old signal and creates a fresh one whenever the\n * client changes, preventing memory leaks and ensuring signals are always\n * bound to the active client.\n */\nexport function toSvelteStore<T, C>(\n clientRef: ClientRef<C>,\n signalFactory: (client: C) => DatafnSignal<T>,\n): DatafnSvelteStore<T>;\n\nexport function toSvelteStore<T, C = any>(\n signalOrClientRef: DatafnSignal<T> | ClientRef<C>,\n signalFactory?: (client: C) => DatafnSignal<T>,\n): DatafnSvelteStore<T> {\n if (signalFactory !== undefined) {\n return toSvelteStoreFactory(signalOrClientRef as ClientRef<C>, signalFactory);\n }\n return toSvelteStoreDirect(signalOrClientRef as DatafnSignal<T>);\n}\n\n/**\n * Direct signal overload: wraps a DatafnSignal as a Svelte readable store.\n * The store emits `{ data, loading, error, refreshing }` on each update.\n * Does NOT call signal.dispose() on teardown — the signal's lifecycle is\n * managed by the LiveSignalRegistry, allowing multiple stores to share a\n * deduplicated signal safely.\n */\nfunction toSvelteStoreDirect<T>(signal: DatafnSignal<T>): DatafnSvelteStore<T> {\n return readable(\n {\n data: signal.get(),\n loading: signal.loading,\n error: signal.error,\n refreshing: signal.refreshing,\n nextCursor: signal.nextCursor ?? null,\n },\n (set) => {\n const unsub = signal.subscribe((value: T) => {\n set({\n data: value,\n loading: signal.loading,\n error: signal.error,\n refreshing: signal.refreshing,\n nextCursor: signal.nextCursor ?? null,\n });\n });\n return () => {\n unsub();\n };\n },\n );\n}\n\n/**\n * Factory overload: subscribes to clientRef, creates/disposes signal on each\n * client change. Returns a DatafnSvelteStore with { data, loading, error, refreshing }.\n */\nfunction toSvelteStoreFactory<T, C>(\n clientRef: ClientRef<C>,\n signalFactory: (client: C) => DatafnSignal<T>,\n): DatafnSvelteStore<T> {\n return readable(\n { data: undefined as unknown as T, loading: true as boolean, error: null as DatafnError | null, refreshing: false as boolean, nextCursor: null as string | null },\n (set) => {\n let currentSignal: DatafnSignal<T> | null = null;\n let currentUnsubSignal: (() => void) | null = null;\n\n const unsubClient = clientRef.subscribe((client) => {\n // Tear down previous signal\n currentUnsubSignal?.();\n currentSignal?.dispose();\n\n // Create a fresh signal from the new client\n currentSignal = signalFactory(client);\n\n // Emit the initial value immediately\n set({\n data: currentSignal.get(),\n loading: currentSignal.loading,\n error: currentSignal.error,\n refreshing: currentSignal.refreshing,\n nextCursor: currentSignal.nextCursor ?? null,\n });\n\n // Subscribe to future updates\n currentUnsubSignal = currentSignal.subscribe((value: T) => {\n set({\n data: value,\n loading: currentSignal!.loading,\n error: currentSignal!.error,\n refreshing: currentSignal!.refreshing,\n nextCursor: currentSignal!.nextCursor ?? null,\n });\n });\n });\n\n // Cleanup when all Svelte subscribers unsubscribe\n return () => {\n unsubClient();\n currentUnsubSignal?.();\n currentSignal?.dispose();\n currentSignal = null;\n };\n },\n );\n}\n"],"mappings":";AAMA,SAAS,gBAAgB;AAsDlB,SAAS,cACd,mBACA,eACsB;AACtB,MAAI,kBAAkB,QAAW;AAC/B,WAAO,qBAAqB,mBAAmC,aAAa;AAAA,EAC9E;AACA,SAAO,oBAAoB,iBAAoC;AACjE;AASA,SAAS,oBAAuB,QAA+C;AAC7E,SAAO;AAAA,IACL;AAAA,MACE,MAAM,OAAO,IAAI;AAAA,MACjB,SAAS,OAAO;AAAA,MAChB,OAAO,OAAO;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,YAAY,OAAO,cAAc;AAAA,IACnC;AAAA,IACA,CAAC,QAAQ;AACP,YAAM,QAAQ,OAAO,UAAU,CAAC,UAAa;AAC3C,YAAI;AAAA,UACF,MAAM;AAAA,UACN,SAAS,OAAO;AAAA,UAChB,OAAO,OAAO;AAAA,UACd,YAAY,OAAO;AAAA,UACnB,YAAY,OAAO,cAAc;AAAA,QACnC,CAAC;AAAA,MACH,CAAC;AACD,aAAO,MAAM;AACX,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;AAMA,SAAS,qBACP,WACA,eACsB;AACtB,SAAO;AAAA,IACL,EAAE,MAAM,QAA2B,SAAS,MAAiB,OAAO,MAA4B,YAAY,OAAkB,YAAY,KAAsB;AAAA,IAChK,CAAC,QAAQ;AACP,UAAI,gBAAwC;AAC5C,UAAI,qBAA0C;AAE9C,YAAM,cAAc,UAAU,UAAU,CAAC,WAAW;AAElD,6BAAqB;AACrB,uBAAe,QAAQ;AAGvB,wBAAgB,cAAc,MAAM;AAGpC,YAAI;AAAA,UACF,MAAM,cAAc,IAAI;AAAA,UACxB,SAAS,cAAc;AAAA,UACvB,OAAO,cAAc;AAAA,UACrB,YAAY,cAAc;AAAA,UAC1B,YAAY,cAAc,cAAc;AAAA,QAC1C,CAAC;AAGD,6BAAqB,cAAc,UAAU,CAAC,UAAa;AACzD,cAAI;AAAA,YACF,MAAM;AAAA,YACN,SAAS,cAAe;AAAA,YACxB,OAAO,cAAe;AAAA,YACtB,YAAY,cAAe;AAAA,YAC3B,YAAY,cAAe,cAAc;AAAA,UAC3C,CAAC;AAAA,QACH,CAAC;AAAA,MACH,CAAC;AAGD,aAAO,MAAM;AACX,oBAAY;AACZ,6BAAqB;AACrB,uBAAe,QAAQ;AACvB,wBAAgB;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@datafn/svelte",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "description": "Svelte adapter for DataFn signals",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest"
20
+ },
21
+ "dependencies": {
22
+ "@datafn/core": "*",
23
+ "svelte": "^4.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "tsup": "^8.0.0",
27
+ "typescript": "^5.3.0",
28
+ "vitest": "^1.0.0"
29
+ },
30
+ "peerDependencies": {
31
+ "svelte": "^3.0.0 || ^4.0.0"
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "author": "21n",
37
+ "license": "MIT",
38
+ "bugs": {
39
+ "url": "https://github.com/21nCo/super-functions/issues"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/21nCo/super-functions.git",
44
+ "directory": "datafn/svelte"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }