reflectdb 0.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/LICENSE +21 -0
- package/README.md +1260 -0
- package/dist/cjs/client/index.cjs +1252 -0
- package/dist/cjs/client/index.d.cts +629 -0
- package/dist/cjs/client/storage/indexeddb.cjs +253 -0
- package/dist/cjs/client/storage/indexeddb.d.cts +85 -0
- package/dist/cjs/core/index.cjs +202 -0
- package/dist/cjs/core/index.d.cts +473 -0
- package/dist/cjs/react/index.cjs +1512 -0
- package/dist/cjs/react/index.d.cts +748 -0
- package/dist/cjs/server/drizzle.cjs +413 -0
- package/dist/cjs/server/drizzle.d.cts +233 -0
- package/dist/cjs/server/index.cjs +4486 -0
- package/dist/cjs/server/index.d.cts +1988 -0
- package/dist/cjs/svelte/index.cjs +1414 -0
- package/dist/cjs/svelte/index.d.cts +645 -0
- package/dist/cjs/transport/bun-ws.cjs +244 -0
- package/dist/cjs/transport/bun-ws.d.cts +215 -0
- package/dist/cjs/transport/polling.cjs +285 -0
- package/dist/cjs/transport/polling.d.cts +210 -0
- package/dist/cjs/transport/sse.cjs +312 -0
- package/dist/cjs/transport/sse.d.cts +205 -0
- package/dist/cjs/transport/ws.cjs +330 -0
- package/dist/cjs/transport/ws.d.cts +236 -0
- package/dist/cjs/vanilla/index.cjs +1430 -0
- package/dist/cjs/vanilla/index.d.cts +657 -0
- package/dist/client/index.d.ts +629 -0
- package/dist/client/index.js +106 -0
- package/dist/client/storage/indexeddb.d.ts +85 -0
- package/dist/client/storage/indexeddb.js +213 -0
- package/dist/core/index.d.ts +473 -0
- package/dist/core/index.js +56 -0
- package/dist/react/index.d.ts +748 -0
- package/dist/react/index.js +366 -0
- package/dist/server/drizzle.d.ts +233 -0
- package/dist/server/drizzle.js +9 -0
- package/dist/server/index.d.ts +1988 -0
- package/dist/server/index.js +3959 -0
- package/dist/shared/esm-3tkwvysa.js +54 -0
- package/dist/shared/esm-b7xs9cde.js +4 -0
- package/dist/shared/esm-rw7jjtrv.js +58 -0
- package/dist/shared/esm-wkwx6bd9.js +25 -0
- package/dist/shared/esm-ytrd3hbq.js +1007 -0
- package/dist/shared/esm-z1xse19c.js +369 -0
- package/dist/svelte/index.d.ts +645 -0
- package/dist/svelte/index.js +262 -0
- package/dist/transport/bun-ws.d.ts +215 -0
- package/dist/transport/bun-ws.js +140 -0
- package/dist/transport/polling.d.ts +210 -0
- package/dist/transport/polling.js +181 -0
- package/dist/transport/sse.d.ts +205 -0
- package/dist/transport/sse.js +208 -0
- package/dist/transport/ws.d.ts +236 -0
- package/dist/transport/ws.js +226 -0
- package/dist/vanilla/index.d.ts +657 -0
- package/dist/vanilla/index.js +278 -0
- package/package.json +253 -0
|
@@ -0,0 +1,1414 @@
|
|
|
1
|
+
var import_node_module = require("node:module");
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
function __accessProp(key) {
|
|
7
|
+
return this[key];
|
|
8
|
+
}
|
|
9
|
+
var __toCommonJS = (from) => {
|
|
10
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
11
|
+
if (entry)
|
|
12
|
+
return entry;
|
|
13
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (var key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(entry, key))
|
|
17
|
+
__defProp(entry, key, {
|
|
18
|
+
get: __accessProp.bind(from, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
__moduleCache.set(from, entry);
|
|
23
|
+
return entry;
|
|
24
|
+
};
|
|
25
|
+
var __moduleCache;
|
|
26
|
+
var __returnValue = (v) => v;
|
|
27
|
+
function __exportSetter(name, newValue) {
|
|
28
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
29
|
+
}
|
|
30
|
+
var __export = (target, all) => {
|
|
31
|
+
for (var name in all)
|
|
32
|
+
__defProp(target, name, {
|
|
33
|
+
get: all[name],
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
set: __exportSetter.bind(all, name)
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
40
|
+
|
|
41
|
+
// src/core/types.ts
|
|
42
|
+
var exports_types = {};
|
|
43
|
+
__export(exports_types, {
|
|
44
|
+
reasonFromError: () => reasonFromError,
|
|
45
|
+
isErrorReason: () => isErrorReason,
|
|
46
|
+
TransportSendError: () => TransportSendError,
|
|
47
|
+
TOMBSTONE_RETENTION_MS: () => TOMBSTONE_RETENTION_MS,
|
|
48
|
+
SUPPORTED_PROTOCOL_VERSIONS: () => SUPPORTED_PROTOCOL_VERSIONS,
|
|
49
|
+
SERVER_TOMBSTONE_RETENTION_MS: () => SERVER_TOMBSTONE_RETENTION_MS,
|
|
50
|
+
PROTOCOL_VERSION: () => PROTOCOL_VERSION,
|
|
51
|
+
MutationError: () => MutationError,
|
|
52
|
+
MAX_CLOCK_DRIFT_MS: () => MAX_CLOCK_DRIFT_MS,
|
|
53
|
+
MAX_BATCH_SIZE: () => MAX_BATCH_SIZE
|
|
54
|
+
});
|
|
55
|
+
function isErrorReason(value) {
|
|
56
|
+
return typeof value === "string" && VALID_ERROR_REASONS.has(value);
|
|
57
|
+
}
|
|
58
|
+
function reasonFromError(err) {
|
|
59
|
+
if (err instanceof MutationError && isErrorReason(err.reason)) {
|
|
60
|
+
return err.reason;
|
|
61
|
+
}
|
|
62
|
+
return "server_error";
|
|
63
|
+
}
|
|
64
|
+
var TransportSendError, PROTOCOL_VERSION = 1, SUPPORTED_PROTOCOL_VERSIONS, MAX_CLOCK_DRIFT_MS, MAX_BATCH_SIZE = 100, TOMBSTONE_RETENTION_MS, SERVER_TOMBSTONE_RETENTION_MS, MutationError, VALID_ERROR_REASONS;
|
|
65
|
+
var init_types = __esm(() => {
|
|
66
|
+
TransportSendError = class TransportSendError extends Error {
|
|
67
|
+
clientId;
|
|
68
|
+
cause;
|
|
69
|
+
constructor(clientId, message, cause) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.clientId = clientId;
|
|
72
|
+
this.cause = cause;
|
|
73
|
+
this.name = "TransportSendError";
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
SUPPORTED_PROTOCOL_VERSIONS = [1];
|
|
77
|
+
MAX_CLOCK_DRIFT_MS = 5 * 60 * 1000;
|
|
78
|
+
TOMBSTONE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
79
|
+
SERVER_TOMBSTONE_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
|
|
80
|
+
MutationError = class MutationError extends Error {
|
|
81
|
+
reason;
|
|
82
|
+
constructor(reason, message) {
|
|
83
|
+
super(message ?? reason);
|
|
84
|
+
this.reason = reason;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
VALID_ERROR_REASONS = new Set([
|
|
88
|
+
"outside_shape",
|
|
89
|
+
"readonly_query",
|
|
90
|
+
"rate_limited",
|
|
91
|
+
"buffer_full",
|
|
92
|
+
"server_conflict",
|
|
93
|
+
"custom_conflict",
|
|
94
|
+
"merge_stale",
|
|
95
|
+
"clock_drift",
|
|
96
|
+
"replay",
|
|
97
|
+
"batch_too_large",
|
|
98
|
+
"schema_outdated",
|
|
99
|
+
"auth_revoked",
|
|
100
|
+
"compacted",
|
|
101
|
+
"mutation_rejected",
|
|
102
|
+
"server_error",
|
|
103
|
+
"unknown_query"
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// src/svelte/index.ts
|
|
108
|
+
var exports_svelte = {};
|
|
109
|
+
__export(exports_svelte, {
|
|
110
|
+
createSyncSvelte: () => createSyncSvelte,
|
|
111
|
+
createSyncStore: () => createSyncStore,
|
|
112
|
+
createBrowserWsTransport: () => createBrowserWsTransport
|
|
113
|
+
});
|
|
114
|
+
module.exports = __toCommonJS(exports_svelte);
|
|
115
|
+
|
|
116
|
+
// src/client/sync-client.ts
|
|
117
|
+
init_types();
|
|
118
|
+
|
|
119
|
+
// src/core/hlc.ts
|
|
120
|
+
init_types();
|
|
121
|
+
var MS_PAD = 19;
|
|
122
|
+
var COUNTER_PAD = 4;
|
|
123
|
+
var COUNTER_MAX = 9999;
|
|
124
|
+
function createHlc(nodeId) {
|
|
125
|
+
return { ms: 0, counter: 0, nodeId };
|
|
126
|
+
}
|
|
127
|
+
function normalize(ms, counter, nodeId) {
|
|
128
|
+
if (counter > COUNTER_MAX) {
|
|
129
|
+
return { ms: ms + 1, counter: 0, nodeId };
|
|
130
|
+
}
|
|
131
|
+
return { ms, counter, nodeId };
|
|
132
|
+
}
|
|
133
|
+
function sendHlc(local) {
|
|
134
|
+
const wall = Date.now();
|
|
135
|
+
const ms = Math.max(wall, local.ms);
|
|
136
|
+
const counter = ms === local.ms ? local.counter + 1 : 0;
|
|
137
|
+
return normalize(ms, counter, local.nodeId);
|
|
138
|
+
}
|
|
139
|
+
function receiveHlc(local, remote) {
|
|
140
|
+
const wall = Date.now();
|
|
141
|
+
const clampedRemoteMs = Math.min(remote.ms, wall + MAX_CLOCK_DRIFT_MS);
|
|
142
|
+
const ms = Math.max(wall, local.ms, clampedRemoteMs);
|
|
143
|
+
const counter = ms === local.ms && ms === clampedRemoteMs ? Math.max(local.counter, remote.counter) + 1 : ms === local.ms ? local.counter + 1 : ms === clampedRemoteMs ? remote.counter + 1 : 0;
|
|
144
|
+
return normalize(ms, counter, local.nodeId);
|
|
145
|
+
}
|
|
146
|
+
function packHlc(hlc) {
|
|
147
|
+
const ms = String(hlc.ms).padStart(MS_PAD, "0");
|
|
148
|
+
const counter = String(hlc.counter).padStart(COUNTER_PAD, "0");
|
|
149
|
+
return `${ms}.${counter}.${hlc.nodeId}`;
|
|
150
|
+
}
|
|
151
|
+
function unpackHlc(packed) {
|
|
152
|
+
const firstDot = packed.indexOf(".");
|
|
153
|
+
const secondDot = firstDot < 0 ? -1 : packed.indexOf(".", firstDot + 1);
|
|
154
|
+
if (firstDot < 0 || secondDot < 0) {
|
|
155
|
+
throw new Error(`Malformed HLC: ${packed}`);
|
|
156
|
+
}
|
|
157
|
+
const ms = Number(packed.slice(0, firstDot));
|
|
158
|
+
const counter = Number(packed.slice(firstDot + 1, secondDot));
|
|
159
|
+
const nodeId = packed.slice(secondDot + 1);
|
|
160
|
+
if (!Number.isFinite(ms) || !Number.isFinite(counter) || nodeId.length === 0) {
|
|
161
|
+
throw new Error(`Malformed HLC: ${packed}`);
|
|
162
|
+
}
|
|
163
|
+
return { ms, counter, nodeId };
|
|
164
|
+
}
|
|
165
|
+
function compareHlc(a, b) {
|
|
166
|
+
if (a < b)
|
|
167
|
+
return -1;
|
|
168
|
+
if (a > b)
|
|
169
|
+
return 1;
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/client/ops.ts
|
|
174
|
+
function randomUUID() {
|
|
175
|
+
const c = globalThis.crypto;
|
|
176
|
+
if (c?.randomUUID)
|
|
177
|
+
return c.randomUUID();
|
|
178
|
+
const bytes = new Uint8Array(16);
|
|
179
|
+
if (c?.getRandomValues) {
|
|
180
|
+
c.getRandomValues(bytes);
|
|
181
|
+
} else {
|
|
182
|
+
for (let i = 0;i < 16; i++)
|
|
183
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
184
|
+
}
|
|
185
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
186
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
187
|
+
const hex = [];
|
|
188
|
+
for (let i = 0;i < 16; i++)
|
|
189
|
+
hex.push(bytes[i].toString(16).padStart(2, "0"));
|
|
190
|
+
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
|
|
191
|
+
}
|
|
192
|
+
function createOpCreator(nodeId, initialHlc) {
|
|
193
|
+
let hlc = initialHlc ?? { ms: 0, counter: 0, nodeId };
|
|
194
|
+
function nextOp(table, op, rowId, payload) {
|
|
195
|
+
hlc = sendHlc(hlc);
|
|
196
|
+
return {
|
|
197
|
+
id: randomUUID(),
|
|
198
|
+
table,
|
|
199
|
+
op,
|
|
200
|
+
rowId,
|
|
201
|
+
payload,
|
|
202
|
+
hlc: packHlc(hlc)
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
insert(table, rowId, payload) {
|
|
207
|
+
return nextOp(table, "insert", rowId, payload);
|
|
208
|
+
},
|
|
209
|
+
update(table, rowId, payload) {
|
|
210
|
+
return nextOp(table, "update", rowId, payload);
|
|
211
|
+
},
|
|
212
|
+
delete(table, rowId) {
|
|
213
|
+
return nextOp(table, "delete", rowId, null);
|
|
214
|
+
},
|
|
215
|
+
createBatch(ops) {
|
|
216
|
+
const batchId = randomUUID();
|
|
217
|
+
return ops.map((spec, i) => {
|
|
218
|
+
const op = nextOp(spec.table, spec.op, spec.rowId, spec.payload);
|
|
219
|
+
return {
|
|
220
|
+
...op,
|
|
221
|
+
batchId,
|
|
222
|
+
batchSize: ops.length,
|
|
223
|
+
batchSeq: i
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
getHlc() {
|
|
228
|
+
return hlc;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/client/store.ts
|
|
234
|
+
class ClientStore {
|
|
235
|
+
pendingOps = [];
|
|
236
|
+
rows = new Map;
|
|
237
|
+
tableMeta = new Map;
|
|
238
|
+
storage;
|
|
239
|
+
pendingWrites = [];
|
|
240
|
+
writeChain = Promise.resolve();
|
|
241
|
+
constructor(storage) {
|
|
242
|
+
this.storage = storage ?? null;
|
|
243
|
+
}
|
|
244
|
+
async hydrate(options) {
|
|
245
|
+
if (!this.storage)
|
|
246
|
+
return;
|
|
247
|
+
const storage = this.storage;
|
|
248
|
+
const scoped = options?.tables;
|
|
249
|
+
const [allRows, pendingOps, tableMetaJson] = await Promise.all([
|
|
250
|
+
scoped ? Promise.all(scoped.map((table) => storage.getRows(table))).then((r) => r.flat()) : storage.getAllRows(),
|
|
251
|
+
storage.getPendingOps(),
|
|
252
|
+
storage.getMeta("tableMeta")
|
|
253
|
+
]);
|
|
254
|
+
for (const row of allRows) {
|
|
255
|
+
let tableMap = this.rows.get(row.table);
|
|
256
|
+
if (!tableMap) {
|
|
257
|
+
tableMap = new Map;
|
|
258
|
+
this.rows.set(row.table, tableMap);
|
|
259
|
+
}
|
|
260
|
+
tableMap.set(row.rowId, row);
|
|
261
|
+
}
|
|
262
|
+
this.pendingOps = pendingOps;
|
|
263
|
+
if (tableMetaJson) {
|
|
264
|
+
const parsed = JSON.parse(tableMetaJson);
|
|
265
|
+
for (const [table, m] of Object.entries(parsed)) {
|
|
266
|
+
this.tableMeta.set(table, {
|
|
267
|
+
serverSet: m.serverSet,
|
|
268
|
+
readonly: m.readonly,
|
|
269
|
+
broadcast: m.broadcast ?? "consistent",
|
|
270
|
+
pk: m.pk ?? "id"
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async flush() {
|
|
276
|
+
this.pendingWrites = [];
|
|
277
|
+
await this.writeChain;
|
|
278
|
+
}
|
|
279
|
+
enqueue(fn) {
|
|
280
|
+
if (!this.storage)
|
|
281
|
+
return;
|
|
282
|
+
this.writeChain = this.writeChain.then(fn).catch((err) => {
|
|
283
|
+
console.error("[reflectdb] storage write failed:", err);
|
|
284
|
+
});
|
|
285
|
+
this.pendingWrites.push(this.writeChain);
|
|
286
|
+
}
|
|
287
|
+
addPendingOp(op) {
|
|
288
|
+
const existing = this.getRow(op.table, op.rowId);
|
|
289
|
+
const pendingOp = {
|
|
290
|
+
op,
|
|
291
|
+
status: "pending",
|
|
292
|
+
rejectedReason: null,
|
|
293
|
+
createdAt: Date.now(),
|
|
294
|
+
preState: existing ? {
|
|
295
|
+
data: existing.data,
|
|
296
|
+
colClocks: { ...existing.colClocks },
|
|
297
|
+
serverHlc: existing.serverHlc
|
|
298
|
+
} : null
|
|
299
|
+
};
|
|
300
|
+
this.pendingOps.push(pendingOp);
|
|
301
|
+
this.enqueue(() => this.storage.appendPendingOps([pendingOp]));
|
|
302
|
+
}
|
|
303
|
+
getPendingOps() {
|
|
304
|
+
return this.pendingOps.filter((p) => p.status === "pending");
|
|
305
|
+
}
|
|
306
|
+
markSynced(opIds) {
|
|
307
|
+
const idSet = new Set(opIds);
|
|
308
|
+
this.pendingOps = this.pendingOps.filter((p) => !idSet.has(p.op.id));
|
|
309
|
+
this.enqueue(() => this.storage.removePendingOps(opIds));
|
|
310
|
+
}
|
|
311
|
+
markRejected(opId, reason) {
|
|
312
|
+
const pending = this.pendingOps.find((p) => p.op.id === opId);
|
|
313
|
+
if (pending) {
|
|
314
|
+
pending.status = "rejected";
|
|
315
|
+
pending.rejectedReason = reason;
|
|
316
|
+
this.enqueue(() => this.storage.updatePendingOps([pending]));
|
|
317
|
+
}
|
|
318
|
+
this.autoTrimRejected();
|
|
319
|
+
}
|
|
320
|
+
rejectBatch(batchId, reason) {
|
|
321
|
+
const updated = [];
|
|
322
|
+
for (const p of this.pendingOps) {
|
|
323
|
+
if (p.op.batchId === batchId) {
|
|
324
|
+
p.status = "rejected";
|
|
325
|
+
p.rejectedReason = reason;
|
|
326
|
+
updated.push(p);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (updated.length > 0) {
|
|
330
|
+
this.enqueue(() => this.storage.updatePendingOps(updated));
|
|
331
|
+
}
|
|
332
|
+
this.autoTrimRejected();
|
|
333
|
+
}
|
|
334
|
+
autoTrimRejected() {
|
|
335
|
+
const cutoff = Date.now() - 60000;
|
|
336
|
+
const before = this.pendingOps.length;
|
|
337
|
+
this.pendingOps = this.pendingOps.filter((p) => p.status !== "rejected" || p.createdAt > cutoff);
|
|
338
|
+
if (this.pendingOps.length < before) {
|
|
339
|
+
this.enqueue(() => this.storage.putPendingOps([...this.pendingOps]));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
clearRejected() {
|
|
343
|
+
this.pendingOps = this.pendingOps.filter((p) => p.status !== "rejected");
|
|
344
|
+
this.enqueue(() => this.storage.putPendingOps([...this.pendingOps]));
|
|
345
|
+
}
|
|
346
|
+
setRow(table, rowId, data, colClocks, serverHlc) {
|
|
347
|
+
if (data === null && serverHlc === null) {
|
|
348
|
+
this.rows.get(table)?.delete(rowId);
|
|
349
|
+
this.enqueue(() => this.storage.deleteRow(table, rowId));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
let tableMap = this.rows.get(table);
|
|
353
|
+
if (!tableMap) {
|
|
354
|
+
tableMap = new Map;
|
|
355
|
+
this.rows.set(table, tableMap);
|
|
356
|
+
}
|
|
357
|
+
const row = { table, rowId, data, colClocks, serverHlc };
|
|
358
|
+
tableMap.set(rowId, row);
|
|
359
|
+
this.enqueue(() => this.storage.putRow(table, rowId, row));
|
|
360
|
+
}
|
|
361
|
+
getRow(table, rowId) {
|
|
362
|
+
const row = this.rows.get(table)?.get(rowId);
|
|
363
|
+
if (!row || row.data === null)
|
|
364
|
+
return;
|
|
365
|
+
return row;
|
|
366
|
+
}
|
|
367
|
+
getRowEntry(table, rowId) {
|
|
368
|
+
return this.rows.get(table)?.get(rowId);
|
|
369
|
+
}
|
|
370
|
+
getRows(table, includeDeleted = false) {
|
|
371
|
+
const tableMap = this.rows.get(table);
|
|
372
|
+
if (!tableMap)
|
|
373
|
+
return [];
|
|
374
|
+
const result = [];
|
|
375
|
+
for (const row of tableMap.values()) {
|
|
376
|
+
if (row.data !== null) {
|
|
377
|
+
if (!includeDeleted && (row.data.deletedAt != null || row.data.deleted_at != null)) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
result.push(row);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
clearTable(table) {
|
|
386
|
+
this.rows.delete(table);
|
|
387
|
+
this.enqueue(() => this.storage.clearTable(table));
|
|
388
|
+
}
|
|
389
|
+
applySnapshot(table, rows, colClocks, append = false, pk) {
|
|
390
|
+
if (!append) {
|
|
391
|
+
this.clearTable(table);
|
|
392
|
+
}
|
|
393
|
+
const effectivePk = pk ?? this.tableMeta.get(table)?.pk ?? "id";
|
|
394
|
+
for (const row of rows) {
|
|
395
|
+
const rowId = row[effectivePk] ?? "";
|
|
396
|
+
const clocks = colClocks[rowId] ?? {};
|
|
397
|
+
this.setRow(table, rowId, row, clocks, clocks._row ?? null);
|
|
398
|
+
}
|
|
399
|
+
for (const p of this.pendingOps) {
|
|
400
|
+
if (p.status === "pending" && p.op.table === table) {
|
|
401
|
+
this.applyOptimistic(p.op);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
applyDelta(table, op, rowId, payload, hlc, colClocks) {
|
|
406
|
+
const existing = this.getRowEntry(table, rowId);
|
|
407
|
+
if (op === "delete") {
|
|
408
|
+
if (existing?.serverHlc && compareHlc(hlc, existing.serverHlc) <= 0) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
this.setRow(table, rowId, null, {}, hlc);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (op === "insert" || !existing) {
|
|
415
|
+
if (existing?.serverHlc && compareHlc(hlc, existing.serverHlc) <= 0) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
this.setRow(table, rowId, payload ?? {}, colClocks ?? { _row: hlc }, hlc);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const incomingClocks = colClocks ?? {};
|
|
422
|
+
const mergedData = { ...existing.data };
|
|
423
|
+
const mergedClocks = { ...existing.colClocks };
|
|
424
|
+
let anyApplied = false;
|
|
425
|
+
for (const [col, value] of Object.entries(payload ?? {})) {
|
|
426
|
+
const incomingClock = incomingClocks[col] ?? hlc;
|
|
427
|
+
const existingClock = existing.colClocks[col];
|
|
428
|
+
if (!existingClock || compareHlc(incomingClock, existingClock) > 0) {
|
|
429
|
+
mergedData[col] = value;
|
|
430
|
+
mergedClocks[col] = incomingClock;
|
|
431
|
+
anyApplied = true;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (!anyApplied) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const newRowHlc = existing.serverHlc && compareHlc(existing.serverHlc, hlc) >= 0 ? existing.serverHlc : hlc;
|
|
438
|
+
mergedClocks._row = newRowHlc;
|
|
439
|
+
this.setRow(table, rowId, mergedData, mergedClocks, newRowHlc);
|
|
440
|
+
}
|
|
441
|
+
revertOp(opId, serverRow) {
|
|
442
|
+
const pending = this.pendingOps.find((p) => p.op.id === opId);
|
|
443
|
+
if (!pending)
|
|
444
|
+
return;
|
|
445
|
+
const { table, rowId } = pending.op;
|
|
446
|
+
if (serverRow != null) {
|
|
447
|
+
const existing = this.getRow(table, rowId);
|
|
448
|
+
this.setRow(table, rowId, serverRow, existing?.colClocks ?? {}, existing?.serverHlc ?? null);
|
|
449
|
+
} else if (pending.preState) {
|
|
450
|
+
this.setRow(table, rowId, pending.preState.data, pending.preState.colClocks, pending.preState.serverHlc);
|
|
451
|
+
} else {
|
|
452
|
+
const tableMap = this.rows.get(table);
|
|
453
|
+
if (tableMap) {
|
|
454
|
+
tableMap.delete(rowId);
|
|
455
|
+
this.enqueue(() => this.storage.deleteRow(table, rowId));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
this.pendingOps = this.pendingOps.filter((p) => p.op.id !== opId);
|
|
459
|
+
this.enqueue(() => this.storage.removePendingOps([opId]));
|
|
460
|
+
}
|
|
461
|
+
setTableMeta(meta) {
|
|
462
|
+
for (const [table, m] of Object.entries(meta)) {
|
|
463
|
+
this.tableMeta.set(table, {
|
|
464
|
+
serverSet: m.serverSet,
|
|
465
|
+
readonly: m.readonly,
|
|
466
|
+
broadcast: m.broadcast ?? "consistent",
|
|
467
|
+
pk: m.pk ?? "id"
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
this.enqueue(() => this.storage.setMeta("tableMeta", JSON.stringify(Object.fromEntries(this.tableMeta))));
|
|
471
|
+
}
|
|
472
|
+
getTableMeta(table) {
|
|
473
|
+
return this.tableMeta.get(table);
|
|
474
|
+
}
|
|
475
|
+
applyOptimistic(op) {
|
|
476
|
+
const meta = this.tableMeta.get(op.table);
|
|
477
|
+
let payload = op.payload;
|
|
478
|
+
if (payload && meta?.serverSet.length) {
|
|
479
|
+
payload = { ...payload };
|
|
480
|
+
for (const field of meta.serverSet) {
|
|
481
|
+
delete payload[field];
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (op.op === "delete") {
|
|
485
|
+
this.setRow(op.table, op.rowId, null, { _row: op.hlc }, op.hlc);
|
|
486
|
+
} else if (op.op === "insert") {
|
|
487
|
+
this.setRow(op.table, op.rowId, payload ?? {}, { _row: op.hlc }, op.hlc);
|
|
488
|
+
} else {
|
|
489
|
+
const existing = this.getRow(op.table, op.rowId);
|
|
490
|
+
if (existing?.data) {
|
|
491
|
+
const merged = { ...existing.data, ...payload };
|
|
492
|
+
this.setRow(op.table, op.rowId, merged, existing.colClocks, existing.serverHlc);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async clear() {
|
|
497
|
+
try {
|
|
498
|
+
await this.writeChain;
|
|
499
|
+
} catch {}
|
|
500
|
+
this.rows.clear();
|
|
501
|
+
this.pendingOps = [];
|
|
502
|
+
this.tableMeta.clear();
|
|
503
|
+
this.pendingWrites = [];
|
|
504
|
+
this.writeChain = Promise.resolve();
|
|
505
|
+
if (this.storage) {
|
|
506
|
+
await this.storage.clear();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/client/sync-client.ts
|
|
512
|
+
class SyncClient {
|
|
513
|
+
config;
|
|
514
|
+
transport;
|
|
515
|
+
opCreator;
|
|
516
|
+
store;
|
|
517
|
+
state = "disconnected";
|
|
518
|
+
negotiatedProtocolVersion = null;
|
|
519
|
+
syncedTables = new Set;
|
|
520
|
+
serverHlc = null;
|
|
521
|
+
listeners = new Set;
|
|
522
|
+
version = 0;
|
|
523
|
+
tableVersions = new Map;
|
|
524
|
+
tableListeners = new Map;
|
|
525
|
+
connectResolve = null;
|
|
526
|
+
connectReject = null;
|
|
527
|
+
connectInFlight = null;
|
|
528
|
+
ephemeralListeners = new Map;
|
|
529
|
+
totalCounts = new Map;
|
|
530
|
+
syncOptions = new Map;
|
|
531
|
+
bootstrapScheduled = false;
|
|
532
|
+
initialized = false;
|
|
533
|
+
syncParams = new Map;
|
|
534
|
+
reconnectTimer = null;
|
|
535
|
+
reconnectAttempts = 0;
|
|
536
|
+
closed = false;
|
|
537
|
+
inFlightOps = new Map;
|
|
538
|
+
static IN_FLIGHT_TTL_MS = 30000;
|
|
539
|
+
pushChain = Promise.resolve();
|
|
540
|
+
pushPending = null;
|
|
541
|
+
constructor(config) {
|
|
542
|
+
this.config = config;
|
|
543
|
+
this.transport = config.transport;
|
|
544
|
+
this.opCreator = createOpCreator(`client:${config.clientId}`);
|
|
545
|
+
this.store = new ClientStore(config.storage);
|
|
546
|
+
this.transport.subscribe((message) => {
|
|
547
|
+
this.handleMessage(message);
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
async init() {
|
|
551
|
+
if (!this.config.storage) {
|
|
552
|
+
this.initialized = true;
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
this.state = "hydrating";
|
|
556
|
+
this.notify();
|
|
557
|
+
const subsJson = await this.config.storage.getMeta("syncSubscriptions");
|
|
558
|
+
if (subsJson) {
|
|
559
|
+
const subs = JSON.parse(subsJson);
|
|
560
|
+
for (const sub of subs) {
|
|
561
|
+
this.syncedTables.add(sub.table);
|
|
562
|
+
if (sub.params) {
|
|
563
|
+
this.syncParams.set(sub.table, sub.params);
|
|
564
|
+
}
|
|
565
|
+
if (sub.options) {
|
|
566
|
+
this.syncOptions.set(sub.table, sub.options);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
await this.store.hydrate(this.config.hydrateAllTables || this.syncedTables.size === 0 ? undefined : { tables: [...this.syncedTables] });
|
|
571
|
+
const serverHlcStr = await this.config.storage.getMeta("serverHlc");
|
|
572
|
+
if (serverHlcStr) {
|
|
573
|
+
this.serverHlc = serverHlcStr;
|
|
574
|
+
}
|
|
575
|
+
const currentNodeId = `client:${this.config.clientId}`;
|
|
576
|
+
const hlcJson = await this.config.storage.getMeta("hlcState");
|
|
577
|
+
let restoredHlc = null;
|
|
578
|
+
if (hlcJson) {
|
|
579
|
+
const parsed = JSON.parse(hlcJson);
|
|
580
|
+
restoredHlc = { ...parsed, nodeId: currentNodeId };
|
|
581
|
+
this.opCreator = createOpCreator(currentNodeId, restoredHlc);
|
|
582
|
+
}
|
|
583
|
+
const pendingOps = this.store.getPendingOps();
|
|
584
|
+
if (pendingOps.length > 0) {
|
|
585
|
+
let maxMs = restoredHlc?.ms ?? 0;
|
|
586
|
+
let maxCounter = restoredHlc?.counter ?? 0;
|
|
587
|
+
for (const p of pendingOps) {
|
|
588
|
+
const opHlc = unpackHlc(p.op.hlc);
|
|
589
|
+
if (opHlc.ms > maxMs || opHlc.ms === maxMs && opHlc.counter > maxCounter) {
|
|
590
|
+
maxMs = opHlc.ms;
|
|
591
|
+
maxCounter = opHlc.counter;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
this.opCreator = createOpCreator(currentNodeId, {
|
|
595
|
+
ms: maxMs,
|
|
596
|
+
counter: maxCounter,
|
|
597
|
+
nodeId: currentNodeId
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
this.state = "disconnected";
|
|
601
|
+
this.initialized = true;
|
|
602
|
+
this.notify();
|
|
603
|
+
}
|
|
604
|
+
async connect() {
|
|
605
|
+
if (!this.initialized && this.config.storage) {
|
|
606
|
+
await this.init();
|
|
607
|
+
}
|
|
608
|
+
if (this.connectInFlight)
|
|
609
|
+
return this.connectInFlight;
|
|
610
|
+
this.state = "connecting";
|
|
611
|
+
this.connectInFlight = new Promise((resolve, reject) => {
|
|
612
|
+
this.connectResolve = resolve;
|
|
613
|
+
this.connectReject = reject;
|
|
614
|
+
}).finally(() => {
|
|
615
|
+
this.connectInFlight = null;
|
|
616
|
+
});
|
|
617
|
+
await this.transport.send({
|
|
618
|
+
type: "hello",
|
|
619
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
620
|
+
supportedVersions: SUPPORTED_PROTOCOL_VERSIONS,
|
|
621
|
+
clientId: this.config.clientId,
|
|
622
|
+
token: this.config.token
|
|
623
|
+
});
|
|
624
|
+
return this.connectInFlight;
|
|
625
|
+
}
|
|
626
|
+
async close() {
|
|
627
|
+
this.closed = true;
|
|
628
|
+
if (this.reconnectTimer) {
|
|
629
|
+
clearTimeout(this.reconnectTimer);
|
|
630
|
+
this.reconnectTimer = null;
|
|
631
|
+
}
|
|
632
|
+
this.state = "disconnected";
|
|
633
|
+
this.notify();
|
|
634
|
+
try {
|
|
635
|
+
await this.store.flush();
|
|
636
|
+
} catch (err) {
|
|
637
|
+
console.error("[reflectdb] store flush during close failed:", err);
|
|
638
|
+
}
|
|
639
|
+
await this.transport.close();
|
|
640
|
+
}
|
|
641
|
+
getState() {
|
|
642
|
+
return this.state;
|
|
643
|
+
}
|
|
644
|
+
getProtocolVersion() {
|
|
645
|
+
return this.negotiatedProtocolVersion;
|
|
646
|
+
}
|
|
647
|
+
async sync(table, params, options) {
|
|
648
|
+
this.syncedTables.add(table);
|
|
649
|
+
if (params) {
|
|
650
|
+
this.syncParams.set(table, params);
|
|
651
|
+
}
|
|
652
|
+
if (options) {
|
|
653
|
+
this.syncOptions.set(table, options);
|
|
654
|
+
}
|
|
655
|
+
this.persistSyncSubscriptions();
|
|
656
|
+
await this.transport.send({
|
|
657
|
+
type: "sync_declare",
|
|
658
|
+
table,
|
|
659
|
+
params,
|
|
660
|
+
window: options?.window
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
async loadMore(table, count) {
|
|
664
|
+
await this.transport.send({
|
|
665
|
+
type: "load_more",
|
|
666
|
+
table,
|
|
667
|
+
count
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
getTotalCount(table) {
|
|
671
|
+
return this.totalCounts.get(table) ?? null;
|
|
672
|
+
}
|
|
673
|
+
async unsync(table) {
|
|
674
|
+
this.syncedTables.delete(table);
|
|
675
|
+
this.syncOptions.delete(table);
|
|
676
|
+
this.syncParams.delete(table);
|
|
677
|
+
this.totalCounts.delete(table);
|
|
678
|
+
this.store.clearTable(table);
|
|
679
|
+
this.persistSyncSubscriptions();
|
|
680
|
+
this.notify([table]);
|
|
681
|
+
await this.transport.send({
|
|
682
|
+
type: "unsync",
|
|
683
|
+
table
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
async bootstrap() {
|
|
687
|
+
this.state = "bootstrapping";
|
|
688
|
+
await this.transport.send({
|
|
689
|
+
type: "bootstrap"
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
scheduleBootstrap() {
|
|
693
|
+
if (this.bootstrapScheduled)
|
|
694
|
+
return;
|
|
695
|
+
this.bootstrapScheduled = true;
|
|
696
|
+
queueMicrotask(() => {
|
|
697
|
+
this.bootstrapScheduled = false;
|
|
698
|
+
this.bootstrap();
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
async resume() {
|
|
702
|
+
if (!this.serverHlc) {
|
|
703
|
+
return this.bootstrap();
|
|
704
|
+
}
|
|
705
|
+
await this.transport.send({
|
|
706
|
+
type: "resume",
|
|
707
|
+
since: this.serverHlc
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
insert(table, rowId, payload) {
|
|
711
|
+
const op = this.opCreator.insert(table, rowId, payload);
|
|
712
|
+
this.store.addPendingOp(op);
|
|
713
|
+
this.store.applyOptimistic(op);
|
|
714
|
+
this.notify([table]);
|
|
715
|
+
return op;
|
|
716
|
+
}
|
|
717
|
+
update(table, rowId, payload) {
|
|
718
|
+
const op = this.opCreator.update(table, rowId, payload);
|
|
719
|
+
this.store.addPendingOp(op);
|
|
720
|
+
this.store.applyOptimistic(op);
|
|
721
|
+
this.notify([table]);
|
|
722
|
+
return op;
|
|
723
|
+
}
|
|
724
|
+
delete(table, rowId) {
|
|
725
|
+
const op = this.opCreator.delete(table, rowId);
|
|
726
|
+
this.store.addPendingOp(op);
|
|
727
|
+
this.store.applyOptimistic(op);
|
|
728
|
+
this.notify([table]);
|
|
729
|
+
return op;
|
|
730
|
+
}
|
|
731
|
+
batch(ops) {
|
|
732
|
+
const batchOps = this.opCreator.createBatch(ops);
|
|
733
|
+
for (const op of batchOps) {
|
|
734
|
+
this.store.addPendingOp(op);
|
|
735
|
+
this.store.applyOptimistic(op);
|
|
736
|
+
}
|
|
737
|
+
this.notify([...new Set(batchOps.map((o) => o.table))]);
|
|
738
|
+
return batchOps;
|
|
739
|
+
}
|
|
740
|
+
push() {
|
|
741
|
+
if (this.pushPending)
|
|
742
|
+
return this.pushPending;
|
|
743
|
+
const run = this.pushChain.catch(() => {}).then(() => {
|
|
744
|
+
this.pushPending = null;
|
|
745
|
+
return this.doPush();
|
|
746
|
+
});
|
|
747
|
+
this.pushPending = run;
|
|
748
|
+
this.pushChain = run.catch(() => {});
|
|
749
|
+
return run;
|
|
750
|
+
}
|
|
751
|
+
async doPush() {
|
|
752
|
+
await this.store.flush();
|
|
753
|
+
const cutoff = Date.now() - SyncClient.IN_FLIGHT_TTL_MS;
|
|
754
|
+
const pending = this.store.getPendingOps().filter((p) => {
|
|
755
|
+
const sentAt = this.inFlightOps.get(p.op.id);
|
|
756
|
+
return sentAt === undefined || sentAt <= cutoff;
|
|
757
|
+
});
|
|
758
|
+
if (pending.length === 0)
|
|
759
|
+
return;
|
|
760
|
+
const ops = pending.map((p) => p.op);
|
|
761
|
+
const chunks = [];
|
|
762
|
+
let current = [];
|
|
763
|
+
let i = 0;
|
|
764
|
+
while (i < ops.length) {
|
|
765
|
+
const op = ops[i];
|
|
766
|
+
if (op.batchId) {
|
|
767
|
+
let end = i + 1;
|
|
768
|
+
while (end < ops.length && ops[end].batchId === op.batchId)
|
|
769
|
+
end++;
|
|
770
|
+
const batchOps = ops.slice(i, end);
|
|
771
|
+
if (batchOps.length > MAX_BATCH_SIZE) {
|
|
772
|
+
if (current.length > 0) {
|
|
773
|
+
chunks.push(current);
|
|
774
|
+
current = [];
|
|
775
|
+
}
|
|
776
|
+
chunks.push(batchOps);
|
|
777
|
+
} else {
|
|
778
|
+
if (current.length + batchOps.length > MAX_BATCH_SIZE) {
|
|
779
|
+
chunks.push(current);
|
|
780
|
+
current = [];
|
|
781
|
+
}
|
|
782
|
+
current.push(...batchOps);
|
|
783
|
+
}
|
|
784
|
+
i = end;
|
|
785
|
+
} else {
|
|
786
|
+
if (current.length + 1 > MAX_BATCH_SIZE) {
|
|
787
|
+
chunks.push(current);
|
|
788
|
+
current = [];
|
|
789
|
+
}
|
|
790
|
+
current.push(op);
|
|
791
|
+
i++;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (current.length > 0)
|
|
795
|
+
chunks.push(current);
|
|
796
|
+
for (const chunk of chunks) {
|
|
797
|
+
const chunkIds = chunk.map((op) => op.id);
|
|
798
|
+
const sentAt = Date.now();
|
|
799
|
+
for (const id of chunkIds)
|
|
800
|
+
this.inFlightOps.set(id, sentAt);
|
|
801
|
+
try {
|
|
802
|
+
await this.transport.send({
|
|
803
|
+
type: "ops",
|
|
804
|
+
ops: chunk,
|
|
805
|
+
token: this.config.token
|
|
806
|
+
});
|
|
807
|
+
} catch (err) {
|
|
808
|
+
for (const id of chunkIds)
|
|
809
|
+
this.inFlightOps.delete(id);
|
|
810
|
+
throw err;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
clearInFlight() {
|
|
815
|
+
this.inFlightOps.clear();
|
|
816
|
+
}
|
|
817
|
+
async sendEphemeral(params) {
|
|
818
|
+
await this.transport.send({
|
|
819
|
+
type: "ephemeral",
|
|
820
|
+
...params
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
subscribeEphemeral(key, listener) {
|
|
824
|
+
let listeners = this.ephemeralListeners.get(key);
|
|
825
|
+
if (!listeners) {
|
|
826
|
+
listeners = new Set;
|
|
827
|
+
this.ephemeralListeners.set(key, listeners);
|
|
828
|
+
}
|
|
829
|
+
listeners.add(listener);
|
|
830
|
+
return () => {
|
|
831
|
+
listeners.delete(listener);
|
|
832
|
+
if (listeners.size === 0) {
|
|
833
|
+
this.ephemeralListeners.delete(key);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
getRows(table, options) {
|
|
838
|
+
return this.store.getRows(table, options?.includeDeleted).map((r) => r.data);
|
|
839
|
+
}
|
|
840
|
+
getRow(table, rowId) {
|
|
841
|
+
const row = this.store.getRow(table, rowId);
|
|
842
|
+
return row?.data ?? null;
|
|
843
|
+
}
|
|
844
|
+
getPendingCount() {
|
|
845
|
+
return this.store.getPendingOps().length;
|
|
846
|
+
}
|
|
847
|
+
getStore() {
|
|
848
|
+
return this.store;
|
|
849
|
+
}
|
|
850
|
+
subscribe(listener) {
|
|
851
|
+
this.listeners.add(listener);
|
|
852
|
+
return () => {
|
|
853
|
+
this.listeners.delete(listener);
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
subscribeTable(table, listener) {
|
|
857
|
+
let listeners = this.tableListeners.get(table);
|
|
858
|
+
if (!listeners) {
|
|
859
|
+
listeners = new Set;
|
|
860
|
+
this.tableListeners.set(table, listeners);
|
|
861
|
+
}
|
|
862
|
+
listeners.add(listener);
|
|
863
|
+
return () => {
|
|
864
|
+
listeners.delete(listener);
|
|
865
|
+
if (listeners.size === 0) {
|
|
866
|
+
this.tableListeners.delete(table);
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
getVersion() {
|
|
871
|
+
return this.version;
|
|
872
|
+
}
|
|
873
|
+
getTableVersion(table) {
|
|
874
|
+
return this.tableVersions.get(table) ?? 0;
|
|
875
|
+
}
|
|
876
|
+
notify(tables) {
|
|
877
|
+
this.version++;
|
|
878
|
+
if (tables) {
|
|
879
|
+
for (const table of tables) {
|
|
880
|
+
this.tableVersions.set(table, (this.tableVersions.get(table) ?? 0) + 1);
|
|
881
|
+
const tableListeners = this.tableListeners.get(table);
|
|
882
|
+
if (tableListeners) {
|
|
883
|
+
for (const listener of [...tableListeners]) {
|
|
884
|
+
try {
|
|
885
|
+
listener();
|
|
886
|
+
} catch (err) {
|
|
887
|
+
console.error("[reflectdb] table listener threw:", err);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
for (const listener of [...this.listeners]) {
|
|
894
|
+
try {
|
|
895
|
+
listener();
|
|
896
|
+
} catch (err) {
|
|
897
|
+
console.error("[reflectdb] listener threw:", err);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
async clear(opts) {
|
|
902
|
+
if (opts?.tables && opts.tables.length > 0) {
|
|
903
|
+
for (const table of opts.tables) {
|
|
904
|
+
await this.store.clearTable(table);
|
|
905
|
+
this.syncedTables.delete(table);
|
|
906
|
+
this.syncOptions.delete(table);
|
|
907
|
+
this.syncParams.delete(table);
|
|
908
|
+
this.totalCounts.delete(table);
|
|
909
|
+
}
|
|
910
|
+
this.notify();
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
await this.store.clear();
|
|
914
|
+
this.serverHlc = null;
|
|
915
|
+
this.syncedTables.clear();
|
|
916
|
+
this.syncOptions.clear();
|
|
917
|
+
this.syncParams.clear();
|
|
918
|
+
this.totalCounts.clear();
|
|
919
|
+
this.opCreator = createOpCreator(`client:${this.config.clientId}`);
|
|
920
|
+
this.clearInFlight();
|
|
921
|
+
this.state = "disconnected";
|
|
922
|
+
this.initialized = false;
|
|
923
|
+
this.notify();
|
|
924
|
+
}
|
|
925
|
+
handleMessage(message) {
|
|
926
|
+
switch (message.type) {
|
|
927
|
+
case "hello_ack":
|
|
928
|
+
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(message.protocolVersion)) {
|
|
929
|
+
this.state = "disconnected";
|
|
930
|
+
this.connectReject?.(new Error(`Server negotiated unsupported version: ${message.protocolVersion}`));
|
|
931
|
+
this.connectResolve = null;
|
|
932
|
+
this.connectReject = null;
|
|
933
|
+
this.notify();
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
this.negotiatedProtocolVersion = message.protocolVersion;
|
|
937
|
+
this.state = "connected";
|
|
938
|
+
this.connectResolve?.();
|
|
939
|
+
this.connectResolve = null;
|
|
940
|
+
this.connectReject = null;
|
|
941
|
+
this.notify();
|
|
942
|
+
break;
|
|
943
|
+
case "hello_reject":
|
|
944
|
+
this.state = "disconnected";
|
|
945
|
+
this.connectReject?.(new Error(message.reason));
|
|
946
|
+
this.connectResolve = null;
|
|
947
|
+
this.connectReject = null;
|
|
948
|
+
this.notify();
|
|
949
|
+
this.config.onError?.({ reason: "local:hello_rejected", message: message.reason });
|
|
950
|
+
break;
|
|
951
|
+
case "snapshot":
|
|
952
|
+
this.store.applySnapshot(message.table, message.rows, message.colClocks, message.append, message.pk);
|
|
953
|
+
if (message.totalCount != null) {
|
|
954
|
+
this.totalCounts.set(message.table, message.totalCount);
|
|
955
|
+
}
|
|
956
|
+
this.notify([message.table]);
|
|
957
|
+
this.config.onSync?.(message.table);
|
|
958
|
+
break;
|
|
959
|
+
case "bootstrap_complete":
|
|
960
|
+
this.serverHlc = message.serverHlc;
|
|
961
|
+
this.store.setTableMeta(message.tableMeta);
|
|
962
|
+
this.state = "synced";
|
|
963
|
+
this.mergeServerHlc(message.serverHlc);
|
|
964
|
+
this.persistMeta();
|
|
965
|
+
this.notify();
|
|
966
|
+
break;
|
|
967
|
+
case "resume_complete":
|
|
968
|
+
this.serverHlc = message.serverHlc;
|
|
969
|
+
this.state = "synced";
|
|
970
|
+
this.mergeServerHlc(message.serverHlc);
|
|
971
|
+
this.persistMeta();
|
|
972
|
+
this.notify();
|
|
973
|
+
break;
|
|
974
|
+
case "delta":
|
|
975
|
+
this.store.applyDelta(message.table, message.op, message.rowId, message.payload, message.hlc, message.colClocks);
|
|
976
|
+
this.notify([message.table]);
|
|
977
|
+
this.config.onSync?.(message.table);
|
|
978
|
+
break;
|
|
979
|
+
case "ack": {
|
|
980
|
+
const idSet = new Set(message.opIds);
|
|
981
|
+
const ackedTables = [
|
|
982
|
+
...new Set(this.store.getPendingOps().filter((p) => idSet.has(p.op.id)).map((p) => p.op.table))
|
|
983
|
+
];
|
|
984
|
+
for (const opId of message.opIds)
|
|
985
|
+
this.inFlightOps.delete(opId);
|
|
986
|
+
this.store.markSynced(message.opIds);
|
|
987
|
+
this.notify(ackedTables.length > 0 ? ackedTables : undefined);
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
case "reject": {
|
|
991
|
+
const rejectedTables = [];
|
|
992
|
+
if (message.opId) {
|
|
993
|
+
this.inFlightOps.delete(message.opId);
|
|
994
|
+
const pending = this.store.getPendingOps().find((p) => p.op.id === message.opId);
|
|
995
|
+
if (pending)
|
|
996
|
+
rejectedTables.push(pending.op.table);
|
|
997
|
+
this.store.markRejected(message.opId, message.reason);
|
|
998
|
+
this.store.revertOp(message.opId, message.serverRow ?? null);
|
|
999
|
+
}
|
|
1000
|
+
if (message.batchId) {
|
|
1001
|
+
const batchOpIds = [];
|
|
1002
|
+
for (const p of this.store.getPendingOps()) {
|
|
1003
|
+
if (p.op.batchId === message.batchId) {
|
|
1004
|
+
rejectedTables.push(p.op.table);
|
|
1005
|
+
batchOpIds.push(p.op.id);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
this.store.rejectBatch(message.batchId, message.reason);
|
|
1009
|
+
for (const opId of batchOpIds) {
|
|
1010
|
+
this.inFlightOps.delete(opId);
|
|
1011
|
+
this.store.revertOp(opId, null);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const uniqueRejectedTables = [...new Set(rejectedTables)];
|
|
1015
|
+
this.notify(uniqueRejectedTables.length > 0 ? uniqueRejectedTables : undefined);
|
|
1016
|
+
this.config.onError?.({
|
|
1017
|
+
opId: message.opId,
|
|
1018
|
+
reason: message.reason
|
|
1019
|
+
});
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
case "resume_rejected":
|
|
1023
|
+
this.serverHlc = null;
|
|
1024
|
+
if (this.config.storage) {
|
|
1025
|
+
this.config.storage.setMeta("serverHlc", "").catch((err) => {
|
|
1026
|
+
console.error("[reflectdb] Failed to clear stale serverHlc:", err);
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
this.bootstrap();
|
|
1030
|
+
break;
|
|
1031
|
+
case "reauth":
|
|
1032
|
+
this.handleReauth();
|
|
1033
|
+
break;
|
|
1034
|
+
case "disconnect":
|
|
1035
|
+
this.state = "disconnected";
|
|
1036
|
+
this.clearInFlight();
|
|
1037
|
+
if (this.connectReject) {
|
|
1038
|
+
this.connectReject(new Error(message.reason));
|
|
1039
|
+
this.connectResolve = null;
|
|
1040
|
+
this.connectReject = null;
|
|
1041
|
+
}
|
|
1042
|
+
this.notify();
|
|
1043
|
+
this.config.onError?.({ reason: "local:disconnected", message: message.reason });
|
|
1044
|
+
this.scheduleReconnect();
|
|
1045
|
+
break;
|
|
1046
|
+
case "shape_changed":
|
|
1047
|
+
this.store.clearTable(message.table);
|
|
1048
|
+
this.notify([message.table]);
|
|
1049
|
+
this.config.onSync?.(message.table);
|
|
1050
|
+
break;
|
|
1051
|
+
case "count_changed":
|
|
1052
|
+
this.totalCounts.set(message.table, message.totalCount);
|
|
1053
|
+
this.notify([message.table]);
|
|
1054
|
+
break;
|
|
1055
|
+
case "ephemeral":
|
|
1056
|
+
{
|
|
1057
|
+
const listeners = this.ephemeralListeners.get(message.key);
|
|
1058
|
+
if (listeners) {
|
|
1059
|
+
for (const listener of listeners) {
|
|
1060
|
+
listener(message);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
mergeServerHlc(packed) {
|
|
1068
|
+
const remote = unpackHlc(packed);
|
|
1069
|
+
const localHlc = this.opCreator.getHlc();
|
|
1070
|
+
const merged = receiveHlc(localHlc, remote);
|
|
1071
|
+
this.opCreator = createOpCreator(localHlc.nodeId, merged);
|
|
1072
|
+
}
|
|
1073
|
+
persistMeta() {
|
|
1074
|
+
if (!this.config.storage)
|
|
1075
|
+
return;
|
|
1076
|
+
const storage = this.config.storage;
|
|
1077
|
+
const hlcState = this.opCreator.getHlc();
|
|
1078
|
+
storage.setMeta("serverHlc", this.serverHlc).catch((err) => {
|
|
1079
|
+
console.error("[reflectdb] Failed to persist serverHlc:", err);
|
|
1080
|
+
});
|
|
1081
|
+
storage.setMeta("hlcState", JSON.stringify(hlcState)).catch((err) => {
|
|
1082
|
+
console.error("[reflectdb] Failed to persist hlcState:", err);
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
persistSyncSubscriptions() {
|
|
1086
|
+
if (!this.config.storage)
|
|
1087
|
+
return;
|
|
1088
|
+
const subs = Array.from(this.syncedTables).map((table) => ({
|
|
1089
|
+
table,
|
|
1090
|
+
params: this.syncParams.get(table),
|
|
1091
|
+
options: this.syncOptions.get(table)
|
|
1092
|
+
}));
|
|
1093
|
+
this.config.storage.setMeta("syncSubscriptions", JSON.stringify(subs)).catch((err) => {
|
|
1094
|
+
console.error("[reflectdb] Failed to persist sync subscriptions:", err);
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
handleReauth() {
|
|
1098
|
+
if (!this.config.onReauth) {
|
|
1099
|
+
this.config.onError?.({
|
|
1100
|
+
reason: "local:reauth_failed",
|
|
1101
|
+
message: "reauth requested but no onReauth handler configured"
|
|
1102
|
+
});
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
this.config.onReauth().then(async (token) => {
|
|
1106
|
+
this.config.token = token;
|
|
1107
|
+
try {
|
|
1108
|
+
await this.transport.send({ type: "auth", token });
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
this.config.onError?.({
|
|
1111
|
+
reason: "local:reauth_failed",
|
|
1112
|
+
message: `auth send failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
}).catch((err) => {
|
|
1116
|
+
this.config.onError?.({ reason: "local:reauth_failed", message: String(err) });
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
scheduleReconnect() {
|
|
1120
|
+
if (this.closed || this.reconnectTimer)
|
|
1121
|
+
return;
|
|
1122
|
+
const cap = this.config.maxReconnectDelayMs ?? 30000;
|
|
1123
|
+
const base = Math.min(1000 * 2 ** this.reconnectAttempts, cap);
|
|
1124
|
+
const delay = Math.round(base * (0.5 + Math.random() * 0.5));
|
|
1125
|
+
this.reconnectAttempts++;
|
|
1126
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
1127
|
+
this.reconnectTimer = null;
|
|
1128
|
+
try {
|
|
1129
|
+
await this.connect();
|
|
1130
|
+
if (this.closed)
|
|
1131
|
+
return;
|
|
1132
|
+
if (this.closed)
|
|
1133
|
+
return;
|
|
1134
|
+
const syncResults = await Promise.allSettled([...this.syncedTables].map((table) => this.sync(table, this.syncParams.get(table), this.syncOptions.get(table))));
|
|
1135
|
+
for (const r of syncResults) {
|
|
1136
|
+
if (r.status === "rejected") {
|
|
1137
|
+
console.error("[reflectdb] reconnect re-sync failed:", r.reason);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (this.closed)
|
|
1141
|
+
return;
|
|
1142
|
+
await this.resume();
|
|
1143
|
+
this.clearInFlight();
|
|
1144
|
+
if (this.closed)
|
|
1145
|
+
return;
|
|
1146
|
+
await this.push();
|
|
1147
|
+
this.reconnectAttempts = 0;
|
|
1148
|
+
} catch (err) {
|
|
1149
|
+
try {
|
|
1150
|
+
this.config.onError?.({
|
|
1151
|
+
reason: "local:reconnect_failed",
|
|
1152
|
+
message: `attempt ${this.reconnectAttempts}: ${err instanceof Error ? err.message : String(err)}`
|
|
1153
|
+
});
|
|
1154
|
+
} catch {}
|
|
1155
|
+
this.scheduleReconnect();
|
|
1156
|
+
}
|
|
1157
|
+
}, delay);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function pushSafely(client) {
|
|
1161
|
+
return client.push().catch((err) => {
|
|
1162
|
+
console.error("[reflectdb] push failed:", err);
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/svelte/stores.ts
|
|
1167
|
+
function createBrowserWsTransport(url) {
|
|
1168
|
+
let ws = null;
|
|
1169
|
+
let handler = null;
|
|
1170
|
+
let intentionalClose = false;
|
|
1171
|
+
const sendQueue = [];
|
|
1172
|
+
function ensureConnected() {
|
|
1173
|
+
if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) {
|
|
1174
|
+
return ws;
|
|
1175
|
+
}
|
|
1176
|
+
const socket = new WebSocket(url);
|
|
1177
|
+
socket.onopen = () => {
|
|
1178
|
+
for (const msg of sendQueue) {
|
|
1179
|
+
socket.send(JSON.stringify(msg));
|
|
1180
|
+
}
|
|
1181
|
+
sendQueue.length = 0;
|
|
1182
|
+
};
|
|
1183
|
+
socket.onmessage = (event) => {
|
|
1184
|
+
try {
|
|
1185
|
+
handler?.(JSON.parse(event.data));
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
console.warn("[reflectdb] svelte WS: malformed message ignored:", err instanceof Error ? err.message : String(err));
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
socket.onclose = () => {
|
|
1191
|
+
ws = null;
|
|
1192
|
+
if (!intentionalClose) {
|
|
1193
|
+
handler?.({ type: "disconnect", reason: "transport_closed" });
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
ws = socket;
|
|
1197
|
+
return socket;
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
async send(message) {
|
|
1201
|
+
const socket = ensureConnected();
|
|
1202
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
1203
|
+
socket.send(JSON.stringify(message));
|
|
1204
|
+
} else {
|
|
1205
|
+
sendQueue.push(message);
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
subscribe(h) {
|
|
1209
|
+
handler = h;
|
|
1210
|
+
},
|
|
1211
|
+
async close() {
|
|
1212
|
+
intentionalClose = true;
|
|
1213
|
+
if (ws) {
|
|
1214
|
+
ws.close();
|
|
1215
|
+
ws = null;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
function stableKey(params) {
|
|
1221
|
+
if (!params)
|
|
1222
|
+
return "";
|
|
1223
|
+
return JSON.stringify(params, Object.keys(params).sort());
|
|
1224
|
+
}
|
|
1225
|
+
function createSyncStore(config) {
|
|
1226
|
+
const managedTables = new Set(config.tables ?? []);
|
|
1227
|
+
const clientId = config.clientId ?? `browser-${crypto.randomUUID().slice(0, 8)}`;
|
|
1228
|
+
const transport = createBrowserWsTransport(config.url);
|
|
1229
|
+
const client = new SyncClient({
|
|
1230
|
+
clientId,
|
|
1231
|
+
transport,
|
|
1232
|
+
token: config.token,
|
|
1233
|
+
storage: config.storage,
|
|
1234
|
+
onReauth: () => config.onReauth?.() ?? Promise.reject(new Error("No onReauth handler")),
|
|
1235
|
+
onError: (err) => config.onError?.(err)
|
|
1236
|
+
});
|
|
1237
|
+
const unmanagedRefCounts = new Map;
|
|
1238
|
+
const unmanagedParamKeys = new Map;
|
|
1239
|
+
const status = {
|
|
1240
|
+
subscribe(cb) {
|
|
1241
|
+
cb(client.getState());
|
|
1242
|
+
return client.subscribe(() => {
|
|
1243
|
+
cb(client.getState());
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
const pendingCount = {
|
|
1248
|
+
subscribe(cb) {
|
|
1249
|
+
cb(client.getPendingCount());
|
|
1250
|
+
return client.subscribe(() => {
|
|
1251
|
+
cb(client.getPendingCount());
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
function sync(table, options) {
|
|
1256
|
+
const isManaged = managedTables.has(table);
|
|
1257
|
+
const paramKey = stableKey(options?.params);
|
|
1258
|
+
const rows = {
|
|
1259
|
+
subscribe(cb) {
|
|
1260
|
+
if (!isManaged) {
|
|
1261
|
+
const currentParamKey = unmanagedParamKeys.get(table);
|
|
1262
|
+
const count = unmanagedRefCounts.get(table) ?? 0;
|
|
1263
|
+
if (count === 0 || currentParamKey !== paramKey) {
|
|
1264
|
+
if (currentParamKey !== undefined && currentParamKey !== paramKey) {
|
|
1265
|
+
client.unsync(table);
|
|
1266
|
+
}
|
|
1267
|
+
client.sync(table, options?.params, options?.window ? { window: options.window } : undefined);
|
|
1268
|
+
client.scheduleBootstrap();
|
|
1269
|
+
unmanagedParamKeys.set(table, paramKey);
|
|
1270
|
+
}
|
|
1271
|
+
unmanagedRefCounts.set(table, count + 1);
|
|
1272
|
+
}
|
|
1273
|
+
cb(client.getRows(table, { includeDeleted: options?.includeDeleted }));
|
|
1274
|
+
const unsub = client.subscribeTable(table, () => {
|
|
1275
|
+
cb(client.getRows(table, { includeDeleted: options?.includeDeleted }));
|
|
1276
|
+
});
|
|
1277
|
+
return () => {
|
|
1278
|
+
unsub();
|
|
1279
|
+
if (!isManaged) {
|
|
1280
|
+
const count = (unmanagedRefCounts.get(table) ?? 1) - 1;
|
|
1281
|
+
if (count <= 0) {
|
|
1282
|
+
unmanagedRefCounts.delete(table);
|
|
1283
|
+
unmanagedParamKeys.delete(table);
|
|
1284
|
+
client.unsync(table);
|
|
1285
|
+
} else {
|
|
1286
|
+
unmanagedRefCounts.set(table, count);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
const insert = (rowId, payload) => {
|
|
1293
|
+
client.insert(table, rowId, payload);
|
|
1294
|
+
pushSafely(client);
|
|
1295
|
+
};
|
|
1296
|
+
const update = (rowId, payload) => {
|
|
1297
|
+
client.update(table, rowId, payload);
|
|
1298
|
+
pushSafely(client);
|
|
1299
|
+
};
|
|
1300
|
+
const remove = (rowId) => {
|
|
1301
|
+
client.delete(table, rowId);
|
|
1302
|
+
pushSafely(client);
|
|
1303
|
+
};
|
|
1304
|
+
return { rows, insert, update, remove };
|
|
1305
|
+
}
|
|
1306
|
+
function row(table, rowId) {
|
|
1307
|
+
return {
|
|
1308
|
+
subscribe(cb) {
|
|
1309
|
+
cb(client.getRow(table, rowId));
|
|
1310
|
+
return client.subscribeTable(table, () => {
|
|
1311
|
+
cb(client.getRow(table, rowId));
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
function ephemeral(cfg) {
|
|
1317
|
+
let current = {};
|
|
1318
|
+
const timers = new Map;
|
|
1319
|
+
const subscribers = new Set;
|
|
1320
|
+
function notifySubscribers() {
|
|
1321
|
+
for (const sub of subscribers) {
|
|
1322
|
+
sub(current);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const unsub = client.subscribeEphemeral(cfg.key, (event) => {
|
|
1326
|
+
current = { ...current, [event.userId]: event.data };
|
|
1327
|
+
notifySubscribers();
|
|
1328
|
+
if (cfg.ttlMs) {
|
|
1329
|
+
const existing = timers.get(event.userId);
|
|
1330
|
+
if (existing) {
|
|
1331
|
+
clearTimeout(existing);
|
|
1332
|
+
}
|
|
1333
|
+
const timer = setTimeout(() => {
|
|
1334
|
+
const next = { ...current };
|
|
1335
|
+
delete next[event.userId];
|
|
1336
|
+
current = next;
|
|
1337
|
+
timers.delete(event.userId);
|
|
1338
|
+
notifySubscribers();
|
|
1339
|
+
}, cfg.ttlMs);
|
|
1340
|
+
timers.set(event.userId, timer);
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
const events = {
|
|
1344
|
+
subscribe(cb) {
|
|
1345
|
+
subscribers.add(cb);
|
|
1346
|
+
cb(current);
|
|
1347
|
+
return () => {
|
|
1348
|
+
subscribers.delete(cb);
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
const broadcast = (data) => client.sendEphemeral({
|
|
1353
|
+
key: cfg.key,
|
|
1354
|
+
userId: cfg.userId,
|
|
1355
|
+
data,
|
|
1356
|
+
ttlMs: cfg.ttlMs
|
|
1357
|
+
});
|
|
1358
|
+
const destroy = () => {
|
|
1359
|
+
unsub();
|
|
1360
|
+
for (const timer of timers.values()) {
|
|
1361
|
+
clearTimeout(timer);
|
|
1362
|
+
}
|
|
1363
|
+
timers.clear();
|
|
1364
|
+
subscribers.clear();
|
|
1365
|
+
};
|
|
1366
|
+
return { events, broadcast, destroy };
|
|
1367
|
+
}
|
|
1368
|
+
function totalCount(table) {
|
|
1369
|
+
return {
|
|
1370
|
+
subscribe(cb) {
|
|
1371
|
+
cb(client.getTotalCount(table));
|
|
1372
|
+
return client.subscribeTable(table, () => {
|
|
1373
|
+
cb(client.getTotalCount(table));
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
function loadMore(table, count) {
|
|
1379
|
+
client.loadMore(table, count);
|
|
1380
|
+
}
|
|
1381
|
+
async function connectLifecycle() {
|
|
1382
|
+
if (config.storage) {
|
|
1383
|
+
await client.init();
|
|
1384
|
+
}
|
|
1385
|
+
await client.connect();
|
|
1386
|
+
for (const table of managedTables) {
|
|
1387
|
+
await client.sync(table);
|
|
1388
|
+
}
|
|
1389
|
+
await client.resume();
|
|
1390
|
+
}
|
|
1391
|
+
async function close() {
|
|
1392
|
+
await client.close();
|
|
1393
|
+
}
|
|
1394
|
+
return {
|
|
1395
|
+
client,
|
|
1396
|
+
status,
|
|
1397
|
+
pendingCount,
|
|
1398
|
+
sync,
|
|
1399
|
+
row,
|
|
1400
|
+
ephemeral,
|
|
1401
|
+
totalCount,
|
|
1402
|
+
loadMore,
|
|
1403
|
+
connect: connectLifecycle,
|
|
1404
|
+
close
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
// src/svelte/typed.ts
|
|
1408
|
+
function createSyncSvelte() {
|
|
1409
|
+
function createStore(config) {
|
|
1410
|
+
const store = createSyncStore(config);
|
|
1411
|
+
return store;
|
|
1412
|
+
}
|
|
1413
|
+
return { createStore };
|
|
1414
|
+
}
|