@sveltebase/sync 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -118
- package/dist/client/index.d.ts +11 -13
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +105 -70
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +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/dist/client/live.svelte.d.ts +0 -18
- package/dist/client/live.svelte.d.ts.map +0 -1
- package/dist/client/live.svelte.js +0 -41
package/README.md
CHANGED
|
@@ -90,25 +90,23 @@ export const sync = new SyncClient<AppDatabaseSchema>({
|
|
|
90
90
|
},
|
|
91
91
|
},
|
|
92
92
|
});
|
|
93
|
-
|
|
94
|
-
// Export typed table wrapper
|
|
95
|
-
export const todosTable = sync.table("todos");
|
|
96
93
|
```
|
|
97
94
|
|
|
98
|
-
Use it in your Svelte 5 components:
|
|
95
|
+
Use it in your Svelte 5 components (using your preferred Dexie reactivity hook, such as `liveQuery` from `dexie` or `dexie-svelte-query`):
|
|
99
96
|
```svelte
|
|
100
97
|
<script lang="ts">
|
|
101
|
-
import {
|
|
98
|
+
import { sync } from "$lib/sync-client";
|
|
99
|
+
import { liveQuery } from "dexie";
|
|
102
100
|
import { Check, Trash } from "lucide-svelte";
|
|
103
101
|
|
|
104
|
-
//
|
|
105
|
-
const todos =
|
|
102
|
+
// Standard Dexie liveQuery updates instantly on mutations & remote syncs
|
|
103
|
+
const todos = liveQuery(() => sync.todos.orderBy("createdAt").reverse().toArray());
|
|
106
104
|
|
|
107
105
|
let title = "";
|
|
108
106
|
|
|
109
107
|
async function addTodo() {
|
|
110
108
|
if (!title.trim()) return;
|
|
111
|
-
await
|
|
109
|
+
await sync.todos.add({
|
|
112
110
|
id: crypto.randomUUID(),
|
|
113
111
|
title,
|
|
114
112
|
completed: false,
|
|
@@ -121,21 +119,15 @@ Use it in your Svelte 5 components:
|
|
|
121
119
|
|
|
122
120
|
<input bind:value={title} onkeydown={(e) => e.key === 'Enter' && addTodo()} />
|
|
123
121
|
|
|
124
|
-
{#
|
|
125
|
-
<div>
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
</button>
|
|
134
|
-
<span>{todo.title}</span>
|
|
135
|
-
<button onclick={() => todosTable.delete(todo.id)}><Trash /></button>
|
|
136
|
-
</div>
|
|
137
|
-
{/each}
|
|
138
|
-
{/if}
|
|
122
|
+
{#each ($todos || []) as todo (todo.id)}
|
|
123
|
+
<div>
|
|
124
|
+
<button onclick={() => sync.todos.update(todo.id, { completed: !todo.completed })}>
|
|
125
|
+
<Check class={todo.completed ? "text-emerald-500" : ""} />
|
|
126
|
+
</button>
|
|
127
|
+
<span>{todo.title}</span>
|
|
128
|
+
<button onclick={() => sync.todos.delete(todo.id)}><Trash /></button>
|
|
129
|
+
</div>
|
|
130
|
+
{/each}
|
|
139
131
|
```
|
|
140
132
|
|
|
141
133
|
---
|
|
@@ -484,95 +476,8 @@ This approach keeps WebSocket URLs clean of private IDs and ensures all active s
|
|
|
484
476
|
|
|
485
477
|
---
|
|
486
478
|
|
|
487
|
-
## 5.
|
|
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();
|
|
479
|
+
## 5. Type-Safe Backend Event Publishing (`createPublisher`)
|
|
554
480
|
|
|
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
481
|
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
482
|
|
|
578
483
|
```typescript
|
|
@@ -584,22 +489,25 @@ type AppSyncSchema = {
|
|
|
584
489
|
todos: Todo;
|
|
585
490
|
};
|
|
586
491
|
|
|
587
|
-
// Create typed
|
|
588
|
-
const
|
|
492
|
+
// Create typed publish function (Option A: Explicit Schema)
|
|
493
|
+
const publish = createPublisher<AppSyncSchema>();
|
|
494
|
+
|
|
495
|
+
// Create typed publish function (Option B: Automatically inferred from Sync Handlers)
|
|
496
|
+
import { handlers } from "./lib/server/sync-handlers.js";
|
|
497
|
+
const publish = createPublisher(handlers);
|
|
589
498
|
|
|
590
499
|
// 1. Publish a create event (expects full Todo payload)
|
|
591
|
-
await
|
|
500
|
+
await publish("todos", "create", todo.id, todo);
|
|
592
501
|
|
|
593
502
|
// 2. Publish an update event (expects Partial<Todo> payload)
|
|
594
|
-
await
|
|
503
|
+
await publish("todos", "update", todo.id, { completed: true });
|
|
595
504
|
|
|
596
505
|
// 3. Publish a delete event (expects optional { updatedAt: string } metadata)
|
|
597
|
-
await
|
|
506
|
+
await publish("todos", "delete", todo.id, undefined);
|
|
598
507
|
|
|
599
508
|
// 4. Supports scoped/dynamic channels (e.g. "channelName:scopeId")
|
|
600
|
-
await
|
|
509
|
+
await publish("todos:user_123", "update", todo.id, { title: "New Title" });
|
|
601
510
|
```
|
|
602
511
|
|
|
603
512
|
|
|
604
513
|
|
|
605
|
-
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import Dexie, { type Table } from "dexie";
|
|
2
|
-
import { useLiveQuery, type LiveQueryResult } from "./live.svelte.js";
|
|
3
|
-
export { useLiveQuery, type LiveQueryResult };
|
|
4
2
|
export type TableConfig = {
|
|
5
3
|
indexes: string;
|
|
6
4
|
channel: string;
|
|
@@ -10,8 +8,7 @@ export type SyncClientOptions = {
|
|
|
10
8
|
url: string | (() => string | Promise<string>);
|
|
11
9
|
tables: Record<string, TableConfig>;
|
|
12
10
|
};
|
|
13
|
-
|
|
14
|
-
db: Dexie;
|
|
11
|
+
declare class SyncClientClass<TSchema extends Record<string, any> = Record<string, any>> extends Dexie {
|
|
15
12
|
private wsUrl;
|
|
16
13
|
private socket;
|
|
17
14
|
private tableConfigs;
|
|
@@ -28,6 +25,7 @@ export declare class SyncClient<TSchema extends Record<string, any> = Record<str
|
|
|
28
25
|
tables: Record<keyof TSchema & string, TableConfig>;
|
|
29
26
|
});
|
|
30
27
|
get status(): "connecting" | "connected" | "disconnected";
|
|
28
|
+
private decorateTables;
|
|
31
29
|
private connect;
|
|
32
30
|
reconnect(): void;
|
|
33
31
|
private startHeartbeat;
|
|
@@ -38,16 +36,16 @@ export declare class SyncClient<TSchema extends Record<string, any> = Record<str
|
|
|
38
36
|
private safeDeleteRow;
|
|
39
37
|
private handleServerMessage;
|
|
40
38
|
private findTableByChannel;
|
|
41
|
-
table<TKey extends keyof TSchema & string>(tableName: TKey): {
|
|
42
|
-
rawTable: Table<TSchema[TKey], string, TSchema[TKey]>;
|
|
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>;
|
|
46
|
-
add: (row: TSchema[TKey]) => Promise<TSchema[TKey]>;
|
|
47
|
-
put: (id: string, changes: Partial<TSchema[TKey]>) => Promise<TSchema[TKey]>;
|
|
48
|
-
delete: (id: string) => Promise<void>;
|
|
49
|
-
};
|
|
50
39
|
private enqueueMutation;
|
|
51
40
|
disconnect(): void;
|
|
52
41
|
}
|
|
42
|
+
export type SyncClient<TSchema extends Record<string, any> = Record<string, any>> = SyncClientClass<TSchema> & {
|
|
43
|
+
[K in keyof TSchema]: Table<TSchema[K]>;
|
|
44
|
+
};
|
|
45
|
+
export declare const SyncClient: new <TSchema extends Record<string, any> = Record<string, any>>(options: {
|
|
46
|
+
name: string;
|
|
47
|
+
url: string | (() => string | Promise<string>);
|
|
48
|
+
tables: Record<keyof TSchema & string, TableConfig>;
|
|
49
|
+
}) => SyncClient<TSchema>;
|
|
50
|
+
export {};
|
|
53
51
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,KAAK,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,KAAK,KAAK,EAAwB,MAAM,OAAO,CAAC;AAGhE,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACrC,CAAC;AAaF,cAAM,eAAe,CACnB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CACzD,SAAQ,KAAK;IACb,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,cAAc,CAA4C;IAClE,OAAO,CAAC,YAAY,CAA6C;IACjE,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,cAAc,CAAqB;IAG3C,OAAO,CAAC,OAAO,CAAqE;IAGpF,OAAO,CAAC,gBAAgB,CAAsC;IAE9D,OAAO,CAAC,aAAa,CAMb;gBAEI,OAAO,EAAE;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,EAAE,WAAW,CAAC,CAAC;KACrD;IAoBD,IAAW,MAAM,gDAEhB;IAED,OAAO,CAAC,cAAc;YAiJR,OAAO;IAuFd,SAAS;IAgBhB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,aAAa;YAOP,kBAAkB;IAoBhC,OAAO,CAAC,kBAAkB;YAkBZ,UAAU;YAiBV,aAAa;YAqBb,mBAAmB;IAwEjC,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,eAAe;IAqChB,UAAU;CAelB;AAED,MAAM,MAAM,UAAU,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,eAAe,CAAC,OAAO,CAAC,GAAG;KAC5G,CAAC,IAAI,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;CACxC,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KACvB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzD,OAAO,EAAE;IACT,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,EAAE,WAAW,CAAC,CAAC;CACrD,KAAK,UAAU,CAAC,OAAO,CAA0B,CAAC"}
|
package/dist/client/index.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import Dexie, {} from "dexie";
|
|
2
2
|
import { parseSyncMessage } from "../protocol.js";
|
|
3
|
-
|
|
4
|
-
export { useLiveQuery };
|
|
5
|
-
export class SyncClient {
|
|
6
|
-
db;
|
|
3
|
+
class SyncClientClass extends Dexie {
|
|
7
4
|
wsUrl;
|
|
8
5
|
socket;
|
|
9
6
|
tableConfigs;
|
|
@@ -18,15 +15,17 @@ export class SyncClient {
|
|
|
18
15
|
// Mutations queued to be sent when connection is established
|
|
19
16
|
mutationQueue = [];
|
|
20
17
|
constructor(options) {
|
|
18
|
+
super(options.name);
|
|
21
19
|
this.wsUrl = options.url;
|
|
22
20
|
this.tableConfigs = options.tables;
|
|
23
21
|
// Initialize Dexie database
|
|
24
|
-
this.db = new Dexie(options.name);
|
|
25
22
|
const schema = {};
|
|
26
23
|
for (const [tableName, config] of Object.entries(options.tables)) {
|
|
27
24
|
schema[tableName] = config.indexes;
|
|
28
25
|
}
|
|
29
|
-
this.
|
|
26
|
+
this.version(1).stores(schema);
|
|
27
|
+
// Decorate tables to intercept native Dexie write operations
|
|
28
|
+
this.decorateTables();
|
|
30
29
|
if (typeof window !== "undefined") {
|
|
31
30
|
this.connect();
|
|
32
31
|
}
|
|
@@ -34,6 +33,96 @@ export class SyncClient {
|
|
|
34
33
|
get status() {
|
|
35
34
|
return this._status;
|
|
36
35
|
}
|
|
36
|
+
decorateTables() {
|
|
37
|
+
for (const [tableName, config] of Object.entries(this.tableConfigs)) {
|
|
38
|
+
const table = this.table(tableName);
|
|
39
|
+
const originalAdd = table.add.bind(table);
|
|
40
|
+
const originalPut = table.put.bind(table);
|
|
41
|
+
const originalUpdate = table.update.bind(table);
|
|
42
|
+
const originalDelete = table.delete.bind(table);
|
|
43
|
+
// Save original methods to bypass sync trigger loops when updating from WebSocket
|
|
44
|
+
table._originalMethods = {
|
|
45
|
+
add: originalAdd,
|
|
46
|
+
put: originalPut,
|
|
47
|
+
update: originalUpdate,
|
|
48
|
+
delete: originalDelete,
|
|
49
|
+
};
|
|
50
|
+
table.add = (async (row) => {
|
|
51
|
+
const id = row.id || crypto.randomUUID();
|
|
52
|
+
const fullRow = { ...row, id };
|
|
53
|
+
const rollback = async () => {
|
|
54
|
+
await originalDelete(id);
|
|
55
|
+
};
|
|
56
|
+
await originalAdd(fullRow);
|
|
57
|
+
return this.enqueueMutation(config.channel, "create", id, fullRow, rollback);
|
|
58
|
+
});
|
|
59
|
+
table.put = (async (rowOrId, changes) => {
|
|
60
|
+
// Overload 1: put(id, changes) - Partial Update
|
|
61
|
+
if (changes !== undefined) {
|
|
62
|
+
const id = rowOrId;
|
|
63
|
+
const existing = await table.get(id);
|
|
64
|
+
if (!existing) {
|
|
65
|
+
throw new Error(`Cannot update item ${id}: not found locally.`);
|
|
66
|
+
}
|
|
67
|
+
const rollback = async () => {
|
|
68
|
+
await originalPut(existing);
|
|
69
|
+
};
|
|
70
|
+
const updatedRow = { ...existing, ...changes };
|
|
71
|
+
await originalPut(updatedRow);
|
|
72
|
+
const diff = {};
|
|
73
|
+
for (const [k, v] of Object.entries(changes)) {
|
|
74
|
+
if (existing[k] !== v) {
|
|
75
|
+
diff[k] = v;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return this.enqueueMutation(config.channel, "update", id, diff, rollback);
|
|
79
|
+
}
|
|
80
|
+
// Overload 2: put(row) - Insert/Replace
|
|
81
|
+
const row = rowOrId;
|
|
82
|
+
const id = row.id;
|
|
83
|
+
if (!id) {
|
|
84
|
+
throw new Error("put operation requires an inline 'id' property.");
|
|
85
|
+
}
|
|
86
|
+
const existing = await table.get(id);
|
|
87
|
+
if (!existing) {
|
|
88
|
+
return table.add(row);
|
|
89
|
+
}
|
|
90
|
+
const rollback = async () => {
|
|
91
|
+
await originalPut(existing);
|
|
92
|
+
};
|
|
93
|
+
const updatedRow = { ...existing, ...row };
|
|
94
|
+
await originalPut(updatedRow);
|
|
95
|
+
const diff = {};
|
|
96
|
+
for (const [k, v] of Object.entries(row)) {
|
|
97
|
+
if (existing[k] !== v) {
|
|
98
|
+
diff[k] = v;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return this.enqueueMutation(config.channel, "update", id, diff, rollback);
|
|
102
|
+
});
|
|
103
|
+
table.update = (async (id, changes) => {
|
|
104
|
+
const existing = await table.get(id);
|
|
105
|
+
if (!existing) {
|
|
106
|
+
throw new Error(`Cannot update item ${id}: not found locally.`);
|
|
107
|
+
}
|
|
108
|
+
const rollback = async () => {
|
|
109
|
+
await originalPut(existing);
|
|
110
|
+
};
|
|
111
|
+
await originalUpdate(id, changes);
|
|
112
|
+
return this.enqueueMutation(config.channel, "update", id, changes, rollback);
|
|
113
|
+
});
|
|
114
|
+
table.delete = (async (id) => {
|
|
115
|
+
const existing = await table.get(id);
|
|
116
|
+
if (!existing)
|
|
117
|
+
return;
|
|
118
|
+
const rollback = async () => {
|
|
119
|
+
await originalPut(existing);
|
|
120
|
+
};
|
|
121
|
+
await originalDelete(id);
|
|
122
|
+
return this.enqueueMutation(config.channel, "delete", id, undefined, rollback);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
37
126
|
async connect() {
|
|
38
127
|
if (this.closedByClient)
|
|
39
128
|
return;
|
|
@@ -146,7 +235,7 @@ export class SyncClient {
|
|
|
146
235
|
let since;
|
|
147
236
|
if (tableName) {
|
|
148
237
|
try {
|
|
149
|
-
const table = this.
|
|
238
|
+
const table = this.table(tableName);
|
|
150
239
|
const latestRow = await table.orderBy("updatedAt").last();
|
|
151
240
|
if (latestRow && latestRow.updatedAt) {
|
|
152
241
|
since = latestRow.updatedAt;
|
|
@@ -176,7 +265,7 @@ export class SyncClient {
|
|
|
176
265
|
}
|
|
177
266
|
}
|
|
178
267
|
async safePutRow(tableName, data) {
|
|
179
|
-
const table = this.
|
|
268
|
+
const table = this.table(tableName);
|
|
180
269
|
if (!data || !data.id)
|
|
181
270
|
return;
|
|
182
271
|
const existing = await table.get(data.id);
|
|
@@ -188,10 +277,11 @@ export class SyncClient {
|
|
|
188
277
|
return;
|
|
189
278
|
}
|
|
190
279
|
}
|
|
191
|
-
|
|
280
|
+
const originalPut = table._originalMethods?.put || table.put.bind(table);
|
|
281
|
+
await originalPut(data);
|
|
192
282
|
}
|
|
193
283
|
async safeDeleteRow(tableName, key, incomingTimeStr) {
|
|
194
|
-
const table = this.
|
|
284
|
+
const table = this.table(tableName);
|
|
195
285
|
if (incomingTimeStr) {
|
|
196
286
|
const existing = await table.get(key);
|
|
197
287
|
if (existing && existing.updatedAt) {
|
|
@@ -203,14 +293,15 @@ export class SyncClient {
|
|
|
203
293
|
}
|
|
204
294
|
}
|
|
205
295
|
}
|
|
206
|
-
|
|
296
|
+
const originalDelete = table._originalMethods?.delete || table.delete.bind(table);
|
|
297
|
+
await originalDelete(key);
|
|
207
298
|
}
|
|
208
299
|
async handleServerMessage(msg) {
|
|
209
300
|
switch (msg.type) {
|
|
210
301
|
case "snapshot": {
|
|
211
302
|
const tableName = this.findTableByChannel(msg.channel);
|
|
212
303
|
if (tableName) {
|
|
213
|
-
const table = this.
|
|
304
|
+
const table = this.table(tableName);
|
|
214
305
|
if (msg.isDelta) {
|
|
215
306
|
// Delta Sync: put changes using Last-Write-Wins
|
|
216
307
|
for (const row of msg.data) {
|
|
@@ -219,7 +310,7 @@ export class SyncClient {
|
|
|
219
310
|
}
|
|
220
311
|
else {
|
|
221
312
|
// Full Snapshot: clear and replace
|
|
222
|
-
await this.
|
|
313
|
+
await this.transaction("rw", table, async () => {
|
|
223
314
|
await table.clear();
|
|
224
315
|
await table.bulkPut(msg.data);
|
|
225
316
|
});
|
|
@@ -282,63 +373,6 @@ export class SyncClient {
|
|
|
282
373
|
}
|
|
283
374
|
return undefined;
|
|
284
375
|
}
|
|
285
|
-
table(tableName) {
|
|
286
|
-
const dexieTable = this.db.table(tableName);
|
|
287
|
-
const config = this.tableConfigs[tableName];
|
|
288
|
-
if (!config) {
|
|
289
|
-
throw new Error(`Table ${tableName} not defined in SyncClient config.`);
|
|
290
|
-
}
|
|
291
|
-
return {
|
|
292
|
-
rawTable: dexieTable,
|
|
293
|
-
liveQuery: (queryFn) => {
|
|
294
|
-
return useLiveQuery(() => queryFn(dexieTable));
|
|
295
|
-
},
|
|
296
|
-
list: () => {
|
|
297
|
-
return useLiveQuery(() => dexieTable.toArray());
|
|
298
|
-
},
|
|
299
|
-
get: async (id) => {
|
|
300
|
-
return dexieTable.get(id);
|
|
301
|
-
},
|
|
302
|
-
add: async (row) => {
|
|
303
|
-
const rowData = row;
|
|
304
|
-
const id = rowData.id || crypto.randomUUID();
|
|
305
|
-
const fullRow = { ...rowData, id };
|
|
306
|
-
// Rollback function
|
|
307
|
-
const rollback = async () => {
|
|
308
|
-
await dexieTable.delete(id);
|
|
309
|
-
};
|
|
310
|
-
// Apply optimistic update
|
|
311
|
-
await dexieTable.put(fullRow);
|
|
312
|
-
return this.enqueueMutation(config.channel, "create", id, fullRow, rollback);
|
|
313
|
-
},
|
|
314
|
-
put: async (id, changes) => {
|
|
315
|
-
const existing = await dexieTable.get(id);
|
|
316
|
-
if (!existing) {
|
|
317
|
-
throw new Error(`Cannot update item ${id}: not found locally.`);
|
|
318
|
-
}
|
|
319
|
-
// Rollback function
|
|
320
|
-
const rollback = async () => {
|
|
321
|
-
await dexieTable.put(existing);
|
|
322
|
-
};
|
|
323
|
-
const updatedRow = { ...existing, ...changes };
|
|
324
|
-
// Apply optimistic update
|
|
325
|
-
await dexieTable.put(updatedRow);
|
|
326
|
-
return this.enqueueMutation(config.channel, "update", id, changes, rollback);
|
|
327
|
-
},
|
|
328
|
-
delete: async (id) => {
|
|
329
|
-
const existing = await dexieTable.get(id);
|
|
330
|
-
if (!existing)
|
|
331
|
-
return; // Already deleted
|
|
332
|
-
// Rollback function
|
|
333
|
-
const rollback = async () => {
|
|
334
|
-
await dexieTable.put(existing);
|
|
335
|
-
};
|
|
336
|
-
// Apply optimistic update
|
|
337
|
-
await dexieTable.delete(id);
|
|
338
|
-
return this.enqueueMutation(config.channel, "delete", id, undefined, rollback);
|
|
339
|
-
},
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
376
|
enqueueMutation(channel, action, key, data, rollback) {
|
|
343
377
|
const mutationId = crypto.randomUUID();
|
|
344
378
|
return new Promise((resolve, reject) => {
|
|
@@ -384,3 +418,4 @@ export class SyncClient {
|
|
|
384
418
|
}
|
|
385
419
|
}
|
|
386
420
|
}
|
|
421
|
+
export const SyncClient = SyncClientClass;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { SyncClient
|
|
1
|
+
export { SyncClient } from "./client/index.js";
|
|
2
2
|
export { defineSync } from "./server/index.js";
|
|
3
3
|
export { handleUpgrade, publishEvent, createPublisher } from "./server/handler.js";
|
|
4
|
-
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,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACnF,YAAY,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AACrF,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAClE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1 +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) {
|
package/package.json
CHANGED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export type LiveQueryResult<T> = {
|
|
2
|
-
status: "loading";
|
|
3
|
-
data: undefined;
|
|
4
|
-
error: undefined;
|
|
5
|
-
isLoading: true;
|
|
6
|
-
} | {
|
|
7
|
-
status: "success";
|
|
8
|
-
data: T;
|
|
9
|
-
error: undefined;
|
|
10
|
-
isLoading: false;
|
|
11
|
-
} | {
|
|
12
|
-
status: "error";
|
|
13
|
-
data: undefined;
|
|
14
|
-
error: any;
|
|
15
|
-
isLoading: false;
|
|
16
|
-
};
|
|
17
|
-
export declare function useLiveQuery<T>(queryFn: () => Promise<T> | T): LiveQueryResult<T>;
|
|
18
|
-
//# sourceMappingURL=live.svelte.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"live.svelte.d.ts","sourceRoot":"","sources":["../../src/client/live.svelte.ts"],"names":[],"mappings":"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,41 +0,0 @@
|
|
|
1
|
-
import { liveQuery } from "dexie";
|
|
2
|
-
export function useLiveQuery(queryFn) {
|
|
3
|
-
let data = $state(undefined);
|
|
4
|
-
let error = $state(undefined);
|
|
5
|
-
let status = $state("loading");
|
|
6
|
-
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
|
-
};
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
return {
|
|
28
|
-
get data() {
|
|
29
|
-
return data;
|
|
30
|
-
},
|
|
31
|
-
get error() {
|
|
32
|
-
return error;
|
|
33
|
-
},
|
|
34
|
-
get status() {
|
|
35
|
-
return status;
|
|
36
|
-
},
|
|
37
|
-
get isLoading() {
|
|
38
|
-
return status === "loading";
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|