@sveltebase/sync 1.0.3 → 1.0.5

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
@@ -482,4 +482,95 @@ export const GET: RequestHandler = (event: RequestEvent) => {
482
482
  ```
483
483
  This approach keeps WebSocket URLs clean of private IDs and ensures all active sockets are automatically authenticated with their verified session roles/IDs.
484
484
 
485
+ ---
486
+
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();
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
+
485
576
 
@@ -7,7 +7,7 @@ export type TableConfig = {
7
7
  };
8
8
  export type SyncClientOptions = {
9
9
  name: string;
10
- url: string;
10
+ url: string | (() => string | Promise<string>);
11
11
  tables: Record<string, TableConfig>;
12
12
  };
13
13
  export declare class SyncClient<TSchema extends Record<string, any> = Record<string, any>> {
@@ -19,14 +19,17 @@ 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;
22
23
  private pendingMutations;
23
24
  private mutationQueue;
24
25
  constructor(options: {
25
26
  name: string;
26
- url: string;
27
+ url: string | (() => string | Promise<string>);
27
28
  tables: Record<keyof TSchema & string, TableConfig>;
28
29
  });
30
+ get status(): "connecting" | "connected" | "disconnected";
29
31
  private connect;
32
+ reconnect(): void;
30
33
  private startHeartbeat;
31
34
  private stopHeartbeat;
32
35
  private subscribeToChannel;
@@ -36,7 +39,10 @@ export declare class SyncClient<TSchema extends Record<string, any> = Record<str
36
39
  private handleServerMessage;
37
40
  private findTableByChannel;
38
41
  table<TKey extends keyof TSchema & string>(tableName: TKey): {
42
+ rawTable: Table<TSchema[TKey], string, TSchema[TKey]>;
39
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>;
40
46
  add: (row: TSchema[TKey]) => Promise<TSchema[TKey]>;
41
47
  put: (id: string, changes: Partial<TSchema[TKey]>) => Promise<TSchema[TKey]>;
42
48
  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,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,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"}
@@ -11,6 +11,8 @@ export class SyncClient {
11
11
  pingInterval;
12
12
  closedByClient = false;
13
13
  activeChannels = new Set();
14
+ // Reactive connection status
15
+ _status = $state("connecting");
14
16
  // Mutations waiting for ack/reject from server
15
17
  pendingMutations = new Map();
16
18
  // Mutations queued to be sent when connection is established
@@ -29,17 +31,37 @@ export class SyncClient {
29
31
  this.connect();
30
32
  }
31
33
  }
32
- connect() {
34
+ get status() {
35
+ return this._status;
36
+ }
37
+ async connect() {
33
38
  if (this.closedByClient)
34
39
  return;
40
+ this._status = "connecting";
35
41
  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
36
42
  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 () => {
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;
42
63
  console.log("SyncClient: WebSocket connected");
64
+ this._status = "connected";
43
65
  this.activeChannels.clear();
44
66
  this.startHeartbeat();
45
67
  // Re-subscribe to all tables (delta-sync aware)
@@ -48,7 +70,7 @@ export class SyncClient {
48
70
  }
49
71
  // Re-send all pending unacknowledged mutations
50
72
  for (const mut of this.pendingMutations.values()) {
51
- this.socket?.send(JSON.stringify({
73
+ socket.send(JSON.stringify({
52
74
  type: "mutate",
53
75
  id: mut.id,
54
76
  channel: mut.channel,
@@ -60,7 +82,9 @@ export class SyncClient {
60
82
  // Flush queued mutations
61
83
  this.flushMutationQueue();
62
84
  });
63
- this.socket.addEventListener("message", async (message) => {
85
+ socket.addEventListener("message", async (message) => {
86
+ if (this.socket !== socket)
87
+ return;
64
88
  if (typeof message.data !== "string")
65
89
  return;
66
90
  if (message.data === "pong")
@@ -70,17 +94,38 @@ export class SyncClient {
70
94
  return;
71
95
  await this.handleServerMessage(msg);
72
96
  });
73
- this.socket.addEventListener("close", () => {
74
- this.socket = undefined;
75
- this.stopHeartbeat();
76
- if (!this.closedByClient) {
77
- this.reconnectTimer = setTimeout(() => this.connect(), 2000);
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
+ }
78
105
  }
79
106
  });
80
- this.socket.addEventListener("error", (err) => {
81
- console.error("SyncClient: WebSocket error", err);
107
+ socket.addEventListener("error", (err) => {
108
+ if (this.socket === socket) {
109
+ console.error("SyncClient: WebSocket error", err);
110
+ }
82
111
  });
83
112
  }
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
+ }
84
129
  startHeartbeat() {
85
130
  this.stopHeartbeat();
86
131
  this.pingInterval = setInterval(() => {
@@ -202,7 +247,11 @@ export class SyncClient {
202
247
  if (pending) {
203
248
  console.warn(`Mutation ${msg.id} rejected by server: ${msg.error}`);
204
249
  await pending.rollback();
205
- pending.reject(new Error(msg.error));
250
+ const errorObj = new Error(msg.error);
251
+ if (msg.validationErrors) {
252
+ errorObj.validationErrors = msg.validationErrors;
253
+ }
254
+ pending.reject(errorObj);
206
255
  this.pendingMutations.delete(msg.id);
207
256
  }
208
257
  break;
@@ -240,9 +289,16 @@ export class SyncClient {
240
289
  throw new Error(`Table ${tableName} not defined in SyncClient config.`);
241
290
  }
242
291
  return {
292
+ rawTable: dexieTable,
243
293
  liveQuery: (queryFn) => {
244
294
  return useLiveQuery(() => queryFn(dexieTable));
245
295
  },
296
+ list: () => {
297
+ return useLiveQuery(() => dexieTable.toArray());
298
+ },
299
+ get: async (id) => {
300
+ return dexieTable.get(id);
301
+ },
246
302
  add: async (row) => {
247
303
  const rowData = row;
248
304
  const id = rowData.id || crypto.randomUUID();
@@ -313,13 +369,17 @@ export class SyncClient {
313
369
  }
314
370
  disconnect() {
315
371
  this.closedByClient = true;
372
+ this._status = "disconnected";
316
373
  this.stopHeartbeat();
317
374
  if (this.reconnectTimer) {
318
375
  clearTimeout(this.reconnectTimer);
319
376
  this.reconnectTimer = undefined;
320
377
  }
321
378
  if (this.socket) {
322
- this.socket.close();
379
+ try {
380
+ this.socket.close();
381
+ }
382
+ catch { }
323
383
  this.socket = undefined;
324
384
  }
325
385
  }
@@ -1 +1 @@
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
+ {"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,26 +1,27 @@
1
- import { onDestroy } from "svelte";
2
1
  import { liveQuery } from "dexie";
3
2
  export function useLiveQuery(queryFn) {
4
3
  let data = $state(undefined);
5
4
  let error = $state(undefined);
6
5
  let status = $state("loading");
7
6
  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();
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
+ };
24
25
  });
25
26
  }
26
27
  return {
@@ -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;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"}
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"}
@@ -160,10 +160,21 @@ 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
+ }
163
173
  conn.send(JSON.stringify({
164
174
  type: "reject",
165
175
  id: msg.id,
166
- error: err.message || "Server error",
176
+ error: errorMessage,
177
+ validationErrors,
167
178
  }));
168
179
  }
169
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltebase/sync",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"