@sveltebase/sync 1.0.7 → 1.1.1
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 +15 -23
- package/dist/client/index.d.ts +18 -12
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +175 -80
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- 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 -40
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
|
---
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
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;
|
|
7
5
|
};
|
|
8
6
|
export type SyncClientOptions = {
|
|
9
7
|
name: string;
|
|
10
|
-
url: string;
|
|
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;
|
|
@@ -19,14 +16,18 @@ export declare class SyncClient<TSchema extends Record<string, any> = Record<str
|
|
|
19
16
|
private pingInterval;
|
|
20
17
|
private closedByClient;
|
|
21
18
|
private activeChannels;
|
|
19
|
+
private _status;
|
|
22
20
|
private pendingMutations;
|
|
23
21
|
private mutationQueue;
|
|
24
22
|
constructor(options: {
|
|
25
23
|
name: string;
|
|
26
|
-
url: string;
|
|
24
|
+
url: string | (() => string | Promise<string>);
|
|
27
25
|
tables: Record<keyof TSchema & string, TableConfig>;
|
|
28
26
|
});
|
|
27
|
+
get status(): "connecting" | "connected" | "disconnected";
|
|
28
|
+
private decorateTables;
|
|
29
29
|
private connect;
|
|
30
|
+
reconnect(): void;
|
|
30
31
|
private startHeartbeat;
|
|
31
32
|
private stopHeartbeat;
|
|
32
33
|
private subscribeToChannel;
|
|
@@ -35,13 +36,18 @@ export declare class SyncClient<TSchema extends Record<string, any> = Record<str
|
|
|
35
36
|
private safeDeleteRow;
|
|
36
37
|
private handleServerMessage;
|
|
37
38
|
private findTableByChannel;
|
|
38
|
-
table<TKey extends keyof TSchema & string>(tableName: TKey): {
|
|
39
|
-
liveQuery: <TResult = TSchema[TKey][]>(queryFn: (table: Table<TSchema[TKey], string>) => Promise<TResult> | TResult) => LiveQueryResult<TResult>;
|
|
40
|
-
add: (row: TSchema[TKey]) => Promise<TSchema[TKey]>;
|
|
41
|
-
put: (id: string, changes: Partial<TSchema[TKey]>) => Promise<TSchema[TKey]>;
|
|
42
|
-
delete: (id: string) => Promise<void>;
|
|
43
|
-
};
|
|
44
39
|
private enqueueMutation;
|
|
45
40
|
disconnect(): void;
|
|
46
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
|
+
[tableName: string]: Table<any>;
|
|
46
|
+
};
|
|
47
|
+
export declare const SyncClient: new <TSchema extends Record<string, any> = Record<string, any>>(options: {
|
|
48
|
+
name: string;
|
|
49
|
+
url: string | (() => string | Promise<string>);
|
|
50
|
+
tables: Record<keyof TSchema & string, TableConfig>;
|
|
51
|
+
}) => SyncClient<TSchema>;
|
|
52
|
+
export {};
|
|
47
53
|
//# 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,GAAG;IACF,CAAC,SAAS,EAAE,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;CACjC,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;
|
|
@@ -11,35 +8,149 @@ export class SyncClient {
|
|
|
11
8
|
pingInterval;
|
|
12
9
|
closedByClient = false;
|
|
13
10
|
activeChannels = new Set();
|
|
11
|
+
// Reactive connection status
|
|
12
|
+
_status = $state("connecting");
|
|
14
13
|
// Mutations waiting for ack/reject from server
|
|
15
14
|
pendingMutations = new Map();
|
|
16
15
|
// Mutations queued to be sent when connection is established
|
|
17
16
|
mutationQueue = [];
|
|
18
17
|
constructor(options) {
|
|
18
|
+
super(options.name);
|
|
19
19
|
this.wsUrl = options.url;
|
|
20
20
|
this.tableConfigs = options.tables;
|
|
21
21
|
// Initialize Dexie database
|
|
22
|
-
this.db = new Dexie(options.name);
|
|
23
22
|
const schema = {};
|
|
24
23
|
for (const [tableName, config] of Object.entries(options.tables)) {
|
|
25
24
|
schema[tableName] = config.indexes;
|
|
26
25
|
}
|
|
27
|
-
this.
|
|
26
|
+
this.version(1).stores(schema);
|
|
27
|
+
// Decorate tables to intercept native Dexie write operations
|
|
28
|
+
this.decorateTables();
|
|
28
29
|
if (typeof window !== "undefined") {
|
|
29
30
|
this.connect();
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
get status() {
|
|
34
|
+
return this._status;
|
|
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
|
+
}
|
|
126
|
+
async connect() {
|
|
33
127
|
if (this.closedByClient)
|
|
34
128
|
return;
|
|
129
|
+
this._status = "connecting";
|
|
35
130
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
36
131
|
const host = window.location.host;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
:
|
|
40
|
-
|
|
41
|
-
|
|
132
|
+
let resolvedUrl;
|
|
133
|
+
try {
|
|
134
|
+
resolvedUrl = typeof this.wsUrl === "function" ? await this.wsUrl() : this.wsUrl;
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
console.error("SyncClient: Failed to resolve wsUrl", err);
|
|
138
|
+
this._status = "disconnected";
|
|
139
|
+
if (!this.closedByClient) {
|
|
140
|
+
this.reconnectTimer = setTimeout(() => this.connect(), 2000);
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const fullUrl = resolvedUrl.startsWith("ws://") || resolvedUrl.startsWith("wss://")
|
|
145
|
+
? resolvedUrl
|
|
146
|
+
: `${protocol}//${host}${resolvedUrl}`;
|
|
147
|
+
const socket = new WebSocket(fullUrl);
|
|
148
|
+
this.socket = socket;
|
|
149
|
+
socket.addEventListener("open", async () => {
|
|
150
|
+
if (this.socket !== socket)
|
|
151
|
+
return;
|
|
42
152
|
console.log("SyncClient: WebSocket connected");
|
|
153
|
+
this._status = "connected";
|
|
43
154
|
this.activeChannels.clear();
|
|
44
155
|
this.startHeartbeat();
|
|
45
156
|
// Re-subscribe to all tables (delta-sync aware)
|
|
@@ -48,7 +159,7 @@ export class SyncClient {
|
|
|
48
159
|
}
|
|
49
160
|
// Re-send all pending unacknowledged mutations
|
|
50
161
|
for (const mut of this.pendingMutations.values()) {
|
|
51
|
-
|
|
162
|
+
socket.send(JSON.stringify({
|
|
52
163
|
type: "mutate",
|
|
53
164
|
id: mut.id,
|
|
54
165
|
channel: mut.channel,
|
|
@@ -60,7 +171,9 @@ export class SyncClient {
|
|
|
60
171
|
// Flush queued mutations
|
|
61
172
|
this.flushMutationQueue();
|
|
62
173
|
});
|
|
63
|
-
|
|
174
|
+
socket.addEventListener("message", async (message) => {
|
|
175
|
+
if (this.socket !== socket)
|
|
176
|
+
return;
|
|
64
177
|
if (typeof message.data !== "string")
|
|
65
178
|
return;
|
|
66
179
|
if (message.data === "pong")
|
|
@@ -70,17 +183,38 @@ export class SyncClient {
|
|
|
70
183
|
return;
|
|
71
184
|
await this.handleServerMessage(msg);
|
|
72
185
|
});
|
|
73
|
-
|
|
74
|
-
this.socket
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.
|
|
186
|
+
socket.addEventListener("close", () => {
|
|
187
|
+
if (this.socket === socket) {
|
|
188
|
+
this.socket = undefined;
|
|
189
|
+
this._status = "disconnected";
|
|
190
|
+
this.stopHeartbeat();
|
|
191
|
+
if (!this.closedByClient) {
|
|
192
|
+
this.reconnectTimer = setTimeout(() => this.connect(), 2000);
|
|
193
|
+
}
|
|
78
194
|
}
|
|
79
195
|
});
|
|
80
|
-
|
|
81
|
-
|
|
196
|
+
socket.addEventListener("error", (err) => {
|
|
197
|
+
if (this.socket === socket) {
|
|
198
|
+
console.error("SyncClient: WebSocket error", err);
|
|
199
|
+
}
|
|
82
200
|
});
|
|
83
201
|
}
|
|
202
|
+
reconnect() {
|
|
203
|
+
this.closedByClient = false;
|
|
204
|
+
if (this.reconnectTimer) {
|
|
205
|
+
clearTimeout(this.reconnectTimer);
|
|
206
|
+
this.reconnectTimer = undefined;
|
|
207
|
+
}
|
|
208
|
+
if (this.socket) {
|
|
209
|
+
try {
|
|
210
|
+
this.socket.close();
|
|
211
|
+
}
|
|
212
|
+
catch { }
|
|
213
|
+
this.socket = undefined;
|
|
214
|
+
this._status = "disconnected";
|
|
215
|
+
}
|
|
216
|
+
this.connect();
|
|
217
|
+
}
|
|
84
218
|
startHeartbeat() {
|
|
85
219
|
this.stopHeartbeat();
|
|
86
220
|
this.pingInterval = setInterval(() => {
|
|
@@ -101,7 +235,7 @@ export class SyncClient {
|
|
|
101
235
|
let since;
|
|
102
236
|
if (tableName) {
|
|
103
237
|
try {
|
|
104
|
-
const table = this.
|
|
238
|
+
const table = this.table(tableName);
|
|
105
239
|
const latestRow = await table.orderBy("updatedAt").last();
|
|
106
240
|
if (latestRow && latestRow.updatedAt) {
|
|
107
241
|
since = latestRow.updatedAt;
|
|
@@ -131,7 +265,7 @@ export class SyncClient {
|
|
|
131
265
|
}
|
|
132
266
|
}
|
|
133
267
|
async safePutRow(tableName, data) {
|
|
134
|
-
const table = this.
|
|
268
|
+
const table = this.table(tableName);
|
|
135
269
|
if (!data || !data.id)
|
|
136
270
|
return;
|
|
137
271
|
const existing = await table.get(data.id);
|
|
@@ -143,10 +277,11 @@ export class SyncClient {
|
|
|
143
277
|
return;
|
|
144
278
|
}
|
|
145
279
|
}
|
|
146
|
-
|
|
280
|
+
const originalPut = table._originalMethods?.put || table.put.bind(table);
|
|
281
|
+
await originalPut(data);
|
|
147
282
|
}
|
|
148
283
|
async safeDeleteRow(tableName, key, incomingTimeStr) {
|
|
149
|
-
const table = this.
|
|
284
|
+
const table = this.table(tableName);
|
|
150
285
|
if (incomingTimeStr) {
|
|
151
286
|
const existing = await table.get(key);
|
|
152
287
|
if (existing && existing.updatedAt) {
|
|
@@ -158,14 +293,15 @@ export class SyncClient {
|
|
|
158
293
|
}
|
|
159
294
|
}
|
|
160
295
|
}
|
|
161
|
-
|
|
296
|
+
const originalDelete = table._originalMethods?.delete || table.delete.bind(table);
|
|
297
|
+
await originalDelete(key);
|
|
162
298
|
}
|
|
163
299
|
async handleServerMessage(msg) {
|
|
164
300
|
switch (msg.type) {
|
|
165
301
|
case "snapshot": {
|
|
166
302
|
const tableName = this.findTableByChannel(msg.channel);
|
|
167
303
|
if (tableName) {
|
|
168
|
-
const table = this.
|
|
304
|
+
const table = this.table(tableName);
|
|
169
305
|
if (msg.isDelta) {
|
|
170
306
|
// Delta Sync: put changes using Last-Write-Wins
|
|
171
307
|
for (const row of msg.data) {
|
|
@@ -174,7 +310,7 @@ export class SyncClient {
|
|
|
174
310
|
}
|
|
175
311
|
else {
|
|
176
312
|
// Full Snapshot: clear and replace
|
|
177
|
-
await this.
|
|
313
|
+
await this.transaction("rw", table, async () => {
|
|
178
314
|
await table.clear();
|
|
179
315
|
await table.bulkPut(msg.data);
|
|
180
316
|
});
|
|
@@ -202,7 +338,11 @@ export class SyncClient {
|
|
|
202
338
|
if (pending) {
|
|
203
339
|
console.warn(`Mutation ${msg.id} rejected by server: ${msg.error}`);
|
|
204
340
|
await pending.rollback();
|
|
205
|
-
|
|
341
|
+
const errorObj = new Error(msg.error);
|
|
342
|
+
if (msg.validationErrors) {
|
|
343
|
+
errorObj.validationErrors = msg.validationErrors;
|
|
344
|
+
}
|
|
345
|
+
pending.reject(errorObj);
|
|
206
346
|
this.pendingMutations.delete(msg.id);
|
|
207
347
|
}
|
|
208
348
|
break;
|
|
@@ -233,56 +373,6 @@ export class SyncClient {
|
|
|
233
373
|
}
|
|
234
374
|
return undefined;
|
|
235
375
|
}
|
|
236
|
-
table(tableName) {
|
|
237
|
-
const dexieTable = this.db.table(tableName);
|
|
238
|
-
const config = this.tableConfigs[tableName];
|
|
239
|
-
if (!config) {
|
|
240
|
-
throw new Error(`Table ${tableName} not defined in SyncClient config.`);
|
|
241
|
-
}
|
|
242
|
-
return {
|
|
243
|
-
liveQuery: (queryFn) => {
|
|
244
|
-
return useLiveQuery(() => queryFn(dexieTable));
|
|
245
|
-
},
|
|
246
|
-
add: async (row) => {
|
|
247
|
-
const rowData = row;
|
|
248
|
-
const id = rowData.id || crypto.randomUUID();
|
|
249
|
-
const fullRow = { ...rowData, id };
|
|
250
|
-
// Rollback function
|
|
251
|
-
const rollback = async () => {
|
|
252
|
-
await dexieTable.delete(id);
|
|
253
|
-
};
|
|
254
|
-
// Apply optimistic update
|
|
255
|
-
await dexieTable.put(fullRow);
|
|
256
|
-
return this.enqueueMutation(config.channel, "create", id, fullRow, rollback);
|
|
257
|
-
},
|
|
258
|
-
put: async (id, changes) => {
|
|
259
|
-
const existing = await dexieTable.get(id);
|
|
260
|
-
if (!existing) {
|
|
261
|
-
throw new Error(`Cannot update item ${id}: not found locally.`);
|
|
262
|
-
}
|
|
263
|
-
// Rollback function
|
|
264
|
-
const rollback = async () => {
|
|
265
|
-
await dexieTable.put(existing);
|
|
266
|
-
};
|
|
267
|
-
const updatedRow = { ...existing, ...changes };
|
|
268
|
-
// Apply optimistic update
|
|
269
|
-
await dexieTable.put(updatedRow);
|
|
270
|
-
return this.enqueueMutation(config.channel, "update", id, changes, rollback);
|
|
271
|
-
},
|
|
272
|
-
delete: async (id) => {
|
|
273
|
-
const existing = await dexieTable.get(id);
|
|
274
|
-
if (!existing)
|
|
275
|
-
return; // Already deleted
|
|
276
|
-
// Rollback function
|
|
277
|
-
const rollback = async () => {
|
|
278
|
-
await dexieTable.put(existing);
|
|
279
|
-
};
|
|
280
|
-
// Apply optimistic update
|
|
281
|
-
await dexieTable.delete(id);
|
|
282
|
-
return this.enqueueMutation(config.channel, "delete", id, undefined, rollback);
|
|
283
|
-
},
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
376
|
enqueueMutation(channel, action, key, data, rollback) {
|
|
287
377
|
const mutationId = crypto.randomUUID();
|
|
288
378
|
return new Promise((resolve, reject) => {
|
|
@@ -313,14 +403,19 @@ export class SyncClient {
|
|
|
313
403
|
}
|
|
314
404
|
disconnect() {
|
|
315
405
|
this.closedByClient = true;
|
|
406
|
+
this._status = "disconnected";
|
|
316
407
|
this.stopHeartbeat();
|
|
317
408
|
if (this.reconnectTimer) {
|
|
318
409
|
clearTimeout(this.reconnectTimer);
|
|
319
410
|
this.reconnectTimer = undefined;
|
|
320
411
|
}
|
|
321
412
|
if (this.socket) {
|
|
322
|
-
|
|
413
|
+
try {
|
|
414
|
+
this.socket.close();
|
|
415
|
+
}
|
|
416
|
+
catch { }
|
|
323
417
|
this.socket = undefined;
|
|
324
418
|
}
|
|
325
419
|
}
|
|
326
420
|
}
|
|
421
|
+
export const SyncClient = SyncClientClass;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
4
|
export type { PublishEventData, InferSchemaFromHandlers } from "./server/handler.js";
|
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
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":"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,40 +0,0 @@
|
|
|
1
|
-
import { onDestroy } from "svelte";
|
|
2
|
-
import { liveQuery } from "dexie";
|
|
3
|
-
export function useLiveQuery(queryFn) {
|
|
4
|
-
let data = $state(undefined);
|
|
5
|
-
let error = $state(undefined);
|
|
6
|
-
let status = $state("loading");
|
|
7
|
-
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();
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
get data() {
|
|
28
|
-
return data;
|
|
29
|
-
},
|
|
30
|
-
get error() {
|
|
31
|
-
return error;
|
|
32
|
-
},
|
|
33
|
-
get status() {
|
|
34
|
-
return status;
|
|
35
|
-
},
|
|
36
|
-
get isLoading() {
|
|
37
|
-
return status === "loading";
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
}
|