@peerbit/shared-log 16.0.39 → 16.0.40

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.
package/src/index.ts CHANGED
@@ -4134,6 +4134,7 @@ export class SharedLog<
4134
4134
  receiveEpoch: object | null;
4135
4135
  signal: AbortSignal;
4136
4136
  requestRemoteFullRearm?: boolean;
4137
+ acknowledgeRearm?: boolean;
4137
4138
  }): Promise<
4138
4139
  { receiverTransportSession: bigint; requestNotBeforeMs: number } | undefined
4139
4140
  > {
@@ -4148,15 +4149,16 @@ export class SharedLog<
4148
4149
  : 0),
4149
4150
  }),
4150
4151
  {
4151
- // The transient rearm is only a bounded recovery hint. The ensuing
4152
- // authenticated Full/Applied exchange is the readiness proof, so do not
4153
- // let an ACK-delivery promise park the recovery scheduler itself.
4154
- mode: properties.requestRemoteFullRearm
4155
- ? new SilentDelivery({ redundancy: 1, to: [properties.target] })
4156
- : new AcknowledgeDelivery({
4157
- redundancy: 1,
4158
- to: [properties.target],
4159
- }),
4152
+ // Watchdog hints stay silent so ACK latency cannot park that scheduler.
4153
+ // Opening adverts still need an ACK before granting local receive
4154
+ // readiness, even when they also solicit reciprocal recovery.
4155
+ mode:
4156
+ properties.requestRemoteFullRearm && !properties.acknowledgeRearm
4157
+ ? new SilentDelivery({ redundancy: 1, to: [properties.target] })
4158
+ : new AcknowledgeDelivery({
4159
+ redundancy: 1,
4160
+ to: [properties.target],
4161
+ }),
4160
4162
  priority: CONVERGENCE_MESSAGE_PRIORITY,
4161
4163
  signal: properties.signal,
4162
4164
  },
@@ -4218,6 +4220,7 @@ export class SharedLog<
4218
4220
  receiveEpoch: properties.receiveEpoch,
4219
4221
  signal: properties.signal,
4220
4222
  requestRemoteFullRearm: properties.requestRemoteFullRearm,
4223
+ acknowledgeRearm: properties.acknowledgeRearm,
4221
4224
  }),
4222
4225
  onRequestError: (error) => {
4223
4226
  if (
@@ -6461,7 +6464,9 @@ export class SharedLog<
6461
6464
  });
6462
6465
  }
6463
6466
  };
6464
- let maxAttemptMs = MAX_PERSISTED_RECEIPT_ATTEMPT_MS;
6467
+ let maxPrerequisiteAttemptMs = MAX_PERSISTED_RECEIPT_ATTEMPT_MS;
6468
+ let maxReceiptResponseAttemptMs = MAX_PERSISTED_RECEIPT_ATTEMPT_MS;
6469
+ let noProgressRetryMs = PERSISTED_RECEIPT_RETRY_MS;
6465
6470
  let initialTransferPending = transferOnFirstRound;
6466
6471
  let needsInitialLeaderCheck = true;
6467
6472
  const carriedAcknowledgements = new Map<
@@ -6653,16 +6658,19 @@ export class SharedLog<
6653
6658
  );
6654
6659
  const roundController = new AbortController();
6655
6660
  const roundSignal = AbortSignal.any([signal, roundController.signal]);
6656
- const getAttemptTimeout = () =>
6661
+ const getAttemptTimeout = (maxMs = maxPrerequisiteAttemptMs) =>
6657
6662
  Math.max(
6658
6663
  1,
6659
6664
  Math.min(
6660
- maxAttemptMs,
6665
+ maxMs,
6661
6666
  Math.floor((deadline.deadline - Date.now()) / candidateWaves),
6662
6667
  ),
6663
6668
  );
6664
6669
  const requests = new Set<Promise<void>>();
6665
6670
  const transferAllOnRound = initialTransferPending;
6671
+ let receivedValidReceipt = false;
6672
+ let receivedNewReceipt = false;
6673
+ let missedReceiptResponse = false;
6666
6674
  try {
6667
6675
  for (const [peer, hashes] of hashesByPeer) {
6668
6676
  let request!: Promise<void>;
@@ -6850,6 +6858,7 @@ export class SharedLog<
6850
6858
  break;
6851
6859
  }
6852
6860
  let responses;
6861
+ let requestIssued = false;
6853
6862
  const attempt = offset / PERSISTED_RECEIPT_CHUNK_SIZE + 1;
6854
6863
  let requestStartedAt: number | undefined;
6855
6864
  let endRequest:
@@ -6875,11 +6884,16 @@ export class SharedLog<
6875
6884
  throw error;
6876
6885
  }
6877
6886
  if (!isPeerRoundCurrent()) break;
6878
- const attemptTimeout = getAttemptTimeout();
6879
6887
  responses =
6880
6888
  (await operationQueue.add(async () => {
6881
6889
  if (!isPeerRoundCurrent()) return [];
6890
+ // Only a missed response grows this bounded wait. Valid empty
6891
+ // receipts instead back off polling while durability catches up.
6892
+ const attemptTimeout = getAttemptTimeout(
6893
+ maxReceiptResponseAttemptMs,
6894
+ );
6882
6895
  requestStartedAt = profile?.now();
6896
+ requestIssued = true;
6883
6897
  const pending = this.rpc.request(
6884
6898
  new RequestPersistedEntriesV1({
6885
6899
  expectedReceiverSession: captured.capabilitySession,
@@ -6909,6 +6923,7 @@ export class SharedLog<
6909
6923
  } catch {
6910
6924
  endRequest?.("rejected", 0);
6911
6925
  if (roundSignal.aborted) break;
6926
+ if (requestIssued) missedReceiptResponse = true;
6912
6927
  // A peer can disconnect or miss this retry while the overall
6913
6928
  // quorum deadline remains active. Replan on the next round.
6914
6929
  break;
@@ -6947,11 +6962,17 @@ export class SharedLog<
6947
6962
  continue;
6948
6963
  }
6949
6964
  receivedValidConfirmation = true;
6965
+ receivedValidReceipt = true;
6950
6966
  for (const hash of unique) {
6951
6967
  confirmed.add(hash);
6952
- carriedAcknowledgements.get(hash)?.set(peer, captured);
6968
+ const acknowledgements = carriedAcknowledgements.get(hash)!;
6969
+ if (!acknowledgements.has(peer)) receivedNewReceipt = true;
6970
+ acknowledgements.set(peer, captured);
6953
6971
  }
6954
6972
  }
6973
+ if (requestIssued && !receivedValidConfirmation) {
6974
+ missedReceiptResponse = true;
6975
+ }
6955
6976
  if (receivedValidConfirmation) {
6956
6977
  const state = ensureRepairState();
6957
6978
  for (const hash of requested) {
@@ -7064,21 +7085,34 @@ export class SharedLog<
7064
7085
  if (isRoundOwnershipCurrent()) {
7065
7086
  initialTransferPending = false;
7066
7087
  }
7067
- // Keep early retries fair and responsive, then let a caller's longer
7068
- // overall deadline accommodate a genuinely slow durability barrier.
7069
- maxAttemptMs = Math.min(
7088
+ // Confirmation and transfer can need a growing preparation window, but
7089
+ // receipt response waits stay bounded independently of that work.
7090
+ maxPrerequisiteAttemptMs = Math.min(
7070
7091
  MAX_PERSISTED_DELIVERY_TIMEOUT_MS,
7071
- maxAttemptMs * 2,
7092
+ maxPrerequisiteAttemptMs * 2,
7072
7093
  );
7094
+ // A genuinely slow receiver may need more than one response window.
7095
+ // Grow on any issued request's miss, even if another peer replied empty,
7096
+ // preserving the existing timer maximum and caller-deadline clamp.
7097
+ if (missedReceiptResponse) {
7098
+ maxReceiptResponseAttemptMs = Math.min(
7099
+ MAX_PERSISTED_DELIVERY_TIMEOUT_MS,
7100
+ maxReceiptResponseAttemptMs * 2,
7101
+ );
7102
+ }
7103
+ // A valid empty/unchanged receipt should reduce polling, not grow the
7104
+ // next RPC's response timeout. New receipts or a round without a valid
7105
+ // response reset this backoff; readiness recovery gets no extra pause.
7106
+ const noReceiptProgress = receivedValidReceipt && !receivedNewReceipt;
7107
+ const retryMs = noReceiptProgress
7108
+ ? noProgressRetryMs
7109
+ : PERSISTED_RECEIPT_RETRY_MS;
7110
+ noProgressRetryMs = noReceiptProgress
7111
+ ? Math.min(1_000, noProgressRetryMs * 2)
7112
+ : PERSISTED_RECEIPT_RETRY_MS;
7073
7113
  await this.waitPersistedReceiptRetry(
7074
7114
  signal,
7075
- Math.max(
7076
- 0,
7077
- Math.min(
7078
- PERSISTED_RECEIPT_RETRY_MS,
7079
- deadline.deadline - Date.now(),
7080
- ),
7081
- ),
7115
+ Math.max(0, Math.min(retryMs, deadline.deadline - Date.now())),
7082
7116
  );
7083
7117
  }
7084
7118
  } catch (error) {
@@ -23350,6 +23384,7 @@ export class SharedLog<
23350
23384
  ));
23351
23385
  if (!joinedPreparedFacts) {
23352
23386
  await trustedLowerLog.join(materializeAllToMergeEntries(), {
23387
+ signal: releasePeerReceiveLease.signal,
23353
23388
  __peerbitBatchIndependent: true,
23354
23389
  __peerbitEntriesAlreadyMissing: true,
23355
23390
  __peerbitCanAppendAlreadyValidated:
@@ -23788,23 +23823,18 @@ export class SharedLog<
23788
23823
  capabilityTimestamp > previousCapabilityTimestamp;
23789
23824
  if (
23790
23825
  freshExactRearm &&
23791
- this._v2Receive.isCurrentActive({
23826
+ this._v2Receive.acceptRemoteRearm({
23792
23827
  peerHash: receiveFromHash,
23793
23828
  peerSession: receiveSession,
23794
23829
  receiveEpoch:
23795
23830
  this._peerSessions.receiveEpoch(receiveFromHash),
23796
- senderTransportSession: capabilityTransportSession,
23797
23831
  })
23798
23832
  ) {
23799
- // Rotate the receiver grant/challenge before requesting Full. A
23800
- // sender rebuilt from no state starts at sequence one, which an
23801
- // active receiver's old sequence fence must otherwise reject.
23802
- this._v2Receive.advanceRecovery({
23803
- peerHash: receiveFromHash,
23804
- peerSession: receiveSession,
23805
- receiveEpoch:
23806
- this._peerSessions.receiveEpoch(receiveFromHash),
23807
- });
23833
+ // A one-sided topic reopening can discard the opposite receive
23834
+ // stream without changing transport. Retire its old Applied proof
23835
+ // before refreshing our capability and requesting a new Full.
23836
+ this._v2Send.reconfirmAfterPeerRecovery(receiveFromHash);
23837
+ this.dispatchPersistedReceiptReadinessChange(receiveFromHash);
23808
23838
  }
23809
23839
  } else if (observed && receiveSession === null) {
23810
23840
  // A capability can arrive before the sender's topic Subscribe after
@@ -28880,6 +28910,11 @@ export class SharedLog<
28880
28910
  peerSession: expectedSubscriptionEpoch,
28881
28911
  receiveEpoch,
28882
28912
  signal: replicationLifecycleController.signal,
28913
+ // Without a signed remote binding, even a fresh local instance may
28914
+ // face a peer that still considers its old topic session fully active.
28915
+ // Solicit one steady capability through the receiver-led Full handshake;
28916
+ // ordinary timestamp refreshes do not themselves request another reply.
28917
+ requestRemoteFullRearm: !this._peerSyncCapabilitySessions.has(peerHash),
28883
28918
  });
28884
28919
  this.scheduleReplicationInfoV2Recovery(
28885
28920
  publicKey,
package/src/ranges.ts CHANGED
@@ -3517,6 +3517,7 @@ export const toRebalance = <R extends "u32" | "u64">(
3517
3517
  return {
3518
3518
  [Symbol.asyncIterator]: async function* () {
3519
3519
  let completed = false;
3520
+ let scannedPage = false;
3520
3521
  try {
3521
3522
  let cursor: RebalanceTaskCursor | undefined;
3522
3523
  for (;;) {
@@ -3535,6 +3536,16 @@ export const toRebalance = <R extends "u32" | "u64">(
3535
3536
  let taskCompleted = false;
3536
3537
  try {
3537
3538
  while (iterator.done() !== true) {
3539
+ if (scannedPage) {
3540
+ // SQLite pages can settle synchronously. Give transport and
3541
+ // lifecycle work a turn before reading the next bounded page.
3542
+ await new Promise<void>((resolve) => {
3543
+ if (typeof setImmediate === "function")
3544
+ setImmediate(resolve);
3545
+ else setTimeout(resolve, 0);
3546
+ });
3547
+ }
3548
+ scannedPage = true;
3538
3549
  const entries = await iterator.next(
3539
3550
  REBALANCE_ITERATOR_BATCH_SIZE,
3540
3551
  );
@@ -111,6 +111,7 @@ export type ReplicationInfoV2LocalCapabilityAdvertisement = {
111
111
  context: ReplicationInfoV2LocalCapabilityContext;
112
112
  attempts: number;
113
113
  ready: boolean;
114
+ requestRemoteFullRearm?: boolean;
114
115
  acknowledgedReady?: LocalCapabilityReady;
115
116
  receiverTransportSession?: bigint;
116
117
  timer?: ReturnType<typeof setTimeout>;
@@ -190,6 +191,7 @@ export type ReplicationInfoV2ReceiveState = {
190
191
  requestsSinceCapabilityRefresh: number;
191
192
  requestParked: boolean;
192
193
  capabilityRefreshRequired: boolean;
194
+ remoteRearmPending?: boolean;
193
195
  reservedAdmission?: ReplicationInfoV2ReceiveAdmission;
194
196
  };
195
197
 
@@ -235,6 +237,7 @@ export type ReplicationInfoV2ReceiveDeps = {
235
237
  receiveEpoch: object | null;
236
238
  signal: AbortSignal;
237
239
  requestRemoteFullRearm?: boolean;
240
+ acknowledgeRearm?: boolean;
238
241
  }) => Promise<ReplicationInfoV2LocalCapabilityRefresh | undefined>;
239
242
  onRequestError?: (error: unknown) => void;
240
243
  onLocalCapabilityError?: (error: unknown) => void;
@@ -596,6 +599,8 @@ export class ReplicationInfoV2ReceiveCoordinator {
596
599
  state.controller.signal,
597
600
  state.lifecycleSignal,
598
601
  ]),
602
+ requestRemoteFullRearm: state.requestRemoteFullRearm,
603
+ acknowledgeRearm: state.requestRemoteFullRearm,
599
604
  });
600
605
  if (
601
606
  !refreshed ||
@@ -653,6 +658,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
653
658
  peerSession: object;
654
659
  receiveEpoch: object | null;
655
660
  signal: AbortSignal;
661
+ requestRemoteFullRearm?: boolean;
656
662
  }): ReplicationInfoV2LocalCapabilityAdvertisementHandle {
657
663
  const peerHash = properties.target.hashcode();
658
664
  if (
@@ -729,6 +735,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
729
735
  context,
730
736
  attempts: 0,
731
737
  ready: false,
738
+ requestRemoteFullRearm: properties.requestRemoteFullRearm,
732
739
  receiverTransportSession: this.deps.getReceiverTransportSession(),
733
740
  };
734
741
  advertisement.onLifecycleAbort = () =>
@@ -1418,6 +1425,28 @@ export class ReplicationInfoV2ReceiveCoordinator {
1418
1425
  });
1419
1426
  }
1420
1427
 
1428
+ /** Coalesce reciprocal capability recovery into the existing request worker. */
1429
+ acceptRemoteRearm(properties: {
1430
+ peerHash: string;
1431
+ peerSession: object;
1432
+ receiveEpoch: object | null;
1433
+ }): boolean {
1434
+ const state = this._receiveStates.get(properties.peerHash);
1435
+ if (
1436
+ !state ||
1437
+ state.peerSession !== properties.peerSession ||
1438
+ state.receiveEpoch !== properties.receiveEpoch ||
1439
+ !this.isStateCurrent(state)
1440
+ ) {
1441
+ return false;
1442
+ }
1443
+ // A newer solicitation can describe another remote topic opening before
1444
+ // the previous Full commits. Invalidating the captured version makes an
1445
+ // in-flight refresh yield to one trailing refresh, not a second worker.
1446
+ state.remoteRearmPending = true;
1447
+ return this.advanceRecovery(properties);
1448
+ }
1449
+
1421
1450
  /** Require a fresh capability-bound grant and authoritative Full. */
1422
1451
  advanceRecovery(properties: {
1423
1452
  peerHash: string;
@@ -1796,11 +1825,11 @@ export class ReplicationInfoV2ReceiveCoordinator {
1796
1825
  return false;
1797
1826
  }
1798
1827
  state.lastSequence = message.sequence;
1799
- state.phase = "active";
1828
+ state.phase = state.remoteRearmPending ? "resync" : "active";
1800
1829
  state.requestAttempts = 0;
1801
1830
  state.requestsSinceCapabilityRefresh = 0;
1802
1831
  state.requestParked = false;
1803
- state.capabilityRefreshRequired = false;
1832
+ state.capabilityRefreshRequired = state.remoteRearmPending === true;
1804
1833
  if (state.requestTimer) {
1805
1834
  clearTimeout(state.requestTimer);
1806
1835
  state.requestTimer = undefined;
@@ -1810,6 +1839,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
1810
1839
  admission.receiveEpoch = state.receiveEpoch;
1811
1840
  admission.committed = true;
1812
1841
  this.release(admission);
1842
+ if (state.remoteRearmPending) this.armRequest(state, 0);
1813
1843
  return true;
1814
1844
  }
1815
1845
 
@@ -1959,6 +1989,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
1959
1989
  state.lastSequence = undefined;
1960
1990
  state.phase = "resync";
1961
1991
  state.capabilityRefreshRequired = false;
1992
+ state.remoteRearmPending = false;
1962
1993
  state.requestsSinceCapabilityRefresh = 0;
1963
1994
  state.version++;
1964
1995
  this.bindLocalCapability(state, ready);
@@ -132,6 +132,7 @@ export type ReplicationInfoV2SendState = {
132
132
  worker?: Promise<void>;
133
133
  applicationConfirmationRequest?: ApplicationConfirmationRequest;
134
134
  appliedRevision?: bigint;
135
+ minimumConfirmationSequence?: bigint;
135
136
  };
136
137
 
137
138
  export type ReplicationInfoV2SendDeps<R extends "u32" | "u64"> = {
@@ -258,6 +259,22 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
258
259
  }
259
260
  }
260
261
 
262
+ /** Re-prove an unchanged transport stream after an authenticated recovery hint. */
263
+ reconfirmAfterPeerRecovery(peerHash: string): void {
264
+ const state = this._sendStates.get(peerHash);
265
+ if (!state || !this.isCurrent(state)) return;
266
+ state.appliedRevision = undefined;
267
+ state.minimumConfirmationSequence = state.nextSequence;
268
+ state.applicationConfirmationRequest?.controller.abort(
269
+ new AbortError("Replication confirmation peer recovery"),
270
+ );
271
+ state.applicationConfirmationRequest = undefined;
272
+ // Keep the stream: older rearm callers may still own its receive grant.
273
+ // A new Full sequence fences even an old Applied packet arriving after a
274
+ // replacement confirmation query has been registered for the same revision.
275
+ this.enqueueState(state, { kind: "snapshot", revision: this._revision });
276
+ }
277
+
261
278
  private clearState(state: ReplicationInfoV2SendState): void {
262
279
  this.trackRetiringWorker(state);
263
280
  this.rejectTargetConfirmations(
@@ -1264,6 +1281,8 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
1264
1281
  !bytesEqual(message.receiverChallenge, state.receiverChallenge) ||
1265
1282
  !bytesEqual(message.senderEpoch, state.senderEpoch) ||
1266
1283
  message.sequence !== requested.sequence ||
1284
+ (state.minimumConfirmationSequence !== undefined &&
1285
+ message.sequence < state.minimumConfirmationSequence) ||
1267
1286
  message.revision !== requested.revision ||
1268
1287
  message.revision > this._revision
1269
1288
  ) {
@@ -1359,6 +1378,8 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
1359
1378
  if (
1360
1379
  this.hasConfirmationFor(state, request.revision) &&
1361
1380
  this.supportsApplicationConfirmation(state) &&
1381
+ (state.minimumConfirmationSequence === undefined ||
1382
+ message.sequence >= state.minimumConfirmationSequence) &&
1362
1383
  this.isCurrent(state)
1363
1384
  ) {
1364
1385
  // Keep one outstanding query until answered or no longer needed. A