@peerbit/shared-log 13.2.34 → 13.2.35

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.
@@ -991,6 +991,44 @@ export class ReplicationInfoV2ReceiveCoordinator {
991
991
  return true;
992
992
  }
993
993
 
994
+ /**
995
+ * Resume only a request generation that exhausted its bounded retries.
996
+ * Wait/liveness callers may nudge recovery without invalidating an active,
997
+ * timed or in-flight request and without advancing the receive epoch.
998
+ */
999
+ resumeParkedRequest(properties: {
1000
+ peerHash: string;
1001
+ peerSession: object;
1002
+ receiveEpoch: object | null;
1003
+ }): boolean {
1004
+ const state = this._receiveStates.get(properties.peerHash);
1005
+ if (
1006
+ !state ||
1007
+ state.peerSession !== properties.peerSession ||
1008
+ state.receiveEpoch !== properties.receiveEpoch ||
1009
+ !this.deps.isPeerStateCurrent(
1010
+ properties.peerHash,
1011
+ properties.peerSession,
1012
+ properties.receiveEpoch,
1013
+ ) ||
1014
+ state.phase === "active" ||
1015
+ !state.requestParked ||
1016
+ state.requestTimer !== undefined ||
1017
+ state.requestInFlight !== undefined ||
1018
+ this._reservedAdmissionsByPeer.has(properties.peerHash) ||
1019
+ state.receiverBinding === undefined ||
1020
+ state.lastSequence === MAX_U64
1021
+ ) {
1022
+ return false;
1023
+ }
1024
+ state.requestAttempts = 0;
1025
+ state.requestsSinceCapabilityRefresh = 0;
1026
+ state.capabilityRefreshRequired = true;
1027
+ state.requestParked = false;
1028
+ this.armRequest(state, 0);
1029
+ return true;
1030
+ }
1031
+
994
1032
  private transitionToResync(
995
1033
  state: ReplicationInfoV2ReceiveState,
996
1034
  options?: { force?: boolean; refreshCapability?: boolean },
@@ -1154,6 +1192,14 @@ export class ReplicationInfoV2ReceiveCoordinator {
1154
1192
  const { state } = admission;
1155
1193
  this._reservedAdmissionsByPeer.set(peerHash, admission);
1156
1194
  state.reservedAdmission = admission;
1195
+ if (state.requestTimer) {
1196
+ clearTimeout(state.requestTimer);
1197
+ state.requestTimer = undefined;
1198
+ }
1199
+ if (state.legacyFallbackTimer) {
1200
+ clearTimeout(state.legacyFallbackTimer);
1201
+ state.legacyFallbackTimer = undefined;
1202
+ }
1157
1203
  return admission;
1158
1204
  }
1159
1205
 
@@ -1174,6 +1220,30 @@ export class ReplicationInfoV2ReceiveCoordinator {
1174
1220
  (currentState !== state && currentState.phase !== "active"))
1175
1221
  ) {
1176
1222
  this.transitionToResync(currentState, { force: true });
1223
+ } else if (
1224
+ currentState === state &&
1225
+ !admission.committed &&
1226
+ this.isStateCurrent(state) &&
1227
+ state.phase !== "active" &&
1228
+ state.requestTimer === undefined &&
1229
+ state.requestInFlight === undefined &&
1230
+ !state.requestParked
1231
+ ) {
1232
+ // Reserving a Full pauses this generation's retry timer. If its host
1233
+ // apply lane releases without committing, restore the bounded request
1234
+ // worker so the exact current generation cannot become stranded.
1235
+ this.armRequest(state, 0);
1236
+ }
1237
+ if (
1238
+ currentState === state &&
1239
+ this.isStateCurrent(state) &&
1240
+ state.phase === "active" &&
1241
+ state.legacyFallbackFingerprint !== undefined
1242
+ ) {
1243
+ // A reservation also pauses compat sidecar recovery. Matching commits
1244
+ // clear the evidence; an uncommitted or non-matching frame restores its
1245
+ // bounded fallback without invalidating work in the host apply lane.
1246
+ this.armLegacyFallback(state);
1177
1247
  }
1178
1248
  }
1179
1249
 
@@ -1281,6 +1351,19 @@ export class ReplicationInfoV2ReceiveCoordinator {
1281
1351
  if (state.legacyFallbackTimer) {
1282
1352
  return true;
1283
1353
  }
1354
+ this.armLegacyFallback(state);
1355
+ return true;
1356
+ }
1357
+
1358
+ private armLegacyFallback(state: ReplicationInfoV2ReceiveState): void {
1359
+ if (
1360
+ state.legacyFallbackTimer ||
1361
+ state.phase !== "active" ||
1362
+ state.legacyFallbackFingerprint === undefined ||
1363
+ this._reservedAdmissionsByPeer.has(state.peerHash)
1364
+ ) {
1365
+ return;
1366
+ }
1284
1367
  state.legacyFallbackTimer = setTimeout(() => {
1285
1368
  state.legacyFallbackTimer = undefined;
1286
1369
  if (this.isStateCurrent(state) && state.phase === "active") {
@@ -1291,7 +1374,6 @@ export class ReplicationInfoV2ReceiveCoordinator {
1291
1374
  }
1292
1375
  }, this.legacyFallbackDelayMs);
1293
1376
  state.legacyFallbackTimer.unref?.();
1294
- return true;
1295
1377
  }
1296
1378
 
1297
1379
  private clearLegacyFallback(state: ReplicationInfoV2ReceiveState): void {
@@ -1418,8 +1500,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
1418
1500
  this._receiveStates.get(state.peerHash) !== state ||
1419
1501
  state.controller.signal.aborted ||
1420
1502
  this.deps.isClosed() ||
1421
- (this._reservedAdmissionsByPeer.has(state.peerHash) &&
1422
- this._reservedAdmissionsByPeer.get(state.peerHash)?.state !== state) ||
1503
+ this._reservedAdmissionsByPeer.has(state.peerHash) ||
1423
1504
  state.receiverBinding === undefined ||
1424
1505
  state.phase === "active" ||
1425
1506
  state.requestParked ||
@@ -1449,6 +1530,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
1449
1530
  private async refreshGrant(
1450
1531
  state: ReplicationInfoV2ReceiveState,
1451
1532
  ): Promise<boolean> {
1533
+ const version = state.version;
1452
1534
  const refreshed = await this.deps.refreshLocalCapability({
1453
1535
  peerHash: state.peerHash,
1454
1536
  target: state.target,
@@ -1458,6 +1540,8 @@ export class ReplicationInfoV2ReceiveCoordinator {
1458
1540
  });
1459
1541
  if (
1460
1542
  !refreshed ||
1543
+ state.version !== version ||
1544
+ this._reservedAdmissionsByPeer.has(state.peerHash) ||
1461
1545
  !this.isStateCurrent(state) ||
1462
1546
  this.deps.getReceiverTransportSession() !==
1463
1547
  refreshed.receiverTransportSession
@@ -1489,7 +1573,10 @@ export class ReplicationInfoV2ReceiveCoordinator {
1489
1573
  if (state.requestInFlight) {
1490
1574
  return;
1491
1575
  }
1492
- if (!this.isStateCurrent(state)) {
1576
+ if (
1577
+ !this.isStateCurrent(state) ||
1578
+ this._reservedAdmissionsByPeer.has(state.peerHash)
1579
+ ) {
1493
1580
  return;
1494
1581
  }
1495
1582
  if (
@@ -1512,7 +1599,10 @@ export class ReplicationInfoV2ReceiveCoordinator {
1512
1599
  return;
1513
1600
  }
1514
1601
  }
1515
- if (!this.isStateCurrent(state)) {
1602
+ if (
1603
+ !this.isStateCurrent(state) ||
1604
+ this._reservedAdmissionsByPeer.has(state.peerHash)
1605
+ ) {
1516
1606
  return;
1517
1607
  }
1518
1608
  const ready = this._localCapabilityReadyBySession.get(state.peerSession);
@@ -1558,6 +1648,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
1558
1648
  if (
1559
1649
  this._receiveStates.get(state.peerHash) === state &&
1560
1650
  !state.controller.signal.aborted &&
1651
+ !this._reservedAdmissionsByPeer.has(state.peerHash) &&
1561
1652
  !state.requestTimer &&
1562
1653
  state.phase !== "active"
1563
1654
  ) {
@@ -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,