@peerbit/shared-log 16.0.26 → 16.0.28

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
@@ -260,10 +260,14 @@ import type {
260
260
  AddedReplicationInfoMutation,
261
261
  FullReplicationInfoMutation,
262
262
  } from "./replication-info-mutation.js";
263
- import { ReplicationInfoV2ReceiveCoordinator } from "./replication-info-v2-receive.js";
263
+ import {
264
+ ReplicationInfoV2ReceiveCoordinator,
265
+ type ReplicationInfoV2ReceiveDiagnostic,
266
+ } from "./replication-info-v2-receive.js";
264
267
  import {
265
268
  type ReplicationInfoV2ConfirmationOptions,
266
269
  ReplicationInfoV2SendCoordinator,
270
+ type ReplicationInfoV2SendDiagnostic,
267
271
  } from "./replication-info-v2-send.js";
268
272
  import {
269
273
  type ReplicationStatus,
@@ -1860,6 +1864,39 @@ export type PersistedReceiptPeerReadinessUnsupportedReason =
1860
1864
  | "persisted-receipts-unsupported"
1861
1865
  | "replication-confirmation-unsupported";
1862
1866
 
1867
+ /**
1868
+ * Bounded, payload-free diagnostics for one persisted-receipt readiness
1869
+ * inspection. Session and transport identities remain opaque; revision and
1870
+ * sequence values are decimal strings so the snapshot is JSON-safe. `reason`
1871
+ * fields classify the current state; rejected-packet and disconnected-peer
1872
+ * history is deliberately not retained.
1873
+ */
1874
+ export type PersistedReceiptPeerReadinessDiagnostic = Readonly<{
1875
+ version: 1;
1876
+ log: "open" | "closed";
1877
+ session: Readonly<{
1878
+ state: "absent" | "current";
1879
+ phase?: "opening" | "open" | "departing" | "superseded";
1880
+ established: boolean;
1881
+ suspended: boolean;
1882
+ openingBarrierActive: boolean;
1883
+ replicationInfoBlocked: boolean;
1884
+ receiveCleanupGateOpen: boolean;
1885
+ }>;
1886
+ capability: Readonly<{
1887
+ state: "absent" | "observed";
1888
+ persistedReceipts: boolean;
1889
+ replicationConfirmation: boolean;
1890
+ }>;
1891
+ sender: ReplicationInfoV2SendDiagnostic;
1892
+ receiver: ReplicationInfoV2ReceiveDiagnostic;
1893
+ }>;
1894
+
1895
+ type PersistedReceiptPeerReadinessDiagnosticAttachment = Readonly<{
1896
+ /** Present only when `diagnostics: true` was requested. */
1897
+ diagnostic?: PersistedReceiptPeerReadinessDiagnostic;
1898
+ }>;
1899
+
1863
1900
  /**
1864
1901
  * Detached view of one public key's current persisted-receipt generation.
1865
1902
  * `generation` is opaque: callers may compare it for equality, but must not
@@ -1868,20 +1905,23 @@ export type PersistedReceiptPeerReadinessUnsupportedReason =
1868
1905
  * leadership and outbound confirmation can still change within a generation.
1869
1906
  */
1870
1907
  export type PersistedReceiptPeerReadiness =
1871
- | Readonly<{
1908
+ | (Readonly<{
1872
1909
  status: "ready";
1873
1910
  generation: string;
1874
- }>
1875
- | Readonly<{
1911
+ }> &
1912
+ PersistedReceiptPeerReadinessDiagnosticAttachment)
1913
+ | (Readonly<{
1876
1914
  status: "pending";
1877
1915
  reason: PersistedReceiptPeerReadinessPendingReason;
1878
1916
  generation?: string;
1879
- }>
1880
- | Readonly<{
1917
+ }> &
1918
+ PersistedReceiptPeerReadinessDiagnosticAttachment)
1919
+ | (Readonly<{
1881
1920
  status: "unsupported";
1882
1921
  reason: PersistedReceiptPeerReadinessUnsupportedReason;
1883
1922
  generation: string;
1884
- }>;
1923
+ }> &
1924
+ PersistedReceiptPeerReadinessDiagnosticAttachment);
1885
1925
 
1886
1926
  export type PersistedReceiptPeerReady = Extract<
1887
1927
  PersistedReceiptPeerReadiness,
@@ -1892,6 +1932,8 @@ export type PersistedReceiptPeerReadinessOptions<
1892
1932
  T,
1893
1933
  R extends "u32" | "u64",
1894
1934
  > = Readonly<{
1935
+ /** Include a bounded, JSON-safe diagnostic snapshot in the result. */
1936
+ diagnostics?: boolean;
1895
1937
  /** Require this peer to be a freshly planned leader for every entry. */
1896
1938
  entries?: readonly (ShallowOrFullEntry<T> | EntryReplicated<R>)[];
1897
1939
  /**
@@ -1952,6 +1994,7 @@ const MAX_PERSISTED_RECEIPT_ATTEMPT_MS = 2_000;
1952
1994
  const MAX_PERSISTED_RECEIPT_REQUESTS_GLOBAL = 8;
1953
1995
  const MAX_PERSISTED_RECEIPT_REQUESTS_PER_PEER = 2;
1954
1996
  const MAX_PERSISTED_RECEIPT_READINESS_WAITERS = 1_024;
1997
+ const MAX_PERSISTED_RECEIPT_READINESS_DIAGNOSTIC_SUMMARY_CHARS = 768;
1955
1998
  const PERSISTED_RECEIPT_INGRESS_PEER_REQUEST_CAPACITY = 16;
1956
1999
  const PERSISTED_RECEIPT_INGRESS_PEER_HASH_CAPACITY = 8_192;
1957
2000
  const PERSISTED_RECEIPT_INGRESS_PEER_REQUESTS_PER_SECOND = 8;
@@ -2680,6 +2723,10 @@ export class SharedLog<
2680
2723
  // The epoch tokens are PeerSession objects (src/peer-session.ts); the map
2681
2724
  // itself lives on the registry. Stage-2 of the fence refactor.
2682
2725
  private _peerSessions!: PeerSessionRegistry;
2726
+ // An exact successor session gets at most one unchanged-Full repair. Weak
2727
+ // ownership keeps retired transport generations collectible without an
2728
+ // identity-keyed cleanup path.
2729
+ private _successorFullRepairSessions!: WeakSet<PeerSession>;
2683
2730
  // A superseded removal may be the queue item that actually observed an active
2684
2731
  // replicator. Carry that leave obligation to the transition that ultimately
2685
2732
  // wins, while a winning reconnect clears it without emitting a stale leave.
@@ -4264,6 +4311,7 @@ export class SharedLog<
4264
4311
  // replication-info blocked set (fence B5) alongside the session maps —
4265
4312
  // the legacy inline `new Set()` that sat above moved there.
4266
4313
  this._peerSessions = this.createPeerSessionRegistry();
4314
+ this._successorFullRepairSessions = new WeakSet();
4267
4315
  this._pendingReplicatorLeaveByPeer = new Set();
4268
4316
  this._activeReceiveHandlersByPeer = new Map();
4269
4317
  this._receiveHandlerDrainByPeer = new Map();
@@ -4447,8 +4495,7 @@ export class SharedLog<
4447
4495
 
4448
4496
  const profile = this._logProperties?.sync?.profile;
4449
4497
  const startedAt = syncProfileStart(profile);
4450
- const mode =
4451
- resolvedRoot === fanoutService.publicKeyHash ? "root" : "node";
4498
+ const mode = resolvedRoot === fanoutService.publicKeyHash ? "root" : "node";
4452
4499
  const before = profile
4453
4500
  ? snapshotFanoutOpenMetrics(fanoutService, this.topic, resolvedRoot)
4454
4501
  : undefined;
@@ -5770,6 +5817,117 @@ export class SharedLog<
5770
5817
  });
5771
5818
  }
5772
5819
 
5820
+ private persistedReceiptReadinessDiagnostic(
5821
+ peerHash: string,
5822
+ ): PersistedReceiptPeerReadinessDiagnostic {
5823
+ const peerSession = this._peerSessions.current(peerHash);
5824
+ const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
5825
+ const capabilitySession = this._peerSyncCapabilitySessions.get(peerHash);
5826
+ const capabilities = this._peerSyncCapabilities.get(peerHash) ?? 0;
5827
+ const replicationInfoBlocked =
5828
+ this._peerSessions.isReplicationInfoBlocked(peerHash);
5829
+ const receiveCleanupGateOpen =
5830
+ this._peerSessions.isReceiveCleanupGateOpen(peerHash);
5831
+ const active = peerSession?.isActive() ?? false;
5832
+ const established =
5833
+ !this.closed &&
5834
+ peerSession?.phase === "open" &&
5835
+ active &&
5836
+ !replicationInfoBlocked &&
5837
+ receiveCleanupGateOpen;
5838
+ const session = Object.freeze({
5839
+ state: peerSession ? ("current" as const) : ("absent" as const),
5840
+ ...(peerSession ? { phase: peerSession.phase } : {}),
5841
+ established,
5842
+ suspended: !established,
5843
+ openingBarrierActive: peerSession?.openingBarrierActive ?? false,
5844
+ replicationInfoBlocked,
5845
+ receiveCleanupGateOpen,
5846
+ });
5847
+ const capabilityObserved =
5848
+ capabilitySession !== undefined &&
5849
+ this._peerSyncCapabilityTimestamps.has(peerHash);
5850
+ const capability = Object.freeze({
5851
+ state: capabilityObserved ? ("observed" as const) : ("absent" as const),
5852
+ persistedReceipts:
5853
+ (capabilities & SYNC_CAPABILITY_PERSISTED_ENTRY_RECEIPTS) !== 0,
5854
+ replicationConfirmation:
5855
+ (capabilities & SYNC_CAPABILITY_REPLICATION_INFO_V2_CONFIRM) !== 0,
5856
+ });
5857
+ return Object.freeze({
5858
+ version: 1 as const,
5859
+ log: this.closed ? ("closed" as const) : ("open" as const),
5860
+ session,
5861
+ capability,
5862
+ sender: this._v2Send.diagnosePeer({
5863
+ peerHash,
5864
+ ...(peerSession ? { peerSession } : {}),
5865
+ ...(capabilitySession === undefined
5866
+ ? {}
5867
+ : { receiverTransportSession: capabilitySession }),
5868
+ }),
5869
+ receiver: this._v2Receive.diagnosePeer({
5870
+ peerHash,
5871
+ ...(peerSession ? { peerSession } : {}),
5872
+ receiveEpoch,
5873
+ ...(capabilitySession === undefined
5874
+ ? {}
5875
+ : { senderTransportSession: capabilitySession }),
5876
+ }),
5877
+ });
5878
+ }
5879
+
5880
+ private attachPersistedReceiptReadinessDiagnostic<
5881
+ Snapshot extends PersistedReceiptPeerReadiness,
5882
+ >(
5883
+ peerHash: string,
5884
+ snapshot: Snapshot,
5885
+ enabled: boolean | undefined,
5886
+ ): Snapshot {
5887
+ if (enabled !== true) return snapshot;
5888
+ return Object.freeze({
5889
+ ...snapshot,
5890
+ diagnostic: this.persistedReceiptReadinessDiagnostic(peerHash),
5891
+ }) as Snapshot;
5892
+ }
5893
+
5894
+ private formatPersistedReceiptReadinessDiagnostic(peerHash: string): string {
5895
+ const diagnostic = this.persistedReceiptReadinessDiagnostic(peerHash);
5896
+ const { session, capability, sender, receiver } = diagnostic;
5897
+ const summary = [
5898
+ `session=${session.state}/${session.phase ?? "none"}/${
5899
+ session.established ? "established" : "suspended"
5900
+ }`,
5901
+ `capability=${capability.state}/persisted:${
5902
+ capability.persistedReceipts ? 1 : 0
5903
+ }/confirm:${capability.replicationConfirmation ? 1 : 0}`,
5904
+ `sender=${sender.state}/${sender.reason}/revision:${
5905
+ sender.currentRevision
5906
+ }/pending:${sender.pendingRevision ?? "none"}/inflight:${
5907
+ sender.inFlightSequence ?? "none"
5908
+ }@${sender.inFlightRevision ?? "none"}/applied:${
5909
+ sender.appliedRevision ?? "none"
5910
+ }/confirmation:${sender.confirmationSequence ?? "none"}@${
5911
+ sender.confirmationRevision ?? "none"
5912
+ }/waiters:${sender.confirmationWaiters}/oldest-ms:${
5913
+ sender.oldestConfirmationAgeMs ?? "none"
5914
+ }/retries:${sender.retryAttempts ?? 0}`,
5915
+ `receiver=${receiver.state}/${receiver.reason}/phase:${
5916
+ receiver.phase ?? "none"
5917
+ }/request:${receiver.requestState ?? "none"}/applied-sequence:${
5918
+ receiver.lastAppliedSequence ?? "none"
5919
+ }/retries:${receiver.requestAttempts ?? 0}/rearm:${
5920
+ receiver.remoteFullRearm
5921
+ }/rearm-attempts:${receiver.remoteFullRearmAttempts}/rearm-outstanding:${
5922
+ receiver.remoteFullRearmOutstanding
5923
+ }/rearm-global:${receiver.remoteFullRearmGlobalOutstanding}`,
5924
+ ].join(" ");
5925
+ return summary.slice(
5926
+ 0,
5927
+ MAX_PERSISTED_RECEIPT_READINESS_DIAGNOSTIC_SUMMARY_CHARS,
5928
+ );
5929
+ }
5930
+
5773
5931
  private dispatchPersistedReceiptReadinessChange(peerHash: string): void {
5774
5932
  this.events.dispatchEvent(
5775
5933
  new CustomEvent<PersistedReceiptPeerReadinessEvent>(
@@ -5861,9 +6019,7 @@ export class SharedLog<
5861
6019
  };
5862
6020
  }
5863
6021
 
5864
- private persistedReceiptPeerSession(
5865
- peerHash: string,
5866
- ):
6022
+ private persistedReceiptPeerSession(peerHash: string):
5867
6023
  | {
5868
6024
  capabilitySession: bigint;
5869
6025
  peerSession: PeerSession;
@@ -17808,6 +17964,7 @@ export class SharedLog<
17808
17964
  })) > 0;
17809
17965
 
17810
17966
  this._gidPeersHistory = new Map();
17967
+ this._successorFullRepairSessions = new WeakSet();
17811
17968
  this.resetGidPeerHistoryCleanupState();
17812
17969
  const replicationChangeOwnershipLifecycleController =
17813
17970
  this.captureReplicationOwnershipLifecycle();
@@ -20607,6 +20764,7 @@ export class SharedLog<
20607
20764
  this._receiveHandlerDrainByPeer?.clear();
20608
20765
  this._openingSyncCapabilitiesByPeer?.clear();
20609
20766
  this._gidPeersHistory?.clear();
20767
+ this._successorFullRepairSessions = new WeakSet();
20610
20768
  this._peerSyncCapabilities?.clear();
20611
20769
  this._peerSyncCapabilitySessions?.clear();
20612
20770
  this._peerSyncCapabilityTimestamps?.clear();
@@ -23875,6 +24033,10 @@ export class SharedLog<
23875
24033
  ) {
23876
24034
  return;
23877
24035
  }
24036
+ const isFirstSuccessorFull =
24037
+ msg instanceof FullReplicationInfoV2Message &&
24038
+ receiveSession.hasPredecessor &&
24039
+ !this._successorFullRepairSessions.has(receiveSession);
23878
24040
  const admission = this._v2Receive.reserve(msg, {
23879
24041
  from,
23880
24042
  peerSession: receiveSession,
@@ -23908,6 +24070,8 @@ export class SharedLog<
23908
24070
  const exactGate = () =>
23909
24071
  hostGate() && this._v2Receive.isAdmissionCurrent(admission);
23910
24072
  let durableCommitted = false;
24073
+ let committedSuccessorFull = false;
24074
+ let repairUnchangedSuccessorFull = false;
23911
24075
 
23912
24076
  try {
23913
24077
  if (
@@ -23951,6 +24115,12 @@ export class SharedLog<
23951
24115
  ) {
23952
24116
  return;
23953
24117
  }
24118
+ committedSuccessorFull =
24119
+ isFirstSuccessorFull &&
24120
+ msg instanceof FullReplicationInfoV2Message &&
24121
+ msg.segments.length > 0;
24122
+ repairUnchangedSuccessorFull =
24123
+ committedSuccessorFull && result.length === 0;
23954
24124
  } else {
23955
24125
  if (!exactGate()) {
23956
24126
  return;
@@ -24024,6 +24194,30 @@ export class SharedLog<
24024
24194
  if (!hostGate()) {
24025
24195
  return;
24026
24196
  }
24197
+ if (committedSuccessorFull) {
24198
+ if (!exactGate()) {
24199
+ return;
24200
+ }
24201
+ this._successorFullRepairSessions.add(receiveSession);
24202
+ if (repairUnchangedSuccessorFull) {
24203
+ // The unchanged range rows belong to the public-key principal, but
24204
+ // delivery evidence may belong to the superseded process. Reuse the
24205
+ // receipt-driven authoritative repair path and bypass only those stale
24206
+ // hints; do not synthesize public membership or range-change events.
24207
+ this._repairFrontierBypassKnownPeersByMode
24208
+ .get("join-authoritative")
24209
+ ?.add(fromHash);
24210
+ const peers = new Set([fromHash]);
24211
+ this.scheduleRepairSweep(
24212
+ { mode: "join-authoritative", peers },
24213
+ lane.ownershipLifecycleController,
24214
+ );
24215
+ this.scheduleJoinAuthoritativeRepair(
24216
+ peers,
24217
+ lane.ownershipLifecycleController,
24218
+ );
24219
+ }
24220
+ }
24027
24221
  this._liveness.markReplicatorActivity(fromHash);
24028
24222
  // A committed V2 announcement is applied progress: the peer answers,
24029
24223
  // so recovery re-solicitation may restart from the base interval.
@@ -24398,8 +24592,7 @@ export class SharedLog<
24398
24592
  if (
24399
24593
  !peerSession ||
24400
24594
  peerSession.phase === "departing" ||
24401
- (peerSession.phase === "opening" &&
24402
- !peerSession.openingBarrierActive)
24595
+ (peerSession.phase === "opening" && !peerSession.openingBarrierActive)
24403
24596
  ) {
24404
24597
  // A barrier rejection deliberately leaves the current session in its
24405
24598
  // fail-closed opening phase after the barrier window has settled. Ask the
@@ -24465,7 +24658,18 @@ export class SharedLog<
24465
24658
  key: PublicSignKey,
24466
24659
  options: PersistedReceiptPeerReadinessOptions<T, R> = {},
24467
24660
  ): Promise<PersistedReceiptPeerReadiness> {
24468
- return this.inspectPersistedReceiptPeerReadiness(key, options);
24661
+ if (options.diagnostics !== true) {
24662
+ return this.inspectPersistedReceiptPeerReadiness(key, options);
24663
+ }
24664
+ const snapshot = await this.inspectPersistedReceiptPeerReadiness(
24665
+ key,
24666
+ options,
24667
+ );
24668
+ return this.attachPersistedReceiptReadinessDiagnostic(
24669
+ key.hashcode(),
24670
+ snapshot,
24671
+ options.diagnostics,
24672
+ );
24469
24673
  }
24470
24674
 
24471
24675
  private async inspectPersistedReceiptPeerReadiness(
@@ -24657,8 +24861,10 @@ export class SharedLog<
24657
24861
  "reason" in lastSnapshot ? `/${lastSnapshot.reason}` : ""
24658
24862
  })`
24659
24863
  : "";
24864
+ const diagnostic =
24865
+ this.formatPersistedReceiptReadinessDiagnostic(peerHash);
24660
24866
  return new TimeoutError(
24661
- `Timeout waiting for persisted-receipt readiness from ${peerHash}${suffix}`,
24867
+ `Timeout waiting for persisted-receipt readiness from ${peerHash}${suffix}; diagnostic: ${diagnostic}`,
24662
24868
  );
24663
24869
  };
24664
24870
 
@@ -24806,7 +25012,13 @@ export class SharedLog<
24806
25012
  // A wake observed while the asynchronous inspection was running may
24807
25013
  // already have invalidated this snapshot. Drain that coalesced wake
24808
25014
  // before publishing readiness.
24809
- resolve(snapshot);
25015
+ resolve(
25016
+ this.attachPersistedReceiptReadinessDiagnostic(
25017
+ peerHash,
25018
+ snapshot,
25019
+ options.diagnostics,
25020
+ ),
25021
+ );
24810
25022
  return;
24811
25023
  }
24812
25024
  if (
@@ -24868,7 +25080,13 @@ export class SharedLog<
24868
25080
  if (snapshot.status === "ready") {
24869
25081
  confirmationRecoveryTarget = undefined;
24870
25082
  if (rerun) return;
24871
- resolve(snapshot);
25083
+ resolve(
25084
+ this.attachPersistedReceiptReadinessDiagnostic(
25085
+ peerHash,
25086
+ snapshot,
25087
+ options.diagnostics,
25088
+ ),
25089
+ );
24872
25090
  return;
24873
25091
  }
24874
25092
  if (
@@ -24966,6 +25184,11 @@ export class SharedLog<
24966
25184
  timeout?: number;
24967
25185
  },
24968
25186
  ) {
25187
+ if (this.closed) {
25188
+ throw new ClosedError();
25189
+ }
25190
+ const closeSignal = this._closeController.signal;
25191
+ if (closeSignal.aborted) throw new ClosedError();
24969
25192
  if (options?.signal?.aborted) {
24970
25193
  throw new AbortError();
24971
25194
  }
@@ -24974,6 +25197,9 @@ export class SharedLog<
24974
25197
  const resolvedRoleAge = options?.eager
24975
25198
  ? undefined
24976
25199
  : (options?.roleAge ?? (await this.getDefaultMinRoleAge()));
25200
+ if (this.closed || closeSignal.aborted) {
25201
+ throw new ClosedError();
25202
+ }
24977
25203
  if (options?.signal?.aborted) {
24978
25204
  throw new AbortError();
24979
25205
  }
@@ -24989,6 +25215,7 @@ export class SharedLog<
24989
25215
  this.events.removeEventListener("replicator:mature", runCheck);
24990
25216
  this.events.removeEventListener("replication:change", runCheck);
24991
25217
  options?.signal?.removeEventListener("abort", onAbort);
25218
+ closeSignal.removeEventListener("abort", onClose);
24992
25219
  if (timer != null) {
24993
25220
  clearTimeout(timer);
24994
25221
  timer = undefined;
@@ -25028,9 +25255,11 @@ export class SharedLog<
25028
25255
  };
25029
25256
 
25030
25257
  const onAbort = () => reject(new AbortError());
25258
+ const onClose = () => reject(new ClosedError());
25031
25259
  if (options?.signal) {
25032
- options.signal.addEventListener("abort", onAbort);
25260
+ options.signal.addEventListener("abort", onAbort, { once: true });
25033
25261
  }
25262
+ closeSignal.addEventListener("abort", onClose, { once: true });
25034
25263
 
25035
25264
  timer = setTimeout(() => {
25036
25265
  reject(
@@ -25073,31 +25302,32 @@ export class SharedLog<
25073
25302
  return;
25074
25303
  }
25075
25304
 
25076
- if (requestAttempts >= maxRequestAttempts) {
25077
- return;
25078
- }
25079
-
25080
- requestAttempts++;
25305
+ if (requestAttempts < maxRequestAttempts) {
25306
+ requestAttempts++;
25081
25307
 
25082
- const peerHash = key.hashcode();
25083
- const peerSession = this._peerSessions.current(peerHash);
25084
- if (peerSession?.phase === "open") {
25085
- this._v2Receive.ensureRequestProgress({
25086
- peerHash,
25087
- peerSession,
25088
- receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
25089
- });
25090
- } else if (peerSession === null || peerSession.phase === "departing") {
25091
- // A peer can be known to routing before its SharedLog topic
25092
- // subscription has been observed. Legacy requests used to bootstrap
25093
- // that case directly; V2 needs an authoritative Subscribe snapshot
25094
- // before it can create a fenced PeerSession and request a Full.
25095
- requestSubscriberSnapshot();
25308
+ const peerHash = key.hashcode();
25309
+ const peerSession = this._peerSessions.current(peerHash);
25310
+ if (peerSession?.phase === "open") {
25311
+ this._v2Receive.ensureRequestProgress({
25312
+ peerHash,
25313
+ peerSession,
25314
+ receiveEpoch: this._peerSessions.receiveEpoch(peerHash),
25315
+ });
25316
+ } else if (peerSession === null || peerSession.phase === "departing") {
25317
+ // A peer can be known to routing before its SharedLog topic
25318
+ // subscription has been observed. Legacy requests used to bootstrap
25319
+ // that case directly; V2 needs an authoritative Subscribe snapshot
25320
+ // before it can create a fenced PeerSession and request a Full.
25321
+ requestSubscriberSnapshot();
25322
+ }
25096
25323
  }
25097
25324
 
25098
- if (requestAttempts < maxRequestAttempts) {
25099
- requestTimer = setTimeout(requestReplicationInfo, requestIntervalMs);
25100
- }
25325
+ // Replication change events are wake hints, not replayable state. Keep
25326
+ // checking the authoritative index after the bounded network recovery
25327
+ // attempts are exhausted so a transiently empty transition read cannot
25328
+ // strand an otherwise-ready waiter until its outer timeout.
25329
+ runCheck();
25330
+ requestTimer = setTimeout(requestReplicationInfo, requestIntervalMs);
25101
25331
  };
25102
25332
 
25103
25333
  const check = async () => {
@@ -25149,8 +25379,9 @@ export class SharedLog<
25149
25379
  // replay a maturity/change event that fires while that read is in flight.
25150
25380
  this.events.addEventListener("replicator:mature", runCheck);
25151
25381
  this.events.addEventListener("replication:change", runCheck);
25152
- requestReplicationInfo();
25153
- runCheck();
25382
+ if (closeSignal.aborted) onClose();
25383
+ else if (options?.signal?.aborted) onAbort();
25384
+ else requestReplicationInfo();
25154
25385
 
25155
25386
  return deferred.promise.finally(clear);
25156
25387
  }