@sveltebase/sync 1.0.6 → 1.1.0

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
@@ -90,25 +90,23 @@ export const sync = new SyncClient<AppDatabaseSchema>({
90
90
  },
91
91
  },
92
92
  });
93
-
94
- // Export typed table wrapper
95
- export const todosTable = sync.table("todos");
96
93
  ```
97
94
 
98
- Use it in your Svelte 5 components:
95
+ Use it in your Svelte 5 components (using your preferred Dexie reactivity hook, such as `liveQuery` from `dexie` or `dexie-svelte-query`):
99
96
  ```svelte
100
97
  <script lang="ts">
101
- import { todosTable } from "$lib/sync-client";
98
+ import { sync } from "$lib/sync-client";
99
+ import { liveQuery } from "dexie";
102
100
  import { Check, Trash } from "lucide-svelte";
103
101
 
104
- // Reactive liveQuery updates instantly on local mutations & remote syncs
105
- const todos = todosTable.liveQuery((t) => t.orderBy("createdAt").reverse().toArray());
102
+ // Standard Dexie liveQuery updates instantly on mutations & remote syncs
103
+ const todos = liveQuery(() => sync.todos.orderBy("createdAt").reverse().toArray());
106
104
 
107
105
  let title = "";
108
106
 
109
107
  async function addTodo() {
110
108
  if (!title.trim()) return;
111
- await todosTable.add({
109
+ await sync.todos.add({
112
110
  id: crypto.randomUUID(),
113
111
  title,
114
112
  completed: false,
@@ -121,21 +119,15 @@ Use it in your Svelte 5 components:
121
119
 
122
120
  <input bind:value={title} onkeydown={(e) => e.key === 'Enter' && addTodo()} />
123
121
 
124
- {#if todos.isLoading}
125
- <div>Loading todos...</div>
126
- {:else if todos.status === "error"}
127
- <div>Error loading database: {todos.error?.message || todos.error}</div>
128
- {:else}
129
- {#each todos.data as todo (todo.id)}
130
- <div>
131
- <button onclick={() => todosTable.put(todo.id, { completed: !todo.completed })}>
132
- <Check class={todo.completed ? "text-emerald-500" : ""} />
133
- </button>
134
- <span>{todo.title}</span>
135
- <button onclick={() => todosTable.delete(todo.id)}><Trash /></button>
136
- </div>
137
- {/each}
138
- {/if}
122
+ {#each ($todos || []) as todo (todo.id)}
123
+ <div>
124
+ <button onclick={() => sync.todos.update(todo.id, { completed: !todo.completed })}>
125
+ <Check class={todo.completed ? "text-emerald-500" : ""} />
126
+ </button>
127
+ <span>{todo.title}</span>
128
+ <button onclick={() => sync.todos.delete(todo.id)}><Trash /></button>
129
+ </div>
130
+ {/each}
139
131
  ```
140
132
 
141
133
  ---
@@ -484,95 +476,8 @@ This approach keeps WebSocket URLs clean of private IDs and ensures all active s
484
476
 
485
477
  ---
486
478
 
487
- ## 5. Developer Experience (DX) & Client Enhancements
488
-
489
- The package features several DX improvements to simplify real-world application building:
490
-
491
- ### A. Reactive Connection Status
492
- You can track the WebSocket's connection state in Svelte 5 reactively using `sync.status`:
493
- ```svelte
494
- <script lang="ts">
495
- import { sync } from "$lib/sync-client";
496
- </script>
497
-
498
- {#if sync.status === "connecting"}
499
- <p class="text-amber-500">Connecting to real-time engine...</p>
500
- {:else if sync.status === "disconnected"}
501
- <p class="text-rose-500">Offline. Reconnecting...</p>
502
- {:else}
503
- <p class="text-emerald-500">Connected and Synced</p>
504
- {/if}
505
- ```
506
-
507
- ### B. Manual Instant Reconnection
508
- To instantly reconnect when the browser detects it has regained network access, you can call `sync.reconnect()`:
509
- ```typescript
510
- if (typeof window !== "undefined") {
511
- window.addEventListener("online", () => {
512
- sync.reconnect(); // Instantly reconnects without waiting for the 2-second backoff
513
- });
514
- }
515
- ```
516
-
517
- ### C. Dynamic & Async Connection URLs (e.g., JWT Auth)
518
- You can configure `url` as a function (optionally returning a Promise) to retrieve a fresh URL or append parameters (like dynamic access tokens) on every reconnection attempt:
519
- ```typescript
520
- export const sync = new SyncClient<AppDatabaseSchema>({
521
- name: "sveltebase-sync",
522
- url: async () => {
523
- const token = await getFreshJWTToken();
524
- return `/api/sync?token=${token}`;
525
- },
526
- tables: { ... }
527
- });
528
- ```
529
-
530
- ### D. Automatic Svelte 5 Parameter Reactivity
531
- `useLiveQuery` is built using Svelte 5 `$effect` under the hood. Any Svelte 5 `$state` or `$derived` variables referenced synchronously within the query function will automatically re-subscribe with the updated query boundaries when they change:
532
- ```svelte
533
- <script lang="ts">
534
- let queryTerm = $state("");
535
-
536
- // Re-subscribes and updates the live query automatically when `queryTerm` changes
537
- const results = todosTable.liveQuery((t) => {
538
- const term = queryTerm;
539
- return t.where("title").startsWith(term).toArray();
540
- });
541
- </script>
542
- ```
543
-
544
- ### E. Table Wrapper Utilities & Direct Dexie Access
545
- The `.table()` wrapper returns helpers to minimize boilerplate and exposes direct typed access to the underlying Dexie table:
546
- ```typescript
547
- const todosTable = sync.table("todos");
548
-
549
- // 1. Get a single row directly (non-reactive Promise)
550
- const todo = await todosTable.get("todo-id");
551
-
552
- // 2. Fetch all rows as a live query
553
- const allTodos = todosTable.list();
479
+ ## 5. Type-Safe Backend Event Publishing (`createPublisher`)
554
480
 
555
- // 3. Direct access to the raw Dexie table for advanced queries
556
- const count = await todosTable.rawTable.count();
557
- const active = await todosTable.rawTable.where("completed").equals(0).toArray();
558
- ```
559
-
560
- ### F. Structured Zod Validation Rejections
561
- When Zod validations (`validate.create` or `validate.update`) fail on the server, the server returns a formatted validation error. The client-side Promise is rejected with an Error that contains a `.validationErrors` array:
562
- ```typescript
563
- try {
564
- await todosTable.add({ title: "" });
565
- } catch (err: any) {
566
- if (err.validationErrors) {
567
- // validationErrors = [{ path: "title", message: "Required" }]
568
- console.error("Validation issues:", err.validationErrors);
569
- } else {
570
- console.error("General error:", err.message);
571
- }
572
- }
573
- ```
574
-
575
- ### G. Type-Safe Backend Event Publishing (`createPublisher`)
576
481
  When publishing backend events (e.g. from standard API routes, message queues, or cron triggers) to push updates to connected clients, you can create a type-safe publisher matched to your application's database schema. This checks channels (including dynamic channel patterns like `"todos:user_123"`), actions, and payloads at compile-time:
577
482
 
578
483
  ```typescript
@@ -584,22 +489,25 @@ type AppSyncSchema = {
584
489
  todos: Todo;
585
490
  };
586
491
 
587
- // Create typed publisher instance
588
- const publisher = createPublisher<AppSyncSchema>();
492
+ // Create typed publish function (Option A: Explicit Schema)
493
+ const publish = createPublisher<AppSyncSchema>();
494
+
495
+ // Create typed publish function (Option B: Automatically inferred from Sync Handlers)
496
+ import { handlers } from "./lib/server/sync-handlers.js";
497
+ const publish = createPublisher(handlers);
589
498
 
590
499
  // 1. Publish a create event (expects full Todo payload)
591
- await publisher.publish("todos", "create", todo.id, todo);
500
+ await publish("todos", "create", todo.id, todo);
592
501
 
593
502
  // 2. Publish an update event (expects Partial<Todo> payload)
594
- await publisher.publish("todos", "update", todo.id, { completed: true });
503
+ await publish("todos", "update", todo.id, { completed: true });
595
504
 
596
505
  // 3. Publish a delete event (expects optional { updatedAt: string } metadata)
597
- await publisher.publish("todos", "delete", todo.id, undefined);
506
+ await publish("todos", "delete", todo.id, undefined);
598
507
 
599
508
  // 4. Supports scoped/dynamic channels (e.g. "channelName:scopeId")
600
- await publisher.publish("todos:user_123", "update", todo.id, { title: "New Title" });
509
+ await publish("todos:user_123", "update", todo.id, { title: "New Title" });
601
510
  ```
602
511
 
603
512
 
604
513
 
605
-
@@ -1,6 +1,4 @@
1
1
  import Dexie, { type Table } from "dexie";
2
- import { useLiveQuery, type LiveQueryResult } from "./live.svelte.js";
3
- export { useLiveQuery, type LiveQueryResult };
4
2
  export type TableConfig = {
5
3
  indexes: string;
6
4
  channel: string;
@@ -10,8 +8,7 @@ export type SyncClientOptions = {
10
8
  url: string | (() => string | Promise<string>);
11
9
  tables: Record<string, TableConfig>;
12
10
  };
13
- export declare class SyncClient<TSchema extends Record<string, any> = Record<string, any>> {
14
- db: Dexie;
11
+ declare class SyncClientClass<TSchema extends Record<string, any> = Record<string, any>> extends Dexie {
15
12
  private wsUrl;
16
13
  private socket;
17
14
  private tableConfigs;
@@ -28,6 +25,7 @@ export declare class SyncClient<TSchema extends Record<string, any> = Record<str
28
25
  tables: Record<keyof TSchema & string, TableConfig>;
29
26
  });
30
27
  get status(): "connecting" | "connected" | "disconnected";
28
+ private decorateTables;
31
29
  private connect;
32
30
  reconnect(): void;
33
31
  private startHeartbeat;
@@ -38,16 +36,16 @@ export declare class SyncClient<TSchema extends Record<string, any> = Record<str
38
36
  private safeDeleteRow;
39
37
  private handleServerMessage;
40
38
  private findTableByChannel;
41
- table<TKey extends keyof TSchema & string>(tableName: TKey): {
42
- rawTable: Table<TSchema[TKey], string, TSchema[TKey]>;
43
- liveQuery: <TResult = TSchema[TKey][]>(queryFn: (table: Table<TSchema[TKey], string>) => Promise<TResult> | TResult) => LiveQueryResult<TResult>;
44
- list: () => LiveQueryResult<TSchema[TKey][]>;
45
- get: (id: string) => Promise<TSchema[TKey] | undefined>;
46
- add: (row: TSchema[TKey]) => Promise<TSchema[TKey]>;
47
- put: (id: string, changes: Partial<TSchema[TKey]>) => Promise<TSchema[TKey]>;
48
- delete: (id: string) => Promise<void>;
49
- };
50
39
  private enqueueMutation;
51
40
  disconnect(): void;
52
41
  }
42
+ export type SyncClient<TSchema extends Record<string, any> = Record<string, any>> = SyncClientClass<TSchema> & {
43
+ [K in keyof TSchema]: Table<TSchema[K]>;
44
+ };
45
+ export declare const SyncClient: new <TSchema extends Record<string, any> = Record<string, any>>(options: {
46
+ name: string;
47
+ url: string | (() => string | Promise<string>);
48
+ tables: Record<keyof TSchema & string, TableConfig>;
49
+ }) => SyncClient<TSchema>;
50
+ export {};
53
51
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,KAAK,KAAK,EAAE,MAAM,OAAO,CAAC;AAE1C,OAAO,EAAE,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEtE,OAAO,EAAE,YAAY,EAAE,KAAK,eAAe,EAAE,CAAC;AAE9C,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACrC,CAAC;AAaF,qBAAa,UAAU,CACrB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAElD,EAAE,EAAE,KAAK,CAAC;IACjB,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,cAAc,CAA4C;IAClE,OAAO,CAAC,YAAY,CAA6C;IACjE,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,cAAc,CAAqB;IAG3C,OAAO,CAAC,OAAO,CAAqE;IAGpF,OAAO,CAAC,gBAAgB,CAAsC;IAE9D,OAAO,CAAC,aAAa,CAMb;gBAEI,OAAO,EAAE;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,EAAE,WAAW,CAAC,CAAC;KACrD;IAiBD,IAAW,MAAM,gDAEhB;YAEa,OAAO;IAuFd,SAAS;IAgBhB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,aAAa;YAOP,kBAAkB;IAoBhC,OAAO,CAAC,kBAAkB;YAkBZ,UAAU;YAgBV,aAAa;YAoBb,mBAAmB;IAwEjC,OAAO,CAAC,kBAAkB;IAOnB,KAAK,CAAC,IAAI,SAAS,MAAM,OAAO,GAAG,MAAM,EAAE,SAAS,EAAE,IAAI;;oBAWjD,OAAO,6BACR,CAAC,KAAK,EAAE,KAAK,gBAAO,MAAM,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO;oBAK3D,eAAe,CAAC,eAAM,CAAC;kBAIjB,MAAM,KAAG,OAAO,CAAC,gBAAO,SAAS,CAAC;qCAI1B,OAAO,eAAM;kBAsBrB,MAAM,WAAW,OAAO,eAAM,KAAG,OAAO,eAAM;qBAyB3C,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;;IAuB7C,OAAO,CAAC,eAAe;IAqChB,UAAU;CAelB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,KAAK,KAAK,EAAwB,MAAM,OAAO,CAAC;AAGhE,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACrC,CAAC;AAaF,cAAM,eAAe,CACnB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CACzD,SAAQ,KAAK;IACb,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,cAAc,CAA4C;IAClE,OAAO,CAAC,YAAY,CAA6C;IACjE,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,cAAc,CAAqB;IAG3C,OAAO,CAAC,OAAO,CAAqE;IAGpF,OAAO,CAAC,gBAAgB,CAAsC;IAE9D,OAAO,CAAC,aAAa,CAMb;gBAEI,OAAO,EAAE;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,EAAE,WAAW,CAAC,CAAC;KACrD;IAoBD,IAAW,MAAM,gDAEhB;IAED,OAAO,CAAC,cAAc;YAiJR,OAAO;IAuFd,SAAS;IAgBhB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,aAAa;YAOP,kBAAkB;IAoBhC,OAAO,CAAC,kBAAkB;YAkBZ,UAAU;YAiBV,aAAa;YAqBb,mBAAmB;IAwEjC,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,eAAe;IAqChB,UAAU;CAelB;AAED,MAAM,MAAM,UAAU,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,eAAe,CAAC,OAAO,CAAC,GAAG;KAC5G,CAAC,IAAI,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;CACxC,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KACvB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzD,OAAO,EAAE;IACT,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,EAAE,WAAW,CAAC,CAAC;CACrD,KAAK,UAAU,CAAC,OAAO,CAA0B,CAAC"}
@@ -1,9 +1,6 @@
1
1
  import Dexie, {} from "dexie";
2
2
  import { parseSyncMessage } from "../protocol.js";
3
- import { useLiveQuery } from "./live.svelte.js";
4
- export { useLiveQuery };
5
- export class SyncClient {
6
- db;
3
+ class SyncClientClass extends Dexie {
7
4
  wsUrl;
8
5
  socket;
9
6
  tableConfigs;
@@ -18,15 +15,17 @@ export class SyncClient {
18
15
  // Mutations queued to be sent when connection is established
19
16
  mutationQueue = [];
20
17
  constructor(options) {
18
+ super(options.name);
21
19
  this.wsUrl = options.url;
22
20
  this.tableConfigs = options.tables;
23
21
  // Initialize Dexie database
24
- this.db = new Dexie(options.name);
25
22
  const schema = {};
26
23
  for (const [tableName, config] of Object.entries(options.tables)) {
27
24
  schema[tableName] = config.indexes;
28
25
  }
29
- this.db.version(1).stores(schema);
26
+ this.version(1).stores(schema);
27
+ // Decorate tables to intercept native Dexie write operations
28
+ this.decorateTables();
30
29
  if (typeof window !== "undefined") {
31
30
  this.connect();
32
31
  }
@@ -34,6 +33,96 @@ export class SyncClient {
34
33
  get status() {
35
34
  return this._status;
36
35
  }
36
+ decorateTables() {
37
+ for (const [tableName, config] of Object.entries(this.tableConfigs)) {
38
+ const table = this.table(tableName);
39
+ const originalAdd = table.add.bind(table);
40
+ const originalPut = table.put.bind(table);
41
+ const originalUpdate = table.update.bind(table);
42
+ const originalDelete = table.delete.bind(table);
43
+ // Save original methods to bypass sync trigger loops when updating from WebSocket
44
+ table._originalMethods = {
45
+ add: originalAdd,
46
+ put: originalPut,
47
+ update: originalUpdate,
48
+ delete: originalDelete,
49
+ };
50
+ table.add = (async (row) => {
51
+ const id = row.id || crypto.randomUUID();
52
+ const fullRow = { ...row, id };
53
+ const rollback = async () => {
54
+ await originalDelete(id);
55
+ };
56
+ await originalAdd(fullRow);
57
+ return this.enqueueMutation(config.channel, "create", id, fullRow, rollback);
58
+ });
59
+ table.put = (async (rowOrId, changes) => {
60
+ // Overload 1: put(id, changes) - Partial Update
61
+ if (changes !== undefined) {
62
+ const id = rowOrId;
63
+ const existing = await table.get(id);
64
+ if (!existing) {
65
+ throw new Error(`Cannot update item ${id}: not found locally.`);
66
+ }
67
+ const rollback = async () => {
68
+ await originalPut(existing);
69
+ };
70
+ const updatedRow = { ...existing, ...changes };
71
+ await originalPut(updatedRow);
72
+ const diff = {};
73
+ for (const [k, v] of Object.entries(changes)) {
74
+ if (existing[k] !== v) {
75
+ diff[k] = v;
76
+ }
77
+ }
78
+ return this.enqueueMutation(config.channel, "update", id, diff, rollback);
79
+ }
80
+ // Overload 2: put(row) - Insert/Replace
81
+ const row = rowOrId;
82
+ const id = row.id;
83
+ if (!id) {
84
+ throw new Error("put operation requires an inline 'id' property.");
85
+ }
86
+ const existing = await table.get(id);
87
+ if (!existing) {
88
+ return table.add(row);
89
+ }
90
+ const rollback = async () => {
91
+ await originalPut(existing);
92
+ };
93
+ const updatedRow = { ...existing, ...row };
94
+ await originalPut(updatedRow);
95
+ const diff = {};
96
+ for (const [k, v] of Object.entries(row)) {
97
+ if (existing[k] !== v) {
98
+ diff[k] = v;
99
+ }
100
+ }
101
+ return this.enqueueMutation(config.channel, "update", id, diff, rollback);
102
+ });
103
+ table.update = (async (id, changes) => {
104
+ const existing = await table.get(id);
105
+ if (!existing) {
106
+ throw new Error(`Cannot update item ${id}: not found locally.`);
107
+ }
108
+ const rollback = async () => {
109
+ await originalPut(existing);
110
+ };
111
+ await originalUpdate(id, changes);
112
+ return this.enqueueMutation(config.channel, "update", id, changes, rollback);
113
+ });
114
+ table.delete = (async (id) => {
115
+ const existing = await table.get(id);
116
+ if (!existing)
117
+ return;
118
+ const rollback = async () => {
119
+ await originalPut(existing);
120
+ };
121
+ await originalDelete(id);
122
+ return this.enqueueMutation(config.channel, "delete", id, undefined, rollback);
123
+ });
124
+ }
125
+ }
37
126
  async connect() {
38
127
  if (this.closedByClient)
39
128
  return;
@@ -146,7 +235,7 @@ export class SyncClient {
146
235
  let since;
147
236
  if (tableName) {
148
237
  try {
149
- const table = this.db.table(tableName);
238
+ const table = this.table(tableName);
150
239
  const latestRow = await table.orderBy("updatedAt").last();
151
240
  if (latestRow && latestRow.updatedAt) {
152
241
  since = latestRow.updatedAt;
@@ -176,7 +265,7 @@ export class SyncClient {
176
265
  }
177
266
  }
178
267
  async safePutRow(tableName, data) {
179
- const table = this.db.table(tableName);
268
+ const table = this.table(tableName);
180
269
  if (!data || !data.id)
181
270
  return;
182
271
  const existing = await table.get(data.id);
@@ -188,10 +277,11 @@ export class SyncClient {
188
277
  return;
189
278
  }
190
279
  }
191
- await table.put(data);
280
+ const originalPut = table._originalMethods?.put || table.put.bind(table);
281
+ await originalPut(data);
192
282
  }
193
283
  async safeDeleteRow(tableName, key, incomingTimeStr) {
194
- const table = this.db.table(tableName);
284
+ const table = this.table(tableName);
195
285
  if (incomingTimeStr) {
196
286
  const existing = await table.get(key);
197
287
  if (existing && existing.updatedAt) {
@@ -203,14 +293,15 @@ export class SyncClient {
203
293
  }
204
294
  }
205
295
  }
206
- await table.delete(key);
296
+ const originalDelete = table._originalMethods?.delete || table.delete.bind(table);
297
+ await originalDelete(key);
207
298
  }
208
299
  async handleServerMessage(msg) {
209
300
  switch (msg.type) {
210
301
  case "snapshot": {
211
302
  const tableName = this.findTableByChannel(msg.channel);
212
303
  if (tableName) {
213
- const table = this.db.table(tableName);
304
+ const table = this.table(tableName);
214
305
  if (msg.isDelta) {
215
306
  // Delta Sync: put changes using Last-Write-Wins
216
307
  for (const row of msg.data) {
@@ -219,7 +310,7 @@ export class SyncClient {
219
310
  }
220
311
  else {
221
312
  // Full Snapshot: clear and replace
222
- await this.db.transaction("rw", table, async () => {
313
+ await this.transaction("rw", table, async () => {
223
314
  await table.clear();
224
315
  await table.bulkPut(msg.data);
225
316
  });
@@ -282,63 +373,6 @@ export class SyncClient {
282
373
  }
283
374
  return undefined;
284
375
  }
285
- table(tableName) {
286
- const dexieTable = this.db.table(tableName);
287
- const config = this.tableConfigs[tableName];
288
- if (!config) {
289
- throw new Error(`Table ${tableName} not defined in SyncClient config.`);
290
- }
291
- return {
292
- rawTable: dexieTable,
293
- liveQuery: (queryFn) => {
294
- return useLiveQuery(() => queryFn(dexieTable));
295
- },
296
- list: () => {
297
- return useLiveQuery(() => dexieTable.toArray());
298
- },
299
- get: async (id) => {
300
- return dexieTable.get(id);
301
- },
302
- add: async (row) => {
303
- const rowData = row;
304
- const id = rowData.id || crypto.randomUUID();
305
- const fullRow = { ...rowData, id };
306
- // Rollback function
307
- const rollback = async () => {
308
- await dexieTable.delete(id);
309
- };
310
- // Apply optimistic update
311
- await dexieTable.put(fullRow);
312
- return this.enqueueMutation(config.channel, "create", id, fullRow, rollback);
313
- },
314
- put: async (id, changes) => {
315
- const existing = await dexieTable.get(id);
316
- if (!existing) {
317
- throw new Error(`Cannot update item ${id}: not found locally.`);
318
- }
319
- // Rollback function
320
- const rollback = async () => {
321
- await dexieTable.put(existing);
322
- };
323
- const updatedRow = { ...existing, ...changes };
324
- // Apply optimistic update
325
- await dexieTable.put(updatedRow);
326
- return this.enqueueMutation(config.channel, "update", id, changes, rollback);
327
- },
328
- delete: async (id) => {
329
- const existing = await dexieTable.get(id);
330
- if (!existing)
331
- return; // Already deleted
332
- // Rollback function
333
- const rollback = async () => {
334
- await dexieTable.put(existing);
335
- };
336
- // Apply optimistic update
337
- await dexieTable.delete(id);
338
- return this.enqueueMutation(config.channel, "delete", id, undefined, rollback);
339
- },
340
- };
341
- }
342
376
  enqueueMutation(channel, action, key, data, rollback) {
343
377
  const mutationId = crypto.randomUUID();
344
378
  return new Promise((resolve, reject) => {
@@ -384,3 +418,4 @@ export class SyncClient {
384
418
  }
385
419
  }
386
420
  }
421
+ export const SyncClient = SyncClientClass;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { SyncClient, useLiveQuery } from "./client/index.js";
1
+ export { SyncClient } from "./client/index.js";
2
2
  export { defineSync } from "./server/index.js";
3
3
  export { handleUpgrade, publishEvent, createPublisher } from "./server/handler.js";
4
- export type { PublishEventData } from "./server/handler.js";
4
+ export type { PublishEventData, InferSchemaFromHandlers } from "./server/handler.js";
5
5
  export type { SyncContext, SyncHandler } from "./server/index.js";
6
6
  export type { SyncMessage } from "./protocol.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACnF,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAClE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACnF,YAAY,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AACrF,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAClE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { SyncClient, useLiveQuery } from "./client/index.js";
1
+ export { SyncClient } from "./client/index.js";
2
2
  export { defineSync } from "./server/index.js";
3
3
  export { handleUpgrade, publishEvent, createPublisher } from "./server/handler.js";
@@ -1 +1 @@
1
- {"version":3,"file":"broker.d.ts","sourceRoot":"","sources":["../../src/server/broker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,YAAY,CAAC;AAG3D,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,OAAO,IAAI,GAAG,CAAC;IACf,OAAO,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;IACzB,qBAAqB,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,mBAAmB,CAAC,CAGV;gBAGhB,QAAQ,EAAE,WAAW,EAAE,EACvB,mBAAmB,CAAC,EAAE,CACpB,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,KAC/B,OAAO,CAAC,GAAG,CAAC;IAOZ,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE;IAanC,kBAAkB,CAAC,IAAI,EAAE,eAAe;IAIxC,gBAAgB,CAAC,IAAI,EAAE,eAAe;IAI7C;;OAEG;IACH,OAAO,CAAC,WAAW;IAqCN,aAAa,CACxB,IAAI,EAAE,eAAe,EACrB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,EAClC,OAAO,EAAE,OAAO;YAuJJ,eAAe;IAoDhB,oBAAoB,CAC/B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,GAAG;CAoBZ"}
1
+ {"version":3,"file":"broker.d.ts","sourceRoot":"","sources":["../../src/server/broker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,YAAY,CAAC;AAG3D,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,OAAO,IAAI,GAAG,CAAC;IACf,OAAO,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;IACzB,qBAAqB,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,mBAAmB,CAAC,CAGV;gBAGhB,QAAQ,EAAE,WAAW,EAAE,EACvB,mBAAmB,CAAC,EAAE,CACpB,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,KAC/B,OAAO,CAAC,GAAG,CAAC;IAOZ,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE;IAanC,kBAAkB,CAAC,IAAI,EAAE,eAAe;IAIxC,gBAAgB,CAAC,IAAI,EAAE,eAAe;IAI7C;;OAEG;IACH,OAAO,CAAC,WAAW;IAqCN,aAAa,CACxB,IAAI,EAAE,eAAe,EACrB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,EAClC,OAAO,EAAE,OAAO;YA0IJ,eAAe;IAoDhB,oBAAoB,CAC/B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,GAAG;CAoBZ"}
@@ -160,21 +160,10 @@ export class SyncBroker {
160
160
  catch (err) {
161
161
  console.error(`SyncBroker: error handling message type=${msg.type}:`, err);
162
162
  if (msg.type === "mutate") {
163
- let errorMessage = err.message || "Server error";
164
- let validationErrors = undefined;
165
- // Formats Zod validation errors for structured client rejection
166
- if (err && err.name === "ZodError" && Array.isArray(err.issues)) {
167
- validationErrors = err.issues.map((issue) => ({
168
- path: issue.path.join("."),
169
- message: issue.message,
170
- }));
171
- errorMessage = `Validation failed: ${err.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")}`;
172
- }
173
163
  conn.send(JSON.stringify({
174
164
  type: "reject",
175
165
  id: msg.id,
176
- error: errorMessage,
177
- validationErrors,
166
+ error: err.message || "Server error",
178
167
  }));
179
168
  }
180
169
  }
@@ -1,9 +1,12 @@
1
+ import type { SyncHandler } from "./index.js";
1
2
  export type PublishEventData<TRecord, TAction extends "create" | "update" | "delete"> = TAction extends "create" ? TRecord : TAction extends "update" ? Partial<TRecord> : {
2
3
  updatedAt?: string;
3
4
  } | undefined;
4
- export declare function createPublisher<TSchema extends Record<string, any>>(): {
5
- publish: <TChannel extends keyof TSchema & string, TAction extends "create" | "update" | "delete">(channel: TChannel | `${TChannel}:${string}`, action: TAction, key: string | undefined, data: PublishEventData<TSchema[TChannel], TAction>) => Promise<void>;
5
+ export type InferSchemaFromHandlers<T extends SyncHandler[]> = {
6
+ [K in T[number] as K["config"]["channel"] extends string ? K["config"]["channel"] : K["config"]["channel"] extends (...args: any[]) => infer R ? R extends string ? R : string : string]: K extends SyncHandler<infer TRow> ? TRow : never;
6
7
  };
8
+ export declare function createPublisher<TSchema extends Record<string, any>>(): <TChannel extends keyof TSchema & string, TAction extends "create" | "update" | "delete">(channel: TChannel | `${TChannel}:${string}`, action: TAction, key: string | undefined, data: PublishEventData<TSchema[TChannel], TAction>) => Promise<void>;
9
+ export declare function createPublisher<THandlers extends SyncHandler[]>(handlers: THandlers): <TChannel extends keyof InferSchemaFromHandlers<THandlers> & string, TAction extends "create" | "update" | "delete">(channel: TChannel | `${TChannel}:${string}`, action: TAction, key: string | undefined, data: PublishEventData<InferSchemaFromHandlers<THandlers>[TChannel], TAction>) => Promise<void>;
7
10
  export declare function publishEvent(channel: string, action: "create" | "update" | "delete", key: string | undefined, data: any): Promise<void>;
8
11
  export declare function handleUpgrade(request: Request, platform: App.Platform | undefined): Promise<Response>;
9
12
  //# sourceMappingURL=handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/server/handler.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,CAAC,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,QAAQ,GAAG,QAAQ,IAClF,OAAO,SAAS,QAAQ,GACpB,OAAO,GACP,OAAO,SAAS,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC,GAChB;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC;AAE3C,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;cAG7D,QAAQ,SAAS,MAAM,OAAO,GAAG,MAAM,EACvC,OAAO,SAAS,QAAQ,GAAG,QAAQ,GAAG,QAAQ,WAErC,QAAQ,GAAG,GAAG,QAAQ,IAAI,MAAM,EAAE,UACnC,OAAO,OACV,MAAM,GAAG,SAAS,QACjB,gBAAgB,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,KACjD,OAAO,CAAC,IAAI,CAAC;EAKnB;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,GAAG,iBAgCV;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,GACjC,OAAO,CAAC,QAAQ,CAAC,CAqBnB"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/server/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,MAAM,gBAAgB,CAAC,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,QAAQ,GAAG,QAAQ,IAClF,OAAO,SAAS,QAAQ,GACpB,OAAO,GACP,OAAO,SAAS,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC,GAChB;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC;AAE3C,MAAM,MAAM,uBAAuB,CAAC,CAAC,SAAS,WAAW,EAAE,IAAI;KAC5D,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,SAAS,MAAM,GACpD,CAAC,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,GACtB,CAAC,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,MAAM,CAAC,GACxD,CAAC,SAAS,MAAM,GACd,CAAC,GACD,MAAM,GACR,MAAM,GAAG,CAAC,SAAS,WAAW,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK;CAChE,CAAC;AAEF,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,CACtE,QAAQ,SAAS,MAAM,OAAO,GAAG,MAAM,EACvC,OAAO,SAAS,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EAE9C,OAAO,EAAE,QAAQ,GAAG,GAAG,QAAQ,IAAI,MAAM,EAAE,EAC3C,MAAM,EAAE,OAAO,EACf,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,gBAAgB,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,KAC/C,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,wBAAgB,eAAe,CAAC,SAAS,SAAS,WAAW,EAAE,EAC7D,QAAQ,EAAE,SAAS,GAClB,CACD,QAAQ,SAAS,MAAM,uBAAuB,CAAC,SAAS,CAAC,GAAG,MAAM,EAClE,OAAO,SAAS,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EAE9C,OAAO,EAAE,QAAQ,GAAG,GAAG,QAAQ,IAAI,MAAM,EAAE,EAC3C,MAAM,EAAE,OAAO,EACf,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,gBAAgB,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,KAC1E,OAAO,CAAC,IAAI,CAAC,CAAC;AAcnB,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,GAAG,iBAgCV;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,GACjC,OAAO,CAAC,QAAQ,CAAC,CAqBnB"}
@@ -1,9 +1,7 @@
1
- export function createPublisher() {
2
- return {
3
- publish: async (channel, action, key, data) => {
4
- const resolvedChannel = String(channel);
5
- return publishEvent(resolvedChannel, action, key, data);
6
- },
1
+ export function createPublisher(handlers) {
2
+ return async (channel, action, key, data) => {
3
+ const resolvedChannel = String(channel);
4
+ return publishEvent(resolvedChannel, action, key, data);
7
5
  };
8
6
  }
9
7
  export async function publishEvent(channel, action, key, data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltebase/sync",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,18 +0,0 @@
1
- export type LiveQueryResult<T> = {
2
- status: "loading";
3
- data: undefined;
4
- error: undefined;
5
- isLoading: true;
6
- } | {
7
- status: "success";
8
- data: T;
9
- error: undefined;
10
- isLoading: false;
11
- } | {
12
- status: "error";
13
- data: undefined;
14
- error: any;
15
- isLoading: false;
16
- };
17
- export declare function useLiveQuery<T>(queryFn: () => Promise<T> | T): LiveQueryResult<T>;
18
- //# sourceMappingURL=live.svelte.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"live.svelte.d.ts","sourceRoot":"","sources":["../../src/client/live.svelte.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,eAAe,CAAC,CAAC,IACzB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,GACzE;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,KAAK,CAAA;CAAE,GAClE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC;IAAC,SAAS,EAAE,KAAK,CAAA;CAAE,CAAC;AAEvE,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CA0CjF"}
@@ -1,41 +0,0 @@
1
- import { liveQuery } from "dexie";
2
- export function useLiveQuery(queryFn) {
3
- let data = $state(undefined);
4
- let error = $state(undefined);
5
- let status = $state("loading");
6
- if (typeof window !== "undefined") {
7
- $effect(() => {
8
- const observable = liveQuery(queryFn);
9
- const subscription = observable.subscribe({
10
- next: (val) => {
11
- data = val;
12
- error = undefined;
13
- status = "success";
14
- },
15
- error: (err) => {
16
- data = undefined;
17
- error = err;
18
- status = "error";
19
- console.error("liveQuery error:", err);
20
- },
21
- });
22
- return () => {
23
- subscription.unsubscribe();
24
- };
25
- });
26
- }
27
- return {
28
- get data() {
29
- return data;
30
- },
31
- get error() {
32
- return error;
33
- },
34
- get status() {
35
- return status;
36
- },
37
- get isLoading() {
38
- return status === "loading";
39
- },
40
- };
41
- }