@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 +11 -95
- package/dist/client/index.d.ts +2 -8
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +17 -77
- package/dist/client/live.svelte.d.ts.map +1 -1
- package/dist/client/live.svelte.js +17 -18
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/server/broker.d.ts.map +1 -1
- package/dist/server/broker.js +1 -12
- package/dist/server/handler.d.ts +5 -2
- package/dist/server/handler.d.ts.map +1 -1
- package/dist/server/handler.js +4 -6
- package/package.json +1 -1
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.
|
|
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
|
|
588
|
-
const
|
|
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
|
|
508
|
+
await publish("todos", "create", todo.id, todo);
|
|
592
509
|
|
|
593
510
|
// 2. Publish an update event (expects Partial<Todo> payload)
|
|
594
|
-
await
|
|
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
|
|
514
|
+
await publish("todos", "delete", todo.id, undefined);
|
|
598
515
|
|
|
599
516
|
// 4. Supports scoped/dynamic channels (e.g. "channelName:scopeId")
|
|
600
|
-
await
|
|
517
|
+
await publish("todos:user_123", "update", todo.id, { title: "New Title" });
|
|
601
518
|
```
|
|
602
519
|
|
|
603
520
|
|
|
604
521
|
|
|
605
|
-
|
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;
|
|
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
|
|
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,
|
|
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"}
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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;
|
|
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"}
|
package/dist/server/broker.js
CHANGED
|
@@ -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:
|
|
177
|
-
validationErrors,
|
|
166
|
+
error: err.message || "Server error",
|
|
178
167
|
}));
|
|
179
168
|
}
|
|
180
169
|
}
|
package/dist/server/handler.d.ts
CHANGED
|
@@ -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
|
|
5
|
-
|
|
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
|
|
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"}
|
package/dist/server/handler.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
export function createPublisher() {
|
|
2
|
-
return {
|
|
3
|
-
|
|
4
|
-
|
|
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) {
|