@delali/sirannon-db 0.1.7 → 0.2.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 (72) hide show
  1. package/README.md +117 -969
  2. package/dist/backup-scheduler/index.d.ts +1 -1
  3. package/dist/backup-scheduler/index.mjs +2 -2
  4. package/dist/baseline-Br77Fnhb.d.ts +6 -0
  5. package/dist/{change-tracker-CbmaMO-N.d.ts → change-tracker-DKRVUC3l.d.ts} +5 -8
  6. package/dist/{chunk-DJLX6CAE.mjs → chunk-4ISB7XMA.mjs} +2 -1
  7. package/dist/chunk-5NOIGN5Y.mjs +18 -0
  8. package/dist/chunk-67M7KAH6.mjs +90 -0
  9. package/dist/{chunk-GEZUUIKV.mjs → chunk-D7LAYTKN.mjs} +5 -2
  10. package/dist/{chunk-4IGMIJQK.mjs → chunk-EBJXPQQO.mjs} +59 -20
  11. package/dist/{chunk-BNUTBHHH.mjs → chunk-FHWTZFI4.mjs} +1 -1
  12. package/dist/chunk-GVCNMPOS.mjs +130 -0
  13. package/dist/chunk-H237TXZW.mjs +1407 -0
  14. package/dist/{chunk-HHRMRFFR.mjs → chunk-H6PIVVDN.mjs} +353 -142
  15. package/dist/chunk-HR5CWTLC.mjs +661 -0
  16. package/dist/chunk-JU64Y7HM.mjs +299 -0
  17. package/dist/{chunk-CW6S3WL5.mjs → chunk-JZGINXTN.mjs} +7 -5
  18. package/dist/{chunk-CJLYFDP5.mjs → chunk-LFZ37BSX.mjs} +1 -1
  19. package/dist/chunk-LNY2VVHE.mjs +18 -0
  20. package/dist/chunk-NVQS53NT.mjs +665 -0
  21. package/dist/{chunk-H5AB6NIR.mjs → chunk-O7SLN3GI.mjs} +2 -15
  22. package/dist/chunk-OQVZBEBY.mjs +283 -0
  23. package/dist/{chunk-VLTICJOD.mjs → chunk-PIKHN33N.mjs} +1 -1
  24. package/dist/chunk-TJF5GZSV.mjs +187 -0
  25. package/dist/chunk-TUD5CJ76.mjs +126 -0
  26. package/dist/{chunk-YPYVQJ4C.mjs → chunk-UC3SCMIN.mjs} +9 -1
  27. package/dist/client/index.d.ts +216 -321
  28. package/dist/client/index.mjs +1192 -1342
  29. package/dist/client/topology.d.ts +61 -0
  30. package/dist/client/topology.mjs +682 -0
  31. package/dist/client-base-CLWmH5Ln.d.ts +234 -0
  32. package/dist/codegen/cli.d.ts +2 -0
  33. package/dist/codegen/cli.mjs +11 -0
  34. package/dist/codegen/index.d.ts +30 -0
  35. package/dist/codegen/index.mjs +5 -0
  36. package/dist/core/index.d.ts +37 -16
  37. package/dist/core/index.mjs +2322 -298
  38. package/dist/core/writer-worker.mjs +28 -6
  39. package/dist/{database-DuGp0Rtr.d.ts → database-BY0L5Q2n.d.ts} +59 -14
  40. package/dist/driver/better-sqlite3.d.ts +1 -1
  41. package/dist/driver/better-sqlite3.mjs +6 -6
  42. package/dist/driver/node.d.ts +1 -1
  43. package/dist/driver/node.mjs +6 -6
  44. package/dist/driver/wa-sqlite.d.ts +1 -136
  45. package/dist/{errors-5Nf5ZAEC.d.ts → errors-Bw5MdNCu.d.ts} +5 -1
  46. package/dist/file-migrations/index.d.ts +7 -5
  47. package/dist/file-migrations/index.mjs +42 -37
  48. package/dist/operation-registry-9DcvxcE5.d.ts +45 -0
  49. package/dist/primary-wins-DPAm2AKG.d.ts +61 -0
  50. package/dist/protocol-6KrSq2Hy.d.ts +66 -0
  51. package/dist/react/index.d.ts +20 -0
  52. package/dist/react/index.mjs +137 -0
  53. package/dist/replication/coordinator/etcd.d.ts +5 -3
  54. package/dist/replication/coordinator/etcd.mjs +554 -462
  55. package/dist/replication/index.d.ts +51 -105
  56. package/dist/replication/index.mjs +1415 -2061
  57. package/dist/server/index.d.ts +111 -87
  58. package/dist/server/index.mjs +1917 -489
  59. package/dist/server-options-1JHu8pid.d.ts +191 -0
  60. package/dist/{sirannon-4SspRvP5.d.ts → sirannon-DaQSyhbJ.d.ts} +7 -2
  61. package/dist/transport/grpc.d.ts +7 -4
  62. package/dist/transport/grpc.mjs +191 -97
  63. package/dist/transport/memory.d.ts +5 -134
  64. package/dist/transport/memory.mjs +3 -1
  65. package/dist/{types-BsjobKbl.d.ts → types-B7gmEsZW.d.ts} +8 -61
  66. package/dist/{types-BEu1I_9_.d.ts → types-BsVabqSI.d.ts} +1 -1
  67. package/dist/types-CL6piSnD.d.ts +43 -0
  68. package/dist/types-C_D8IhpO.d.ts +60 -0
  69. package/dist/{types-D_hQW1hr.d.ts → types-zhnRXrsb.d.ts} +55 -165
  70. package/package.json +44 -6
  71. package/dist/chunk-TKGHYWQ6.mjs +0 -35
  72. package/dist/types-D4p4UyDK.d.ts +0 -26
@@ -0,0 +1,661 @@
1
+ import { query, execute, executeBatch } from './chunk-JZGINXTN.mjs';
2
+ import { ensureMetaTable, selectMetaValue, computeChecksum, IDENTIFIER_RE, selectAppliedSourceSeqsInRange, prepareInsertAppliedChange, validateDdlSafety, extractDroppedTable, selectMaxChangeSeq, updateChangeStampsAfterSeqSql, selectOutboundChangesSql, selectNodeChangesAfterSeqSql, prepareDeleteRowColumnVersionsUpToHlc, canonicaliseForChecksum, prepareUpsertNewerColumnVersion, upsertMetaValue, selectMaxColumnVersionHlcForRow, selectMaxRowChangeHlc, validateIdentifier, prepareDeleteRowColumnVersions, prepareUpsertColumnVersion } from './chunk-NVQS53NT.mjs';
3
+ import { ReplicationError, BatchValidationError } from './chunk-5NOIGN5Y.mjs';
4
+ import { CHANGES_TABLE, decodeTaggedValues } from './chunk-D7LAYTKN.mjs';
5
+
6
+ // src/core/transaction.ts
7
+ var Transaction = class _Transaction {
8
+ constructor(conn) {
9
+ this.conn = conn;
10
+ }
11
+ _lastInsertRowId = 0;
12
+ async query(sql, params) {
13
+ return query(this.conn, sql, params);
14
+ }
15
+ async execute(sql, params) {
16
+ const result = await execute(this.conn, sql, params);
17
+ this._lastInsertRowId = result.lastInsertRowId;
18
+ return result;
19
+ }
20
+ async executeBatch(sql, paramsBatch) {
21
+ const results = await executeBatch(this.conn, sql, paramsBatch);
22
+ if (results.length > 0) {
23
+ this._lastInsertRowId = results[results.length - 1].lastInsertRowId;
24
+ }
25
+ return results;
26
+ }
27
+ get lastInsertRowId() {
28
+ return this._lastInsertRowId;
29
+ }
30
+ static async run(conn, fn) {
31
+ return conn.transaction(async (txConn) => {
32
+ const tx = new _Transaction(txConn);
33
+ return fn(tx);
34
+ });
35
+ }
36
+ };
37
+
38
+ // src/core/sync/hlc.ts
39
+ var MAX_LOGICAL = 65535;
40
+ var HLC = class _HLC {
41
+ wallMs;
42
+ logical;
43
+ nodeId;
44
+ constructor(nodeId) {
45
+ this.nodeId = nodeId;
46
+ this.wallMs = 0;
47
+ this.logical = 0;
48
+ }
49
+ /** Generate a new HLC timestamp, advancing the clock. */
50
+ now() {
51
+ const physicalMs = Date.now();
52
+ if (physicalMs > this.wallMs) {
53
+ this.wallMs = physicalMs;
54
+ this.logical = 0;
55
+ } else {
56
+ this.logical += 1;
57
+ if (this.logical > MAX_LOGICAL) {
58
+ throw new ReplicationError("HLC logical counter overflow");
59
+ }
60
+ }
61
+ return _HLC.encode(this.wallMs, this.logical, this.nodeId);
62
+ }
63
+ /** Merge a remote HLC timestamp into the local clock and return the updated value. */
64
+ receive(remote) {
65
+ const r = _HLC.decode(remote);
66
+ const physicalMs = Date.now();
67
+ if (physicalMs > this.wallMs && physicalMs > r.wallMs) {
68
+ this.wallMs = physicalMs;
69
+ this.logical = 0;
70
+ } else if (r.wallMs > this.wallMs) {
71
+ this.wallMs = r.wallMs;
72
+ this.logical = r.logical + 1;
73
+ } else if (this.wallMs > r.wallMs) {
74
+ this.logical += 1;
75
+ } else {
76
+ this.logical = Math.max(this.logical, r.logical) + 1;
77
+ }
78
+ if (this.logical > MAX_LOGICAL) {
79
+ throw new ReplicationError("HLC logical counter overflow");
80
+ }
81
+ return _HLC.encode(this.wallMs, this.logical, this.nodeId);
82
+ }
83
+ /** Lexicographic comparison of two encoded HLC strings. Returns -1, 0, or 1. */
84
+ static compare(a, b) {
85
+ if (a < b) return -1;
86
+ if (a > b) return 1;
87
+ return 0;
88
+ }
89
+ /** Parse an encoded HLC string into its wall-clock, logical, and nodeId components. */
90
+ static decode(hlc) {
91
+ const parts = hlc.split("-");
92
+ if (parts.length < 3) {
93
+ throw new ReplicationError(`Invalid HLC format: ${hlc}`);
94
+ }
95
+ return {
96
+ wallMs: Number.parseInt(parts[0], 16),
97
+ logical: Number.parseInt(parts[1], 16),
98
+ nodeId: parts.slice(2).join("-")
99
+ };
100
+ }
101
+ static encode(wallMs, logical, nodeId) {
102
+ const wallHex = wallMs.toString(16).padStart(12, "0");
103
+ const logicalHex = logical.toString(16).padStart(4, "0");
104
+ return `${wallHex}-${logicalHex}-${nodeId}`;
105
+ }
106
+ };
107
+
108
+ // src/core/sync/conflict/lww.ts
109
+ var LWWResolver = class {
110
+ resolve(ctx) {
111
+ if (ctx.remoteChange.operation === "delete") {
112
+ return { action: "accept_remote" };
113
+ }
114
+ if (ctx.localHlc === null) {
115
+ return { action: "accept_remote" };
116
+ }
117
+ const cmp = HLC.compare(ctx.remoteHlc, ctx.localHlc);
118
+ if (cmp > 0) {
119
+ return { action: "accept_remote" };
120
+ }
121
+ if (cmp < 0) {
122
+ return { action: "keep_local" };
123
+ }
124
+ if (ctx.remoteChange.nodeId > (ctx.localChange?.nodeId ?? "")) {
125
+ return { action: "accept_remote" };
126
+ }
127
+ return { action: "keep_local" };
128
+ }
129
+ };
130
+
131
+ // src/core/sync/hlc-store.ts
132
+ var HLC_CLOCK_META_KEY = "hlc_clock";
133
+ function isWellFormedHlc(candidate) {
134
+ if (candidate.length === 0) return false;
135
+ let decoded;
136
+ try {
137
+ decoded = HLC.decode(candidate);
138
+ } catch {
139
+ return false;
140
+ }
141
+ if (!Number.isInteger(decoded.wallMs) || decoded.wallMs < 0) return false;
142
+ if (!Number.isInteger(decoded.logical) || decoded.logical < 0) return false;
143
+ if (decoded.nodeId.length === 0) return false;
144
+ return true;
145
+ }
146
+ async function persistHlcClock(conn, encoded) {
147
+ await ensureMetaTable(conn);
148
+ await upsertMetaValue(conn, HLC_CLOCK_META_KEY, encoded);
149
+ }
150
+ async function loadPersistedHlc(conn) {
151
+ await ensureMetaTable(conn);
152
+ const value = await selectMetaValue(conn, HLC_CLOCK_META_KEY);
153
+ if (value === null || !isWellFormedHlc(value)) return null;
154
+ return value;
155
+ }
156
+
157
+ // src/core/sync/origins.ts
158
+ var REMOTE_ORIGIN_NODE_ID = "remote";
159
+
160
+ // src/core/sync/row-lookup.ts
161
+ async function findRowByPk(tx, table, pkColumns, sourceData) {
162
+ const conditions = [];
163
+ const values = [];
164
+ for (const col of pkColumns) {
165
+ if (!validateIdentifier(col)) return void 0;
166
+ if (!(col in sourceData)) return void 0;
167
+ conditions.push(`"${col}" = ?`);
168
+ values.push(sourceData[col]);
169
+ }
170
+ if (conditions.length === 0) return void 0;
171
+ const stmt = await tx.prepare(`SELECT * FROM "${table}" WHERE ${conditions.join(" AND ")} LIMIT 1`);
172
+ return stmt.get(...values);
173
+ }
174
+
175
+ // src/core/sync/row-writer.ts
176
+ var RowWriter = class {
177
+ constructor(pkResolver, changesTable) {
178
+ this.pkResolver = pkResolver;
179
+ this.changesTable = changesTable;
180
+ }
181
+ async findExistingRow(tx, change) {
182
+ if (!IDENTIFIER_RE.test(change.table)) return void 0;
183
+ const pkColumns = await this.pkResolver.forTable(change.table);
184
+ const result = await findRowByPk(tx, change.table, pkColumns, change.newData ?? change.oldData ?? {});
185
+ if (result) return result;
186
+ if (change.operation === "update" && change.oldData) {
187
+ return findRowByPk(tx, change.table, pkColumns, change.oldData);
188
+ }
189
+ return void 0;
190
+ }
191
+ async getLocalHlcForRow(tx, table, rowId) {
192
+ const versioned = await selectMaxColumnVersionHlcForRow(tx, table, rowId);
193
+ const logged = await selectMaxRowChangeHlc(tx, this.changesTable, table, rowId);
194
+ if (versioned === null) return logged;
195
+ if (logged === null) return versioned;
196
+ return HLC.compare(logged, versioned) > 0 ? logged : versioned;
197
+ }
198
+ async insertRow(tx, change) {
199
+ if (!change.newData) return;
200
+ const columns = Object.keys(change.newData).filter(validateIdentifier);
201
+ if (columns.length === 0) return;
202
+ const placeholders = columns.map(() => "?").join(", ");
203
+ const colNames = columns.map((c) => `"${c}"`).join(", ");
204
+ const values = columns.map((c) => change.newData?.[c]);
205
+ const stmt = await tx.prepare(`INSERT INTO "${change.table}" (${colNames}) VALUES (${placeholders})`);
206
+ await stmt.run(...values);
207
+ }
208
+ async applyRemoteChange(tx, change) {
209
+ if (change.operation === "delete") {
210
+ await this.deleteRow(tx, change);
211
+ return;
212
+ }
213
+ if (!change.newData) return;
214
+ const pkColumns = await this.pkResolver.forTable(change.table);
215
+ const sourceData = change.newData;
216
+ const wherePkSource = change.oldData ?? sourceData;
217
+ const setClauses = [];
218
+ const setValues = [];
219
+ const whereConditions = [];
220
+ const whereValues = [];
221
+ const pkSet = new Set(pkColumns);
222
+ for (const [col, val] of Object.entries(sourceData)) {
223
+ if (!validateIdentifier(col)) continue;
224
+ if (pkSet.has(col)) continue;
225
+ setClauses.push(`"${col}" = ?`);
226
+ setValues.push(val);
227
+ }
228
+ for (const col of pkColumns) {
229
+ if (!validateIdentifier(col)) continue;
230
+ whereConditions.push(`"${col}" = ?`);
231
+ whereValues.push(wherePkSource[col]);
232
+ }
233
+ if (setClauses.length === 0 || whereConditions.length === 0) return;
234
+ const stmt = await tx.prepare(
235
+ `UPDATE "${change.table}" SET ${setClauses.join(", ")} WHERE ${whereConditions.join(" AND ")}`
236
+ );
237
+ await stmt.run(...setValues, ...whereValues);
238
+ }
239
+ async applyMergedData(tx, change, mergedData) {
240
+ const pkColumns = await this.pkResolver.forTable(change.table);
241
+ const sourceData = change.newData ?? change.oldData ?? {};
242
+ const setClauses = [];
243
+ const setValues = [];
244
+ const whereConditions = [];
245
+ const whereValues = [];
246
+ for (const [col, val] of Object.entries(mergedData)) {
247
+ if (!validateIdentifier(col)) continue;
248
+ if (!pkColumns.includes(col)) {
249
+ setClauses.push(`"${col}" = ?`);
250
+ setValues.push(val);
251
+ }
252
+ }
253
+ for (const col of pkColumns) {
254
+ if (!validateIdentifier(col)) continue;
255
+ whereConditions.push(`"${col}" = ?`);
256
+ whereValues.push(sourceData[col]);
257
+ }
258
+ if (setClauses.length === 0 || whereConditions.length === 0) return;
259
+ const stmt = await tx.prepare(
260
+ `UPDATE "${change.table}" SET ${setClauses.join(", ")} WHERE ${whereConditions.join(" AND ")}`
261
+ );
262
+ await stmt.run(...setValues, ...whereValues);
263
+ }
264
+ async deleteRow(tx, change) {
265
+ const pkColumns = await this.pkResolver.forTable(change.table);
266
+ const sourceData = change.oldData ?? change.newData ?? {};
267
+ const conditions = [];
268
+ const values = [];
269
+ for (const col of pkColumns) {
270
+ if (!validateIdentifier(col)) continue;
271
+ conditions.push(`"${col}" = ?`);
272
+ values.push(sourceData[col]);
273
+ }
274
+ if (conditions.length === 0) return;
275
+ const stmt = await tx.prepare(`DELETE FROM "${change.table}" WHERE ${conditions.join(" AND ")}`);
276
+ await stmt.run(...values);
277
+ }
278
+ async recordColumnVersions(tx, change, data) {
279
+ if (change.operation === "delete") {
280
+ const delStmt = await prepareDeleteRowColumnVersions(tx);
281
+ await delStmt.run(change.table, change.rowId);
282
+ return;
283
+ }
284
+ if (!data) return;
285
+ const upsertStmt = await prepareUpsertColumnVersion(tx);
286
+ for (const col of Object.keys(data)) {
287
+ if (!validateIdentifier(col)) continue;
288
+ await upsertStmt.run(change.table, change.rowId, col, change.hlc, change.nodeId);
289
+ }
290
+ }
291
+ };
292
+
293
+ // src/core/sync/batch-applier.ts
294
+ var BatchApplier = class {
295
+ constructor(conn, localNodeId, hlc, pkResolver, getLastAppliedSeq, tracker, changesTable = CHANGES_TABLE, beforeApply) {
296
+ this.conn = conn;
297
+ this.localNodeId = localNodeId;
298
+ this.hlc = hlc;
299
+ this.getLastAppliedSeq = getLastAppliedSeq;
300
+ this.tracker = tracker;
301
+ this.changesTable = changesTable;
302
+ this.beforeApply = beforeApply;
303
+ this.rows = new RowWriter(pkResolver, changesTable);
304
+ }
305
+ rows;
306
+ async applyBatch(batch, resolver) {
307
+ const expectedChecksum = computeChecksum(batch.changes);
308
+ if (batch.checksum !== expectedChecksum) {
309
+ throw new BatchValidationError(`Checksum mismatch: expected ${expectedChecksum}, got ${batch.checksum}`);
310
+ }
311
+ for (const change of batch.changes) {
312
+ if (change.operation !== "ddl") {
313
+ if (!IDENTIFIER_RE.test(change.table)) {
314
+ throw new BatchValidationError(`Invalid table name: ${change.table}`);
315
+ }
316
+ }
317
+ }
318
+ const lastApplied = await this.getLastAppliedSeq(batch.sourceNodeId);
319
+ if (batch.toSeq <= lastApplied) {
320
+ return { applied: 0, skipped: batch.changes.length, conflicts: 0, droppedTables: [] };
321
+ }
322
+ const needsPartialDedup = batch.fromSeq <= lastApplied;
323
+ let appliedSeqSet = null;
324
+ if (needsPartialDedup) {
325
+ appliedSeqSet = await selectAppliedSourceSeqsInRange(this.conn, batch.sourceNodeId, batch.fromSeq, batch.toSeq);
326
+ }
327
+ let applied = 0;
328
+ let skipped = 0;
329
+ let conflicts = 0;
330
+ const droppedTables = [];
331
+ const changesByTx = /* @__PURE__ */ new Map();
332
+ for (const change of batch.changes) {
333
+ const txGroup = changesByTx.get(change.txId);
334
+ if (txGroup) {
335
+ txGroup.push(change);
336
+ } else {
337
+ changesByTx.set(change.txId, [change]);
338
+ }
339
+ }
340
+ for (const [txId, txChanges] of changesByTx) {
341
+ const result = await this.applyGroup({
342
+ sourceNodeId: batch.sourceNodeId,
343
+ txId,
344
+ changes: txChanges,
345
+ resolver
346
+ });
347
+ applied += result.applied;
348
+ skipped += result.skipped;
349
+ conflicts += result.conflicts;
350
+ droppedTables.push(...result.droppedTables);
351
+ }
352
+ const recordStmt = await prepareInsertAppliedChange(this.conn);
353
+ const nowSec = Date.now() / 1e3;
354
+ for (let seq = batch.fromSeq; seq <= batch.toSeq; seq += 1n) {
355
+ if (appliedSeqSet?.has(seq.toString())) continue;
356
+ await recordStmt.run(batch.sourceNodeId, seq.toString(), nowSec);
357
+ }
358
+ await this.mergeHlc(batch.hlcRange.max);
359
+ return { applied, skipped, conflicts, droppedTables };
360
+ }
361
+ async applyGroup(options) {
362
+ const { sourceNodeId, txId, changes, resolver, withinTx } = options;
363
+ const droppedTables = [];
364
+ const result = await this.conn.transaction(async (tx) => {
365
+ const seqBefore = await this.selectMaxChangeSeq(tx);
366
+ if (this.beforeApply) {
367
+ await this.beforeApply(tx);
368
+ }
369
+ let applied = 0;
370
+ let skipped = 0;
371
+ let conflicts = 0;
372
+ const ddlChanges = changes.filter((c) => c.operation === "ddl");
373
+ const dataChanges = changes.filter((c) => c.operation !== "ddl");
374
+ for (const ddl of ddlChanges) {
375
+ const ddlSql = ddl.ddlStatement;
376
+ if (!ddlSql || !validateDdlSafety(ddlSql)) {
377
+ throw new BatchValidationError(`Unsafe or missing DDL statement: ${ddlSql ?? "none"}`);
378
+ }
379
+ await tx.exec(ddlSql);
380
+ const droppedTable = extractDroppedTable(ddlSql);
381
+ if (droppedTable !== null) {
382
+ droppedTables.push(droppedTable);
383
+ }
384
+ if (this.tracker) {
385
+ await this.tracker.refreshAllTriggersUsingConnection(tx);
386
+ }
387
+ applied += 1;
388
+ }
389
+ const tally = { applied, skipped, conflicts };
390
+ for (const change of dataChanges) {
391
+ await this.applyDataChange(tx, change, resolver, tally);
392
+ }
393
+ applied = tally.applied;
394
+ skipped = tally.skipped;
395
+ conflicts = tally.conflicts;
396
+ await this.stampAppliedEcho(tx, seqBefore, sourceNodeId, txId, changes);
397
+ if (withinTx) {
398
+ await withinTx(tx);
399
+ }
400
+ return { applied, skipped, conflicts };
401
+ });
402
+ return { ...result, droppedTables };
403
+ }
404
+ /**
405
+ * Applies one pulled transaction whose changes are read from disk in
406
+ * batches, so a transaction of any size is applied with bounded memory.
407
+ * The whole group runs in one local transaction: the batches, the echo
408
+ * stamping, and `withinTx` commit together or not at all.
409
+ */
410
+ async applyStagedGroup(options) {
411
+ const { source, resolver, withinTx } = options;
412
+ return this.conn.transaction(async (tx) => {
413
+ const seqBefore = await this.selectMaxChangeSeq(tx);
414
+ if (this.beforeApply) {
415
+ await this.beforeApply(tx);
416
+ }
417
+ const tally = { applied: 0, skipped: 0, conflicts: 0 };
418
+ let cursor = 0n;
419
+ let lastSeq = null;
420
+ let txId = "";
421
+ let sawFirst = false;
422
+ let sourceNodeId = REMOTE_ORIGIN_NODE_ID;
423
+ let maxHlc = "";
424
+ while (true) {
425
+ const batch = await source.nextBatch(cursor);
426
+ if (batch.length === 0) break;
427
+ for (const entry of batch) {
428
+ const change = entry.change;
429
+ if (change.operation === "ddl") {
430
+ throw new BatchValidationError("A staged device transaction cannot carry a DDL change");
431
+ }
432
+ if (!IDENTIFIER_RE.test(change.table)) {
433
+ throw new BatchValidationError(`Invalid table name: ${change.table}`);
434
+ }
435
+ if (!sawFirst) {
436
+ txId = change.txId;
437
+ sawFirst = true;
438
+ } else if (change.txId !== txId) {
439
+ throw new BatchValidationError(`Staged rows span more than one transaction: '${txId}' and '${change.txId}'`);
440
+ }
441
+ if (sourceNodeId === REMOTE_ORIGIN_NODE_ID && change.nodeId !== "") {
442
+ sourceNodeId = change.nodeId;
443
+ }
444
+ if (change.hlc > maxHlc) {
445
+ maxHlc = change.hlc;
446
+ }
447
+ await this.applyDataChange(tx, change, resolver, tally);
448
+ cursor = entry.seq;
449
+ lastSeq = entry.seq;
450
+ }
451
+ }
452
+ await this.stampAppliedEchoStamps(tx, seqBefore, sourceNodeId, txId, maxHlc);
453
+ if (withinTx) {
454
+ await withinTx(tx);
455
+ }
456
+ return { ...tally, droppedTables: [], maxHlc, lastSeq };
457
+ });
458
+ }
459
+ async applyDataChange(tx, change, resolver, tally) {
460
+ const existingRow = await this.rows.findExistingRow(tx, change);
461
+ if (existingRow === void 0) {
462
+ if (change.operation === "insert" && change.newData) {
463
+ await this.rows.insertRow(tx, change);
464
+ await this.rows.recordColumnVersions(tx, change, change.newData);
465
+ tally.applied += 1;
466
+ } else if (change.operation === "delete") {
467
+ tally.applied += 1;
468
+ } else {
469
+ tally.skipped += 1;
470
+ }
471
+ return;
472
+ }
473
+ tally.conflicts += 1;
474
+ const localHlc = await this.rows.getLocalHlcForRow(tx, change.table, change.rowId);
475
+ const localChange = {
476
+ table: change.table,
477
+ operation: "update",
478
+ rowId: change.rowId,
479
+ primaryKey: change.primaryKey,
480
+ hlc: localHlc ?? "",
481
+ txId: "",
482
+ nodeId: this.localNodeId,
483
+ newData: existingRow,
484
+ oldData: null
485
+ };
486
+ const changeResolver = typeof resolver === "function" ? resolver(change.table) : resolver;
487
+ const resolution = await changeResolver.resolve({
488
+ table: change.table,
489
+ rowId: change.rowId,
490
+ localChange,
491
+ remoteChange: change,
492
+ localHlc,
493
+ remoteHlc: change.hlc
494
+ });
495
+ if (resolution.action === "accept_remote") {
496
+ await this.rows.applyRemoteChange(tx, change);
497
+ await this.rows.recordColumnVersions(tx, change, change.newData);
498
+ tally.applied += 1;
499
+ } else if (resolution.action === "merge" && resolution.mergedData) {
500
+ await this.rows.applyMergedData(tx, change, resolution.mergedData);
501
+ await this.rows.recordColumnVersions(tx, change, resolution.mergedData);
502
+ tally.applied += 1;
503
+ } else {
504
+ tally.skipped += 1;
505
+ }
506
+ }
507
+ async mergeHlc(remoteHlc) {
508
+ try {
509
+ const merged = this.hlc.receive(remoteHlc);
510
+ await persistHlcClock(this.conn, merged);
511
+ } catch {
512
+ return;
513
+ }
514
+ }
515
+ async selectMaxChangeSeq(tx) {
516
+ return (await selectMaxChangeSeq(tx, this.changesTable)).toString();
517
+ }
518
+ async stampAppliedEcho(tx, seqBefore, sourceNodeId, txId, txChanges) {
519
+ let maxHlc = "";
520
+ for (const change of txChanges) {
521
+ if (change.hlc > maxHlc) {
522
+ maxHlc = change.hlc;
523
+ }
524
+ }
525
+ await this.stampAppliedEchoStamps(tx, seqBefore, sourceNodeId, txId, maxHlc);
526
+ }
527
+ async stampAppliedEchoStamps(tx, seqBefore, sourceNodeId, txId, maxHlc) {
528
+ const stmt = await tx.prepare(updateChangeStampsAfterSeqSql(this.changesTable));
529
+ await stmt.run(sourceNodeId, txId, maxHlc, seqBefore);
530
+ }
531
+ };
532
+
533
+ // src/core/sync/batch-reader.ts
534
+ var BatchReader = class {
535
+ constructor(conn, localNodeId, changesTable, pkResolver, excludeDdl = false) {
536
+ this.conn = conn;
537
+ this.localNodeId = localNodeId;
538
+ this.changesTable = changesTable;
539
+ this.pkResolver = pkResolver;
540
+ this.excludeDdl = excludeDdl;
541
+ }
542
+ async readBatch(afterSeq, batchSize) {
543
+ const stmt = await this.conn.prepare(selectOutboundChangesSql(this.changesTable, this.excludeDdl));
544
+ const rows = await stmt.all(afterSeq.toString(), this.localNodeId, batchSize);
545
+ if (rows.length === 0) {
546
+ return null;
547
+ }
548
+ const changes = [];
549
+ let minHlc = rows[0].hlc;
550
+ let maxHlc = rows[0].hlc;
551
+ for (const row of rows) {
552
+ const operation = row.operation.toLowerCase();
553
+ const isDdl = operation === "ddl";
554
+ const rawNewData = row.new_data ? decodeTaggedValues(JSON.parse(row.new_data)) : null;
555
+ const rawOldData = row.old_data ? decodeTaggedValues(JSON.parse(row.old_data)) : null;
556
+ let ddlStatement;
557
+ let newData = rawNewData;
558
+ let oldData = rawOldData;
559
+ if (isDdl) {
560
+ const candidate = rawNewData?.ddlStatement;
561
+ if (typeof candidate === "string") {
562
+ ddlStatement = candidate;
563
+ }
564
+ newData = null;
565
+ oldData = null;
566
+ }
567
+ const pkColumns = isDdl ? [] : await this.pkResolver.forTable(row.table_name);
568
+ const primaryKey = {};
569
+ if (!isDdl) {
570
+ const sourceData = rawNewData ?? rawOldData ?? {};
571
+ for (const col of pkColumns) {
572
+ if (col in sourceData) {
573
+ primaryKey[col] = sourceData[col];
574
+ }
575
+ }
576
+ }
577
+ const change = {
578
+ table: row.table_name,
579
+ operation,
580
+ rowId: String(row.row_id),
581
+ primaryKey,
582
+ hlc: row.hlc,
583
+ txId: row.tx_id,
584
+ nodeId: row.node_id,
585
+ newData,
586
+ oldData
587
+ };
588
+ if (ddlStatement !== void 0) {
589
+ change.ddlStatement = ddlStatement;
590
+ }
591
+ changes.push(change);
592
+ if (HLC.compare(row.hlc, minHlc) < 0) {
593
+ minHlc = row.hlc;
594
+ }
595
+ if (HLC.compare(row.hlc, maxHlc) > 0) {
596
+ maxHlc = row.hlc;
597
+ }
598
+ }
599
+ const fromSeq = BigInt(rows[0].seq);
600
+ const toSeq = BigInt(rows[rows.length - 1].seq);
601
+ const checksum = computeChecksum(changes);
602
+ return {
603
+ sourceNodeId: this.localNodeId,
604
+ batchId: `${this.localNodeId}-${fromSeq}-${toSeq}`,
605
+ fromSeq,
606
+ toSeq,
607
+ hlcRange: { min: minHlc, max: maxHlc },
608
+ changes,
609
+ checksum
610
+ };
611
+ }
612
+ };
613
+
614
+ // src/core/sync/stamp-ops.ts
615
+ async function recordLocalColumnVersions(tx, changesTable, localNodeId, afterSeq) {
616
+ const selectStmt = await tx.prepare(selectNodeChangesAfterSeqSql(changesTable));
617
+ const rows = await selectStmt.all(afterSeq.toString(), localNodeId);
618
+ for (const row of rows) {
619
+ if (row.operation === "DELETE") {
620
+ const delStmt = await prepareDeleteRowColumnVersionsUpToHlc(tx);
621
+ await delStmt.run(row.table_name, String(row.row_id), row.hlc);
622
+ continue;
623
+ }
624
+ const oldData = row.old_data ? decodeTaggedValues(JSON.parse(row.old_data)) : {};
625
+ const newData = row.new_data ? decodeTaggedValues(JSON.parse(row.new_data)) : {};
626
+ const changedCols = [];
627
+ if (row.operation === "INSERT") {
628
+ for (const key of Object.keys(newData)) {
629
+ changedCols.push(key);
630
+ }
631
+ } else {
632
+ for (const key of Object.keys(newData)) {
633
+ if (canonicaliseForChecksum(newData[key]) !== canonicaliseForChecksum(oldData[key])) {
634
+ changedCols.push(key);
635
+ }
636
+ }
637
+ }
638
+ const upsertStmt = await prepareUpsertNewerColumnVersion(tx);
639
+ for (const col of changedCols) {
640
+ await upsertStmt.run(row.table_name, String(row.row_id), col, row.hlc, row.node_id);
641
+ }
642
+ }
643
+ }
644
+ var StampOps = class {
645
+ constructor(localNodeId, hlc, changesTable) {
646
+ this.localNodeId = localNodeId;
647
+ this.hlc = hlc;
648
+ this.changesTable = changesTable;
649
+ }
650
+ async stampChanges(tx, afterSeq, txId) {
651
+ const hlcValue = this.hlc.now();
652
+ const stmt = await tx.prepare(updateChangeStampsAfterSeqSql(this.changesTable));
653
+ await stmt.run(this.localNodeId, txId, hlcValue, afterSeq.toString());
654
+ await upsertMetaValue(tx, HLC_CLOCK_META_KEY, hlcValue);
655
+ }
656
+ updateColumnVersions(tx, afterSeq) {
657
+ return recordLocalColumnVersions(tx, this.changesTable, this.localNodeId, afterSeq);
658
+ }
659
+ };
660
+
661
+ export { BatchApplier, BatchReader, HLC, HLC_CLOCK_META_KEY, LWWResolver, REMOTE_ORIGIN_NODE_ID, StampOps, Transaction, isWellFormedHlc, loadPersistedHlc, recordLocalColumnVersions };