@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 +91 -0
- package/dist/client/index.d.ts +8 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +77 -17
- package/dist/client/live.svelte.d.ts.map +1 -1
- package/dist/client/live.svelte.js +18 -17
- package/dist/server/broker.d.ts.map +1 -1
- package/dist/server/broker.js +12 -1
- package/package.json +1 -1
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
|
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
:
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
this.socket
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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;
|
|
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"}
|
package/dist/server/broker.js
CHANGED
|
@@ -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:
|
|
176
|
+
error: errorMessage,
|
|
177
|
+
validationErrors,
|
|
167
178
|
}));
|
|
168
179
|
}
|
|
169
180
|
}
|