@peerbit/shared-log 16.0.0 → 16.0.2

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 (38) hide show
  1. package/dist/src/checked-prune.d.ts.map +1 -1
  2. package/dist/src/checked-prune.js +12 -0
  3. package/dist/src/checked-prune.js.map +1 -1
  4. package/dist/src/index.d.ts +10 -1
  5. package/dist/src/index.d.ts.map +1 -1
  6. package/dist/src/index.js +55 -19
  7. package/dist/src/index.js.map +1 -1
  8. package/dist/src/instance-lifecycle.d.ts.map +1 -1
  9. package/dist/src/instance-lifecycle.js +9 -0
  10. package/dist/src/instance-lifecycle.js.map +1 -1
  11. package/dist/src/peer-session.d.ts.map +1 -1
  12. package/dist/src/peer-session.js +9 -0
  13. package/dist/src/peer-session.js.map +1 -1
  14. package/dist/src/pid.d.ts +0 -2
  15. package/dist/src/pid.d.ts.map +1 -1
  16. package/dist/src/pid.js +0 -5
  17. package/dist/src/pid.js.map +1 -1
  18. package/dist/src/replication-info-v2-receive.d.ts +9 -7
  19. package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
  20. package/dist/src/replication-info-v2-receive.js +14 -8
  21. package/dist/src/replication-info-v2-receive.js.map +1 -1
  22. package/dist/src/role.d.ts +0 -4
  23. package/dist/src/role.d.ts.map +1 -1
  24. package/dist/src/role.js +0 -9
  25. package/dist/src/role.js.map +1 -1
  26. package/dist/src/sync/simple.d.ts +1 -0
  27. package/dist/src/sync/simple.d.ts.map +1 -1
  28. package/dist/src/sync/simple.js +1 -1
  29. package/dist/src/sync/simple.js.map +1 -1
  30. package/package.json +11 -11
  31. package/src/checked-prune.ts +12 -0
  32. package/src/index.ts +63 -23
  33. package/src/instance-lifecycle.ts +9 -0
  34. package/src/peer-session.ts +9 -0
  35. package/src/pid.ts +0 -6
  36. package/src/replication-info-v2-receive.ts +14 -14
  37. package/src/role.ts +0 -13
  38. package/src/sync/simple.ts +1 -1
package/src/index.ts CHANGED
@@ -290,6 +290,7 @@ import {
290
290
  } from "./sync/profile.js";
291
291
  import {
292
292
  ConfirmEntriesMessage,
293
+ RECENT_KNOWN_EXCHANGE_HEAD_SUPPRESSION_MS,
293
294
  SYNC_MESSAGE_PRIORITY,
294
295
  SimpleSyncronizer,
295
296
  } from "./sync/simple.js";
@@ -1439,6 +1440,16 @@ const JOIN_AUTHORITATIVE_RETRY_SCHEDULE_MS = [
1439
1440
  ];
1440
1441
  const APPEND_BACKFILL_RETRY_SCHEDULE_MS = [0, 1_000, 3_000, 7_000];
1441
1442
  const RECENT_KNOWN_REPAIR_SUPPRESSION_MS = 30_000;
1443
+ // `_entryKnownPeerObservedAt` is read ONLY through isEntryRecentlyKnownByPeer,
1444
+ // which treats an over-age row and an absent row identically (both false). So
1445
+ // rows older than the longest horizon any caller asks about are dead weight,
1446
+ // and dropping them is behaviour-identical rather than merely safe. Derived
1447
+ // from the horizons themselves -- never hardcode it -- so a future caller with
1448
+ // a longer window cannot silently outlive the retention that serves it.
1449
+ const ENTRY_KNOWN_PEER_OBSERVED_AT_RETENTION_MS = Math.max(
1450
+ RECENT_KNOWN_REPAIR_SUPPRESSION_MS,
1451
+ RECENT_KNOWN_EXCHANGE_HEAD_SUPPRESSION_MS,
1452
+ );
1442
1453
  const JOIN_AUTHORITATIVE_REPAIR_DELAY_MS = 2_000;
1443
1454
  const JOIN_AUTHORITATIVE_REPAIR_SWEEP_DELAYS_MS = [
1444
1455
  JOIN_AUTHORITATIVE_REPAIR_DELAY_MS,
@@ -2017,7 +2028,6 @@ export class SharedLog<
2017
2028
 
2018
2029
  uniqueReplicators!: Set<string>;
2019
2030
  private _replicatorJoinEmitted!: Set<string>;
2020
- private _replicatorsReconciled!: boolean;
2021
2031
 
2022
2032
  /* private _totalParticipation!: number; */
2023
2033
 
@@ -3295,6 +3305,7 @@ export class SharedLog<
3295
3305
  private _repairSweepOptimisticGidsByPeer!: Map<string, Set<string>>;
3296
3306
  private _entryKnownPeers!: Map<string, Set<string>>;
3297
3307
  private _entryKnownPeerObservedAt!: Map<string, Map<string, number>>;
3308
+ private _entryKnownPeerObservedAtSweptAt = 0;
3298
3309
  private _joinAuthoritativeRepairTimersByDelay!: Map<
3299
3310
  number,
3300
3311
  ReturnType<typeof setTimeout>
@@ -3645,6 +3656,7 @@ export class SharedLog<
3645
3656
  this._repairSweepOptimisticGidsByPeer = new Map();
3646
3657
  this._entryKnownPeers = new Map();
3647
3658
  this._entryKnownPeerObservedAt = new Map();
3659
+ this._entryKnownPeerObservedAtSweptAt = 0;
3648
3660
  this._joinAuthoritativeRepairTimersByDelay = new Map();
3649
3661
  this._joinAuthoritativeRepairPeersByDelay = new Map();
3650
3662
  this._appendBackfillPendingByTarget = new Map();
@@ -3658,7 +3670,6 @@ export class SharedLog<
3658
3670
  this.recentlyRebalanced = new Cache<string>({ max: 1e4, ttl: 1e5 });
3659
3671
  this.uniqueReplicators = new Set();
3660
3672
  this._replicatorJoinEmitted = new Set();
3661
- this._replicatorsReconciled = false;
3662
3673
  this._liveness = this.createReplicatorLivenessMonitor();
3663
3674
  this._v2Receive = this.createReplicationInfoV2ReceiveCoordinator();
3664
3675
  this._v2Send = this.createReplicationInfoV2SendCoordinator();
@@ -6779,7 +6790,7 @@ export class SharedLog<
6779
6790
  rebalance?: boolean;
6780
6791
  checkDuplicates?: boolean;
6781
6792
  timestamp?: number;
6782
- allowLegacyOrderedReplacementPairs?: boolean;
6793
+ allowOrderedReplacementPairs?: boolean;
6783
6794
  onConfirmedDurableStateChanged?: () => void;
6784
6795
  onDurableApplyCommitted?: () => boolean | void;
6785
6796
  shouldApply?: () => boolean;
@@ -6821,7 +6832,7 @@ export class SharedLog<
6821
6832
  checkDuplicates,
6822
6833
  timestamp: ts,
6823
6834
  rebalance,
6824
- allowLegacyOrderedReplacementPairs,
6835
+ allowOrderedReplacementPairs,
6825
6836
  onConfirmedDurableStateChanged,
6826
6837
  onDurableApplyCommitted,
6827
6838
  shouldApply,
@@ -6830,7 +6841,7 @@ export class SharedLog<
6830
6841
  rebalance?: boolean;
6831
6842
  checkDuplicates?: boolean;
6832
6843
  timestamp?: number;
6833
- allowLegacyOrderedReplacementPairs?: boolean;
6844
+ allowOrderedReplacementPairs?: boolean;
6834
6845
  onConfirmedDurableStateChanged?: () => void;
6835
6846
  onDurableApplyCommitted?: () => boolean | void;
6836
6847
  shouldApply?: () => boolean;
@@ -6869,17 +6880,19 @@ export class SharedLog<
6869
6880
  incomingRangeCountsById.set(range.idString, count);
6870
6881
  if (
6871
6882
  count > 1 &&
6872
- (!allowLegacyOrderedReplacementPairs || reset === true || count > 2)
6883
+ (!allowOrderedReplacementPairs || reset === true || count > 2)
6873
6884
  ) {
6874
6885
  throw new Error(
6875
6886
  `Duplicate replication range id in announcement: ${range.idString}`,
6876
6887
  );
6877
6888
  }
6878
- // Released peers represented a non-reset replacement as the retired
6879
- // geometry followed by the current geometry under the same id. The
6880
- // sender is already authorized to replace that id with the final item,
6881
- // so collapsing an exact two-item incremental pair to its last item
6882
- // preserves rolling-upgrade compatibility without broadening authority.
6889
+ // Rolling-upgrade relaxation on the live V2 Added path: a peer may
6890
+ // still express a non-reset replacement as the retired geometry
6891
+ // followed by the current geometry under one id. The sender is already
6892
+ // authorized to replace that id with the final item, so collapsing an
6893
+ // EXACT two-item incremental pair to its last item accepts the older
6894
+ // wire shape without broadening authority. Deliberately narrow — reset
6895
+ // announcements and any run longer than two still fail as duplicates.
6883
6896
  incomingRangesById.set(range.idString, range);
6884
6897
  }
6885
6898
  const incomingRanges = [...incomingRangesById.values()];
@@ -7649,6 +7662,16 @@ export class SharedLog<
7649
7662
  this._nativeSharedLogState?.markEntriesKnownByPeer(hashArray, peer);
7650
7663
  this._nativeBackbone?.markEntriesKnownByPeer(hashArray, peer);
7651
7664
  const now = Date.now();
7665
+ // Growth is driven by writes, so the sweep rides the write path rather
7666
+ // than a timer or the rebalance pass: cost stays proportional to the
7667
+ // traffic that creates rows. Rate-limited to one pass per retention
7668
+ // window, over a map that after the first pass holds one window of marks.
7669
+ if (
7670
+ now - this._entryKnownPeerObservedAtSweptAt >=
7671
+ ENTRY_KNOWN_PEER_OBSERVED_AT_RETENTION_MS
7672
+ ) {
7673
+ this.sweepEntryKnownPeerObservedAt(now);
7674
+ }
7652
7675
  for (const hash of hashArray) {
7653
7676
  let peers = this._entryKnownPeers.get(hash);
7654
7677
  if (!peers) {
@@ -7718,6 +7741,28 @@ export class SharedLog<
7718
7741
  return observedAt != null && Date.now() - observedAt <= maxAgeMs;
7719
7742
  }
7720
7743
 
7744
+ /** Drop recency marks no reader can still act on.
7745
+ *
7746
+ * Touches ONLY `_entryKnownPeerObservedAt`. `_entryKnownPeers` carries
7747
+ * membership, not recency, and its rows stay until the peer dimension
7748
+ * clears them; the native mirrors have no recency dimension at all
7749
+ * (mark/remove/removePeer only), so this must not call into them or the
7750
+ * two sides would disagree.
7751
+ */
7752
+ private sweepEntryKnownPeerObservedAt(now: number) {
7753
+ for (const [hash, observedAt] of this._entryKnownPeerObservedAt) {
7754
+ for (const [peer, timestamp] of observedAt) {
7755
+ if (now - timestamp > ENTRY_KNOWN_PEER_OBSERVED_AT_RETENTION_MS) {
7756
+ observedAt.delete(peer);
7757
+ }
7758
+ }
7759
+ if (observedAt.size === 0) {
7760
+ this._entryKnownPeerObservedAt.delete(hash);
7761
+ }
7762
+ }
7763
+ this._entryKnownPeerObservedAtSweptAt = now;
7764
+ }
7765
+
7721
7766
  private markRepairSweepOptimisticPeer(
7722
7767
  gid: string,
7723
7768
  peer: string,
@@ -14213,6 +14258,7 @@ export class SharedLog<
14213
14258
  this._repairSweepOptimisticGidsByPeer = new Map();
14214
14259
  this._entryKnownPeers = new Map();
14215
14260
  this._entryKnownPeerObservedAt = new Map();
14261
+ this._entryKnownPeerObservedAtSweptAt = 0;
14216
14262
  this._joinAuthoritativeRepairTimersByDelay = new Map();
14217
14263
  this._joinAuthoritativeRepairPeersByDelay = new Map();
14218
14264
  this._assumeSyncedRepairSuppressedUntil = 0;
@@ -14235,7 +14281,6 @@ export class SharedLog<
14235
14281
 
14236
14282
  this.uniqueReplicators = new Set();
14237
14283
  this._replicatorJoinEmitted = new Set();
14238
- this._replicatorsReconciled = false;
14239
14284
  // Deserialized instances never ran the constructor; create the monitor and
14240
14285
  // coordinator lazily on first open. Reopens keep the SAME instances so
14241
14286
  // stale async continuations observe resets via property lookup.
@@ -15513,16 +15558,8 @@ export class SharedLog<
15513
15558
  const existingSubscribersPromise = this.node.services.pubsub.getSubscribers(
15514
15559
  this.topic,
15515
15560
  );
15516
- const replicationLifecycleController =
15517
- this._instanceLifecycle?.membershipLifecycleController;
15518
-
15519
15561
  // We do this here, because these calls requires this.closed == false
15520
15562
  void this.pruneOfflineReplicators()
15521
- .then(() => {
15522
- if (this.isReplicationLifecycleActive(replicationLifecycleController)) {
15523
- this._replicatorsReconciled = true;
15524
- }
15525
- })
15526
15563
  .catch((error) => {
15527
15564
  if (isNotStartedError(error as Error)) {
15528
15565
  return;
@@ -19652,7 +19689,7 @@ export class SharedLog<
19652
19689
  reset: msg instanceof FullReplicationInfoV2Message,
19653
19690
  checkDuplicates: true,
19654
19691
  timestamp: Number(context.message.header.timestamp),
19655
- allowLegacyOrderedReplacementPairs:
19692
+ allowOrderedReplacementPairs:
19656
19693
  msg instanceof AddedReplicationInfoV2Message,
19657
19694
  shouldApply: () => {
19658
19695
  mutationGateChecked = true;
@@ -19761,7 +19798,6 @@ export class SharedLog<
19761
19798
  }
19762
19799
  }
19763
19800
 
19764
-
19765
19801
  async calculateTotalParticipation(options?: { sum?: boolean }) {
19766
19802
  if (options?.sum) {
19767
19803
  const ranges = await this.replicationIndex.iterate().all();
@@ -23063,7 +23099,11 @@ export class SharedLog<
23063
23099
  }
23064
23100
  const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
23065
23101
  if (
23066
- !this._v2Receive.isRequestParked({ peerHash, peerSession, receiveEpoch })
23102
+ !this._v2Receive.isRequestParked({
23103
+ peerHash,
23104
+ peerSession,
23105
+ receiveEpoch,
23106
+ })
23067
23107
  ) {
23068
23108
  // Active, or a bounded request cycle is still running its own
23069
23109
  // exponential retries. Keep polling for the next park.
@@ -66,10 +66,19 @@ export class InstanceLifecycle {
66
66
  // code. Incremented synchronously with leader-cache invalidation so the
67
67
  // handler can detect whether its pre-join plan needs one fresh
68
68
  // post-persist audit.
69
+ //
70
+ // PERMANENT (fence census closed NO-GO 2026-08-12): a sub-generation
71
+ // WITHIN one lifecycle — it advances while the lifecycle identity is
72
+ // deliberately unchanged, so an identity/session token cannot tell a
73
+ // pre-invalidation plan from a post-invalidation one.
69
74
  public _receiveOwnershipRevision = 0;
70
75
  // Count of ownership-changing range mutations from queue admission
71
76
  // through settlement, including mutations already pending when a receive
72
77
  // starts.
78
+ //
79
+ // PERMANENT (fence census closed NO-GO 2026-08-12): a concurrency-DEPTH
80
+ // refcount, not a staleness token — identity answers "which generation
81
+ // started this?", never "how many mutation lanes are open right now?".
73
82
  public _receiveOwnershipMutationAdmissions = 0;
74
83
 
75
84
  // ---- stage 4: physically owned controllers (moved from SharedLog) ----
@@ -131,6 +131,11 @@ export class PeerSessionRegistry {
131
131
  // peer stays subscribed), and it must also fence a peer that never had a
132
132
  // session. Unlike sessions this map IS cleared at _close (see
133
133
  // clearReceiveEpochsForClose) and replaced at open.
134
+ //
135
+ // PERMANENT (fence census closed NO-GO 2026-08-12): the per-PEER lifetime
136
+ // above is the whole mechanism — a session token would rotate at exactly
137
+ // the moments this map must survive, and could not fence a session-less
138
+ // peer at all.
134
139
  _replicationInfoReceiveEpochByPeer!: Map<string, object>;
135
140
  // Moved from SharedLog (fence B6, same name — the sanctioned file-to-file
136
141
  // ratchet move). Refcount of in-flight destructive peer cleanups: while
@@ -140,6 +145,10 @@ export class PeerSessionRegistry {
140
145
  // reconnect may rotate the session; a fresh session with a zero gate
141
146
  // would reopen receive admission mid-drain. The map instance is replaced
142
147
  // only at open (resetForOpen) and cleared in place at _close.
148
+ //
149
+ // PERMANENT (fence census closed NO-GO 2026-08-12): both structural
150
+ // reasons at once — a concurrency-DEPTH refcount whose per-PEER lifetime
151
+ // deliberately spans the session rotation a reconnect causes mid-drain.
143
152
  _receiveCleanupGateByPeer!: Map<string, number>;
144
153
  // Moved from SharedLog (fence B5, same name). Peers whose replication-info
145
154
  // is fenced: added when a departure/unsubscribe rotation or a reconnect
package/src/pid.ts CHANGED
@@ -3,8 +3,6 @@ const MIN_MEMORY_HEADROOM_BALANCE_SCALER = 0.25;
3
3
  export class PIDReplicationController {
4
4
  integral!: number;
5
5
  prevError!: number;
6
- prevMemoryUsage!: number;
7
- prevTotalFactor!: number;
8
6
  kp: number;
9
7
  ki: number;
10
8
  kd: number;
@@ -42,9 +40,6 @@ export class PIDReplicationController {
42
40
  let { memoryUsage, totalFactor, peerCount, cpuUsage, currentFactor } =
43
41
  properties;
44
42
 
45
- this.prevTotalFactor = totalFactor;
46
- this.prevMemoryUsage = memoryUsage;
47
-
48
43
  const estimatedTotalSize =
49
44
  currentFactor > 0 ? memoryUsage / currentFactor : 1e5;
50
45
 
@@ -238,6 +233,5 @@ export class PIDReplicationController {
238
233
  reset() {
239
234
  this.prevError = 0;
240
235
  this.integral = 0;
241
- this.prevMemoryUsage = 0;
242
236
  }
243
237
  }
@@ -84,12 +84,6 @@ type LocalCapabilityReadyProperties = {
84
84
 
85
85
  export type ReplicationInfoV2LocalCapabilityAdvertisementHandle = {
86
86
  firstAttempt: Promise<void>;
87
- /**
88
- * B12: the two-phase legacy barrier is retired — an ACKed advert promotes
89
- * readiness immediately. Retained as a no-op so the handle shape (and the
90
- * tests that drive it) stay stable.
91
- */
92
- releaseLegacyBarrier(): void;
93
87
  };
94
88
 
95
89
  export type ReplicationInfoV2LocalCapabilityContext = {
@@ -203,6 +197,14 @@ export type ReplicationInfoV2ReceiveDeps = {
203
197
  */
204
198
  export class ReplicationInfoV2ReceiveCoordinator {
205
199
  _receiveStates!: Map<string, ReplicationInfoV2ReceiveState>;
200
+ /**
201
+ * Post-B12 V2 resync memory: sessions that have already taken one full
202
+ * replication-info snapshot. Two load-bearing reads, both in
203
+ * observeCapability: phase selection (a remembered session opens in
204
+ * "resync" instead of "awaiting-full"), and capability refresh (the
205
+ * preserveCutover decision, which keeps the memory only when a re-advert
206
+ * arrives under the same session with a ready sender).
207
+ */
206
208
  _cutoverPeerSessions!: WeakSet<object>;
207
209
  _localCapabilityReadyBySession!: WeakMap<object, LocalCapabilityReady>;
208
210
  _localCapabilityContextBySession!: WeakMap<
@@ -292,15 +294,17 @@ export class ReplicationInfoV2ReceiveCoordinator {
292
294
  }
293
295
 
294
296
  /** Revoke an unauthenticated or downgraded capability generation. */
295
- revokePeerCapability(peerHash: string, reopenLegacy = true): void {
297
+ revokePeerCapability(peerHash: string): void {
296
298
  const state = this._receiveStates.get(peerHash);
297
299
  if (!state) {
298
300
  return;
299
301
  }
300
302
  this.clearState(state);
301
- if (reopenLegacy) {
302
- this._cutoverPeerSessions.delete(state.peerSession);
303
- }
303
+ // Unconditional: the revoked session must lose its resync memory so a
304
+ // re-advert starts from the first phase again. (This used to sit behind
305
+ // an opt-out parameter that defaulted to true, had no else-arm, and that
306
+ // no caller ever overrode.)
307
+ this._cutoverPeerSessions.delete(state.peerSession);
304
308
  }
305
309
 
306
310
  private clearState(state: ReplicationInfoV2ReceiveState): void {
@@ -531,7 +535,6 @@ export class ReplicationInfoV2ReceiveCoordinator {
531
535
  ) {
532
536
  return {
533
537
  firstAttempt: Promise.resolve(),
534
- releaseLegacyBarrier: () => {},
535
538
  };
536
539
  }
537
540
  let context = this._localCapabilityContextBySession.get(
@@ -545,7 +548,6 @@ export class ReplicationInfoV2ReceiveCoordinator {
545
548
  ) {
546
549
  return {
547
550
  firstAttempt: Promise.resolve(),
548
- releaseLegacyBarrier: () => {},
549
551
  };
550
552
  }
551
553
  if (!context) {
@@ -584,7 +586,6 @@ export class ReplicationInfoV2ReceiveCoordinator {
584
586
  ) {
585
587
  return {
586
588
  firstAttempt: Promise.resolve(),
587
- releaseLegacyBarrier: () => {},
588
589
  };
589
590
  }
590
591
  if (!state) {
@@ -619,7 +620,6 @@ export class ReplicationInfoV2ReceiveCoordinator {
619
620
  (state.firstAttempt = this.runLocalCapabilityAdvertisement(state));
620
621
  return {
621
622
  firstAttempt,
622
- releaseLegacyBarrier: () => {},
623
623
  };
624
624
  }
625
625
 
package/src/role.ts CHANGED
@@ -6,19 +6,10 @@
6
6
  import { field, variant, vec } from "@dao-xyz/borsh";
7
7
  import { MAX_U32, denormalizer } from "./integers.js";
8
8
 
9
- export const overlaps = (x1: number, x2: number, y1: number, y2: number) => {
10
- if (x1 <= y2 && y1 <= x2) {
11
- return true;
12
- }
13
- return false;
14
- };
15
-
16
9
  export abstract class Role {
17
10
  abstract equals(other: Role): boolean;
18
11
  }
19
12
 
20
- export const NO_TYPE_VARIANT = new Uint8Array([0]);
21
-
22
13
  @variant(0)
23
14
  export class NoType extends Role {
24
15
  equals(other: Role) {
@@ -26,8 +17,6 @@ export class NoType extends Role {
26
17
  }
27
18
  }
28
19
 
29
- export const OBSERVER_TYPE_VARIANT = new Uint8Array([1]);
30
-
31
20
  @variant(1)
32
21
  export class Observer extends Role {
33
22
  equals(other: Role) {
@@ -35,8 +24,6 @@ export class Observer extends Role {
35
24
  }
36
25
  }
37
26
 
38
- export const REPLICATOR_TYPE_VARIANT = new Uint8Array([2]);
39
-
40
27
  const denormalizeru32 = denormalizer("u32");
41
28
  export class RoleReplicationSegment {
42
29
  @field({ type: "u64" })
@@ -325,7 +325,7 @@ export const SYNC_MESSAGE_PRIORITY = CONVERGENCE_MESSAGE_PRIORITY;
325
325
  // large historical backfills.
326
326
  const SIMPLE_SYNC_RETRY_AFTER_MS = 10_000;
327
327
  const EXCHANGE_HEAD_RESPONSE_DEDUPE_TTL_MS = SIMPLE_SYNC_RETRY_AFTER_MS - 1_000;
328
- const RECENT_KNOWN_EXCHANGE_HEAD_SUPPRESSION_MS = 30_000;
328
+ export const RECENT_KNOWN_EXCHANGE_HEAD_SUPPRESSION_MS = 30_000;
329
329
  const PENDING_MAYBE_SYNC_RESPONSE_TTL_MS = 30_000;
330
330
  // An incoming maybe-sync claim keeps one retry candidate in both
331
331
  // syncInFlightQueue and syncInFlightQueueInverted. Bound associations rather