@sveltebase/sync 1.0.6 → 1.0.7

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
@@ -484,95 +484,8 @@ This approach keeps WebSocket URLs clean of private IDs and ensures all active s
484
484
 
485
485
  ---
486
486
 
487
- ## 5. Developer Experience (DX) & Client Enhancements
487
+ ## 5. Type-Safe Backend Event Publishing (`createPublisher`)
488
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();
554
-
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
489
  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
490
 
578
491
  ```typescript
@@ -584,22 +497,25 @@ type AppSyncSchema = {
584
497
  todos: Todo;
585
498
  };
586
499
 
587
- // Create typed publisher instance
588
- const publisher = createPublisher<AppSyncSchema>();
500
+ // Create typed publish function (Option A: Explicit Schema)
501
+ const publish = createPublisher<AppSyncSchema>();
502
+
503
+ // Create typed publish function (Option B: Automatically inferred from Sync Handlers)
504
+ import { handlers } from "./lib/server/sync-handlers.js";
505
+ const publish = createPublisher(handlers);
589
506
 
590
507
  // 1. Publish a create event (expects full Todo payload)
591
- await publisher.publish("todos", "create", todo.id, todo);
508
+ await publish("todos", "create", todo.id, todo);
592
509
 
593
510
  // 2. Publish an update event (expects Partial<Todo> payload)
594
- await publisher.publish("todos", "update", todo.id, { completed: true });
511
+ await publish("todos", "update", todo.id, { completed: true });
595
512
 
596
513
  // 3. Publish a delete event (expects optional { updatedAt: string } metadata)
597
- await publisher.publish("todos", "delete", todo.id, undefined);
514
+ await publish("todos", "delete", todo.id, undefined);
598
515
 
599
516
  // 4. Supports scoped/dynamic channels (e.g. "channelName:scopeId")
600
- await publisher.publish("todos:user_123", "update", todo.id, { title: "New Title" });
517
+ await publish("todos:user_123", "update", todo.id, { title: "New Title" });
601
518
  ```
602
519
 
603
520
 
604
521
 
605
-
@@ -7,7 +7,7 @@ export type TableConfig = {
7
7
  };
8
8
  export type SyncClientOptions = {
9
9
  name: string;
10
- url: string | (() => string | Promise<string>);
10
+ url: string;
11
11
  tables: Record<string, TableConfig>;
12
12
  };
13
13
  export declare class SyncClient<TSchema extends Record<string, any> = Record<string, any>> {
@@ -19,17 +19,14 @@ export declare class SyncClient<TSchema extends Record<string, any> = Record<str
19
19
  private pingInterval;
20
20
  private closedByClient;
21
21
  private activeChannels;
22
- private _status;
23
22
  private pendingMutations;
24
23
  private mutationQueue;
25
24
  constructor(options: {
26
25
  name: string;
27
- url: string | (() => string | Promise<string>);
26
+ url: string;
28
27
  tables: Record<keyof TSchema & string, TableConfig>;
29
28
  });
30
- get status(): "connecting" | "connected" | "disconnected";
31
29
  private connect;
32
- reconnect(): void;
33
30
  private startHeartbeat;
34
31
  private stopHeartbeat;
35
32
  private subscribeToChannel;
@@ -39,10 +36,7 @@ export declare class SyncClient<TSchema extends Record<string, any> = Record<str
39
36
  private handleServerMessage;
40
37
  private findTableByChannel;
41
38
  table<TKey extends keyof TSchema & string>(tableName: TKey): {
42
- rawTable: Table<TSchema[TKey], string, TSchema[TKey]>;
43
39
  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
40
  add: (row: TSchema[TKey]) => Promise<TSchema[TKey]>;
47
41
  put: (id: string, changes: Partial<TSchema[TKey]>) => Promise<TSchema[TKey]>;
48
42
  delete: (id: string) => Promise<void>;
@@ -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,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"}
@@ -11,8 +11,6 @@ export class SyncClient {
11
11
  pingInterval;
12
12
  closedByClient = false;
13
13
  activeChannels = new Set();
14
- // Reactive connection status
15
- _status = $state("connecting");
16
14
  // Mutations waiting for ack/reject from server
17
15
  pendingMutations = new Map();
18
16
  // Mutations queued to be sent when connection is established
@@ -31,37 +29,17 @@ export class SyncClient {
31
29
  this.connect();
32
30
  }
33
31
  }
34
- get status() {
35
- return this._status;
36
- }
37
- async connect() {
32
+ connect() {
38
33
  if (this.closedByClient)
39
34
  return;
40
- this._status = "connecting";
41
35
  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
42
36
  const host = window.location.host;
43
- let resolvedUrl;
44
- try {
45
- resolvedUrl = typeof this.wsUrl === "function" ? await this.wsUrl() : this.wsUrl;
46
- }
47
- catch (err) {
48
- console.error("SyncClient: Failed to resolve wsUrl", err);
49
- this._status = "disconnected";
50
- if (!this.closedByClient) {
51
- this.reconnectTimer = setTimeout(() => this.connect(), 2000);
52
- }
53
- return;
54
- }
55
- const fullUrl = resolvedUrl.startsWith("ws://") || resolvedUrl.startsWith("wss://")
56
- ? resolvedUrl
57
- : `${protocol}//${host}${resolvedUrl}`;
58
- const socket = new WebSocket(fullUrl);
59
- this.socket = socket;
60
- socket.addEventListener("open", async () => {
61
- if (this.socket !== socket)
62
- return;
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 () => {
63
42
  console.log("SyncClient: WebSocket connected");
64
- this._status = "connected";
65
43
  this.activeChannels.clear();
66
44
  this.startHeartbeat();
67
45
  // Re-subscribe to all tables (delta-sync aware)
@@ -70,7 +48,7 @@ export class SyncClient {
70
48
  }
71
49
  // Re-send all pending unacknowledged mutations
72
50
  for (const mut of this.pendingMutations.values()) {
73
- socket.send(JSON.stringify({
51
+ this.socket?.send(JSON.stringify({
74
52
  type: "mutate",
75
53
  id: mut.id,
76
54
  channel: mut.channel,
@@ -82,9 +60,7 @@ export class SyncClient {
82
60
  // Flush queued mutations
83
61
  this.flushMutationQueue();
84
62
  });
85
- socket.addEventListener("message", async (message) => {
86
- if (this.socket !== socket)
87
- return;
63
+ this.socket.addEventListener("message", async (message) => {
88
64
  if (typeof message.data !== "string")
89
65
  return;
90
66
  if (message.data === "pong")
@@ -94,38 +70,17 @@ export class SyncClient {
94
70
  return;
95
71
  await this.handleServerMessage(msg);
96
72
  });
97
- socket.addEventListener("close", () => {
98
- if (this.socket === socket) {
99
- this.socket = undefined;
100
- this._status = "disconnected";
101
- this.stopHeartbeat();
102
- if (!this.closedByClient) {
103
- this.reconnectTimer = setTimeout(() => this.connect(), 2000);
104
- }
73
+ this.socket.addEventListener("close", () => {
74
+ this.socket = undefined;
75
+ this.stopHeartbeat();
76
+ if (!this.closedByClient) {
77
+ this.reconnectTimer = setTimeout(() => this.connect(), 2000);
105
78
  }
106
79
  });
107
- socket.addEventListener("error", (err) => {
108
- if (this.socket === socket) {
109
- console.error("SyncClient: WebSocket error", err);
110
- }
80
+ this.socket.addEventListener("error", (err) => {
81
+ console.error("SyncClient: WebSocket error", err);
111
82
  });
112
83
  }
113
- reconnect() {
114
- this.closedByClient = false;
115
- if (this.reconnectTimer) {
116
- clearTimeout(this.reconnectTimer);
117
- this.reconnectTimer = undefined;
118
- }
119
- if (this.socket) {
120
- try {
121
- this.socket.close();
122
- }
123
- catch { }
124
- this.socket = undefined;
125
- this._status = "disconnected";
126
- }
127
- this.connect();
128
- }
129
84
  startHeartbeat() {
130
85
  this.stopHeartbeat();
131
86
  this.pingInterval = setInterval(() => {
@@ -247,11 +202,7 @@ export class SyncClient {
247
202
  if (pending) {
248
203
  console.warn(`Mutation ${msg.id} rejected by server: ${msg.error}`);
249
204
  await pending.rollback();
250
- const errorObj = new Error(msg.error);
251
- if (msg.validationErrors) {
252
- errorObj.validationErrors = msg.validationErrors;
253
- }
254
- pending.reject(errorObj);
205
+ pending.reject(new Error(msg.error));
255
206
  this.pendingMutations.delete(msg.id);
256
207
  }
257
208
  break;
@@ -289,16 +240,9 @@ export class SyncClient {
289
240
  throw new Error(`Table ${tableName} not defined in SyncClient config.`);
290
241
  }
291
242
  return {
292
- rawTable: dexieTable,
293
243
  liveQuery: (queryFn) => {
294
244
  return useLiveQuery(() => queryFn(dexieTable));
295
245
  },
296
- list: () => {
297
- return useLiveQuery(() => dexieTable.toArray());
298
- },
299
- get: async (id) => {
300
- return dexieTable.get(id);
301
- },
302
246
  add: async (row) => {
303
247
  const rowData = row;
304
248
  const id = rowData.id || crypto.randomUUID();
@@ -369,17 +313,13 @@ export class SyncClient {
369
313
  }
370
314
  disconnect() {
371
315
  this.closedByClient = true;
372
- this._status = "disconnected";
373
316
  this.stopHeartbeat();
374
317
  if (this.reconnectTimer) {
375
318
  clearTimeout(this.reconnectTimer);
376
319
  this.reconnectTimer = undefined;
377
320
  }
378
321
  if (this.socket) {
379
- try {
380
- this.socket.close();
381
- }
382
- catch { }
322
+ this.socket.close();
383
323
  this.socket = undefined;
384
324
  }
385
325
  }
@@ -1 +1 @@
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
+ {"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,27 +1,26 @@
1
+ import { onDestroy } from "svelte";
1
2
  import { liveQuery } from "dexie";
2
3
  export function useLiveQuery(queryFn) {
3
4
  let data = $state(undefined);
4
5
  let error = $state(undefined);
5
6
  let status = $state("loading");
6
7
  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
- };
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();
25
24
  });
26
25
  }
27
26
  return {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { SyncClient, useLiveQuery } 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,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 +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.0.7",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"