@delali/sirannon-db 0.1.8 → 0.2.1-next.18

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