@sveltebase/sync 1.0.7 → 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
  ---
@@ -1,17 +1,14 @@
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;
7
5
  };
8
6
  export type SyncClientOptions = {
9
7
  name: string;
10
- url: string;
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;
@@ -19,14 +16,18 @@ export declare class SyncClient<TSchema extends Record<string, any> = Record<str
19
16
  private pingInterval;
20
17
  private closedByClient;
21
18
  private activeChannels;
19
+ private _status;
22
20
  private pendingMutations;
23
21
  private mutationQueue;
24
22
  constructor(options: {
25
23
  name: string;
26
- url: string;
24
+ url: string | (() => string | Promise<string>);
27
25
  tables: Record<keyof TSchema & string, TableConfig>;
28
26
  });
27
+ get status(): "connecting" | "connected" | "disconnected";
28
+ private decorateTables;
29
29
  private connect;
30
+ reconnect(): void;
30
31
  private startHeartbeat;
31
32
  private stopHeartbeat;
32
33
  private subscribeToChannel;
@@ -35,13 +36,16 @@ export declare class SyncClient<TSchema extends Record<string, any> = Record<str
35
36
  private safeDeleteRow;
36
37
  private handleServerMessage;
37
38
  private findTableByChannel;
38
- table<TKey extends keyof TSchema & string>(tableName: TKey): {
39
- liveQuery: <TResult = TSchema[TKey][]>(queryFn: (table: Table<TSchema[TKey], string>) => Promise<TResult> | TResult) => LiveQueryResult<TResult>;
40
- add: (row: TSchema[TKey]) => Promise<TSchema[TKey]>;
41
- put: (id: string, changes: Partial<TSchema[TKey]>) => Promise<TSchema[TKey]>;
42
- delete: (id: string) => Promise<void>;
43
- };
44
39
  private enqueueMutation;
45
40
  disconnect(): void;
46
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 {};
47
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,CAAC;IACZ,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,CAAS;IACtB,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,gBAAgB,CAAsC;IAE9D,OAAO,CAAC,aAAa,CAMb;gBAEI,OAAO,EAAE;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,EAAE,WAAW,CAAC,CAAC;KACrD;IAiBD,OAAO,CAAC,OAAO;IA+Df,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,aAAa;YAOP,kBAAkB;IAoBhC,OAAO,CAAC,kBAAkB;YAkBZ,UAAU;YAgBV,aAAa;YAoBb,mBAAmB;IAkEjC,OAAO,CAAC,kBAAkB;IAOnB,KAAK,CAAC,IAAI,SAAS,MAAM,OAAO,GAAG,MAAM,EAAE,SAAS,EAAE,IAAI;oBASjD,OAAO,6BACR,CAAC,KAAK,EAAE,KAAK,gBAAO,MAAM,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO;qCAK7C,OAAO,eAAM;kBAsBrB,MAAM,WAAW,OAAO,eAAM,KAAG,OAAO,eAAM;qBAyB3C,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;;IAuB7C,OAAO,CAAC,eAAe;IAqChB,UAAU;CAYlB"}
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;
@@ -11,35 +8,149 @@ export class SyncClient {
11
8
  pingInterval;
12
9
  closedByClient = false;
13
10
  activeChannels = new Set();
11
+ // Reactive connection status
12
+ _status = $state("connecting");
14
13
  // Mutations waiting for ack/reject from server
15
14
  pendingMutations = new Map();
16
15
  // Mutations queued to be sent when connection is established
17
16
  mutationQueue = [];
18
17
  constructor(options) {
18
+ super(options.name);
19
19
  this.wsUrl = options.url;
20
20
  this.tableConfigs = options.tables;
21
21
  // Initialize Dexie database
22
- this.db = new Dexie(options.name);
23
22
  const schema = {};
24
23
  for (const [tableName, config] of Object.entries(options.tables)) {
25
24
  schema[tableName] = config.indexes;
26
25
  }
27
- this.db.version(1).stores(schema);
26
+ this.version(1).stores(schema);
27
+ // Decorate tables to intercept native Dexie write operations
28
+ this.decorateTables();
28
29
  if (typeof window !== "undefined") {
29
30
  this.connect();
30
31
  }
31
32
  }
32
- connect() {
33
+ get status() {
34
+ return this._status;
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
+ }
126
+ async connect() {
33
127
  if (this.closedByClient)
34
128
  return;
129
+ this._status = "connecting";
35
130
  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
36
131
  const host = window.location.host;
37
- const fullUrl = this.wsUrl.startsWith("ws://") || this.wsUrl.startsWith("wss://")
38
- ? this.wsUrl
39
- : `${protocol}//${host}${this.wsUrl}`;
40
- this.socket = new WebSocket(fullUrl);
41
- this.socket.addEventListener("open", async () => {
132
+ let resolvedUrl;
133
+ try {
134
+ resolvedUrl = typeof this.wsUrl === "function" ? await this.wsUrl() : this.wsUrl;
135
+ }
136
+ catch (err) {
137
+ console.error("SyncClient: Failed to resolve wsUrl", err);
138
+ this._status = "disconnected";
139
+ if (!this.closedByClient) {
140
+ this.reconnectTimer = setTimeout(() => this.connect(), 2000);
141
+ }
142
+ return;
143
+ }
144
+ const fullUrl = resolvedUrl.startsWith("ws://") || resolvedUrl.startsWith("wss://")
145
+ ? resolvedUrl
146
+ : `${protocol}//${host}${resolvedUrl}`;
147
+ const socket = new WebSocket(fullUrl);
148
+ this.socket = socket;
149
+ socket.addEventListener("open", async () => {
150
+ if (this.socket !== socket)
151
+ return;
42
152
  console.log("SyncClient: WebSocket connected");
153
+ this._status = "connected";
43
154
  this.activeChannels.clear();
44
155
  this.startHeartbeat();
45
156
  // Re-subscribe to all tables (delta-sync aware)
@@ -48,7 +159,7 @@ export class SyncClient {
48
159
  }
49
160
  // Re-send all pending unacknowledged mutations
50
161
  for (const mut of this.pendingMutations.values()) {
51
- this.socket?.send(JSON.stringify({
162
+ socket.send(JSON.stringify({
52
163
  type: "mutate",
53
164
  id: mut.id,
54
165
  channel: mut.channel,
@@ -60,7 +171,9 @@ export class SyncClient {
60
171
  // Flush queued mutations
61
172
  this.flushMutationQueue();
62
173
  });
63
- this.socket.addEventListener("message", async (message) => {
174
+ socket.addEventListener("message", async (message) => {
175
+ if (this.socket !== socket)
176
+ return;
64
177
  if (typeof message.data !== "string")
65
178
  return;
66
179
  if (message.data === "pong")
@@ -70,17 +183,38 @@ export class SyncClient {
70
183
  return;
71
184
  await this.handleServerMessage(msg);
72
185
  });
73
- this.socket.addEventListener("close", () => {
74
- this.socket = undefined;
75
- this.stopHeartbeat();
76
- if (!this.closedByClient) {
77
- this.reconnectTimer = setTimeout(() => this.connect(), 2000);
186
+ socket.addEventListener("close", () => {
187
+ if (this.socket === socket) {
188
+ this.socket = undefined;
189
+ this._status = "disconnected";
190
+ this.stopHeartbeat();
191
+ if (!this.closedByClient) {
192
+ this.reconnectTimer = setTimeout(() => this.connect(), 2000);
193
+ }
78
194
  }
79
195
  });
80
- this.socket.addEventListener("error", (err) => {
81
- console.error("SyncClient: WebSocket error", err);
196
+ socket.addEventListener("error", (err) => {
197
+ if (this.socket === socket) {
198
+ console.error("SyncClient: WebSocket error", err);
199
+ }
82
200
  });
83
201
  }
202
+ reconnect() {
203
+ this.closedByClient = false;
204
+ if (this.reconnectTimer) {
205
+ clearTimeout(this.reconnectTimer);
206
+ this.reconnectTimer = undefined;
207
+ }
208
+ if (this.socket) {
209
+ try {
210
+ this.socket.close();
211
+ }
212
+ catch { }
213
+ this.socket = undefined;
214
+ this._status = "disconnected";
215
+ }
216
+ this.connect();
217
+ }
84
218
  startHeartbeat() {
85
219
  this.stopHeartbeat();
86
220
  this.pingInterval = setInterval(() => {
@@ -101,7 +235,7 @@ export class SyncClient {
101
235
  let since;
102
236
  if (tableName) {
103
237
  try {
104
- const table = this.db.table(tableName);
238
+ const table = this.table(tableName);
105
239
  const latestRow = await table.orderBy("updatedAt").last();
106
240
  if (latestRow && latestRow.updatedAt) {
107
241
  since = latestRow.updatedAt;
@@ -131,7 +265,7 @@ export class SyncClient {
131
265
  }
132
266
  }
133
267
  async safePutRow(tableName, data) {
134
- const table = this.db.table(tableName);
268
+ const table = this.table(tableName);
135
269
  if (!data || !data.id)
136
270
  return;
137
271
  const existing = await table.get(data.id);
@@ -143,10 +277,11 @@ export class SyncClient {
143
277
  return;
144
278
  }
145
279
  }
146
- await table.put(data);
280
+ const originalPut = table._originalMethods?.put || table.put.bind(table);
281
+ await originalPut(data);
147
282
  }
148
283
  async safeDeleteRow(tableName, key, incomingTimeStr) {
149
- const table = this.db.table(tableName);
284
+ const table = this.table(tableName);
150
285
  if (incomingTimeStr) {
151
286
  const existing = await table.get(key);
152
287
  if (existing && existing.updatedAt) {
@@ -158,14 +293,15 @@ export class SyncClient {
158
293
  }
159
294
  }
160
295
  }
161
- await table.delete(key);
296
+ const originalDelete = table._originalMethods?.delete || table.delete.bind(table);
297
+ await originalDelete(key);
162
298
  }
163
299
  async handleServerMessage(msg) {
164
300
  switch (msg.type) {
165
301
  case "snapshot": {
166
302
  const tableName = this.findTableByChannel(msg.channel);
167
303
  if (tableName) {
168
- const table = this.db.table(tableName);
304
+ const table = this.table(tableName);
169
305
  if (msg.isDelta) {
170
306
  // Delta Sync: put changes using Last-Write-Wins
171
307
  for (const row of msg.data) {
@@ -174,7 +310,7 @@ export class SyncClient {
174
310
  }
175
311
  else {
176
312
  // Full Snapshot: clear and replace
177
- await this.db.transaction("rw", table, async () => {
313
+ await this.transaction("rw", table, async () => {
178
314
  await table.clear();
179
315
  await table.bulkPut(msg.data);
180
316
  });
@@ -202,7 +338,11 @@ export class SyncClient {
202
338
  if (pending) {
203
339
  console.warn(`Mutation ${msg.id} rejected by server: ${msg.error}`);
204
340
  await pending.rollback();
205
- pending.reject(new Error(msg.error));
341
+ const errorObj = new Error(msg.error);
342
+ if (msg.validationErrors) {
343
+ errorObj.validationErrors = msg.validationErrors;
344
+ }
345
+ pending.reject(errorObj);
206
346
  this.pendingMutations.delete(msg.id);
207
347
  }
208
348
  break;
@@ -233,56 +373,6 @@ export class SyncClient {
233
373
  }
234
374
  return undefined;
235
375
  }
236
- table(tableName) {
237
- const dexieTable = this.db.table(tableName);
238
- const config = this.tableConfigs[tableName];
239
- if (!config) {
240
- throw new Error(`Table ${tableName} not defined in SyncClient config.`);
241
- }
242
- return {
243
- liveQuery: (queryFn) => {
244
- return useLiveQuery(() => queryFn(dexieTable));
245
- },
246
- add: async (row) => {
247
- const rowData = row;
248
- const id = rowData.id || crypto.randomUUID();
249
- const fullRow = { ...rowData, id };
250
- // Rollback function
251
- const rollback = async () => {
252
- await dexieTable.delete(id);
253
- };
254
- // Apply optimistic update
255
- await dexieTable.put(fullRow);
256
- return this.enqueueMutation(config.channel, "create", id, fullRow, rollback);
257
- },
258
- put: async (id, changes) => {
259
- const existing = await dexieTable.get(id);
260
- if (!existing) {
261
- throw new Error(`Cannot update item ${id}: not found locally.`);
262
- }
263
- // Rollback function
264
- const rollback = async () => {
265
- await dexieTable.put(existing);
266
- };
267
- const updatedRow = { ...existing, ...changes };
268
- // Apply optimistic update
269
- await dexieTable.put(updatedRow);
270
- return this.enqueueMutation(config.channel, "update", id, changes, rollback);
271
- },
272
- delete: async (id) => {
273
- const existing = await dexieTable.get(id);
274
- if (!existing)
275
- return; // Already deleted
276
- // Rollback function
277
- const rollback = async () => {
278
- await dexieTable.put(existing);
279
- };
280
- // Apply optimistic update
281
- await dexieTable.delete(id);
282
- return this.enqueueMutation(config.channel, "delete", id, undefined, rollback);
283
- },
284
- };
285
- }
286
376
  enqueueMutation(channel, action, key, data, rollback) {
287
377
  const mutationId = crypto.randomUUID();
288
378
  return new Promise((resolve, reject) => {
@@ -313,14 +403,19 @@ export class SyncClient {
313
403
  }
314
404
  disconnect() {
315
405
  this.closedByClient = true;
406
+ this._status = "disconnected";
316
407
  this.stopHeartbeat();
317
408
  if (this.reconnectTimer) {
318
409
  clearTimeout(this.reconnectTimer);
319
410
  this.reconnectTimer = undefined;
320
411
  }
321
412
  if (this.socket) {
322
- this.socket.close();
413
+ try {
414
+ this.socket.close();
415
+ }
416
+ catch { }
323
417
  this.socket = undefined;
324
418
  }
325
419
  }
326
420
  }
421
+ export const SyncClient = SyncClientClass;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
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
4
  export type { PublishEventData, InferSchemaFromHandlers } from "./server/handler.js";
@@ -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,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AACrF,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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltebase/sync",
3
- "version": "1.0.7",
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":"AAGA,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,CAuCjF"}
@@ -1,40 +0,0 @@
1
- import { onDestroy } from "svelte";
2
- import { liveQuery } from "dexie";
3
- export function useLiveQuery(queryFn) {
4
- let data = $state(undefined);
5
- let error = $state(undefined);
6
- let status = $state("loading");
7
- if (typeof window !== "undefined") {
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
- onDestroy(() => {
23
- subscription.unsubscribe();
24
- });
25
- }
26
- return {
27
- get data() {
28
- return data;
29
- },
30
- get error() {
31
- return error;
32
- },
33
- get status() {
34
- return status;
35
- },
36
- get isLoading() {
37
- return status === "loading";
38
- },
39
- };
40
- }