@sveltebase/sync 1.0.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 +480 -0
- package/dist/client/index.d.ts +47 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +326 -0
- package/dist/client/live.svelte.d.ts +18 -0
- package/dist/client/live.svelte.d.ts.map +1 -0
- package/dist/client/live.svelte.js +40 -0
- package/dist/global.d.ts +25 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/protocol.d.ts +41 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +14 -0
- package/dist/server/broker.d.ts +27 -0
- package/dist/server/broker.d.ts.map +1 -0
- package/dist/server/broker.js +230 -0
- package/dist/server/dev-engine.d.ts +10 -0
- package/dist/server/dev-engine.d.ts.map +1 -0
- package/dist/server/dev-engine.js +117 -0
- package/dist/server/engine.d.ts +13 -0
- package/dist/server/engine.d.ts.map +1 -0
- package/dist/server/engine.js +117 -0
- package/dist/server/handler.d.ts +3 -0
- package/dist/server/handler.d.ts.map +1 -0
- package/dist/server/handler.js +51 -0
- package/dist/server/index.d.ts +25 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +10 -0
- package/dist/vite.d.ts +6 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +55 -0
- package/package.json +49 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import Dexie, {} from "dexie";
|
|
2
|
+
import { parseSyncMessage } from "../protocol.js";
|
|
3
|
+
import { useLiveQuery } from "./live.svelte.js";
|
|
4
|
+
export { useLiveQuery };
|
|
5
|
+
export class SyncClient {
|
|
6
|
+
db;
|
|
7
|
+
wsUrl;
|
|
8
|
+
socket;
|
|
9
|
+
tableConfigs;
|
|
10
|
+
reconnectTimer;
|
|
11
|
+
pingInterval;
|
|
12
|
+
closedByClient = false;
|
|
13
|
+
activeChannels = new Set();
|
|
14
|
+
// Mutations waiting for ack/reject from server
|
|
15
|
+
pendingMutations = new Map();
|
|
16
|
+
// Mutations queued to be sent when connection is established
|
|
17
|
+
mutationQueue = [];
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.wsUrl = options.url;
|
|
20
|
+
this.tableConfigs = options.tables;
|
|
21
|
+
// Initialize Dexie database
|
|
22
|
+
this.db = new Dexie(options.name);
|
|
23
|
+
const schema = {};
|
|
24
|
+
for (const [tableName, config] of Object.entries(options.tables)) {
|
|
25
|
+
schema[tableName] = config.indexes;
|
|
26
|
+
}
|
|
27
|
+
this.db.version(1).stores(schema);
|
|
28
|
+
if (typeof window !== "undefined") {
|
|
29
|
+
this.connect();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
connect() {
|
|
33
|
+
if (this.closedByClient)
|
|
34
|
+
return;
|
|
35
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
36
|
+
const host = window.location.host;
|
|
37
|
+
const fullUrl = this.wsUrl.startsWith("ws://") || this.wsUrl.startsWith("wss://")
|
|
38
|
+
? this.wsUrl
|
|
39
|
+
: `${protocol}//${host}${this.wsUrl}`;
|
|
40
|
+
this.socket = new WebSocket(fullUrl);
|
|
41
|
+
this.socket.addEventListener("open", async () => {
|
|
42
|
+
console.log("SyncClient: WebSocket connected");
|
|
43
|
+
this.activeChannels.clear();
|
|
44
|
+
this.startHeartbeat();
|
|
45
|
+
// Re-subscribe to all tables (delta-sync aware)
|
|
46
|
+
for (const config of Object.values(this.tableConfigs)) {
|
|
47
|
+
await this.subscribeToChannel(config.channel);
|
|
48
|
+
}
|
|
49
|
+
// Re-send all pending unacknowledged mutations
|
|
50
|
+
for (const mut of this.pendingMutations.values()) {
|
|
51
|
+
this.socket?.send(JSON.stringify({
|
|
52
|
+
type: "mutate",
|
|
53
|
+
id: mut.id,
|
|
54
|
+
channel: mut.channel,
|
|
55
|
+
action: mut.action,
|
|
56
|
+
key: mut.key,
|
|
57
|
+
data: mut.data,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
// Flush queued mutations
|
|
61
|
+
this.flushMutationQueue();
|
|
62
|
+
});
|
|
63
|
+
this.socket.addEventListener("message", async (message) => {
|
|
64
|
+
if (typeof message.data !== "string")
|
|
65
|
+
return;
|
|
66
|
+
if (message.data === "pong")
|
|
67
|
+
return;
|
|
68
|
+
const msg = parseSyncMessage(message.data);
|
|
69
|
+
if (!msg)
|
|
70
|
+
return;
|
|
71
|
+
await this.handleServerMessage(msg);
|
|
72
|
+
});
|
|
73
|
+
this.socket.addEventListener("close", () => {
|
|
74
|
+
this.socket = undefined;
|
|
75
|
+
this.stopHeartbeat();
|
|
76
|
+
if (!this.closedByClient) {
|
|
77
|
+
this.reconnectTimer = setTimeout(() => this.connect(), 2000);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
this.socket.addEventListener("error", (err) => {
|
|
81
|
+
console.error("SyncClient: WebSocket error", err);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
startHeartbeat() {
|
|
85
|
+
this.stopHeartbeat();
|
|
86
|
+
this.pingInterval = setInterval(() => {
|
|
87
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
88
|
+
this.socket.send(JSON.stringify({ type: "ping" }));
|
|
89
|
+
}
|
|
90
|
+
}, 55000); // 55 seconds
|
|
91
|
+
}
|
|
92
|
+
stopHeartbeat() {
|
|
93
|
+
if (this.pingInterval) {
|
|
94
|
+
clearInterval(this.pingInterval);
|
|
95
|
+
this.pingInterval = undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async subscribeToChannel(channel) {
|
|
99
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
100
|
+
const tableName = this.findTableByChannel(channel);
|
|
101
|
+
let since;
|
|
102
|
+
if (tableName) {
|
|
103
|
+
try {
|
|
104
|
+
const table = this.db.table(tableName);
|
|
105
|
+
const latestRow = await table.orderBy("updatedAt").last();
|
|
106
|
+
if (latestRow && latestRow.updatedAt) {
|
|
107
|
+
since = latestRow.updatedAt;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Ignore if query fails or table is empty
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
this.socket.send(JSON.stringify({ type: "subscribe", channel, since }));
|
|
115
|
+
this.activeChannels.add(channel);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
flushMutationQueue() {
|
|
119
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN)
|
|
120
|
+
return;
|
|
121
|
+
while (this.mutationQueue.length > 0) {
|
|
122
|
+
const mut = this.mutationQueue.shift();
|
|
123
|
+
this.socket.send(JSON.stringify({
|
|
124
|
+
type: "mutate",
|
|
125
|
+
id: mut.id,
|
|
126
|
+
channel: mut.channel,
|
|
127
|
+
action: mut.action,
|
|
128
|
+
key: mut.key,
|
|
129
|
+
data: mut.data,
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async safePutRow(tableName, data) {
|
|
134
|
+
const table = this.db.table(tableName);
|
|
135
|
+
if (!data || !data.id)
|
|
136
|
+
return;
|
|
137
|
+
const existing = await table.get(data.id);
|
|
138
|
+
if (existing && existing.updatedAt && data.updatedAt) {
|
|
139
|
+
const existingTime = new Date(existing.updatedAt).getTime();
|
|
140
|
+
const incomingTime = new Date(data.updatedAt).getTime();
|
|
141
|
+
if (incomingTime < existingTime) {
|
|
142
|
+
// Ignore older update (Last-Write-Wins)
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
await table.put(data);
|
|
147
|
+
}
|
|
148
|
+
async safeDeleteRow(tableName, key, incomingTimeStr) {
|
|
149
|
+
const table = this.db.table(tableName);
|
|
150
|
+
if (incomingTimeStr) {
|
|
151
|
+
const existing = await table.get(key);
|
|
152
|
+
if (existing && existing.updatedAt) {
|
|
153
|
+
const existingTime = new Date(existing.updatedAt).getTime();
|
|
154
|
+
const incomingTime = new Date(incomingTimeStr).getTime();
|
|
155
|
+
if (incomingTime < existingTime) {
|
|
156
|
+
// Ignore older delete
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await table.delete(key);
|
|
162
|
+
}
|
|
163
|
+
async handleServerMessage(msg) {
|
|
164
|
+
switch (msg.type) {
|
|
165
|
+
case "snapshot": {
|
|
166
|
+
const tableName = this.findTableByChannel(msg.channel);
|
|
167
|
+
if (tableName) {
|
|
168
|
+
const table = this.db.table(tableName);
|
|
169
|
+
if (msg.isDelta) {
|
|
170
|
+
// Delta Sync: put changes using Last-Write-Wins
|
|
171
|
+
for (const row of msg.data) {
|
|
172
|
+
await this.safePutRow(tableName, row);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// Full Snapshot: clear and replace
|
|
177
|
+
await this.db.transaction("rw", table, async () => {
|
|
178
|
+
await table.clear();
|
|
179
|
+
await table.bulkPut(msg.data);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "ack": {
|
|
186
|
+
const pending = this.pendingMutations.get(msg.id);
|
|
187
|
+
if (pending) {
|
|
188
|
+
// If server returned canonical data, update local Dexie (respecting LWW)
|
|
189
|
+
if (msg.data) {
|
|
190
|
+
const tableName = this.findTableByChannel(pending.channel);
|
|
191
|
+
if (tableName) {
|
|
192
|
+
await this.safePutRow(tableName, msg.data);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
pending.resolve(msg.data);
|
|
196
|
+
this.pendingMutations.delete(msg.id);
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "reject": {
|
|
201
|
+
const pending = this.pendingMutations.get(msg.id);
|
|
202
|
+
if (pending) {
|
|
203
|
+
console.warn(`Mutation ${msg.id} rejected by server: ${msg.error}`);
|
|
204
|
+
await pending.rollback();
|
|
205
|
+
pending.reject(new Error(msg.error));
|
|
206
|
+
this.pendingMutations.delete(msg.id);
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case "change": {
|
|
211
|
+
// Prevent sync loops: if we sent this mutation, ignore the echo change
|
|
212
|
+
if (msg.mutationId && this.pendingMutations.has(msg.mutationId)) {
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
const tableName = this.findTableByChannel(msg.channel);
|
|
216
|
+
if (!tableName)
|
|
217
|
+
break;
|
|
218
|
+
if (msg.action === "create" || msg.action === "update") {
|
|
219
|
+
await this.safePutRow(tableName, msg.data);
|
|
220
|
+
}
|
|
221
|
+
else if (msg.action === "delete" && msg.key) {
|
|
222
|
+
const incomingTimeStr = msg.data?.updatedAt;
|
|
223
|
+
await this.safeDeleteRow(tableName, msg.key, incomingTimeStr);
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
findTableByChannel(channel) {
|
|
230
|
+
for (const [tableName, config] of Object.entries(this.tableConfigs)) {
|
|
231
|
+
if (config.channel === channel)
|
|
232
|
+
return tableName;
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
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
|
+
enqueueMutation(channel, action, key, data, rollback) {
|
|
287
|
+
const mutationId = crypto.randomUUID();
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
this.pendingMutations.set(mutationId, {
|
|
290
|
+
id: mutationId,
|
|
291
|
+
channel,
|
|
292
|
+
action,
|
|
293
|
+
key,
|
|
294
|
+
data,
|
|
295
|
+
rollback,
|
|
296
|
+
resolve,
|
|
297
|
+
reject,
|
|
298
|
+
});
|
|
299
|
+
const msg = {
|
|
300
|
+
id: mutationId,
|
|
301
|
+
channel,
|
|
302
|
+
action,
|
|
303
|
+
key,
|
|
304
|
+
data,
|
|
305
|
+
};
|
|
306
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
307
|
+
this.socket.send(JSON.stringify({ type: "mutate", ...msg }));
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
this.mutationQueue.push(msg);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
disconnect() {
|
|
315
|
+
this.closedByClient = true;
|
|
316
|
+
this.stopHeartbeat();
|
|
317
|
+
if (this.reconnectTimer) {
|
|
318
|
+
clearTimeout(this.reconnectTimer);
|
|
319
|
+
this.reconnectTimer = undefined;
|
|
320
|
+
}
|
|
321
|
+
if (this.socket) {
|
|
322
|
+
this.socket.close();
|
|
323
|
+
this.socket = undefined;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live.svelte.d.ts","sourceRoot":"","sources":["../../src/client/live.svelte.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,eAAe,CAAC,CAAC,IACzB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,GACzE;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,KAAK,CAAA;CAAE,GAClE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC;IAAC,SAAS,EAAE,KAAK,CAAA;CAAE,CAAC;AAEvE,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAuCjF"}
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
}
|
package/dist/global.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
namespace App {
|
|
2
|
+
interface Platform {
|
|
3
|
+
env: {
|
|
4
|
+
SYNC_ENGINE: any;
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
};
|
|
7
|
+
context: any;
|
|
8
|
+
caches: any;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Env {
|
|
13
|
+
SYNC_ENGINE: any;
|
|
14
|
+
[key: string]: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare module "$app/environment" {
|
|
18
|
+
export const dev: boolean;
|
|
19
|
+
export const browser: boolean;
|
|
20
|
+
export const building: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare module "$app/server" {
|
|
24
|
+
export function getRequestEvent(): any;
|
|
25
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { SyncClient, useLiveQuery } from "./client/index.js";
|
|
2
|
+
export { defineSync } from "./server/index.js";
|
|
3
|
+
export { handleUpgrade, publishEvent } from "./server/handler.js";
|
|
4
|
+
export type { SyncContext, SyncHandler } from "./server/index.js";
|
|
5
|
+
export type { SyncMessage } from "./protocol.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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,MAAM,qBAAqB,CAAC;AAClE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAClE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type SyncMessage = {
|
|
2
|
+
type: "subscribe";
|
|
3
|
+
channel: string;
|
|
4
|
+
since?: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: "unsubscribe";
|
|
7
|
+
channel: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: "mutate";
|
|
10
|
+
id: string;
|
|
11
|
+
channel: string;
|
|
12
|
+
action: "create" | "update" | "delete";
|
|
13
|
+
key?: string;
|
|
14
|
+
data?: any;
|
|
15
|
+
} | {
|
|
16
|
+
type: "ping";
|
|
17
|
+
} | {
|
|
18
|
+
type: "pong";
|
|
19
|
+
} | {
|
|
20
|
+
type: "snapshot";
|
|
21
|
+
channel: string;
|
|
22
|
+
data: any[];
|
|
23
|
+
isDelta?: boolean;
|
|
24
|
+
} | {
|
|
25
|
+
type: "ack";
|
|
26
|
+
id: string;
|
|
27
|
+
data?: any;
|
|
28
|
+
} | {
|
|
29
|
+
type: "reject";
|
|
30
|
+
id: string;
|
|
31
|
+
error: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: "change";
|
|
34
|
+
channel: string;
|
|
35
|
+
action: "create" | "update" | "delete";
|
|
36
|
+
key?: string;
|
|
37
|
+
data?: any;
|
|
38
|
+
mutationId?: string;
|
|
39
|
+
};
|
|
40
|
+
export declare function parseSyncMessage(data: string): SyncMessage | null;
|
|
41
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACxC;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ,GACD;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,GAAG,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GACrE;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC7C;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEN,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAcjE"}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function parseSyncMessage(data) {
|
|
2
|
+
try {
|
|
3
|
+
const parsed = JSON.parse(data);
|
|
4
|
+
if (parsed &&
|
|
5
|
+
typeof parsed === "object" &&
|
|
6
|
+
typeof parsed.type === "string") {
|
|
7
|
+
return parsed;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// Ignore malformed JSON
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SyncHandler } from "./index.js";
|
|
2
|
+
export interface ISyncConnection {
|
|
3
|
+
send(data: string): void;
|
|
4
|
+
close(code?: number, reason?: string): void;
|
|
5
|
+
getAuth(): any;
|
|
6
|
+
setAuth(auth: any): void;
|
|
7
|
+
getSubscribedChannels(): Set<string>;
|
|
8
|
+
readonly headers: Headers;
|
|
9
|
+
readonly url: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class SyncBroker {
|
|
12
|
+
private handlers;
|
|
13
|
+
private connections;
|
|
14
|
+
private authorizeConnection?;
|
|
15
|
+
constructor(handlers: SyncHandler[], authorizeConnection?: (request: Request, platform: App.Platform | undefined) => Promise<any>);
|
|
16
|
+
setHandlers(handlers: SyncHandler[]): void;
|
|
17
|
+
registerConnection(conn: ISyncConnection): void;
|
|
18
|
+
removeConnection(conn: ISyncConnection): void;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the appropriate handler for a channel name.
|
|
21
|
+
*/
|
|
22
|
+
private findHandler;
|
|
23
|
+
handleMessage(conn: ISyncConnection, rawMessage: string, platform: App.Platform | undefined, request: Request): Promise<void>;
|
|
24
|
+
private broadcastChange;
|
|
25
|
+
handleExternalChange(channel: string, action: "create" | "update" | "delete", key: string | undefined, data: any): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=broker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broker.d.ts","sourceRoot":"","sources":["../../src/server/broker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,YAAY,CAAC;AAG3D,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,OAAO,IAAI,GAAG,CAAC;IACf,OAAO,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;IACzB,qBAAqB,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,mBAAmB,CAAC,CAGV;gBAGhB,QAAQ,EAAE,WAAW,EAAE,EACvB,mBAAmB,CAAC,EAAE,CACpB,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,KAC/B,OAAO,CAAC,GAAG,CAAC;IAOZ,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE;IAanC,kBAAkB,CAAC,IAAI,EAAE,eAAe;IAIxC,gBAAgB,CAAC,IAAI,EAAE,eAAe;IAI7C;;OAEG;IACH,OAAO,CAAC,WAAW;IAqCN,aAAa,CACxB,IAAI,EAAE,eAAe,EACrB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,EAClC,OAAO,EAAE,OAAO;YA0IJ,eAAe;IAoDhB,oBAAoB,CAC/B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,GAAG;CAoBZ"}
|