@peerbit/shared-log 13.2.34 → 13.2.36

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.
@@ -204,9 +204,10 @@ export type ReplicationAnnouncementDeps<R extends "u32" | "u64"> = {
204
204
  // Owner-routed so coordinator spies keep observing re-entrant queueing.
205
205
  queueCurrentReplicationStateAnnouncementRepair: () => void;
206
206
  queueCurrentReplicationStateAnnouncementRetry: (error: unknown) => boolean;
207
- // Best-effort sidecar. The legacy publish remains the primary operation and
208
- // retains its exact rejection/retry semantics during mixed-version rollout.
207
+ // V2 is always current. Legacy publication is enabled only for an explicit
208
+ // compatibility open and retains its exact rejection/retry semantics there.
209
209
  enqueueReplicationInfoV2: (message: LegacyReplicationInfoMessage) => void;
210
+ isLegacyReplicationInfoEnabled: () => boolean;
210
211
  };
211
212
 
212
213
  type LegacyReplicationInfoMessage =
@@ -270,6 +271,7 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
270
271
 
271
272
  queueCurrentReplicationStateAnnouncementRetry(error: unknown): boolean {
272
273
  if (
274
+ !this.deps.isLegacyReplicationInfoEnabled() ||
273
275
  this.deps.isClosed() ||
274
276
  this.deps.getCloseSignal().aborted ||
275
277
  this._replicationAnnouncementRetryController.signal.aborted ||
@@ -362,6 +364,7 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
362
364
 
363
365
  queueCurrentReplicationStateAnnouncementRepair(): void {
364
366
  if (
367
+ !this.deps.isLegacyReplicationInfoEnabled() ||
365
368
  this.deps.isClosed() ||
366
369
  this.deps.getCloseSignal().aborted ||
367
370
  this._replicationAnnouncementRepairController.signal.aborted ||
@@ -437,6 +440,11 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
437
440
  async repairCurrentReplicationStateAnnouncement(
438
441
  context?: ReplicationAnnouncementRepairWorkerContext,
439
442
  ): Promise<void> {
443
+ if (!this.deps.isLegacyReplicationInfoEnabled()) {
444
+ this._announcementRepairBinding.pending = false;
445
+ this._announcementRepairBinding.targets.clear();
446
+ return;
447
+ }
440
448
  if (!this._announcementRepairBinding.pending) {
441
449
  return;
442
450
  }
@@ -645,6 +653,9 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
645
653
  this.rotateAnnouncementSession();
646
654
  this.advanceCurrentReplicationStateAnnouncementRepairGeneration();
647
655
  this.deps.enqueueReplicationInfoV2(message);
656
+ if (!this.deps.isLegacyReplicationInfoEnabled()) {
657
+ return;
658
+ }
648
659
  try {
649
660
  await this.deps.getRpc().send(message, {
650
661
  priority: CONVERGENCE_MESSAGE_PRIORITY,
@@ -677,6 +688,10 @@ export class ReplicationAnnouncementCoordinator<R extends "u32" | "u64"> {
677
688
  }
678
689
 
679
690
  async retryCurrentReplicationStateAnnouncement(): Promise<void> {
691
+ if (!this.deps.isLegacyReplicationInfoEnabled()) {
692
+ this._replicationAnnouncementRetryPending = false;
693
+ return;
694
+ }
680
695
  const session = this._announcementSession;
681
696
  const controller = this._replicationAnnouncementRetryController;
682
697
  try {
@@ -991,6 +991,83 @@ export class ReplicationInfoV2ReceiveCoordinator {
991
991
  return true;
992
992
  }
993
993
 
994
+ /**
995
+ * Whether the request generation for this exact peer state is parked: the
996
+ * bounded retry cycle exhausted its attempts and no timer or send is in
997
+ * flight. Read-only probe for the host's recovery scheduler.
998
+ */
999
+ isRequestParked(properties: {
1000
+ peerHash: string;
1001
+ peerSession: object;
1002
+ receiveEpoch: object | null;
1003
+ }): boolean {
1004
+ const state = this._receiveStates.get(properties.peerHash);
1005
+ return (
1006
+ state !== undefined &&
1007
+ state.peerSession === properties.peerSession &&
1008
+ state.receiveEpoch === properties.receiveEpoch &&
1009
+ state.phase !== "active" &&
1010
+ state.requestParked &&
1011
+ state.requestTimer === undefined &&
1012
+ state.requestInFlight === undefined
1013
+ );
1014
+ }
1015
+
1016
+ /**
1017
+ * Resume only a request generation that exhausted its bounded retries.
1018
+ * Wait/liveness callers may nudge recovery without invalidating an active,
1019
+ * timed or in-flight request and without advancing the receive epoch.
1020
+ */
1021
+ resumeParkedRequest(properties: {
1022
+ peerHash: string;
1023
+ peerSession: object;
1024
+ receiveEpoch: object | null;
1025
+ }): boolean {
1026
+ const state = this._receiveStates.get(properties.peerHash);
1027
+ if (
1028
+ !state ||
1029
+ state.peerSession !== properties.peerSession ||
1030
+ state.receiveEpoch !== properties.receiveEpoch ||
1031
+ !this.deps.isPeerStateCurrent(
1032
+ properties.peerHash,
1033
+ properties.peerSession,
1034
+ properties.receiveEpoch,
1035
+ ) ||
1036
+ state.phase === "active" ||
1037
+ !state.requestParked ||
1038
+ state.requestTimer !== undefined ||
1039
+ state.requestInFlight !== undefined ||
1040
+ this._reservedAdmissionsByPeer.has(properties.peerHash) ||
1041
+ state.receiverBinding === undefined ||
1042
+ state.lastSequence === MAX_U64
1043
+ ) {
1044
+ return false;
1045
+ }
1046
+ state.requestAttempts = 0;
1047
+ state.requestsSinceCapabilityRefresh = 0;
1048
+ if (!state.capabilityRefreshRequired) {
1049
+ // Only rotate the grant when it is genuinely stale: the peer's
1050
+ // capability rotation paths already flagged a refresh, and a missing
1051
+ // or transport-outdated local grant cannot authorize a request. A
1052
+ // still-current grant must keep its challenge so a Full already in
1053
+ // flight for the pre-park request generation still applies.
1054
+ const ready = this._localCapabilityReadyBySession.get(state.peerSession);
1055
+ const grantCurrent =
1056
+ ready !== undefined &&
1057
+ ready.peerHash === state.peerHash &&
1058
+ ready.receiveEpoch === state.receiveEpoch &&
1059
+ ready.receiverTransportSession === state.receiverTransportSession &&
1060
+ ready.receiverTransportSession ===
1061
+ this.deps.getReceiverTransportSession();
1062
+ if (!grantCurrent) {
1063
+ state.capabilityRefreshRequired = true;
1064
+ }
1065
+ }
1066
+ state.requestParked = false;
1067
+ this.armRequest(state, 0);
1068
+ return true;
1069
+ }
1070
+
994
1071
  private transitionToResync(
995
1072
  state: ReplicationInfoV2ReceiveState,
996
1073
  options?: { force?: boolean; refreshCapability?: boolean },
@@ -1154,6 +1231,14 @@ export class ReplicationInfoV2ReceiveCoordinator {
1154
1231
  const { state } = admission;
1155
1232
  this._reservedAdmissionsByPeer.set(peerHash, admission);
1156
1233
  state.reservedAdmission = admission;
1234
+ if (state.requestTimer) {
1235
+ clearTimeout(state.requestTimer);
1236
+ state.requestTimer = undefined;
1237
+ }
1238
+ if (state.legacyFallbackTimer) {
1239
+ clearTimeout(state.legacyFallbackTimer);
1240
+ state.legacyFallbackTimer = undefined;
1241
+ }
1157
1242
  return admission;
1158
1243
  }
1159
1244
 
@@ -1174,6 +1259,30 @@ export class ReplicationInfoV2ReceiveCoordinator {
1174
1259
  (currentState !== state && currentState.phase !== "active"))
1175
1260
  ) {
1176
1261
  this.transitionToResync(currentState, { force: true });
1262
+ } else if (
1263
+ currentState === state &&
1264
+ !admission.committed &&
1265
+ this.isStateCurrent(state) &&
1266
+ state.phase !== "active" &&
1267
+ state.requestTimer === undefined &&
1268
+ state.requestInFlight === undefined &&
1269
+ !state.requestParked
1270
+ ) {
1271
+ // Reserving a Full pauses this generation's retry timer. If its host
1272
+ // apply lane releases without committing, restore the bounded request
1273
+ // worker so the exact current generation cannot become stranded.
1274
+ this.armRequest(state, 0);
1275
+ }
1276
+ if (
1277
+ currentState === state &&
1278
+ this.isStateCurrent(state) &&
1279
+ state.phase === "active" &&
1280
+ state.legacyFallbackFingerprint !== undefined
1281
+ ) {
1282
+ // A reservation also pauses compat sidecar recovery. Matching commits
1283
+ // clear the evidence; an uncommitted or non-matching frame restores its
1284
+ // bounded fallback without invalidating work in the host apply lane.
1285
+ this.armLegacyFallback(state);
1177
1286
  }
1178
1287
  }
1179
1288
 
@@ -1281,6 +1390,19 @@ export class ReplicationInfoV2ReceiveCoordinator {
1281
1390
  if (state.legacyFallbackTimer) {
1282
1391
  return true;
1283
1392
  }
1393
+ this.armLegacyFallback(state);
1394
+ return true;
1395
+ }
1396
+
1397
+ private armLegacyFallback(state: ReplicationInfoV2ReceiveState): void {
1398
+ if (
1399
+ state.legacyFallbackTimer ||
1400
+ state.phase !== "active" ||
1401
+ state.legacyFallbackFingerprint === undefined ||
1402
+ this._reservedAdmissionsByPeer.has(state.peerHash)
1403
+ ) {
1404
+ return;
1405
+ }
1284
1406
  state.legacyFallbackTimer = setTimeout(() => {
1285
1407
  state.legacyFallbackTimer = undefined;
1286
1408
  if (this.isStateCurrent(state) && state.phase === "active") {
@@ -1291,7 +1413,6 @@ export class ReplicationInfoV2ReceiveCoordinator {
1291
1413
  }
1292
1414
  }, this.legacyFallbackDelayMs);
1293
1415
  state.legacyFallbackTimer.unref?.();
1294
- return true;
1295
1416
  }
1296
1417
 
1297
1418
  private clearLegacyFallback(state: ReplicationInfoV2ReceiveState): void {
@@ -1418,8 +1539,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
1418
1539
  this._receiveStates.get(state.peerHash) !== state ||
1419
1540
  state.controller.signal.aborted ||
1420
1541
  this.deps.isClosed() ||
1421
- (this._reservedAdmissionsByPeer.has(state.peerHash) &&
1422
- this._reservedAdmissionsByPeer.get(state.peerHash)?.state !== state) ||
1542
+ this._reservedAdmissionsByPeer.has(state.peerHash) ||
1423
1543
  state.receiverBinding === undefined ||
1424
1544
  state.phase === "active" ||
1425
1545
  state.requestParked ||
@@ -1449,6 +1569,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
1449
1569
  private async refreshGrant(
1450
1570
  state: ReplicationInfoV2ReceiveState,
1451
1571
  ): Promise<boolean> {
1572
+ const version = state.version;
1452
1573
  const refreshed = await this.deps.refreshLocalCapability({
1453
1574
  peerHash: state.peerHash,
1454
1575
  target: state.target,
@@ -1458,6 +1579,8 @@ export class ReplicationInfoV2ReceiveCoordinator {
1458
1579
  });
1459
1580
  if (
1460
1581
  !refreshed ||
1582
+ state.version !== version ||
1583
+ this._reservedAdmissionsByPeer.has(state.peerHash) ||
1461
1584
  !this.isStateCurrent(state) ||
1462
1585
  this.deps.getReceiverTransportSession() !==
1463
1586
  refreshed.receiverTransportSession
@@ -1489,7 +1612,10 @@ export class ReplicationInfoV2ReceiveCoordinator {
1489
1612
  if (state.requestInFlight) {
1490
1613
  return;
1491
1614
  }
1492
- if (!this.isStateCurrent(state)) {
1615
+ if (
1616
+ !this.isStateCurrent(state) ||
1617
+ this._reservedAdmissionsByPeer.has(state.peerHash)
1618
+ ) {
1493
1619
  return;
1494
1620
  }
1495
1621
  if (
@@ -1512,7 +1638,10 @@ export class ReplicationInfoV2ReceiveCoordinator {
1512
1638
  return;
1513
1639
  }
1514
1640
  }
1515
- if (!this.isStateCurrent(state)) {
1641
+ if (
1642
+ !this.isStateCurrent(state) ||
1643
+ this._reservedAdmissionsByPeer.has(state.peerHash)
1644
+ ) {
1516
1645
  return;
1517
1646
  }
1518
1647
  const ready = this._localCapabilityReadyBySession.get(state.peerSession);
@@ -1558,6 +1687,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
1558
1687
  if (
1559
1688
  this._receiveStates.get(state.peerHash) === state &&
1560
1689
  !state.controller.signal.aborted &&
1690
+ !this._reservedAdmissionsByPeer.has(state.peerHash) &&
1561
1691
  !state.requestTimer &&
1562
1692
  state.phase !== "active"
1563
1693
  ) {
@@ -649,7 +649,27 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
649
649
  sends.push(Promise.reject(error));
650
650
  }
651
651
  }
652
- await Promise.allSettled(sends);
652
+ if (sends.length === 0 || signal.aborted) {
653
+ return;
654
+ }
655
+
656
+ // Terminal reset is best-effort and the caller owns its close/drop bound.
657
+ // A transport is expected to observe the signal, but a disconnected
658
+ // acknowledgement path can leave its promise pending indefinitely. Do not
659
+ // let such a transport retain the whole terminal operation past the bound.
660
+ await new Promise<void>((resolve) => {
661
+ let finished = false;
662
+ const finish = () => {
663
+ if (finished) return;
664
+ finished = true;
665
+ signal.removeEventListener("abort", finish);
666
+ resolve();
667
+ };
668
+ signal.addEventListener("abort", finish, { once: true });
669
+ void Promise.allSettled(sends).then(finish);
670
+ // Close the check/listener race if the caller aborted synchronously.
671
+ if (signal.aborted) finish();
672
+ });
653
673
  }
654
674
 
655
675
  async drain(signal?: AbortSignal): Promise<void> {
@@ -114,7 +114,7 @@ export function createSyncronizer<R extends "u32" | "u64">(
114
114
  sendRawExchangeHeads: props.sendRawExchangeHeads,
115
115
  });
116
116
  } else {
117
- if (props.compatibility && props.compatibility < 10) {
117
+ if (props.compatibility !== undefined && props.compatibility < 10) {
118
118
  return new SimpleSyncronizer({
119
119
  log: props.log,
120
120
  rpc: props.rpc,