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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1260 -0
  3. package/dist/cjs/client/index.cjs +1252 -0
  4. package/dist/cjs/client/index.d.cts +629 -0
  5. package/dist/cjs/client/storage/indexeddb.cjs +253 -0
  6. package/dist/cjs/client/storage/indexeddb.d.cts +85 -0
  7. package/dist/cjs/core/index.cjs +202 -0
  8. package/dist/cjs/core/index.d.cts +473 -0
  9. package/dist/cjs/react/index.cjs +1512 -0
  10. package/dist/cjs/react/index.d.cts +748 -0
  11. package/dist/cjs/server/drizzle.cjs +413 -0
  12. package/dist/cjs/server/drizzle.d.cts +233 -0
  13. package/dist/cjs/server/index.cjs +4486 -0
  14. package/dist/cjs/server/index.d.cts +1988 -0
  15. package/dist/cjs/svelte/index.cjs +1414 -0
  16. package/dist/cjs/svelte/index.d.cts +645 -0
  17. package/dist/cjs/transport/bun-ws.cjs +244 -0
  18. package/dist/cjs/transport/bun-ws.d.cts +215 -0
  19. package/dist/cjs/transport/polling.cjs +285 -0
  20. package/dist/cjs/transport/polling.d.cts +210 -0
  21. package/dist/cjs/transport/sse.cjs +312 -0
  22. package/dist/cjs/transport/sse.d.cts +205 -0
  23. package/dist/cjs/transport/ws.cjs +330 -0
  24. package/dist/cjs/transport/ws.d.cts +236 -0
  25. package/dist/cjs/vanilla/index.cjs +1430 -0
  26. package/dist/cjs/vanilla/index.d.cts +657 -0
  27. package/dist/client/index.d.ts +629 -0
  28. package/dist/client/index.js +106 -0
  29. package/dist/client/storage/indexeddb.d.ts +85 -0
  30. package/dist/client/storage/indexeddb.js +213 -0
  31. package/dist/core/index.d.ts +473 -0
  32. package/dist/core/index.js +56 -0
  33. package/dist/react/index.d.ts +748 -0
  34. package/dist/react/index.js +366 -0
  35. package/dist/server/drizzle.d.ts +233 -0
  36. package/dist/server/drizzle.js +9 -0
  37. package/dist/server/index.d.ts +1988 -0
  38. package/dist/server/index.js +3959 -0
  39. package/dist/shared/esm-3tkwvysa.js +54 -0
  40. package/dist/shared/esm-b7xs9cde.js +4 -0
  41. package/dist/shared/esm-rw7jjtrv.js +58 -0
  42. package/dist/shared/esm-wkwx6bd9.js +25 -0
  43. package/dist/shared/esm-ytrd3hbq.js +1007 -0
  44. package/dist/shared/esm-z1xse19c.js +369 -0
  45. package/dist/svelte/index.d.ts +645 -0
  46. package/dist/svelte/index.js +262 -0
  47. package/dist/transport/bun-ws.d.ts +215 -0
  48. package/dist/transport/bun-ws.js +140 -0
  49. package/dist/transport/polling.d.ts +210 -0
  50. package/dist/transport/polling.js +181 -0
  51. package/dist/transport/sse.d.ts +205 -0
  52. package/dist/transport/sse.js +208 -0
  53. package/dist/transport/ws.d.ts +236 -0
  54. package/dist/transport/ws.js +226 -0
  55. package/dist/vanilla/index.d.ts +657 -0
  56. package/dist/vanilla/index.js +278 -0
  57. package/package.json +253 -0
@@ -0,0 +1,1252 @@
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/client/index.ts
108
+ var exports_client = {};
109
+ __export(exports_client, {
110
+ pushSafely: () => pushSafely,
111
+ createSyncClient: () => createSyncClient,
112
+ createOpCreator: () => createOpCreator,
113
+ createMemoryStorage: () => createMemoryStorage,
114
+ SyncClient: () => SyncClient,
115
+ ClientStore: () => ClientStore
116
+ });
117
+ module.exports = __toCommonJS(exports_client);
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
+ // src/client/store.ts
233
+ class ClientStore {
234
+ pendingOps = [];
235
+ rows = new Map;
236
+ tableMeta = new Map;
237
+ storage;
238
+ pendingWrites = [];
239
+ writeChain = Promise.resolve();
240
+ constructor(storage) {
241
+ this.storage = storage ?? null;
242
+ }
243
+ async hydrate(options) {
244
+ if (!this.storage)
245
+ return;
246
+ const storage = this.storage;
247
+ const scoped = options?.tables;
248
+ const [allRows, pendingOps, tableMetaJson] = await Promise.all([
249
+ scoped ? Promise.all(scoped.map((table) => storage.getRows(table))).then((r) => r.flat()) : storage.getAllRows(),
250
+ storage.getPendingOps(),
251
+ storage.getMeta("tableMeta")
252
+ ]);
253
+ for (const row of allRows) {
254
+ let tableMap = this.rows.get(row.table);
255
+ if (!tableMap) {
256
+ tableMap = new Map;
257
+ this.rows.set(row.table, tableMap);
258
+ }
259
+ tableMap.set(row.rowId, row);
260
+ }
261
+ this.pendingOps = pendingOps;
262
+ if (tableMetaJson) {
263
+ const parsed = JSON.parse(tableMetaJson);
264
+ for (const [table, m] of Object.entries(parsed)) {
265
+ this.tableMeta.set(table, {
266
+ serverSet: m.serverSet,
267
+ readonly: m.readonly,
268
+ broadcast: m.broadcast ?? "consistent",
269
+ pk: m.pk ?? "id"
270
+ });
271
+ }
272
+ }
273
+ }
274
+ async flush() {
275
+ this.pendingWrites = [];
276
+ await this.writeChain;
277
+ }
278
+ enqueue(fn) {
279
+ if (!this.storage)
280
+ return;
281
+ this.writeChain = this.writeChain.then(fn).catch((err) => {
282
+ console.error("[reflectdb] storage write failed:", err);
283
+ });
284
+ this.pendingWrites.push(this.writeChain);
285
+ }
286
+ addPendingOp(op) {
287
+ const existing = this.getRow(op.table, op.rowId);
288
+ const pendingOp = {
289
+ op,
290
+ status: "pending",
291
+ rejectedReason: null,
292
+ createdAt: Date.now(),
293
+ preState: existing ? {
294
+ data: existing.data,
295
+ colClocks: { ...existing.colClocks },
296
+ serverHlc: existing.serverHlc
297
+ } : null
298
+ };
299
+ this.pendingOps.push(pendingOp);
300
+ this.enqueue(() => this.storage.appendPendingOps([pendingOp]));
301
+ }
302
+ getPendingOps() {
303
+ return this.pendingOps.filter((p) => p.status === "pending");
304
+ }
305
+ markSynced(opIds) {
306
+ const idSet = new Set(opIds);
307
+ this.pendingOps = this.pendingOps.filter((p) => !idSet.has(p.op.id));
308
+ this.enqueue(() => this.storage.removePendingOps(opIds));
309
+ }
310
+ markRejected(opId, reason) {
311
+ const pending = this.pendingOps.find((p) => p.op.id === opId);
312
+ if (pending) {
313
+ pending.status = "rejected";
314
+ pending.rejectedReason = reason;
315
+ this.enqueue(() => this.storage.updatePendingOps([pending]));
316
+ }
317
+ this.autoTrimRejected();
318
+ }
319
+ rejectBatch(batchId, reason) {
320
+ const updated = [];
321
+ for (const p of this.pendingOps) {
322
+ if (p.op.batchId === batchId) {
323
+ p.status = "rejected";
324
+ p.rejectedReason = reason;
325
+ updated.push(p);
326
+ }
327
+ }
328
+ if (updated.length > 0) {
329
+ this.enqueue(() => this.storage.updatePendingOps(updated));
330
+ }
331
+ this.autoTrimRejected();
332
+ }
333
+ autoTrimRejected() {
334
+ const cutoff = Date.now() - 60000;
335
+ const before = this.pendingOps.length;
336
+ this.pendingOps = this.pendingOps.filter((p) => p.status !== "rejected" || p.createdAt > cutoff);
337
+ if (this.pendingOps.length < before) {
338
+ this.enqueue(() => this.storage.putPendingOps([...this.pendingOps]));
339
+ }
340
+ }
341
+ clearRejected() {
342
+ this.pendingOps = this.pendingOps.filter((p) => p.status !== "rejected");
343
+ this.enqueue(() => this.storage.putPendingOps([...this.pendingOps]));
344
+ }
345
+ setRow(table, rowId, data, colClocks, serverHlc) {
346
+ if (data === null && serverHlc === null) {
347
+ this.rows.get(table)?.delete(rowId);
348
+ this.enqueue(() => this.storage.deleteRow(table, rowId));
349
+ return;
350
+ }
351
+ let tableMap = this.rows.get(table);
352
+ if (!tableMap) {
353
+ tableMap = new Map;
354
+ this.rows.set(table, tableMap);
355
+ }
356
+ const row = { table, rowId, data, colClocks, serverHlc };
357
+ tableMap.set(rowId, row);
358
+ this.enqueue(() => this.storage.putRow(table, rowId, row));
359
+ }
360
+ getRow(table, rowId) {
361
+ const row = this.rows.get(table)?.get(rowId);
362
+ if (!row || row.data === null)
363
+ return;
364
+ return row;
365
+ }
366
+ getRowEntry(table, rowId) {
367
+ return this.rows.get(table)?.get(rowId);
368
+ }
369
+ getRows(table, includeDeleted = false) {
370
+ const tableMap = this.rows.get(table);
371
+ if (!tableMap)
372
+ return [];
373
+ const result = [];
374
+ for (const row of tableMap.values()) {
375
+ if (row.data !== null) {
376
+ if (!includeDeleted && (row.data.deletedAt != null || row.data.deleted_at != null)) {
377
+ continue;
378
+ }
379
+ result.push(row);
380
+ }
381
+ }
382
+ return result;
383
+ }
384
+ clearTable(table) {
385
+ this.rows.delete(table);
386
+ this.enqueue(() => this.storage.clearTable(table));
387
+ }
388
+ applySnapshot(table, rows, colClocks, append = false, pk) {
389
+ if (!append) {
390
+ this.clearTable(table);
391
+ }
392
+ const effectivePk = pk ?? this.tableMeta.get(table)?.pk ?? "id";
393
+ for (const row of rows) {
394
+ const rowId = row[effectivePk] ?? "";
395
+ const clocks = colClocks[rowId] ?? {};
396
+ this.setRow(table, rowId, row, clocks, clocks._row ?? null);
397
+ }
398
+ for (const p of this.pendingOps) {
399
+ if (p.status === "pending" && p.op.table === table) {
400
+ this.applyOptimistic(p.op);
401
+ }
402
+ }
403
+ }
404
+ applyDelta(table, op, rowId, payload, hlc, colClocks) {
405
+ const existing = this.getRowEntry(table, rowId);
406
+ if (op === "delete") {
407
+ if (existing?.serverHlc && compareHlc(hlc, existing.serverHlc) <= 0) {
408
+ return;
409
+ }
410
+ this.setRow(table, rowId, null, {}, hlc);
411
+ return;
412
+ }
413
+ if (op === "insert" || !existing) {
414
+ if (existing?.serverHlc && compareHlc(hlc, existing.serverHlc) <= 0) {
415
+ return;
416
+ }
417
+ this.setRow(table, rowId, payload ?? {}, colClocks ?? { _row: hlc }, hlc);
418
+ return;
419
+ }
420
+ const incomingClocks = colClocks ?? {};
421
+ const mergedData = { ...existing.data };
422
+ const mergedClocks = { ...existing.colClocks };
423
+ let anyApplied = false;
424
+ for (const [col, value] of Object.entries(payload ?? {})) {
425
+ const incomingClock = incomingClocks[col] ?? hlc;
426
+ const existingClock = existing.colClocks[col];
427
+ if (!existingClock || compareHlc(incomingClock, existingClock) > 0) {
428
+ mergedData[col] = value;
429
+ mergedClocks[col] = incomingClock;
430
+ anyApplied = true;
431
+ }
432
+ }
433
+ if (!anyApplied) {
434
+ return;
435
+ }
436
+ const newRowHlc = existing.serverHlc && compareHlc(existing.serverHlc, hlc) >= 0 ? existing.serverHlc : hlc;
437
+ mergedClocks._row = newRowHlc;
438
+ this.setRow(table, rowId, mergedData, mergedClocks, newRowHlc);
439
+ }
440
+ revertOp(opId, serverRow) {
441
+ const pending = this.pendingOps.find((p) => p.op.id === opId);
442
+ if (!pending)
443
+ return;
444
+ const { table, rowId } = pending.op;
445
+ if (serverRow != null) {
446
+ const existing = this.getRow(table, rowId);
447
+ this.setRow(table, rowId, serverRow, existing?.colClocks ?? {}, existing?.serverHlc ?? null);
448
+ } else if (pending.preState) {
449
+ this.setRow(table, rowId, pending.preState.data, pending.preState.colClocks, pending.preState.serverHlc);
450
+ } else {
451
+ const tableMap = this.rows.get(table);
452
+ if (tableMap) {
453
+ tableMap.delete(rowId);
454
+ this.enqueue(() => this.storage.deleteRow(table, rowId));
455
+ }
456
+ }
457
+ this.pendingOps = this.pendingOps.filter((p) => p.op.id !== opId);
458
+ this.enqueue(() => this.storage.removePendingOps([opId]));
459
+ }
460
+ setTableMeta(meta) {
461
+ for (const [table, m] of Object.entries(meta)) {
462
+ this.tableMeta.set(table, {
463
+ serverSet: m.serverSet,
464
+ readonly: m.readonly,
465
+ broadcast: m.broadcast ?? "consistent",
466
+ pk: m.pk ?? "id"
467
+ });
468
+ }
469
+ this.enqueue(() => this.storage.setMeta("tableMeta", JSON.stringify(Object.fromEntries(this.tableMeta))));
470
+ }
471
+ getTableMeta(table) {
472
+ return this.tableMeta.get(table);
473
+ }
474
+ applyOptimistic(op) {
475
+ const meta = this.tableMeta.get(op.table);
476
+ let payload = op.payload;
477
+ if (payload && meta?.serverSet.length) {
478
+ payload = { ...payload };
479
+ for (const field of meta.serverSet) {
480
+ delete payload[field];
481
+ }
482
+ }
483
+ if (op.op === "delete") {
484
+ this.setRow(op.table, op.rowId, null, { _row: op.hlc }, op.hlc);
485
+ } else if (op.op === "insert") {
486
+ this.setRow(op.table, op.rowId, payload ?? {}, { _row: op.hlc }, op.hlc);
487
+ } else {
488
+ const existing = this.getRow(op.table, op.rowId);
489
+ if (existing?.data) {
490
+ const merged = { ...existing.data, ...payload };
491
+ this.setRow(op.table, op.rowId, merged, existing.colClocks, existing.serverHlc);
492
+ }
493
+ }
494
+ }
495
+ async clear() {
496
+ try {
497
+ await this.writeChain;
498
+ } catch {}
499
+ this.rows.clear();
500
+ this.pendingOps = [];
501
+ this.tableMeta.clear();
502
+ this.pendingWrites = [];
503
+ this.writeChain = Promise.resolve();
504
+ if (this.storage) {
505
+ await this.storage.clear();
506
+ }
507
+ }
508
+ }
509
+ // src/client/sync-client.ts
510
+ init_types();
511
+ class SyncClient {
512
+ config;
513
+ transport;
514
+ opCreator;
515
+ store;
516
+ state = "disconnected";
517
+ negotiatedProtocolVersion = null;
518
+ syncedTables = new Set;
519
+ serverHlc = null;
520
+ listeners = new Set;
521
+ version = 0;
522
+ tableVersions = new Map;
523
+ tableListeners = new Map;
524
+ connectResolve = null;
525
+ connectReject = null;
526
+ connectInFlight = null;
527
+ ephemeralListeners = new Map;
528
+ totalCounts = new Map;
529
+ syncOptions = new Map;
530
+ bootstrapScheduled = false;
531
+ initialized = false;
532
+ syncParams = new Map;
533
+ reconnectTimer = null;
534
+ reconnectAttempts = 0;
535
+ closed = false;
536
+ inFlightOps = new Map;
537
+ static IN_FLIGHT_TTL_MS = 30000;
538
+ pushChain = Promise.resolve();
539
+ pushPending = null;
540
+ constructor(config) {
541
+ this.config = config;
542
+ this.transport = config.transport;
543
+ this.opCreator = createOpCreator(`client:${config.clientId}`);
544
+ this.store = new ClientStore(config.storage);
545
+ this.transport.subscribe((message) => {
546
+ this.handleMessage(message);
547
+ });
548
+ }
549
+ async init() {
550
+ if (!this.config.storage) {
551
+ this.initialized = true;
552
+ return;
553
+ }
554
+ this.state = "hydrating";
555
+ this.notify();
556
+ const subsJson = await this.config.storage.getMeta("syncSubscriptions");
557
+ if (subsJson) {
558
+ const subs = JSON.parse(subsJson);
559
+ for (const sub of subs) {
560
+ this.syncedTables.add(sub.table);
561
+ if (sub.params) {
562
+ this.syncParams.set(sub.table, sub.params);
563
+ }
564
+ if (sub.options) {
565
+ this.syncOptions.set(sub.table, sub.options);
566
+ }
567
+ }
568
+ }
569
+ await this.store.hydrate(this.config.hydrateAllTables || this.syncedTables.size === 0 ? undefined : { tables: [...this.syncedTables] });
570
+ const serverHlcStr = await this.config.storage.getMeta("serverHlc");
571
+ if (serverHlcStr) {
572
+ this.serverHlc = serverHlcStr;
573
+ }
574
+ const currentNodeId = `client:${this.config.clientId}`;
575
+ const hlcJson = await this.config.storage.getMeta("hlcState");
576
+ let restoredHlc = null;
577
+ if (hlcJson) {
578
+ const parsed = JSON.parse(hlcJson);
579
+ restoredHlc = { ...parsed, nodeId: currentNodeId };
580
+ this.opCreator = createOpCreator(currentNodeId, restoredHlc);
581
+ }
582
+ const pendingOps = this.store.getPendingOps();
583
+ if (pendingOps.length > 0) {
584
+ let maxMs = restoredHlc?.ms ?? 0;
585
+ let maxCounter = restoredHlc?.counter ?? 0;
586
+ for (const p of pendingOps) {
587
+ const opHlc = unpackHlc(p.op.hlc);
588
+ if (opHlc.ms > maxMs || opHlc.ms === maxMs && opHlc.counter > maxCounter) {
589
+ maxMs = opHlc.ms;
590
+ maxCounter = opHlc.counter;
591
+ }
592
+ }
593
+ this.opCreator = createOpCreator(currentNodeId, {
594
+ ms: maxMs,
595
+ counter: maxCounter,
596
+ nodeId: currentNodeId
597
+ });
598
+ }
599
+ this.state = "disconnected";
600
+ this.initialized = true;
601
+ this.notify();
602
+ }
603
+ async connect() {
604
+ if (!this.initialized && this.config.storage) {
605
+ await this.init();
606
+ }
607
+ if (this.connectInFlight)
608
+ return this.connectInFlight;
609
+ this.state = "connecting";
610
+ this.connectInFlight = new Promise((resolve, reject) => {
611
+ this.connectResolve = resolve;
612
+ this.connectReject = reject;
613
+ }).finally(() => {
614
+ this.connectInFlight = null;
615
+ });
616
+ await this.transport.send({
617
+ type: "hello",
618
+ protocolVersion: PROTOCOL_VERSION,
619
+ supportedVersions: SUPPORTED_PROTOCOL_VERSIONS,
620
+ clientId: this.config.clientId,
621
+ token: this.config.token
622
+ });
623
+ return this.connectInFlight;
624
+ }
625
+ async close() {
626
+ this.closed = true;
627
+ if (this.reconnectTimer) {
628
+ clearTimeout(this.reconnectTimer);
629
+ this.reconnectTimer = null;
630
+ }
631
+ this.state = "disconnected";
632
+ this.notify();
633
+ try {
634
+ await this.store.flush();
635
+ } catch (err) {
636
+ console.error("[reflectdb] store flush during close failed:", err);
637
+ }
638
+ await this.transport.close();
639
+ }
640
+ getState() {
641
+ return this.state;
642
+ }
643
+ getProtocolVersion() {
644
+ return this.negotiatedProtocolVersion;
645
+ }
646
+ async sync(table, params, options) {
647
+ this.syncedTables.add(table);
648
+ if (params) {
649
+ this.syncParams.set(table, params);
650
+ }
651
+ if (options) {
652
+ this.syncOptions.set(table, options);
653
+ }
654
+ this.persistSyncSubscriptions();
655
+ await this.transport.send({
656
+ type: "sync_declare",
657
+ table,
658
+ params,
659
+ window: options?.window
660
+ });
661
+ }
662
+ async loadMore(table, count) {
663
+ await this.transport.send({
664
+ type: "load_more",
665
+ table,
666
+ count
667
+ });
668
+ }
669
+ getTotalCount(table) {
670
+ return this.totalCounts.get(table) ?? null;
671
+ }
672
+ async unsync(table) {
673
+ this.syncedTables.delete(table);
674
+ this.syncOptions.delete(table);
675
+ this.syncParams.delete(table);
676
+ this.totalCounts.delete(table);
677
+ this.store.clearTable(table);
678
+ this.persistSyncSubscriptions();
679
+ this.notify([table]);
680
+ await this.transport.send({
681
+ type: "unsync",
682
+ table
683
+ });
684
+ }
685
+ async bootstrap() {
686
+ this.state = "bootstrapping";
687
+ await this.transport.send({
688
+ type: "bootstrap"
689
+ });
690
+ }
691
+ scheduleBootstrap() {
692
+ if (this.bootstrapScheduled)
693
+ return;
694
+ this.bootstrapScheduled = true;
695
+ queueMicrotask(() => {
696
+ this.bootstrapScheduled = false;
697
+ this.bootstrap();
698
+ });
699
+ }
700
+ async resume() {
701
+ if (!this.serverHlc) {
702
+ return this.bootstrap();
703
+ }
704
+ await this.transport.send({
705
+ type: "resume",
706
+ since: this.serverHlc
707
+ });
708
+ }
709
+ insert(table, rowId, payload) {
710
+ const op = this.opCreator.insert(table, rowId, payload);
711
+ this.store.addPendingOp(op);
712
+ this.store.applyOptimistic(op);
713
+ this.notify([table]);
714
+ return op;
715
+ }
716
+ update(table, rowId, payload) {
717
+ const op = this.opCreator.update(table, rowId, payload);
718
+ this.store.addPendingOp(op);
719
+ this.store.applyOptimistic(op);
720
+ this.notify([table]);
721
+ return op;
722
+ }
723
+ delete(table, rowId) {
724
+ const op = this.opCreator.delete(table, rowId);
725
+ this.store.addPendingOp(op);
726
+ this.store.applyOptimistic(op);
727
+ this.notify([table]);
728
+ return op;
729
+ }
730
+ batch(ops) {
731
+ const batchOps = this.opCreator.createBatch(ops);
732
+ for (const op of batchOps) {
733
+ this.store.addPendingOp(op);
734
+ this.store.applyOptimistic(op);
735
+ }
736
+ this.notify([...new Set(batchOps.map((o) => o.table))]);
737
+ return batchOps;
738
+ }
739
+ push() {
740
+ if (this.pushPending)
741
+ return this.pushPending;
742
+ const run = this.pushChain.catch(() => {}).then(() => {
743
+ this.pushPending = null;
744
+ return this.doPush();
745
+ });
746
+ this.pushPending = run;
747
+ this.pushChain = run.catch(() => {});
748
+ return run;
749
+ }
750
+ async doPush() {
751
+ await this.store.flush();
752
+ const cutoff = Date.now() - SyncClient.IN_FLIGHT_TTL_MS;
753
+ const pending = this.store.getPendingOps().filter((p) => {
754
+ const sentAt = this.inFlightOps.get(p.op.id);
755
+ return sentAt === undefined || sentAt <= cutoff;
756
+ });
757
+ if (pending.length === 0)
758
+ return;
759
+ const ops = pending.map((p) => p.op);
760
+ const chunks = [];
761
+ let current = [];
762
+ let i = 0;
763
+ while (i < ops.length) {
764
+ const op = ops[i];
765
+ if (op.batchId) {
766
+ let end = i + 1;
767
+ while (end < ops.length && ops[end].batchId === op.batchId)
768
+ end++;
769
+ const batchOps = ops.slice(i, end);
770
+ if (batchOps.length > MAX_BATCH_SIZE) {
771
+ if (current.length > 0) {
772
+ chunks.push(current);
773
+ current = [];
774
+ }
775
+ chunks.push(batchOps);
776
+ } else {
777
+ if (current.length + batchOps.length > MAX_BATCH_SIZE) {
778
+ chunks.push(current);
779
+ current = [];
780
+ }
781
+ current.push(...batchOps);
782
+ }
783
+ i = end;
784
+ } else {
785
+ if (current.length + 1 > MAX_BATCH_SIZE) {
786
+ chunks.push(current);
787
+ current = [];
788
+ }
789
+ current.push(op);
790
+ i++;
791
+ }
792
+ }
793
+ if (current.length > 0)
794
+ chunks.push(current);
795
+ for (const chunk of chunks) {
796
+ const chunkIds = chunk.map((op) => op.id);
797
+ const sentAt = Date.now();
798
+ for (const id of chunkIds)
799
+ this.inFlightOps.set(id, sentAt);
800
+ try {
801
+ await this.transport.send({
802
+ type: "ops",
803
+ ops: chunk,
804
+ token: this.config.token
805
+ });
806
+ } catch (err) {
807
+ for (const id of chunkIds)
808
+ this.inFlightOps.delete(id);
809
+ throw err;
810
+ }
811
+ }
812
+ }
813
+ clearInFlight() {
814
+ this.inFlightOps.clear();
815
+ }
816
+ async sendEphemeral(params) {
817
+ await this.transport.send({
818
+ type: "ephemeral",
819
+ ...params
820
+ });
821
+ }
822
+ subscribeEphemeral(key, listener) {
823
+ let listeners = this.ephemeralListeners.get(key);
824
+ if (!listeners) {
825
+ listeners = new Set;
826
+ this.ephemeralListeners.set(key, listeners);
827
+ }
828
+ listeners.add(listener);
829
+ return () => {
830
+ listeners.delete(listener);
831
+ if (listeners.size === 0) {
832
+ this.ephemeralListeners.delete(key);
833
+ }
834
+ };
835
+ }
836
+ getRows(table, options) {
837
+ return this.store.getRows(table, options?.includeDeleted).map((r) => r.data);
838
+ }
839
+ getRow(table, rowId) {
840
+ const row = this.store.getRow(table, rowId);
841
+ return row?.data ?? null;
842
+ }
843
+ getPendingCount() {
844
+ return this.store.getPendingOps().length;
845
+ }
846
+ getStore() {
847
+ return this.store;
848
+ }
849
+ subscribe(listener) {
850
+ this.listeners.add(listener);
851
+ return () => {
852
+ this.listeners.delete(listener);
853
+ };
854
+ }
855
+ subscribeTable(table, listener) {
856
+ let listeners = this.tableListeners.get(table);
857
+ if (!listeners) {
858
+ listeners = new Set;
859
+ this.tableListeners.set(table, listeners);
860
+ }
861
+ listeners.add(listener);
862
+ return () => {
863
+ listeners.delete(listener);
864
+ if (listeners.size === 0) {
865
+ this.tableListeners.delete(table);
866
+ }
867
+ };
868
+ }
869
+ getVersion() {
870
+ return this.version;
871
+ }
872
+ getTableVersion(table) {
873
+ return this.tableVersions.get(table) ?? 0;
874
+ }
875
+ notify(tables) {
876
+ this.version++;
877
+ if (tables) {
878
+ for (const table of tables) {
879
+ this.tableVersions.set(table, (this.tableVersions.get(table) ?? 0) + 1);
880
+ const tableListeners = this.tableListeners.get(table);
881
+ if (tableListeners) {
882
+ for (const listener of [...tableListeners]) {
883
+ try {
884
+ listener();
885
+ } catch (err) {
886
+ console.error("[reflectdb] table listener threw:", err);
887
+ }
888
+ }
889
+ }
890
+ }
891
+ }
892
+ for (const listener of [...this.listeners]) {
893
+ try {
894
+ listener();
895
+ } catch (err) {
896
+ console.error("[reflectdb] listener threw:", err);
897
+ }
898
+ }
899
+ }
900
+ async clear(opts) {
901
+ if (opts?.tables && opts.tables.length > 0) {
902
+ for (const table of opts.tables) {
903
+ await this.store.clearTable(table);
904
+ this.syncedTables.delete(table);
905
+ this.syncOptions.delete(table);
906
+ this.syncParams.delete(table);
907
+ this.totalCounts.delete(table);
908
+ }
909
+ this.notify();
910
+ return;
911
+ }
912
+ await this.store.clear();
913
+ this.serverHlc = null;
914
+ this.syncedTables.clear();
915
+ this.syncOptions.clear();
916
+ this.syncParams.clear();
917
+ this.totalCounts.clear();
918
+ this.opCreator = createOpCreator(`client:${this.config.clientId}`);
919
+ this.clearInFlight();
920
+ this.state = "disconnected";
921
+ this.initialized = false;
922
+ this.notify();
923
+ }
924
+ handleMessage(message) {
925
+ switch (message.type) {
926
+ case "hello_ack":
927
+ if (!SUPPORTED_PROTOCOL_VERSIONS.includes(message.protocolVersion)) {
928
+ this.state = "disconnected";
929
+ this.connectReject?.(new Error(`Server negotiated unsupported version: ${message.protocolVersion}`));
930
+ this.connectResolve = null;
931
+ this.connectReject = null;
932
+ this.notify();
933
+ break;
934
+ }
935
+ this.negotiatedProtocolVersion = message.protocolVersion;
936
+ this.state = "connected";
937
+ this.connectResolve?.();
938
+ this.connectResolve = null;
939
+ this.connectReject = null;
940
+ this.notify();
941
+ break;
942
+ case "hello_reject":
943
+ this.state = "disconnected";
944
+ this.connectReject?.(new Error(message.reason));
945
+ this.connectResolve = null;
946
+ this.connectReject = null;
947
+ this.notify();
948
+ this.config.onError?.({ reason: "local:hello_rejected", message: message.reason });
949
+ break;
950
+ case "snapshot":
951
+ this.store.applySnapshot(message.table, message.rows, message.colClocks, message.append, message.pk);
952
+ if (message.totalCount != null) {
953
+ this.totalCounts.set(message.table, message.totalCount);
954
+ }
955
+ this.notify([message.table]);
956
+ this.config.onSync?.(message.table);
957
+ break;
958
+ case "bootstrap_complete":
959
+ this.serverHlc = message.serverHlc;
960
+ this.store.setTableMeta(message.tableMeta);
961
+ this.state = "synced";
962
+ this.mergeServerHlc(message.serverHlc);
963
+ this.persistMeta();
964
+ this.notify();
965
+ break;
966
+ case "resume_complete":
967
+ this.serverHlc = message.serverHlc;
968
+ this.state = "synced";
969
+ this.mergeServerHlc(message.serverHlc);
970
+ this.persistMeta();
971
+ this.notify();
972
+ break;
973
+ case "delta":
974
+ this.store.applyDelta(message.table, message.op, message.rowId, message.payload, message.hlc, message.colClocks);
975
+ this.notify([message.table]);
976
+ this.config.onSync?.(message.table);
977
+ break;
978
+ case "ack": {
979
+ const idSet = new Set(message.opIds);
980
+ const ackedTables = [
981
+ ...new Set(this.store.getPendingOps().filter((p) => idSet.has(p.op.id)).map((p) => p.op.table))
982
+ ];
983
+ for (const opId of message.opIds)
984
+ this.inFlightOps.delete(opId);
985
+ this.store.markSynced(message.opIds);
986
+ this.notify(ackedTables.length > 0 ? ackedTables : undefined);
987
+ break;
988
+ }
989
+ case "reject": {
990
+ const rejectedTables = [];
991
+ if (message.opId) {
992
+ this.inFlightOps.delete(message.opId);
993
+ const pending = this.store.getPendingOps().find((p) => p.op.id === message.opId);
994
+ if (pending)
995
+ rejectedTables.push(pending.op.table);
996
+ this.store.markRejected(message.opId, message.reason);
997
+ this.store.revertOp(message.opId, message.serverRow ?? null);
998
+ }
999
+ if (message.batchId) {
1000
+ const batchOpIds = [];
1001
+ for (const p of this.store.getPendingOps()) {
1002
+ if (p.op.batchId === message.batchId) {
1003
+ rejectedTables.push(p.op.table);
1004
+ batchOpIds.push(p.op.id);
1005
+ }
1006
+ }
1007
+ this.store.rejectBatch(message.batchId, message.reason);
1008
+ for (const opId of batchOpIds) {
1009
+ this.inFlightOps.delete(opId);
1010
+ this.store.revertOp(opId, null);
1011
+ }
1012
+ }
1013
+ const uniqueRejectedTables = [...new Set(rejectedTables)];
1014
+ this.notify(uniqueRejectedTables.length > 0 ? uniqueRejectedTables : undefined);
1015
+ this.config.onError?.({
1016
+ opId: message.opId,
1017
+ reason: message.reason
1018
+ });
1019
+ break;
1020
+ }
1021
+ case "resume_rejected":
1022
+ this.serverHlc = null;
1023
+ if (this.config.storage) {
1024
+ this.config.storage.setMeta("serverHlc", "").catch((err) => {
1025
+ console.error("[reflectdb] Failed to clear stale serverHlc:", err);
1026
+ });
1027
+ }
1028
+ this.bootstrap();
1029
+ break;
1030
+ case "reauth":
1031
+ this.handleReauth();
1032
+ break;
1033
+ case "disconnect":
1034
+ this.state = "disconnected";
1035
+ this.clearInFlight();
1036
+ if (this.connectReject) {
1037
+ this.connectReject(new Error(message.reason));
1038
+ this.connectResolve = null;
1039
+ this.connectReject = null;
1040
+ }
1041
+ this.notify();
1042
+ this.config.onError?.({ reason: "local:disconnected", message: message.reason });
1043
+ this.scheduleReconnect();
1044
+ break;
1045
+ case "shape_changed":
1046
+ this.store.clearTable(message.table);
1047
+ this.notify([message.table]);
1048
+ this.config.onSync?.(message.table);
1049
+ break;
1050
+ case "count_changed":
1051
+ this.totalCounts.set(message.table, message.totalCount);
1052
+ this.notify([message.table]);
1053
+ break;
1054
+ case "ephemeral":
1055
+ {
1056
+ const listeners = this.ephemeralListeners.get(message.key);
1057
+ if (listeners) {
1058
+ for (const listener of listeners) {
1059
+ listener(message);
1060
+ }
1061
+ }
1062
+ }
1063
+ break;
1064
+ }
1065
+ }
1066
+ mergeServerHlc(packed) {
1067
+ const remote = unpackHlc(packed);
1068
+ const localHlc = this.opCreator.getHlc();
1069
+ const merged = receiveHlc(localHlc, remote);
1070
+ this.opCreator = createOpCreator(localHlc.nodeId, merged);
1071
+ }
1072
+ persistMeta() {
1073
+ if (!this.config.storage)
1074
+ return;
1075
+ const storage = this.config.storage;
1076
+ const hlcState = this.opCreator.getHlc();
1077
+ storage.setMeta("serverHlc", this.serverHlc).catch((err) => {
1078
+ console.error("[reflectdb] Failed to persist serverHlc:", err);
1079
+ });
1080
+ storage.setMeta("hlcState", JSON.stringify(hlcState)).catch((err) => {
1081
+ console.error("[reflectdb] Failed to persist hlcState:", err);
1082
+ });
1083
+ }
1084
+ persistSyncSubscriptions() {
1085
+ if (!this.config.storage)
1086
+ return;
1087
+ const subs = Array.from(this.syncedTables).map((table) => ({
1088
+ table,
1089
+ params: this.syncParams.get(table),
1090
+ options: this.syncOptions.get(table)
1091
+ }));
1092
+ this.config.storage.setMeta("syncSubscriptions", JSON.stringify(subs)).catch((err) => {
1093
+ console.error("[reflectdb] Failed to persist sync subscriptions:", err);
1094
+ });
1095
+ }
1096
+ handleReauth() {
1097
+ if (!this.config.onReauth) {
1098
+ this.config.onError?.({
1099
+ reason: "local:reauth_failed",
1100
+ message: "reauth requested but no onReauth handler configured"
1101
+ });
1102
+ return;
1103
+ }
1104
+ this.config.onReauth().then(async (token) => {
1105
+ this.config.token = token;
1106
+ try {
1107
+ await this.transport.send({ type: "auth", token });
1108
+ } catch (err) {
1109
+ this.config.onError?.({
1110
+ reason: "local:reauth_failed",
1111
+ message: `auth send failed: ${err instanceof Error ? err.message : String(err)}`
1112
+ });
1113
+ }
1114
+ }).catch((err) => {
1115
+ this.config.onError?.({ reason: "local:reauth_failed", message: String(err) });
1116
+ });
1117
+ }
1118
+ scheduleReconnect() {
1119
+ if (this.closed || this.reconnectTimer)
1120
+ return;
1121
+ const cap = this.config.maxReconnectDelayMs ?? 30000;
1122
+ const base = Math.min(1000 * 2 ** this.reconnectAttempts, cap);
1123
+ const delay = Math.round(base * (0.5 + Math.random() * 0.5));
1124
+ this.reconnectAttempts++;
1125
+ this.reconnectTimer = setTimeout(async () => {
1126
+ this.reconnectTimer = null;
1127
+ try {
1128
+ await this.connect();
1129
+ if (this.closed)
1130
+ return;
1131
+ if (this.closed)
1132
+ return;
1133
+ const syncResults = await Promise.allSettled([...this.syncedTables].map((table) => this.sync(table, this.syncParams.get(table), this.syncOptions.get(table))));
1134
+ for (const r of syncResults) {
1135
+ if (r.status === "rejected") {
1136
+ console.error("[reflectdb] reconnect re-sync failed:", r.reason);
1137
+ }
1138
+ }
1139
+ if (this.closed)
1140
+ return;
1141
+ await this.resume();
1142
+ this.clearInFlight();
1143
+ if (this.closed)
1144
+ return;
1145
+ await this.push();
1146
+ this.reconnectAttempts = 0;
1147
+ } catch (err) {
1148
+ try {
1149
+ this.config.onError?.({
1150
+ reason: "local:reconnect_failed",
1151
+ message: `attempt ${this.reconnectAttempts}: ${err instanceof Error ? err.message : String(err)}`
1152
+ });
1153
+ } catch {}
1154
+ this.scheduleReconnect();
1155
+ }
1156
+ }, delay);
1157
+ }
1158
+ }
1159
+ function pushSafely(client) {
1160
+ return client.push().catch((err) => {
1161
+ console.error("[reflectdb] push failed:", err);
1162
+ });
1163
+ }
1164
+ // src/client/storage/memory.ts
1165
+ function createMemoryStorage() {
1166
+ const rows = new Map;
1167
+ let pendingOps = [];
1168
+ const meta = new Map;
1169
+ return {
1170
+ async getRows(table) {
1171
+ return Array.from(rows.get(table)?.values() ?? []);
1172
+ },
1173
+ async getAllRows() {
1174
+ const all = [];
1175
+ for (const tableRows of rows.values()) {
1176
+ for (const row of tableRows.values()) {
1177
+ all.push(row);
1178
+ }
1179
+ }
1180
+ return all;
1181
+ },
1182
+ async putRow(table, rowId, row) {
1183
+ let tableRows = rows.get(table);
1184
+ if (!tableRows) {
1185
+ tableRows = new Map;
1186
+ rows.set(table, tableRows);
1187
+ }
1188
+ tableRows.set(rowId, row);
1189
+ },
1190
+ async deleteRow(table, rowId) {
1191
+ rows.get(table)?.delete(rowId);
1192
+ },
1193
+ async clearTable(table) {
1194
+ rows.delete(table);
1195
+ },
1196
+ async getPendingOps() {
1197
+ return [...pendingOps];
1198
+ },
1199
+ async putPendingOps(ops) {
1200
+ pendingOps = [...ops];
1201
+ },
1202
+ async appendPendingOps(ops) {
1203
+ pendingOps.push(...ops);
1204
+ },
1205
+ async updatePendingOps(ops) {
1206
+ const updateMap = new Map;
1207
+ for (const op of ops) {
1208
+ updateMap.set(op.op.id, op);
1209
+ }
1210
+ for (let i = 0;i < pendingOps.length; i++) {
1211
+ const updated = updateMap.get(pendingOps[i].op.id);
1212
+ if (updated) {
1213
+ pendingOps[i] = updated;
1214
+ }
1215
+ }
1216
+ },
1217
+ async removePendingOps(opIds) {
1218
+ const idSet = new Set(opIds);
1219
+ pendingOps = pendingOps.filter((p) => !idSet.has(p.op.id));
1220
+ },
1221
+ async clearPendingOps() {
1222
+ pendingOps = [];
1223
+ },
1224
+ async getMeta(key) {
1225
+ return meta.get(key) ?? null;
1226
+ },
1227
+ async setMeta(key, value) {
1228
+ meta.set(key, value);
1229
+ },
1230
+ async deleteMeta(key) {
1231
+ meta.delete(key);
1232
+ },
1233
+ async clear() {
1234
+ rows.clear();
1235
+ pendingOps = [];
1236
+ meta.clear();
1237
+ }
1238
+ };
1239
+ }
1240
+ // src/client/typed-client.ts
1241
+ function createSyncClient(config) {
1242
+ const { queries, autoSync, ...clientConfig } = config;
1243
+ const client = new SyncClient(clientConfig);
1244
+ if (autoSync) {
1245
+ const origConnect = client.connect.bind(client);
1246
+ client.connect = async () => {
1247
+ await origConnect();
1248
+ await Promise.all(Object.keys(queries).filter((name) => !("params" in queries[name])).map((name) => client.sync(name)));
1249
+ };
1250
+ }
1251
+ return client;
1252
+ }