@delali/sirannon-db 0.3.4-next.62 → 0.3.4

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 (108) hide show
  1. package/README.md +79 -56
  2. package/dist/backup/index.d.ts +44 -45
  3. package/dist/backup/index.mjs +7 -7
  4. package/dist/backup-scheduler/index.d.ts +42 -41
  5. package/dist/backup-scheduler/index.mjs +4 -4
  6. package/dist/baseline-Co3iIsN_.d.ts +19 -0
  7. package/dist/change-tracker-DqCdeEyx.d.ts +128 -0
  8. package/dist/{chunk-VOSJBZ6Q.mjs → chunk-2ES3DT4A.mjs} +71 -33
  9. package/dist/{chunk-6CMCO7S4.mjs → chunk-2O3PP4CF.mjs} +1 -1
  10. package/dist/{chunk-E7Q7HE46.mjs → chunk-3BZLZ47R.mjs} +26 -26
  11. package/dist/chunk-4KASJKKK.mjs +2501 -0
  12. package/dist/{chunk-MYNI2WFS.mjs → chunk-52XW3Z76.mjs} +40 -8
  13. package/dist/chunk-55YRLSM2.mjs +57 -0
  14. package/dist/{chunk-KDAHYFW7.mjs → chunk-6AOJCNRU.mjs} +5 -5
  15. package/dist/{chunk-KXTYR2OQ.mjs → chunk-A3PHCO6W.mjs} +39 -111
  16. package/dist/chunk-ADNWEHGZ.mjs +299 -0
  17. package/dist/{chunk-2QLXDHAP.mjs → chunk-BLMRNSUG.mjs} +11 -2
  18. package/dist/chunk-BVMZJOVM.mjs +5 -0
  19. package/dist/{chunk-IWGIYDMZ.mjs → chunk-BZWDPF4G.mjs} +1 -1
  20. package/dist/{chunk-YVF2ZJTE.mjs → chunk-CZL4D3BM.mjs} +1 -1
  21. package/dist/{chunk-7SC2TZAS.mjs → chunk-DX4WGJL7.mjs} +2 -2
  22. package/dist/{chunk-45MKELYA.mjs → chunk-ESTZZPT5.mjs} +132 -13
  23. package/dist/{chunk-QP5L5DFJ.mjs → chunk-FJPR2CJX.mjs} +3 -3
  24. package/dist/chunk-HR7K3GZW.mjs +36 -0
  25. package/dist/{chunk-LXCGPZPA.mjs → chunk-IEKIYO4V.mjs} +1 -1
  26. package/dist/{chunk-TUD5CJ76.mjs → chunk-KB4OLJAD.mjs} +26 -19
  27. package/dist/{chunk-PBRXXISQ.mjs → chunk-KINDFBQP.mjs} +3 -2
  28. package/dist/{chunk-E6IMX7RS.mjs → chunk-KJLNYOOL.mjs} +2 -2
  29. package/dist/{chunk-BTTFW4Z4.mjs → chunk-KU32OT4H.mjs} +2 -10
  30. package/dist/{chunk-JQWGHSEH.mjs → chunk-NVSQ2T5Q.mjs} +71 -70
  31. package/dist/{chunk-W4EUTJNT.mjs → chunk-NZGSDALX.mjs} +94 -96
  32. package/dist/{chunk-U445A3C4.mjs → chunk-QGVRRKB2.mjs} +1 -1
  33. package/dist/{chunk-GYCEJNU4.mjs → chunk-R7GBXNTO.mjs} +1 -1
  34. package/dist/{chunk-7C36BCSN.mjs → chunk-VDN6FHU4.mjs} +1 -1
  35. package/dist/{chunk-6IYS4WLH.mjs → chunk-VO3ZWA2G.mjs} +142 -115
  36. package/dist/chunk-WOMADPM4.mjs +12 -0
  37. package/dist/{chunk-OUSWVNWT.mjs → chunk-YCMB6GR4.mjs} +1 -1
  38. package/dist/{chunk-YNTDOFTN.mjs → chunk-YW7IKQ7T.mjs} +1 -1
  39. package/dist/{chunk-M2BEAH5I.mjs → chunk-ZN4HI5FK.mjs} +126 -72
  40. package/dist/{chunk-SBL6GN43.mjs → chunk-ZYWI45EN.mjs} +1 -1
  41. package/dist/client/index.d.ts +135 -137
  42. package/dist/client/index.mjs +177 -82
  43. package/dist/client/topology.d.ts +31 -31
  44. package/dist/client/topology.mjs +42 -23
  45. package/dist/client-base-BxTocU1x.d.ts +398 -0
  46. package/dist/client-streams-YKANXMWO.mjs +112 -0
  47. package/dist/codegen/cli.mjs +4 -4
  48. package/dist/codegen/index.d.ts +36 -29
  49. package/dist/codegen/index.mjs +4 -4
  50. package/dist/core/index.d.ts +130 -115
  51. package/dist/core/index.mjs +387 -297
  52. package/dist/core/writer-worker.mjs +3 -3
  53. package/dist/database-CGDsXW1K.d.ts +959 -0
  54. package/dist/database-backup-DcLhxoa1.d.ts +143 -0
  55. package/dist/driver/better-sqlite3.d.ts +11 -10
  56. package/dist/driver/better-sqlite3.mjs +11 -11
  57. package/dist/driver/bun.d.ts +9 -8
  58. package/dist/driver/expo.d.ts +6 -7
  59. package/dist/driver/node.d.ts +11 -10
  60. package/dist/driver/node.mjs +13 -11
  61. package/dist/driver/wa-sqlite.d.ts +6 -6
  62. package/dist/errors-CJUkQ-KL.d.ts +199 -0
  63. package/dist/file-migrations/index.d.ts +20 -18
  64. package/dist/file-migrations/index.mjs +3 -3
  65. package/dist/forward-rpc-VRWFZXHN.mjs +44 -0
  66. package/dist/operation-registry-CGhNT-Sn.d.ts +96 -0
  67. package/dist/primary-wins-CHNOYuN7.d.ts +81 -0
  68. package/dist/protocol-DFpNNsIT.d.ts +153 -0
  69. package/dist/query-types-5B02ixH0.d.ts +129 -0
  70. package/dist/react/index.d.ts +22 -22
  71. package/dist/replication/coordinator/etcd.d.ts +37 -31
  72. package/dist/replication/coordinator/etcd.mjs +296 -131
  73. package/dist/replication/index.d.ts +201 -161
  74. package/dist/replication/index.mjs +630 -529
  75. package/dist/restore-options-CH8mE-Tb.d.ts +84 -0
  76. package/dist/server/index.d.ts +201 -191
  77. package/dist/server/index.mjs +736 -619
  78. package/dist/server-options-aYtCkefX.d.ts +317 -0
  79. package/dist/server-streams-24XL4T43.mjs +254 -0
  80. package/dist/sirannon-J3EgPVD7.d.ts +179 -0
  81. package/dist/transport/grpc.d.ts +37 -24
  82. package/dist/transport/grpc.mjs +83 -3199
  83. package/dist/transport/memory.d.ts +25 -26
  84. package/dist/transport/memory.mjs +18 -9
  85. package/dist/types-BrAiDGOb.d.ts +505 -0
  86. package/dist/types-CEJG0eFS.d.ts +88 -0
  87. package/dist/types-Dc8oWlxj.d.ts +1580 -0
  88. package/dist/types-DqODKeOq.d.ts +123 -0
  89. package/dist/types-guEKSzMr.d.ts +367 -0
  90. package/package.json +7 -7
  91. package/dist/baseline-D93hcIEE.d.ts +0 -17
  92. package/dist/change-tracker-CjRFuZdB.d.ts +0 -144
  93. package/dist/client-base-Cmd9EVme.d.ts +0 -397
  94. package/dist/database-BLQvqjsw.d.ts +0 -917
  95. package/dist/database-backup-B7oszQ2n.d.ts +0 -141
  96. package/dist/errors-Dei4GdBb.d.ts +0 -196
  97. package/dist/operation-registry-oiPuSYI6.d.ts +0 -94
  98. package/dist/primary-wins-B0np8JS3.d.ts +0 -85
  99. package/dist/protocol-HI-nY8DD.d.ts +0 -152
  100. package/dist/query-types-p1liOP-b.d.ts +0 -127
  101. package/dist/restore-options-CksYsH1l.d.ts +0 -83
  102. package/dist/server-options-Bj5iJnjx.d.ts +0 -292
  103. package/dist/sirannon-BPxC5tC2.d.ts +0 -178
  104. package/dist/types-B0EhgDbr.d.ts +0 -340
  105. package/dist/types-CbjfRwrK.d.ts +0 -499
  106. package/dist/types-CjhxcjhA.d.ts +0 -123
  107. package/dist/types-DPSmjU54.d.ts +0 -1518
  108. package/dist/types-DTO4CYhW.d.ts +0 -88
@@ -1,16 +1,17 @@
1
1
  import { compatibilityAllowsPromotion } from '../chunk-ER7ODTDA.mjs';
2
- import { CoordinatorError, WriteConcernError, AuthorityError, SyncError, TopologyError, StalePrimaryError, NodeDrainingError, NodeNotInSyncError, ReadConcernError, ProtocolVersionMismatchError } from '../chunk-BTTFW4Z4.mjs';
3
- export { AuthorityError, ConflictError, CoordinatorError, FailoverError, NoSafePrimaryError, NodeDrainingError, NodeNotInSyncError, ProtocolVersionMismatchError, ReadConcernError, StalePrimaryError, SyncError, TopologyError, TransportError, UnsafeRecoveryRequiredError, WriteConcernError } from '../chunk-BTTFW4Z4.mjs';
4
- import { LWWResolver, HLC, BatchReader, BatchApplier, StampOps, isWellFormedHlc, Transaction, loadPersistedHlc } from '../chunk-KXTYR2OQ.mjs';
5
- export { HLC, LWWResolver } from '../chunk-KXTYR2OQ.mjs';
6
- import '../chunk-MYNI2WFS.mjs';
7
- import { DumpOps, dumpSchema, tablesInFkOrder } from '../chunk-QP5L5DFJ.mjs';
8
- import { canonicaliseForChecksum, PkResolver, setForeignKeysEnabled, selectMaxChangeHlc, selectMaxColumnVersionHlc, selectMaxAppliedSourceSeq, upsertPeerAckedSeq, selectPeerAckedSeq, selectMaxChangeSeq, selectMinPeerAckedSeq, upsertSyncTableStatus, upsertSyncMeta, selectSyncMetaRow, selectCompletedSyncTableNames, ensureChangesTable, ensureReplicationStateTables, ensureMetaTable, IDENTIFIER_RE, deleteSyncTableStates, extractDroppedTable, insertDdlChange, validateDdlSafety, prepareInsertAppliedChange, selectMaxAppliedSourceSeqByNode } from '../chunk-45MKELYA.mjs';
9
- import { ReplicationError, BatchValidationError } from '../chunk-2QLXDHAP.mjs';
10
- export { BatchValidationError, ReplicationError } from '../chunk-2QLXDHAP.mjs';
11
- import { CHANGES_TABLE, INTERNAL_TABLE_PREFIX, isReservedIdentifier } from '../chunk-6CMCO7S4.mjs';
2
+ import { CoordinatorError, WriteConcernError, AuthorityError, SyncError, TopologyError, StalePrimaryError, NodeDrainingError, NodeNotInSyncError, ReadConcernError, ProtocolVersionMismatchError } from '../chunk-KU32OT4H.mjs';
3
+ export { AuthorityError, CoordinatorError, FailoverError, NoSafePrimaryError, NodeDrainingError, NodeNotInSyncError, ProtocolVersionMismatchError, ReadConcernError, StalePrimaryError, SyncError, TopologyError, TransportError, UnsafeRecoveryRequiredError, WriteConcernError } from '../chunk-KU32OT4H.mjs';
4
+ import { LWWResolver, BatchReader, BatchApplier, StampOps, isWellFormedHlc, Transaction, loadPersistedHlc } from '../chunk-A3PHCO6W.mjs';
5
+ export { LWWResolver } from '../chunk-A3PHCO6W.mjs';
6
+ import { runWriterTransaction, execute } from '../chunk-52XW3Z76.mjs';
7
+ import { DumpOps, dumpSchema, tablesInFkOrder } from '../chunk-FJPR2CJX.mjs';
8
+ import { canonicaliseForChecksum, HLC, PkResolver, selectMaxChangeHlc, selectMaxColumnVersionHlc, selectMaxAppliedSourceSeq, upsertPeerAckedSeq, selectPeerAckedSeq, selectMaxChangeSeq, selectMinPeerAckedSeq, upsertSyncTableStatus, upsertSyncMeta, selectSyncMetaRow, selectCompletedSyncTableNames, ensureChangesTable, ensureReplicationStateTables, ensureMetaTable, IDENTIFIER_RE, setForeignKeysEnabled, deleteSyncTableStates, extractDroppedTable, insertDdlChange, validateDdlSafety, prepareInsertAppliedChange, selectMaxAppliedSourceSeqByNode } from '../chunk-ESTZZPT5.mjs';
9
+ export { HLC } from '../chunk-ESTZZPT5.mjs';
10
+ import { ReplicationError, BatchValidationError } from '../chunk-BLMRNSUG.mjs';
11
+ export { BatchValidationError, ConflictError, ReplicationError } from '../chunk-BLMRNSUG.mjs';
12
+ import { CHANGES_TABLE, INTERNAL_TABLE_PREFIX, isReservedIdentifier } from '../chunk-2O3PP4CF.mjs';
12
13
  import '../chunk-GVCNMPOS.mjs';
13
- import '../chunk-PBRXXISQ.mjs';
14
+ import '../chunk-KINDFBQP.mjs';
14
15
  import { EventEmitter } from 'events';
15
16
  import { randomBytes, randomUUID, createHash } from 'crypto';
16
17
 
@@ -22,10 +23,10 @@ var FieldMergeResolver = class {
22
23
  this.getColumnVersions = getColumnVersions;
23
24
  }
24
25
  /**
25
- * Merges columns only one side changed, and settles overlapping columns by their per-column stamps.
26
+ * Returns the resolution for one conflicting row, merging changes to different columns and settling each column that both sides changed by its per-column stamp.
26
27
  *
27
28
  * @param ctx - The local and incoming versions of one row.
28
- * @returns Which version to write, or the merged row.
29
+ * @returns The version to keep, or the merged row.
29
30
  */
30
31
  async resolve(ctx) {
31
32
  const columnVersions = await this.getColumnVersions(ctx.table, ctx.rowId);
@@ -102,7 +103,7 @@ var PrimaryWinsResolver = class {
102
103
  this.primaryNodeId = primaryNodeId;
103
104
  }
104
105
  /**
105
- * Takes the version authored by the configured primary node, and falls back to last-writer-wins otherwise.
106
+ * Returns the version that the primary node wrote, or the last-writer-wins result when neither version comes from the primary.
106
107
  *
107
108
  * @param ctx - The local and incoming versions of one row.
108
109
  * @returns Which version to write.
@@ -174,8 +175,9 @@ function toClusterStatusInfo(status, options) {
174
175
  };
175
176
  }
176
177
  function toClusterReadEndpoints(coordinator, endpoints) {
178
+ const live = coordinator.liveNodeIds;
177
179
  return coordinator.votingDataBearingNodeIds.filter(
178
- (nodeId) => !coordinator.faultedNodeIds.includes(nodeId) && !coordinator.drainingNodeIds.includes(nodeId) && !coordinator.repairingNodeIds.includes(nodeId)
180
+ (nodeId) => !coordinator.faultedNodeIds.includes(nodeId) && !coordinator.drainingNodeIds.includes(nodeId) && !coordinator.repairingNodeIds.includes(nodeId) && (live === void 0 || live.includes(nodeId))
179
181
  ).map((nodeId) => ({
180
182
  nodeId,
181
183
  endpoint: endpoints[nodeId] ?? "",
@@ -451,6 +453,12 @@ var PeerTracker = class {
451
453
  }
452
454
  this.checkWaiters();
453
455
  }
456
+ onBatchApplied(nodeId, highestHlc) {
457
+ const peer = this.peers.get(nodeId);
458
+ if (peer && (peer.lastReceivedHlc === "" || HLC.compare(highestHlc, peer.lastReceivedHlc) > 0)) {
459
+ peer.lastReceivedHlc = highestHlc;
460
+ }
461
+ }
454
462
  recordInFlightBatch(nodeId, batch) {
455
463
  const peer = this.peers.get(nodeId);
456
464
  if (peer) {
@@ -927,7 +935,9 @@ function getCoordinatorRuntimeStatus(engine) {
927
935
  const config = engine.config.coordinator;
928
936
  const state = engine.coordinatorState;
929
937
  if (!config || !state) return void 0;
938
+ const connected = isCoordinatorConnected(engine);
930
939
  return {
940
+ ...connected && engine.liveNodeIds ? { liveNodeIds: [...engine.liveNodeIds] } : {},
931
941
  clusterId: config.clusterId,
932
942
  groupId: config.groupId,
933
943
  currentPrimary: state.currentPrimary ? { ...state.currentPrimary } : null,
@@ -938,7 +948,7 @@ function getCoordinatorRuntimeStatus(engine) {
938
948
  faultedNodeIds: [...state.faultedNodeIds],
939
949
  votingDataBearingNodeIds: [...state.votingDataBearingNodeIds],
940
950
  authority: engine.coordinatorAuthority,
941
- connected: isCoordinatorConnected(engine),
951
+ connected,
942
952
  controllerState: engine.controllerState
943
953
  };
944
954
  }
@@ -1148,268 +1158,6 @@ async function evaluateFormerPrimaryHistory(engine, state) {
1148
1158
  });
1149
1159
  return nextState;
1150
1160
  }
1151
-
1152
- // src/replication/engine/coordinator-lifecycle.ts
1153
- var DEFAULT_CONTROLLER_LEASE_TTL_MS = 1e4;
1154
- var DEFAULT_CONTROLLER_TICK_INTERVAL_MS = 1e3;
1155
- var IN_SYNC_RECONCILE_INTERVAL_MS = 1e3;
1156
- function unrefTimer(timer) {
1157
- const unref = timer.unref;
1158
- unref?.call(timer);
1159
- }
1160
- function localCoordinatorPrimary(engine) {
1161
- const endpoint = engine.config.coordinator?.endpoint;
1162
- return endpoint ? { nodeId: engine.nodeId, endpoint } : { nodeId: engine.nodeId };
1163
- }
1164
- async function startCoordinatorMode(engine) {
1165
- const config = engine.config.coordinator;
1166
- if (!config) return;
1167
- const coordinator = config.coordinator;
1168
- let state = await coordinator.getReplicationGroupState(config.clusterId, config.groupId);
1169
- if (!state && config.votingDataBearingNodeIds) {
1170
- state = await coordinator.setReplicationGroupState({
1171
- clusterId: config.clusterId,
1172
- groupId: config.groupId,
1173
- votingDataBearingNodeIds: config.votingDataBearingNodeIds,
1174
- currentPrimary: engine.config.topology.role === "primary" ? localCoordinatorPrimary(engine) : null,
1175
- primaryTerm: 1n,
1176
- inSyncNodeIds: [engine.nodeId],
1177
- compatibility: config.compatibility
1178
- });
1179
- }
1180
- if (!state) {
1181
- throw new CoordinatorError(`Replication group '${config.groupId}' is not registered`);
1182
- }
1183
- noteCoordinatorContact(engine);
1184
- engine.coordinatorState = state;
1185
- engine.coordinatorAuthority = hasCurrentPrimaryAuthorityFor(engine, state);
1186
- const session = await coordinator.registerNodeSession({
1187
- clusterId: config.clusterId,
1188
- nodeId: engine.nodeId,
1189
- ttlMs: config.sessionTtlMs ?? DEFAULT_COORDINATOR_SESSION_TTL_MS,
1190
- endpoint: config.endpoint,
1191
- groupIds: [config.groupId],
1192
- dataBearing: true,
1193
- voting: state.votingDataBearingNodeIds.includes(engine.nodeId),
1194
- compatibility: config.compatibility
1195
- });
1196
- engine.nodeSessionLeaseId = session.lease.id;
1197
- noteCoordinatorContact(engine);
1198
- engine.coordinatorWatchDisposer = await coordinator.watchReplicationGroup(config.clusterId, config.groupId, (next) => {
1199
- handleCoordinatorStateUpdate(engine, next);
1200
- });
1201
- startCoordinatorLeaseRenewal(engine);
1202
- startInSyncReconcileLoop(engine);
1203
- startControllerLoop(engine);
1204
- }
1205
- function startInSyncReconcileLoop(engine) {
1206
- const timer = setInterval(() => {
1207
- reconcileInSyncSet(engine).catch((err) => {
1208
- const wrappedErr = err instanceof Error ? err : new Error(String(err));
1209
- engine.emitError({ error: wrappedErr, operation: "coordinator-in-sync-reconcile", recoverable: true });
1210
- });
1211
- }, IN_SYNC_RECONCILE_INTERVAL_MS);
1212
- unrefTimer(timer);
1213
- engine.inSyncReconcileTimer = timer;
1214
- }
1215
- function handleCoordinatorStateUpdate(engine, next) {
1216
- const previous = engine.coordinatorState;
1217
- const wasPrimary = previous ? hasCurrentPrimaryAuthorityFor(engine, previous) : engine.coordinatorAuthority;
1218
- noteCoordinatorContact(engine);
1219
- engine.coordinatorState = next;
1220
- engine.coordinatorAuthority = hasCurrentPrimaryAuthorityFor(engine, next);
1221
- if (wasPrimary && !engine.coordinatorAuthority && next.primaryTerm > (previous?.primaryTerm ?? 0n)) {
1222
- handleFormerPrimaryDemotion(engine, next).catch((err) => {
1223
- const wrappedErr = err instanceof Error ? err : new Error(String(err));
1224
- engine.emitError({ error: wrappedErr, operation: "coordinator-former-primary-demotion", recoverable: false });
1225
- });
1226
- return;
1227
- }
1228
- startCoordinatorRejoinSyncIfReady(engine, next);
1229
- }
1230
- function startCoordinatorLeaseRenewal(engine) {
1231
- const config = engine.config.coordinator;
1232
- if (!config || !engine.nodeSessionLeaseId) return;
1233
- const ttlMs = config.sessionTtlMs ?? DEFAULT_COORDINATOR_SESSION_TTL_MS;
1234
- const timer = setInterval(
1235
- () => {
1236
- void keepCoordinatorSessionAlive(engine, ttlMs);
1237
- },
1238
- Math.max(1e3, Math.floor(ttlMs / 3))
1239
- );
1240
- unrefTimer(timer);
1241
- engine.coordinatorLeaseTimer = timer;
1242
- }
1243
- function applyAuthorityFromKnownState(engine) {
1244
- const state = engine.coordinatorState;
1245
- engine.coordinatorAuthority = state ? hasCurrentPrimaryAuthorityFor(engine, state) : false;
1246
- }
1247
- async function keepCoordinatorSessionAlive(engine, ttlMs) {
1248
- const config = engine.config.coordinator;
1249
- if (!config || !engine.running || engine.coordinatorSessionRestoring) return;
1250
- const leaseId = engine.nodeSessionLeaseId;
1251
- if (leaseId) {
1252
- try {
1253
- if (await config.coordinator.renewLease(leaseId, ttlMs)) {
1254
- noteCoordinatorContact(engine);
1255
- applyAuthorityFromKnownState(engine);
1256
- return;
1257
- }
1258
- } catch (err) {
1259
- engine.coordinatorAuthority = false;
1260
- const wrappedErr = err instanceof Error ? err : new Error(String(err));
1261
- engine.emitError({ error: wrappedErr, operation: "coordinator-session-renew", recoverable: true });
1262
- return;
1263
- }
1264
- }
1265
- engine.coordinatorAuthority = false;
1266
- engine.nodeSessionLeaseId = null;
1267
- await restoreCoordinatorSession(engine, ttlMs);
1268
- }
1269
- async function restoreCoordinatorSession(engine, ttlMs) {
1270
- const config = engine.config.coordinator;
1271
- if (!config) return;
1272
- engine.coordinatorSessionRestoring = true;
1273
- try {
1274
- const state = await config.coordinator.getReplicationGroupState(config.clusterId, config.groupId);
1275
- if (!state) {
1276
- throw new CoordinatorError(`Replication group '${config.groupId}' is not registered`);
1277
- }
1278
- const session = await config.coordinator.registerNodeSession({
1279
- clusterId: config.clusterId,
1280
- nodeId: engine.nodeId,
1281
- ttlMs,
1282
- endpoint: config.endpoint,
1283
- groupIds: [config.groupId],
1284
- dataBearing: true,
1285
- voting: state.votingDataBearingNodeIds.includes(engine.nodeId),
1286
- compatibility: config.compatibility
1287
- });
1288
- if (!engine.running) {
1289
- await config.coordinator.releaseLease(session.lease.id).catch(() => void 0);
1290
- return;
1291
- }
1292
- engine.nodeSessionLeaseId = session.lease.id;
1293
- engine.coordinatorState = state;
1294
- noteCoordinatorContact(engine);
1295
- applyAuthorityFromKnownState(engine);
1296
- } catch (err) {
1297
- const wrappedErr = err instanceof Error ? err : new Error(String(err));
1298
- engine.emitError({ error: wrappedErr, operation: "coordinator-session-restore", recoverable: true });
1299
- } finally {
1300
- engine.coordinatorSessionRestoring = false;
1301
- }
1302
- }
1303
- function startControllerLoop(engine) {
1304
- const config = engine.config.coordinator;
1305
- if (!config) return;
1306
- const controllerConfig = typeof config.controller === "object" ? config.controller : {};
1307
- const enabled = typeof config.controller === "boolean" ? config.controller : controllerConfig.enabled ?? true;
1308
- if (!enabled) {
1309
- engine.controllerState = "disabled";
1310
- return;
1311
- }
1312
- engine.controllerState = "standby";
1313
- const ttlMs = controllerConfig.leaseTtlMs ?? DEFAULT_CONTROLLER_LEASE_TTL_MS;
1314
- const holderId = controllerConfig.holderId ?? engine.nodeId;
1315
- const tickMs = controllerConfig.tickIntervalMs ?? DEFAULT_CONTROLLER_TICK_INTERVAL_MS;
1316
- const timer = setInterval(() => {
1317
- controllerTick(engine, holderId, ttlMs).catch((err) => {
1318
- const wrappedErr = err instanceof Error ? err : new Error(String(err));
1319
- engine.controllerState = "lost";
1320
- engine.emitError({ error: wrappedErr, operation: "coordinator-controller", recoverable: true });
1321
- });
1322
- }, tickMs);
1323
- unrefTimer(timer);
1324
- engine.controllerTimer = timer;
1325
- }
1326
- async function controllerTick(engine, holderId, ttlMs) {
1327
- const config = engine.config.coordinator;
1328
- if (!config) return;
1329
- if (engine.controllerLeaseId) {
1330
- const renewed = await config.coordinator.renewLease(engine.controllerLeaseId, ttlMs);
1331
- if (!renewed) {
1332
- engine.controllerLeaseId = null;
1333
- engine.controllerState = "lost";
1334
- return;
1335
- }
1336
- noteCoordinatorContact(engine);
1337
- engine.controllerState = "active";
1338
- await runControllerPromotionCheck(engine);
1339
- return;
1340
- }
1341
- const acquired = await config.coordinator.tryAcquireControllerLease({
1342
- clusterId: config.clusterId,
1343
- holderId,
1344
- ttlMs
1345
- });
1346
- noteCoordinatorContact(engine);
1347
- if (acquired.acquired) {
1348
- engine.controllerLeaseId = acquired.lease.id;
1349
- engine.controllerState = "active";
1350
- await runControllerPromotionCheck(engine);
1351
- } else {
1352
- engine.controllerState = "standby";
1353
- }
1354
- }
1355
- async function runControllerPromotionCheck(engine) {
1356
- const config = engine.config.coordinator;
1357
- if (!config) return;
1358
- const state = await refreshCoordinatorState(engine);
1359
- if (!state) return;
1360
- const primaryNodeId = state.currentPrimary?.nodeId;
1361
- const primaryLive = primaryNodeId ? await config.coordinator.getLiveNodeSession(config.clusterId, primaryNodeId) : null;
1362
- const primaryCanKeepDuty = primaryNodeId && primaryLive && compatibilityAllowsPromotion(state.compatibility, primaryLive.compatibility) && !state.drainingNodeIds.includes(primaryNodeId) && !state.repairingNodeIds.includes(primaryNodeId) && !state.faultedNodeIds.includes(primaryNodeId);
1363
- if (primaryCanKeepDuty) {
1364
- return;
1365
- }
1366
- try {
1367
- const promoted = await config.coordinator.promoteEligibleReplica({
1368
- clusterId: config.clusterId,
1369
- groupId: config.groupId,
1370
- excludeNodeIds: primaryNodeId ? [primaryNodeId] : []
1371
- });
1372
- engine.coordinatorState = promoted;
1373
- engine.coordinatorAuthority = hasCurrentPrimaryAuthorityFor(engine, promoted);
1374
- } catch (err) {
1375
- const wrappedErr = err instanceof Error ? err : new Error(String(err));
1376
- engine.emitError({ error: wrappedErr, operation: "coordinator-promotion", recoverable: true });
1377
- }
1378
- }
1379
- function stopCoordinatorTimers(engine) {
1380
- if (engine.coordinatorLeaseTimer) {
1381
- clearInterval(engine.coordinatorLeaseTimer);
1382
- engine.coordinatorLeaseTimer = null;
1383
- }
1384
- if (engine.inSyncReconcileTimer) {
1385
- clearInterval(engine.inSyncReconcileTimer);
1386
- engine.inSyncReconcileTimer = null;
1387
- }
1388
- if (engine.controllerTimer) {
1389
- clearInterval(engine.controllerTimer);
1390
- engine.controllerTimer = null;
1391
- }
1392
- }
1393
- async function stopCoordinatorMode(engine) {
1394
- const config = engine.config.coordinator;
1395
- if (!config) return;
1396
- if (engine.coordinatorWatchDisposer) {
1397
- await engine.coordinatorWatchDisposer();
1398
- engine.coordinatorWatchDisposer = null;
1399
- }
1400
- if (engine.controllerLeaseId) {
1401
- const leaseId = engine.controllerLeaseId;
1402
- engine.controllerLeaseId = null;
1403
- await config.coordinator.releaseLease(leaseId).catch((err) => {
1404
- const wrappedErr = err instanceof Error ? err : new Error(String(err));
1405
- engine.emitError({ error: wrappedErr, operation: "coordinator-controller-release", recoverable: true });
1406
- });
1407
- }
1408
- await config.coordinator.deregisterNodeSession(config.clusterId, engine.nodeId).catch((err) => {
1409
- const wrappedErr = err instanceof Error ? err : new Error(String(err));
1410
- engine.emitError({ error: wrappedErr, operation: "coordinator-session-deregister", recoverable: true });
1411
- });
1412
- }
1413
1161
  function assertReadyPhase(engine, activity) {
1414
1162
  if (engine.syncState.phase !== "ready") {
1415
1163
  throw new SyncError(`Node is in '${engine.syncState.phase}' phase and cannot ${activity}`);
@@ -1420,11 +1168,11 @@ async function query(engine, sql, params, options) {
1420
1168
  await assertReadConcern(engine, options?.readConcern?.level);
1421
1169
  return engine.database.query(sql, params, options);
1422
1170
  }
1423
- async function execute(engine, sql, params, options) {
1171
+ async function execute2(engine, sql, params, options) {
1424
1172
  assertReadyPhase(engine, "accept operations");
1425
1173
  if (!await canAcceptLocalWrite(engine)) {
1426
1174
  if (engine.config.writeForwarding) {
1427
- const result = await forwardStatements(engine, [{ sql, params }]);
1175
+ const result = await forwardStatements(engine, [{ sql, params }], options);
1428
1176
  const first = result.results[0];
1429
1177
  if (!first) {
1430
1178
  return { changes: 0, lastInsertRowId: 0 };
@@ -1443,7 +1191,7 @@ async function executeBatch(engine, sql, paramsBatch, options) {
1443
1191
  if (!await canAcceptLocalWrite(engine)) {
1444
1192
  if (engine.config.writeForwarding) {
1445
1193
  const statements = paramsBatch.map((p) => ({ sql, params: p }));
1446
- const result = await forwardStatements(engine, statements);
1194
+ const result = await forwardStatements(engine, statements, options);
1447
1195
  return result.results.map((r) => ({
1448
1196
  changes: r.changes,
1449
1197
  lastInsertRowId: typeof r.lastInsertRowId === "string" ? BigInt(r.lastInsertRowId) : Number(r.lastInsertRowId)
@@ -1451,12 +1199,7 @@ async function executeBatch(engine, sql, paramsBatch, options) {
1451
1199
  }
1452
1200
  throwNotCurrentPrimary(engine);
1453
1201
  }
1454
- const results = [];
1455
- for (const params of paramsBatch) {
1456
- const r = await engine.localExecutor.executeLocally(sql, params, options);
1457
- results.push(r);
1458
- }
1459
- return results;
1202
+ return engine.localExecutor.executeBatchLocally(sql, paramsBatch, options);
1460
1203
  }
1461
1204
  async function transaction(engine, fn, options) {
1462
1205
  assertReadyPhase(engine, "accept operations");
@@ -1465,9 +1208,10 @@ async function transaction(engine, fn, options) {
1465
1208
  }
1466
1209
  return engine.localExecutor.executeTransactionLocally(fn, options);
1467
1210
  }
1468
- async function forwardStatements(engine, statements, _options) {
1211
+ async function forwardStatements(engine, statements, options) {
1212
+ const writeConcern = options?.writeConcern;
1469
1213
  if (await canAcceptLocalWrite(engine)) {
1470
- return engine.localExecutor.executeForwardedLocally(statements);
1214
+ return engine.localExecutor.executeForwardedLocally(statements, writeConcern);
1471
1215
  }
1472
1216
  const primaryPeerId = getForwardingPrimaryPeerId(engine);
1473
1217
  if (primaryPeerId === null) {
@@ -1476,7 +1220,8 @@ async function forwardStatements(engine, statements, _options) {
1476
1220
  return engine.config.transport.forward(primaryPeerId, {
1477
1221
  statements,
1478
1222
  requestId: randomUUID(),
1479
- ...getCoordinatorMessageFields(engine)
1223
+ ...getCoordinatorMessageFields(engine),
1224
+ ...writeConcern === void 0 ? {} : { writeConcern }
1480
1225
  });
1481
1226
  }
1482
1227
 
@@ -1537,17 +1282,15 @@ var LocalExecutor = class {
1537
1282
  this.engine = engine;
1538
1283
  }
1539
1284
  /**
1540
- * Serialises every `executeTransactionLocally` call against itself.
1285
+ * Makes each `executeTransactionLocally` call wait until the previous one finishes.
1541
1286
  *
1542
- * Reason: SQLite only allows one active transaction per connection; the
1543
- * writer pool exposes a single writer connection (see `ConnectionPool`).
1544
- * Two parallel `engine.transaction(fn)` callers therefore both reach
1545
- * `await conn.exec('BEGIN')` on the same connection, and the second
1546
- * `BEGIN` errors with "cannot start a transaction within a transaction".
1547
- * Chaining onto this promise turns the second caller into a strict
1548
- * follow-on without changing the public API. A rejection is swallowed at
1549
- * the chain level so a failed transaction never poisons the queue; the
1550
- * original error still surfaces to the caller that initiated it.
1287
+ * The engine writes through the single connection `engine.writerConn`,
1288
+ * which SQLite limits to one open transaction. Without this queue, two
1289
+ * concurrent `engine.transaction(fn)` calls would both send `BEGIN` on that
1290
+ * connection, so SQLite would reject the second with 'cannot start a
1291
+ * transaction within a transaction'. The queue catches each rejection so
1292
+ * that a later transaction still starts after an earlier one fails, while
1293
+ * the caller that started the failed transaction still receives its error.
1551
1294
  */
1552
1295
  transactionQueue = Promise.resolve();
1553
1296
  async executeLocally(sql, params, options) {
@@ -1558,11 +1301,9 @@ var LocalExecutor = class {
1558
1301
  }
1559
1302
  const txId = randomUUID();
1560
1303
  const droppedTable = isDdl ? extractDroppedTable(sql) : null;
1561
- const result = await engine.writerConn.transaction(async (tx) => {
1304
+ const result = await runWriterTransaction(engine.writerConn, async (tx) => {
1562
1305
  const seqBefore = await engine.log.getLocalSeq();
1563
- const bindValues = params ? Array.isArray(params) ? params : [params] : [];
1564
- const stmt = await tx.prepare(sql);
1565
- const r = await stmt.run(...bindValues);
1306
+ const r = await execute(tx, sql, params);
1566
1307
  if (isDdl) {
1567
1308
  await insertDdlChange(tx, CHANGES_TABLE, {
1568
1309
  ddlStatement: sql,
@@ -1592,12 +1333,13 @@ var LocalExecutor = class {
1592
1333
  }
1593
1334
  return result;
1594
1335
  }
1595
- async executeForwardedLocally(statements) {
1596
- const engine = this.engine;
1597
- const requestId = randomUUID();
1598
- const results = [];
1599
- const txId = randomUUID();
1600
- const hook = engine.config.onBeforeForwardedQuery;
1336
+ async executeBatchLocally(sql, paramsBatch, options) {
1337
+ const { results, newSeq } = await this.executeInOneTransaction(paramsBatch.map((params) => ({ sql, params })));
1338
+ await this.waitForWriteConcernOf(newSeq, options?.writeConcern);
1339
+ return results;
1340
+ }
1341
+ async executeForwardedLocally(statements, statedWriteConcern) {
1342
+ const hook = this.engine.config.onBeforeForwardedQuery;
1601
1343
  for (const { sql } of statements) {
1602
1344
  if (!SAFE_SQL_PREFIX_RE.test(sql)) {
1603
1345
  throw new ReplicationError("Forwarded statement rejected: only DML and safe DDL are allowed");
@@ -1608,22 +1350,37 @@ var LocalExecutor = class {
1608
1350
  hook(sql, params);
1609
1351
  }
1610
1352
  }
1353
+ const { results, newSeq } = await this.executeInOneTransaction(statements);
1354
+ await this.waitForWriteConcernOf(newSeq, statedWriteConcern);
1355
+ return {
1356
+ results: results.map((r) => ({
1357
+ changes: r.changes,
1358
+ lastInsertRowId: typeof r.lastInsertRowId === "bigint" ? r.lastInsertRowId.toString() : r.lastInsertRowId
1359
+ })),
1360
+ requestId: randomUUID()
1361
+ };
1362
+ }
1363
+ async waitForWriteConcernOf(seq, stated) {
1364
+ const writeConcern = resolveWriteConcern(this.engine, stated);
1365
+ if (writeConcern) {
1366
+ await waitForWriteConcern(this.engine, seq, writeConcern);
1367
+ }
1368
+ }
1369
+ async executeInOneTransaction(statements) {
1370
+ const engine = this.engine;
1371
+ const results = [];
1372
+ const txId = randomUUID();
1611
1373
  let sawDdl = false;
1612
1374
  const droppedTables = [];
1613
- await engine.writerConn.transaction(async (tx) => {
1375
+ await runWriterTransaction(engine.writerConn, async (tx) => {
1614
1376
  const seqBefore = await engine.log.getLocalSeq();
1615
1377
  for (const { sql, params } of statements) {
1616
1378
  const isDdl = DDL_PREFIX_RE.test(sql);
1617
1379
  if (isDdl && sql.includes(";")) {
1618
1380
  throw new ReplicationError("DDL statements containing semicolons are not allowed for replication safety");
1619
1381
  }
1620
- const bindValues = params ? Array.isArray(params) ? params : [params] : [];
1621
- const stmt = await tx.prepare(sql);
1622
- const r = await stmt.run(...bindValues);
1623
- results.push({
1624
- changes: r.changes,
1625
- lastInsertRowId: typeof r.lastInsertRowId === "bigint" ? r.lastInsertRowId.toString() : r.lastInsertRowId
1626
- });
1382
+ const r = await execute(tx, sql, params);
1383
+ results.push({ changes: r.changes, lastInsertRowId: r.lastInsertRowId });
1627
1384
  if (isDdl) {
1628
1385
  sawDdl = true;
1629
1386
  const droppedTable = extractDroppedTable(sql);
@@ -1654,7 +1411,7 @@ var LocalExecutor = class {
1654
1411
  }
1655
1412
  await refreshTriggersAfterDdl(engine);
1656
1413
  }
1657
- return { results, requestId };
1414
+ return { results, newSeq };
1658
1415
  }
1659
1416
  async executeTransactionLocally(fn, options) {
1660
1417
  const ticket = this.transactionQueue.then(
@@ -1674,7 +1431,7 @@ var LocalExecutor = class {
1674
1431
  throw new ReplicationError("Internal error: DDL hook invoked outside an active transaction");
1675
1432
  }
1676
1433
  };
1677
- const userResult = await engine.writerConn.transaction(async (tx) => {
1434
+ const userResult = await runWriterTransaction(engine.writerConn, async (tx) => {
1678
1435
  const seqBefore = await engine.log.getLocalSeq();
1679
1436
  hooks.onDdl = async (sql) => {
1680
1437
  const droppedTable = extractDroppedTable(sql);
@@ -1705,167 +1462,485 @@ var LocalExecutor = class {
1705
1462
  if (hooks.droppedTables.length > 0 && engine.tracker) {
1706
1463
  await engine.tracker.pruneDroppedTables(engine.writerConn, hooks.droppedTables);
1707
1464
  }
1708
- await refreshTriggersAfterDdl(engine);
1709
- }
1710
- const writeConcern = resolveWriteConcern(engine, options?.writeConcern);
1711
- if (writeConcern) {
1712
- await waitForWriteConcern(engine, newSeq, writeConcern);
1465
+ await refreshTriggersAfterDdl(engine);
1466
+ }
1467
+ const writeConcern = resolveWriteConcern(engine, options?.writeConcern);
1468
+ if (writeConcern) {
1469
+ await waitForWriteConcern(engine, newSeq, writeConcern);
1470
+ }
1471
+ return userResult;
1472
+ }
1473
+ };
1474
+
1475
+ // src/replication/engine/node-health.ts
1476
+ function isSidelined(state, nodeId) {
1477
+ return state.drainingNodeIds.includes(nodeId) || state.repairingNodeIds.includes(nodeId) || state.faultedNodeIds.includes(nodeId);
1478
+ }
1479
+ function excludingReason(state, nodeId) {
1480
+ if (state.drainingNodeIds.includes(nodeId)) return "draining";
1481
+ if (state.faultedNodeIds.includes(nodeId)) return "faulted";
1482
+ return "sync-pending";
1483
+ }
1484
+ function staticHealth(engine, phase) {
1485
+ if (phase !== "ready") {
1486
+ return { state: "unavailable", reason: "sync-pending", canRead: false, canWrite: false };
1487
+ }
1488
+ return { state: "healthy", reason: "in-sync", canRead: true, canWrite: engine.config.topology.canWrite() };
1489
+ }
1490
+ function computeNodeHealth(engine) {
1491
+ const phase = engine.syncState.phase;
1492
+ if (phase === "syncing" || phase === "catching-up") {
1493
+ return { state: "syncing", reason: "sync-pending", canRead: false, canWrite: false };
1494
+ }
1495
+ if (!engine.isCoordinatorMode()) {
1496
+ return staticHealth(engine, phase);
1497
+ }
1498
+ const state = engine.coordinatorState;
1499
+ if (!state) {
1500
+ return { state: "unavailable", reason: "no-group-state", canRead: false, canWrite: false };
1501
+ }
1502
+ const nodeId = engine.nodeId;
1503
+ const canRead = phase === "ready" && !isSidelined(state, nodeId);
1504
+ const canWrite = canRead && engine.coordinatorAuthority;
1505
+ if (state.repairingNodeIds.includes(nodeId)) {
1506
+ return { state: "repairing", reason: "repairing", canRead, canWrite };
1507
+ }
1508
+ if (engine.coordinatorAuthority && !canWrite) {
1509
+ return { state: "failing_over", reason: excludingReason(state, nodeId), canRead, canWrite };
1510
+ }
1511
+ if (!canRead) {
1512
+ return { state: "unavailable", reason: excludingReason(state, nodeId), canRead, canWrite };
1513
+ }
1514
+ if (!isCoordinatorConnected(engine)) {
1515
+ return { state: "degraded", reason: "coordinator-unreachable", canRead, canWrite };
1516
+ }
1517
+ if (!state.inSyncNodeIds.includes(nodeId)) {
1518
+ return { state: "degraded", reason: "lagging", canRead, canWrite };
1519
+ }
1520
+ return { state: "healthy", reason: "in-sync", canRead, canWrite };
1521
+ }
1522
+
1523
+ // src/replication/engine/sender-loop.ts
1524
+ var SenderLoop = class {
1525
+ constructor(engine) {
1526
+ this.engine = engine;
1527
+ }
1528
+ senderTimer = null;
1529
+ start() {
1530
+ const engine = this.engine;
1531
+ if (!engine.running) return;
1532
+ const timer = setTimeout(async () => {
1533
+ if (!engine.running) return;
1534
+ try {
1535
+ engine.lastSentSeq = await engine.log.getLocalSeq();
1536
+ await this.sendPendingBatches();
1537
+ await this.updatePruneBoundary();
1538
+ } catch (err) {
1539
+ const wrappedErr = err instanceof Error ? err : new Error(String(err));
1540
+ engine.emitError({ error: wrappedErr, operation: "sender-loop", recoverable: true });
1541
+ }
1542
+ this.start();
1543
+ }, engine.batchIntervalMs);
1544
+ timer.unref?.();
1545
+ this.senderTimer = timer;
1546
+ }
1547
+ stop() {
1548
+ if (this.senderTimer !== null) {
1549
+ clearTimeout(this.senderTimer);
1550
+ this.senderTimer = null;
1551
+ }
1552
+ }
1553
+ async updatePruneBoundary() {
1554
+ const engine = this.engine;
1555
+ if (!engine.tracker) return;
1556
+ const minAcked = await engine.log.getMinAckedSeq();
1557
+ if (minAcked === null) {
1558
+ engine.tracker.clearPruneBoundary("replication");
1559
+ } else {
1560
+ engine.tracker.setPruneBoundary("replication", minAcked);
1561
+ }
1562
+ }
1563
+ async sendPendingBatches() {
1564
+ const engine = this.engine;
1565
+ const peers = engine.config.transport.peers();
1566
+ const nowMs = Date.now();
1567
+ for (const [peerId, peerInfo] of peers) {
1568
+ if (!this.shouldReplicateTo(peerId, peerInfo.role)) {
1569
+ continue;
1570
+ }
1571
+ const peerState = engine.peerTracker.getPeerState(peerId);
1572
+ if (peerState) {
1573
+ engine.peerTracker.expireTimedOutBatches(peerId, nowMs, engine.ackTimeoutMs);
1574
+ }
1575
+ if (peerState && peerState.pendingBatches >= engine.maxPendingBatches) {
1576
+ continue;
1577
+ }
1578
+ const fromSeq = peerState?.lastSentSeq ?? engine.lastSentSeq;
1579
+ const rawBatch = await engine.log.readBatch(fromSeq, engine.batchSize);
1580
+ if (!rawBatch) continue;
1581
+ const batch = engine.decorate(rawBatch);
1582
+ const previousSeq = peerState?.lastSentSeq ?? 0n;
1583
+ if (peerState) {
1584
+ peerState.pendingBatches += 1;
1585
+ peerState.lastSentSeq = batch.toSeq;
1586
+ engine.peerTracker.recordInFlightBatch(peerId, {
1587
+ batchId: batch.batchId,
1588
+ fromSeq: batch.fromSeq,
1589
+ toSeq: batch.toSeq,
1590
+ sentAt: nowMs
1591
+ });
1592
+ }
1593
+ engine.config.transport.send(peerId, batch).catch((err) => {
1594
+ const wrappedErr = err instanceof Error ? err : new Error(String(err));
1595
+ engine.emitError({ error: wrappedErr, operation: "transport-send", peerId, recoverable: true });
1596
+ if (peerState) {
1597
+ const idx = peerState.inFlightBatches.findIndex((b) => b.batchId === batch.batchId);
1598
+ if (idx >= 0) {
1599
+ peerState.inFlightBatches.splice(idx, 1);
1600
+ }
1601
+ if (peerState.pendingBatches > 0) {
1602
+ peerState.pendingBatches -= 1;
1603
+ }
1604
+ if (previousSeq < peerState.lastSentSeq) {
1605
+ peerState.lastSentSeq = previousSeq;
1606
+ }
1607
+ }
1608
+ });
1609
+ if (engine.config.flowControl?.maxLagSeconds && peerState) {
1610
+ const lagMs = Number(batch.toSeq - peerState.lastAckedSeq) * engine.batchIntervalMs;
1611
+ const maxLagMs = engine.config.flowControl.maxLagSeconds * 1e3;
1612
+ if (lagMs > maxLagMs && engine.config.flowControl.onLagExceeded) {
1613
+ engine.config.flowControl.onLagExceeded(peerId, lagMs);
1614
+ }
1615
+ }
1616
+ }
1617
+ }
1618
+ shouldReplicateTo(peerId, peerRole) {
1619
+ const engine = this.engine;
1620
+ if (!engine.isCoordinatorMode()) {
1621
+ return engine.config.topology.shouldReplicateTo(peerId, peerRole);
1622
+ }
1623
+ return engine.coordinatorAuthority && peerId !== engine.nodeId;
1624
+ }
1625
+ };
1626
+
1627
+ // src/replication/engine/timers.ts
1628
+ function unrefTimer(timer) {
1629
+ const unref = timer.unref;
1630
+ unref?.call(timer);
1631
+ }
1632
+
1633
+ // src/replication/engine/controller-loop.ts
1634
+ var DEFAULT_CONTROLLER_LEASE_TTL_MS = 1e4;
1635
+ var DEFAULT_CONTROLLER_TICK_INTERVAL_MS = 1e3;
1636
+ async function startControllerLoop(engine) {
1637
+ const config = engine.config.coordinator;
1638
+ if (!config) return;
1639
+ const controllerConfig = typeof config.controller === "object" ? config.controller : {};
1640
+ const enabled = typeof config.controller === "boolean" ? config.controller : controllerConfig.enabled ?? true;
1641
+ if (!enabled) {
1642
+ engine.controllerState = "disabled";
1643
+ return;
1644
+ }
1645
+ engine.controllerState = "standby";
1646
+ const bid = {
1647
+ holderId: controllerConfig.holderId ?? engine.nodeId,
1648
+ ttlMs: controllerConfig.leaseTtlMs ?? DEFAULT_CONTROLLER_LEASE_TTL_MS
1649
+ };
1650
+ const coordinator = config.coordinator;
1651
+ if (coordinator.watchControllerLease) {
1652
+ engine.controllerLeaseWatchDisposer = await coordinator.watchControllerLease(config.clusterId, (lease) => {
1653
+ engine.observedControllerLease = lease;
1654
+ if (lease === null) {
1655
+ bidForControllerLease(engine, bid).catch((err) => reportControllerFailure(engine, err));
1656
+ }
1657
+ });
1658
+ }
1659
+ const timer = setInterval(() => {
1660
+ controllerTick(engine, bid).catch((err) => reportControllerFailure(engine, err));
1661
+ }, controllerConfig.tickIntervalMs ?? DEFAULT_CONTROLLER_TICK_INTERVAL_MS);
1662
+ unrefTimer(timer);
1663
+ engine.controllerTimer = timer;
1664
+ }
1665
+ async function stopControllerLeaseWatch(engine) {
1666
+ const dispose = engine.controllerLeaseWatchDisposer;
1667
+ engine.controllerLeaseWatchDisposer = null;
1668
+ engine.observedControllerLease = null;
1669
+ if (dispose) {
1670
+ await dispose();
1671
+ }
1672
+ }
1673
+ async function controllerTick(engine, bid) {
1674
+ const config = engine.config.coordinator;
1675
+ if (!config) return;
1676
+ if (engine.controllerLeaseId) {
1677
+ const renewed = await config.coordinator.renewLease(engine.controllerLeaseId, bid.ttlMs);
1678
+ if (!renewed) {
1679
+ engine.controllerLeaseId = null;
1680
+ engine.controllerState = "lost";
1681
+ return;
1682
+ }
1683
+ noteCoordinatorContact(engine);
1684
+ engine.controllerState = "active";
1685
+ await runControllerPromotionCheck(engine);
1686
+ return;
1687
+ }
1688
+ if (watchShowsLiveHolder(engine)) {
1689
+ engine.controllerState = "standby";
1690
+ return;
1691
+ }
1692
+ await bidForControllerLease(engine, bid);
1693
+ }
1694
+ function watchShowsLiveHolder(engine) {
1695
+ const observed = engine.observedControllerLease;
1696
+ return engine.controllerLeaseWatchDisposer !== null && observed !== null && observed.expiresAtMs > Date.now();
1697
+ }
1698
+ async function bidForControllerLease(engine, bid) {
1699
+ const config = engine.config.coordinator;
1700
+ if (!config || !engine.running || engine.controllerLeaseId || engine.controllerBidding) return;
1701
+ engine.controllerBidding = true;
1702
+ try {
1703
+ const acquired = await config.coordinator.tryAcquireControllerLease({
1704
+ clusterId: config.clusterId,
1705
+ holderId: bid.holderId,
1706
+ ttlMs: bid.ttlMs
1707
+ });
1708
+ noteCoordinatorContact(engine);
1709
+ if (!acquired.acquired) {
1710
+ engine.controllerState = "standby";
1711
+ return;
1712
+ }
1713
+ if (!engine.running) {
1714
+ await config.coordinator.releaseLease(acquired.lease.id);
1715
+ return;
1716
+ }
1717
+ engine.controllerLeaseId = acquired.lease.id;
1718
+ engine.controllerState = "active";
1719
+ } finally {
1720
+ engine.controllerBidding = false;
1721
+ }
1722
+ await runControllerPromotionCheck(engine);
1723
+ }
1724
+ function reportControllerFailure(engine, err) {
1725
+ const wrappedErr = err instanceof Error ? err : new Error(String(err));
1726
+ engine.controllerState = "lost";
1727
+ engine.emitError({ error: wrappedErr, operation: "coordinator-controller", recoverable: true });
1728
+ }
1729
+ async function runControllerPromotionCheck(engine) {
1730
+ const config = engine.config.coordinator;
1731
+ if (!config) return;
1732
+ const state = await refreshCoordinatorState(engine);
1733
+ if (!state) return;
1734
+ const primaryNodeId = state.currentPrimary?.nodeId;
1735
+ const primaryLive = primaryNodeId ? await config.coordinator.getLiveNodeSession(config.clusterId, primaryNodeId) : null;
1736
+ const primaryCanKeepDuty = primaryNodeId && primaryLive && compatibilityAllowsPromotion(state.compatibility, primaryLive.compatibility) && !state.drainingNodeIds.includes(primaryNodeId) && !state.repairingNodeIds.includes(primaryNodeId) && !state.faultedNodeIds.includes(primaryNodeId);
1737
+ if (primaryCanKeepDuty) {
1738
+ return;
1739
+ }
1740
+ try {
1741
+ const promoted = await config.coordinator.promoteEligibleReplica({
1742
+ clusterId: config.clusterId,
1743
+ groupId: config.groupId,
1744
+ excludeNodeIds: primaryNodeId ? [primaryNodeId] : []
1745
+ });
1746
+ engine.coordinatorState = promoted;
1747
+ engine.coordinatorAuthority = hasCurrentPrimaryAuthorityFor(engine, promoted);
1748
+ } catch (err) {
1749
+ const wrappedErr = err instanceof Error ? err : new Error(String(err));
1750
+ engine.emitError({ error: wrappedErr, operation: "coordinator-promotion", recoverable: true });
1751
+ }
1752
+ }
1753
+
1754
+ // src/replication/engine/coordinator-lifecycle.ts
1755
+ var IN_SYNC_RECONCILE_INTERVAL_MS = 1e3;
1756
+ function localCoordinatorPrimary(engine) {
1757
+ const endpoint = engine.config.coordinator?.endpoint;
1758
+ return endpoint ? { nodeId: engine.nodeId, endpoint } : { nodeId: engine.nodeId };
1759
+ }
1760
+ async function startCoordinatorMode(engine) {
1761
+ const config = engine.config.coordinator;
1762
+ if (!config) return;
1763
+ const coordinator = config.coordinator;
1764
+ let state = await coordinator.getReplicationGroupState(config.clusterId, config.groupId);
1765
+ if (!state && config.votingDataBearingNodeIds) {
1766
+ state = await coordinator.setReplicationGroupState({
1767
+ clusterId: config.clusterId,
1768
+ groupId: config.groupId,
1769
+ votingDataBearingNodeIds: config.votingDataBearingNodeIds,
1770
+ currentPrimary: engine.config.topology.role === "primary" ? localCoordinatorPrimary(engine) : null,
1771
+ primaryTerm: 1n,
1772
+ inSyncNodeIds: [engine.nodeId],
1773
+ compatibility: config.compatibility
1774
+ });
1775
+ }
1776
+ if (!state) {
1777
+ throw new CoordinatorError(`Replication group '${config.groupId}' is not registered`);
1778
+ }
1779
+ noteCoordinatorContact(engine);
1780
+ engine.coordinatorState = state;
1781
+ engine.coordinatorAuthority = hasCurrentPrimaryAuthorityFor(engine, state);
1782
+ const session = await coordinator.registerNodeSession({
1783
+ clusterId: config.clusterId,
1784
+ nodeId: engine.nodeId,
1785
+ ttlMs: config.sessionTtlMs ?? DEFAULT_COORDINATOR_SESSION_TTL_MS,
1786
+ endpoint: config.endpoint,
1787
+ groupIds: [config.groupId],
1788
+ dataBearing: true,
1789
+ voting: state.votingDataBearingNodeIds.includes(engine.nodeId),
1790
+ compatibility: config.compatibility
1791
+ });
1792
+ engine.nodeSessionLeaseId = session.lease.id;
1793
+ noteCoordinatorContact(engine);
1794
+ engine.coordinatorWatchDisposer = await coordinator.watchReplicationGroup(config.clusterId, config.groupId, (next) => {
1795
+ handleCoordinatorStateUpdate(engine, next);
1796
+ });
1797
+ if (coordinator.watchNodeSessions) {
1798
+ engine.nodeSessionWatchDisposer = await coordinator.watchNodeSessions(config.clusterId, (liveNodeIds) => {
1799
+ engine.liveNodeIds = [...liveNodeIds];
1800
+ });
1801
+ }
1802
+ startCoordinatorLeaseRenewal(engine);
1803
+ startInSyncReconcileLoop(engine);
1804
+ await startControllerLoop(engine);
1805
+ }
1806
+ function startInSyncReconcileLoop(engine) {
1807
+ const timer = setInterval(() => {
1808
+ reconcileInSyncSet(engine).catch((err) => {
1809
+ const wrappedErr = err instanceof Error ? err : new Error(String(err));
1810
+ engine.emitError({ error: wrappedErr, operation: "coordinator-in-sync-reconcile", recoverable: true });
1811
+ });
1812
+ }, IN_SYNC_RECONCILE_INTERVAL_MS);
1813
+ unrefTimer(timer);
1814
+ engine.inSyncReconcileTimer = timer;
1815
+ }
1816
+ function handleCoordinatorStateUpdate(engine, next) {
1817
+ const previous = engine.coordinatorState;
1818
+ const wasPrimary = previous ? hasCurrentPrimaryAuthorityFor(engine, previous) : engine.coordinatorAuthority;
1819
+ noteCoordinatorContact(engine);
1820
+ engine.coordinatorState = next;
1821
+ engine.coordinatorAuthority = hasCurrentPrimaryAuthorityFor(engine, next);
1822
+ if (wasPrimary && !engine.coordinatorAuthority && next.primaryTerm > (previous?.primaryTerm ?? 0n)) {
1823
+ handleFormerPrimaryDemotion(engine, next).catch((err) => {
1824
+ const wrappedErr = err instanceof Error ? err : new Error(String(err));
1825
+ engine.emitError({ error: wrappedErr, operation: "coordinator-former-primary-demotion", recoverable: false });
1826
+ });
1827
+ return;
1828
+ }
1829
+ startCoordinatorRejoinSyncIfReady(engine, next);
1830
+ }
1831
+ function startCoordinatorLeaseRenewal(engine) {
1832
+ const config = engine.config.coordinator;
1833
+ if (!config || !engine.nodeSessionLeaseId) return;
1834
+ const ttlMs = config.sessionTtlMs ?? DEFAULT_COORDINATOR_SESSION_TTL_MS;
1835
+ const timer = setInterval(
1836
+ () => {
1837
+ void keepCoordinatorSessionAlive(engine, ttlMs);
1838
+ },
1839
+ Math.max(1e3, Math.floor(ttlMs / 3))
1840
+ );
1841
+ unrefTimer(timer);
1842
+ engine.coordinatorLeaseTimer = timer;
1843
+ }
1844
+ function applyAuthorityFromKnownState(engine) {
1845
+ const state = engine.coordinatorState;
1846
+ engine.coordinatorAuthority = state ? hasCurrentPrimaryAuthorityFor(engine, state) : false;
1847
+ }
1848
+ async function keepCoordinatorSessionAlive(engine, ttlMs) {
1849
+ const config = engine.config.coordinator;
1850
+ if (!config || !engine.running || engine.coordinatorSessionRestoring) return;
1851
+ const leaseId = engine.nodeSessionLeaseId;
1852
+ if (leaseId) {
1853
+ try {
1854
+ if (await config.coordinator.renewLease(leaseId, ttlMs)) {
1855
+ noteCoordinatorContact(engine);
1856
+ applyAuthorityFromKnownState(engine);
1857
+ return;
1858
+ }
1859
+ } catch (err) {
1860
+ engine.coordinatorAuthority = false;
1861
+ const wrappedErr = err instanceof Error ? err : new Error(String(err));
1862
+ engine.emitError({ error: wrappedErr, operation: "coordinator-session-renew", recoverable: true });
1863
+ return;
1713
1864
  }
1714
- return userResult;
1715
1865
  }
1716
- };
1717
-
1718
- // src/replication/engine/node-health.ts
1719
- function isSidelined(state, nodeId) {
1720
- return state.drainingNodeIds.includes(nodeId) || state.repairingNodeIds.includes(nodeId) || state.faultedNodeIds.includes(nodeId);
1721
- }
1722
- function excludingReason(state, nodeId) {
1723
- if (state.drainingNodeIds.includes(nodeId)) return "draining";
1724
- if (state.faultedNodeIds.includes(nodeId)) return "faulted";
1725
- return "sync-pending";
1866
+ engine.coordinatorAuthority = false;
1867
+ engine.nodeSessionLeaseId = null;
1868
+ await restoreCoordinatorSession(engine, ttlMs);
1726
1869
  }
1727
- function staticHealth(engine, phase) {
1728
- if (phase !== "ready") {
1729
- return { state: "unavailable", reason: "sync-pending", canRead: false, canWrite: false };
1870
+ async function restoreCoordinatorSession(engine, ttlMs) {
1871
+ const config = engine.config.coordinator;
1872
+ if (!config) return;
1873
+ engine.coordinatorSessionRestoring = true;
1874
+ try {
1875
+ const state = await config.coordinator.getReplicationGroupState(config.clusterId, config.groupId);
1876
+ if (!state) {
1877
+ throw new CoordinatorError(`Replication group '${config.groupId}' is not registered`);
1878
+ }
1879
+ const session = await config.coordinator.registerNodeSession({
1880
+ clusterId: config.clusterId,
1881
+ nodeId: engine.nodeId,
1882
+ ttlMs,
1883
+ endpoint: config.endpoint,
1884
+ groupIds: [config.groupId],
1885
+ dataBearing: true,
1886
+ voting: state.votingDataBearingNodeIds.includes(engine.nodeId),
1887
+ compatibility: config.compatibility
1888
+ });
1889
+ if (!engine.running) {
1890
+ await config.coordinator.releaseLease(session.lease.id).catch(() => void 0);
1891
+ return;
1892
+ }
1893
+ engine.nodeSessionLeaseId = session.lease.id;
1894
+ engine.coordinatorState = state;
1895
+ noteCoordinatorContact(engine);
1896
+ applyAuthorityFromKnownState(engine);
1897
+ } catch (err) {
1898
+ const wrappedErr = err instanceof Error ? err : new Error(String(err));
1899
+ engine.emitError({ error: wrappedErr, operation: "coordinator-session-restore", recoverable: true });
1900
+ } finally {
1901
+ engine.coordinatorSessionRestoring = false;
1730
1902
  }
1731
- return { state: "healthy", reason: "in-sync", canRead: true, canWrite: engine.config.topology.canWrite() };
1732
1903
  }
1733
- function computeNodeHealth(engine) {
1734
- const phase = engine.syncState.phase;
1735
- if (phase === "syncing" || phase === "catching-up") {
1736
- return { state: "syncing", reason: "sync-pending", canRead: false, canWrite: false };
1737
- }
1738
- if (!engine.isCoordinatorMode()) {
1739
- return staticHealth(engine, phase);
1740
- }
1741
- const state = engine.coordinatorState;
1742
- if (!state) {
1743
- return { state: "unavailable", reason: "no-group-state", canRead: false, canWrite: false };
1744
- }
1745
- const nodeId = engine.nodeId;
1746
- const canRead = phase === "ready" && !isSidelined(state, nodeId);
1747
- const canWrite = canRead && engine.coordinatorAuthority;
1748
- if (state.repairingNodeIds.includes(nodeId)) {
1749
- return { state: "repairing", reason: "repairing", canRead, canWrite };
1750
- }
1751
- if (engine.coordinatorAuthority && !canWrite) {
1752
- return { state: "failing_over", reason: excludingReason(state, nodeId), canRead, canWrite };
1753
- }
1754
- if (!canRead) {
1755
- return { state: "unavailable", reason: excludingReason(state, nodeId), canRead, canWrite };
1904
+ function stopCoordinatorTimers(engine) {
1905
+ if (engine.coordinatorLeaseTimer) {
1906
+ clearInterval(engine.coordinatorLeaseTimer);
1907
+ engine.coordinatorLeaseTimer = null;
1756
1908
  }
1757
- if (!isCoordinatorConnected(engine)) {
1758
- return { state: "degraded", reason: "coordinator-unreachable", canRead, canWrite };
1909
+ if (engine.inSyncReconcileTimer) {
1910
+ clearInterval(engine.inSyncReconcileTimer);
1911
+ engine.inSyncReconcileTimer = null;
1759
1912
  }
1760
- if (!state.inSyncNodeIds.includes(nodeId)) {
1761
- return { state: "degraded", reason: "lagging", canRead, canWrite };
1913
+ if (engine.controllerTimer) {
1914
+ clearInterval(engine.controllerTimer);
1915
+ engine.controllerTimer = null;
1762
1916
  }
1763
- return { state: "healthy", reason: "in-sync", canRead, canWrite };
1764
1917
  }
1765
-
1766
- // src/replication/engine/sender-loop.ts
1767
- var SenderLoop = class {
1768
- constructor(engine) {
1769
- this.engine = engine;
1770
- }
1771
- senderTimer = null;
1772
- start() {
1773
- const engine = this.engine;
1774
- if (!engine.running) return;
1775
- const timer = setTimeout(async () => {
1776
- if (!engine.running) return;
1777
- try {
1778
- engine.lastSentSeq = await engine.log.getLocalSeq();
1779
- await this.sendPendingBatches();
1780
- await this.updatePruneBoundary();
1781
- } catch (err) {
1782
- const wrappedErr = err instanceof Error ? err : new Error(String(err));
1783
- engine.emitError({ error: wrappedErr, operation: "sender-loop", recoverable: true });
1784
- }
1785
- this.start();
1786
- }, engine.batchIntervalMs);
1787
- timer.unref?.();
1788
- this.senderTimer = timer;
1789
- }
1790
- stop() {
1791
- if (this.senderTimer !== null) {
1792
- clearTimeout(this.senderTimer);
1793
- this.senderTimer = null;
1794
- }
1795
- }
1796
- async updatePruneBoundary() {
1797
- const engine = this.engine;
1798
- if (!engine.tracker) return;
1799
- const minAcked = await engine.log.getMinAckedSeq();
1800
- if (minAcked === null) {
1801
- engine.tracker.clearPruneBoundary();
1802
- } else {
1803
- engine.tracker.setPruneBoundary(minAcked);
1804
- }
1918
+ async function stopCoordinatorMode(engine) {
1919
+ const config = engine.config.coordinator;
1920
+ if (!config) return;
1921
+ if (engine.coordinatorWatchDisposer) {
1922
+ await engine.coordinatorWatchDisposer();
1923
+ engine.coordinatorWatchDisposer = null;
1805
1924
  }
1806
- async sendPendingBatches() {
1807
- const engine = this.engine;
1808
- const peers = engine.config.transport.peers();
1809
- const nowMs = Date.now();
1810
- for (const [peerId, peerInfo] of peers) {
1811
- if (!this.shouldReplicateTo(peerId, peerInfo.role)) {
1812
- continue;
1813
- }
1814
- const peerState = engine.peerTracker.getPeerState(peerId);
1815
- if (peerState) {
1816
- engine.peerTracker.expireTimedOutBatches(peerId, nowMs, engine.ackTimeoutMs);
1817
- }
1818
- if (peerState && peerState.pendingBatches >= engine.maxPendingBatches) {
1819
- continue;
1820
- }
1821
- const fromSeq = peerState?.lastSentSeq ?? engine.lastSentSeq;
1822
- const rawBatch = await engine.log.readBatch(fromSeq, engine.batchSize);
1823
- if (!rawBatch) continue;
1824
- const batch = engine.decorate(rawBatch);
1825
- const previousSeq = peerState?.lastSentSeq ?? 0n;
1826
- if (peerState) {
1827
- peerState.pendingBatches += 1;
1828
- peerState.lastSentSeq = batch.toSeq;
1829
- engine.peerTracker.recordInFlightBatch(peerId, {
1830
- batchId: batch.batchId,
1831
- fromSeq: batch.fromSeq,
1832
- toSeq: batch.toSeq,
1833
- sentAt: nowMs
1834
- });
1835
- }
1836
- engine.config.transport.send(peerId, batch).catch((err) => {
1837
- const wrappedErr = err instanceof Error ? err : new Error(String(err));
1838
- engine.emitError({ error: wrappedErr, operation: "transport-send", peerId, recoverable: true });
1839
- if (peerState) {
1840
- const idx = peerState.inFlightBatches.findIndex((b) => b.batchId === batch.batchId);
1841
- if (idx >= 0) {
1842
- peerState.inFlightBatches.splice(idx, 1);
1843
- }
1844
- if (peerState.pendingBatches > 0) {
1845
- peerState.pendingBatches -= 1;
1846
- }
1847
- if (previousSeq < peerState.lastSentSeq) {
1848
- peerState.lastSentSeq = previousSeq;
1849
- }
1850
- }
1851
- });
1852
- if (engine.config.flowControl?.maxLagSeconds && peerState) {
1853
- const lagMs = Number(batch.toSeq - peerState.lastAckedSeq) * engine.batchIntervalMs;
1854
- const maxLagMs = engine.config.flowControl.maxLagSeconds * 1e3;
1855
- if (lagMs > maxLagMs && engine.config.flowControl.onLagExceeded) {
1856
- engine.config.flowControl.onLagExceeded(peerId, lagMs);
1857
- }
1858
- }
1859
- }
1925
+ if (engine.nodeSessionWatchDisposer) {
1926
+ await engine.nodeSessionWatchDisposer();
1927
+ engine.nodeSessionWatchDisposer = null;
1860
1928
  }
1861
- shouldReplicateTo(peerId, peerRole) {
1862
- const engine = this.engine;
1863
- if (!engine.isCoordinatorMode()) {
1864
- return engine.config.topology.shouldReplicateTo(peerId, peerRole);
1865
- }
1866
- return engine.coordinatorAuthority && peerId !== engine.nodeId;
1929
+ engine.liveNodeIds = null;
1930
+ await stopControllerLeaseWatch(engine);
1931
+ if (engine.controllerLeaseId) {
1932
+ const leaseId = engine.controllerLeaseId;
1933
+ engine.controllerLeaseId = null;
1934
+ await config.coordinator.releaseLease(leaseId).catch((err) => {
1935
+ const wrappedErr = err instanceof Error ? err : new Error(String(err));
1936
+ engine.emitError({ error: wrappedErr, operation: "coordinator-controller-release", recoverable: true });
1937
+ });
1867
1938
  }
1868
- };
1939
+ await config.coordinator.deregisterNodeSession(config.clusterId, engine.nodeId).catch((err) => {
1940
+ const wrappedErr = err instanceof Error ? err : new Error(String(err));
1941
+ engine.emitError({ error: wrappedErr, operation: "coordinator-session-deregister", recoverable: true });
1942
+ });
1943
+ }
1869
1944
 
1870
1945
  // src/replication/engine/test-hooks.ts
1871
1946
  var ackDelayByEngine = /* @__PURE__ */ new WeakMap();
@@ -1958,6 +2033,7 @@ function wireTransportHandlers(engine) {
1958
2033
  await refreshTriggersAfterDdl(engine);
1959
2034
  }
1960
2035
  await engine.log.setLastAppliedSeq(fromPeerId, batch.toSeq);
2036
+ engine.peerTracker.onBatchApplied(fromPeerId, batch.hlcRange.max);
1961
2037
  const previousApplied = engine.appliedSeqByPeer.get(fromPeerId) ?? 0n;
1962
2038
  if (batch.toSeq > previousApplied) {
1963
2039
  engine.appliedSeqByPeer.set(fromPeerId, batch.toSeq);
@@ -2018,7 +2094,7 @@ function wireTransportHandlers(engine) {
2018
2094
  throw new ReplicationError(`Rejected forward from unknown peer: ${fromPeerId}`);
2019
2095
  }
2020
2096
  try {
2021
- const result = await engine.localExecutor.executeForwardedLocally(request.statements);
2097
+ const result = await engine.localExecutor.executeForwardedLocally(request.statements, request.writeConcern);
2022
2098
  return engine.decorate(result);
2023
2099
  } catch (err) {
2024
2100
  const wrappedErr = err instanceof Error ? err : new Error(String(err));
@@ -2205,6 +2281,27 @@ async function startEngine(engine) {
2205
2281
  }
2206
2282
  engine.senderLoop.start();
2207
2283
  }
2284
+ async function stopEngine(engine) {
2285
+ if (!engine.running) return;
2286
+ engine.running = false;
2287
+ stopCoordinatorTimers(engine);
2288
+ engine.syncJoiner.stopTimers();
2289
+ engine.syncServer.abortAll();
2290
+ if (engine.syncState.phase === "syncing") {
2291
+ try {
2292
+ await setForeignKeysEnabled(engine.writerConn, true);
2293
+ } catch (err) {
2294
+ const wrappedErr = err instanceof Error ? err : new Error(String(err));
2295
+ engine.emitError({ error: wrappedErr, operation: "engine-stop-pragma-restore", recoverable: false });
2296
+ }
2297
+ }
2298
+ engine.senderLoop.stop();
2299
+ if (engine.tracker) {
2300
+ engine.tracker.clearPruneBoundary("replication");
2301
+ }
2302
+ await stopCoordinatorMode(engine);
2303
+ await engine.config.transport.disconnect();
2304
+ }
2208
2305
 
2209
2306
  // src/replication/engine/sync-recovery.ts
2210
2307
  var SYNC_RETRY_MIN_DELAY_MS = 1e3;
@@ -2984,7 +3081,7 @@ var ReplicationEngine = class extends EventEmitter {
2984
3081
  /** @internal */
2985
3082
  config;
2986
3083
  /**
2987
- * Identifier of this node, which every change it authors carries.
3084
+ * Identifies this node, and the engine stamps it on every change that this node writes.
2988
3085
  */
2989
3086
  nodeId;
2990
3087
  /** @internal */
@@ -3042,6 +3139,16 @@ var ReplicationEngine = class extends EventEmitter {
3042
3139
  /** @internal */
3043
3140
  coordinatorWatchDisposer = null;
3044
3141
  /** @internal */
3142
+ nodeSessionWatchDisposer = null;
3143
+ /** @internal */
3144
+ controllerLeaseWatchDisposer = null;
3145
+ /** @internal */
3146
+ observedControllerLease = null;
3147
+ /** @internal */
3148
+ controllerBidding = false;
3149
+ /** @internal */
3150
+ liveNodeIds = null;
3151
+ /** @internal */
3045
3152
  coordinatorLeaseTimer = null;
3046
3153
  /** @internal */
3047
3154
  controllerTimer = null;
@@ -3112,39 +3219,28 @@ var ReplicationEngine = class extends EventEmitter {
3112
3219
  installTestHooks(this);
3113
3220
  }
3114
3221
  /**
3115
- * Connects the transport, pulls a full copy when this node needs one, and starts replicating.
3222
+ * Connects the transport and starts replicating. When this node is a replica without a complete, current copy, it
3223
+ * also requests a full copy from a source peer, and the returned promise resolves before that copy finishes.
3116
3224
  */
3117
- start() {
3118
- return startEngine(this);
3225
+ async start() {
3226
+ try {
3227
+ await startEngine(this);
3228
+ } catch (err) {
3229
+ await this.stop().catch(() => {
3230
+ });
3231
+ throw err;
3232
+ }
3119
3233
  }
3120
3234
  /**
3121
- * Stops replicating, abandons any sync in flight, and disconnects the transport.
3235
+ * Stops replicating, aborts every first sync that this node serves, and disconnects the transport.
3122
3236
  */
3123
3237
  async stop() {
3124
- if (!this.running) return;
3125
- this.running = false;
3126
- stopCoordinatorTimers(this);
3127
- this.syncJoiner.stopTimers();
3128
- this.syncServer.abortAll();
3129
- if (this.syncState.phase === "syncing") {
3130
- try {
3131
- await setForeignKeysEnabled(this.writerConn, true);
3132
- } catch (err) {
3133
- const wrappedErr = err instanceof Error ? err : new Error(String(err));
3134
- this.emitError({ error: wrappedErr, operation: "engine-stop-pragma-restore", recoverable: false });
3135
- }
3136
- }
3137
- this.senderLoop.stop();
3138
- if (this.tracker) {
3139
- this.tracker.clearPruneBoundary();
3140
- }
3141
- await stopCoordinatorMode(this);
3142
- await this.config.transport.disconnect();
3238
+ await stopEngine(this);
3143
3239
  }
3144
3240
  /**
3145
- * Reports where this node stands.
3241
+ * Returns this node's replication status.
3146
3242
  *
3147
- * @returns The node's role, its peers, its progress, its health, and its group state.
3243
+ * @returns The node's role, peers, sequence position, sync state, health, and coordinator group state.
3148
3244
  */
3149
3245
  status() {
3150
3246
  return {
@@ -3159,74 +3255,78 @@ var ReplicationEngine = class extends EventEmitter {
3159
3255
  };
3160
3256
  }
3161
3257
  /**
3162
- * Returns the highest change-log position this node has recorded locally.
3258
+ * Returns the highest position in this node's local change log.
3163
3259
  *
3164
- * @returns That position, which a caller waits for a replica to reach.
3260
+ * @returns That position, which a caller can wait for a replica to reach.
3165
3261
  */
3166
3262
  getCurrentSeq() {
3167
3263
  return this.lastLocalSeq;
3168
3264
  }
3169
3265
  /**
3170
- * Returns how far this node has applied one peer's changes.
3266
+ * Returns the highest applied position from one peer's change log.
3171
3267
  *
3172
- * @param peerId - Identifier of the peer.
3173
- * @returns The highest position from that peer this node has applied.
3268
+ * @param peerId - The peer's node ID.
3269
+ * @returns That position, or `0n` before this node applies any change from that peer.
3174
3270
  */
3175
3271
  getAppliedSeq(peerId) {
3176
3272
  return this.appliedSeqByPeer.get(peerId) ?? 0n;
3177
3273
  }
3178
3274
  /**
3179
- * Runs a read, refusing it when the node cannot meet the read concern.
3275
+ * Executes a read, and throws when this node is still syncing or cannot meet the read concern.
3180
3276
  *
3181
- * @param sql - The statement to run.
3182
- * @param params - Values bound to the statement, named or positional.
3183
- * @param options - Read concern for this statement.
3184
- * @returns The rows the statement produced.
3277
+ * @param sql - The statement to execute.
3278
+ * @param params - The values to bind to the statement, named or positional.
3279
+ * @param options - The read concern for this statement.
3280
+ * @returns The rows that the statement returns.
3185
3281
  */
3186
3282
  query(sql, params, options) {
3187
3283
  return query(this, sql, params, options);
3188
3284
  }
3189
3285
  /**
3190
- * Runs one write, forwarding it to the primary when this node accepts no writes and forwarding is on.
3286
+ * Executes one write on this node, or forwards it to the primary when this node cannot accept writes and
3287
+ * `writeForwarding` is on.
3191
3288
  *
3192
- * @param sql - The statement to run.
3193
- * @param params - Values bound to the statement, named or positional.
3194
- * @param options - Write concern for this statement.
3195
- * @returns How many rows changed, and the last inserted row id.
3289
+ * @param sql - The statement to execute.
3290
+ * @param params - The values to bind to the statement, named or positional.
3291
+ * @param options - The write concern for this statement, which the primary also applies to a forwarded write before it answers.
3292
+ * @returns The number of rows that changed and the ID of the last inserted row.
3196
3293
  */
3197
3294
  execute(sql, params, options) {
3198
- return execute(this, sql, params, options);
3295
+ return execute2(this, sql, params, options);
3199
3296
  }
3200
3297
  /**
3201
- * Runs one statement over many parameter sets in a single transaction.
3298
+ * Executes one statement once for each parameter set and commits every set in one transaction, on this node or, when
3299
+ * this node forwards the batch, on the primary.
3202
3300
  *
3203
- * @param sql - The statement to run for each parameter set.
3204
- * @param paramsBatch - One parameter set per run.
3205
- * @param options - Write concern for the transaction.
3301
+ * @param sql - The statement to execute for each parameter set.
3302
+ * @param paramsBatch - One parameter set per execution.
3303
+ * @param options - The write concern for the batch, which the primary also applies to a forwarded batch before it answers.
3206
3304
  * @returns One result per parameter set, in order.
3207
3305
  */
3208
3306
  executeBatch(sql, paramsBatch, options) {
3209
3307
  return executeBatch(this, sql, paramsBatch, options);
3210
3308
  }
3211
3309
  /**
3212
- * Runs a function inside one transaction on this node.
3310
+ * Calls `fn` inside one transaction on this node. On a node that cannot accept writes, it throws a `TopologyError`,
3311
+ * since the engine forwards only `execute` and `executeBatch` writes to the primary.
3213
3312
  *
3214
- * @param fn - Receives the transaction and runs statements on it.
3215
- * @param options - Write concern for the transaction.
3216
- * @returns Whatever the function returned.
3313
+ * @param fn - The function that receives the transaction and executes statements on it.
3314
+ * @param options - The write concern to wait for after the transaction commits.
3315
+ * @returns The value that `fn` resolves to.
3217
3316
  */
3218
3317
  transaction(fn, options) {
3219
3318
  return transaction(this, fn, options);
3220
3319
  }
3221
3320
  /**
3222
- * Sends a write to the primary and waits for its result.
3321
+ * Sends the statements to the primary, which executes them in one transaction, and returns the primary's result.
3322
+ * When this node is the primary, it executes them here in the same way.
3223
3323
  *
3224
- * @param statements - The statements to run, in order, each with its own parameters.
3225
- * @param options - Write concern the primary applies.
3226
- * @returns What the primary reported for each statement.
3324
+ * @param statements - The statements to execute, in order, each with its own parameters.
3325
+ * @param options - The write concern that the primary applies to the statements before it answers.
3326
+ * @returns The result of each statement, in order, and the request ID.
3227
3327
  */
3228
3328
  forwardStatements(statements, options) {
3229
- return forwardStatements(this, statements);
3329
+ return forwardStatements(this, statements, options);
3230
3330
  }
3231
3331
  /** @internal */
3232
3332
  emitError(event) {
@@ -3254,7 +3354,7 @@ var ReplicationEngine = class extends EventEmitter {
3254
3354
  return getForwardingPrimaryPeerId(this);
3255
3355
  }
3256
3356
  /**
3257
- * Stamps an outgoing replication message with this node's group and primary term.
3357
+ * Returns a copy of an outgoing replication message with this node's group ID and primary term added.
3258
3358
  *
3259
3359
  * @internal
3260
3360
  */
@@ -3266,7 +3366,7 @@ var ReplicationEngine = class extends EventEmitter {
3266
3366
  // src/replication/topology/primary-replica.ts
3267
3367
  var PrimaryReplicaTopology = class {
3268
3368
  /**
3269
- * Whether this node accepts writes or serves reads.
3369
+ * Records whether this node is the primary, which accepts writes, or a replica, which serves reads.
3270
3370
  */
3271
3371
  role;
3272
3372
  constructor(role) {
@@ -3291,7 +3391,7 @@ var PrimaryReplicaTopology = class {
3291
3391
  return this.role === "primary" && peerRole === "replica";
3292
3392
  }
3293
3393
  /**
3294
- * Reports whether this node applies changes arriving from a given peer.
3394
+ * Reports whether this node applies the changes that a given peer sends.
3295
3395
  *
3296
3396
  * @param _peerId - Identifier of the peer, which this topology ignores.
3297
3397
  * @param peerRole - Role of the peer.
@@ -3301,7 +3401,8 @@ var PrimaryReplicaTopology = class {
3301
3401
  return this.role === "replica" && peerRole === "primary";
3302
3402
  }
3303
3403
  /**
3304
- * Reports whether incoming changes need a resolver, which a single writer removes the need for.
3404
+ * Reports whether the engine has to resolve conflicts for this topology. Only the primary writes, so the answer is
3405
+ * always false.
3305
3406
  *
3306
3407
  * @returns False.
3307
3408
  */