@peerbit/shared-log 13.1.16 → 13.1.17

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 (39) hide show
  1. package/dist/benchmark/native-graph.d.ts +2 -0
  2. package/dist/benchmark/native-graph.d.ts.map +1 -0
  3. package/dist/benchmark/native-graph.js +249 -0
  4. package/dist/benchmark/native-graph.js.map +1 -0
  5. package/dist/benchmark/sync-batch-sweep.js +72 -24
  6. package/dist/benchmark/sync-batch-sweep.js.map +1 -1
  7. package/dist/benchmark/sync-phase-profile.d.ts +2 -0
  8. package/dist/benchmark/sync-phase-profile.d.ts.map +1 -0
  9. package/dist/benchmark/sync-phase-profile.js +303 -0
  10. package/dist/benchmark/sync-phase-profile.js.map +1 -0
  11. package/dist/src/checked-prune.d.ts +55 -0
  12. package/dist/src/checked-prune.d.ts.map +1 -0
  13. package/dist/src/checked-prune.js +244 -0
  14. package/dist/src/checked-prune.js.map +1 -0
  15. package/dist/src/index.d.ts +10 -9
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/index.js +265 -164
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/sync/index.d.ts +9 -1
  20. package/dist/src/sync/index.d.ts.map +1 -1
  21. package/dist/src/sync/profile.d.ts +3 -0
  22. package/dist/src/sync/profile.d.ts.map +1 -0
  23. package/dist/src/sync/profile.js +2 -0
  24. package/dist/src/sync/profile.js.map +1 -0
  25. package/dist/src/sync/rateless-iblt.d.ts +25 -11
  26. package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
  27. package/dist/src/sync/rateless-iblt.js +597 -106
  28. package/dist/src/sync/rateless-iblt.js.map +1 -1
  29. package/dist/src/sync/simple.d.ts +5 -0
  30. package/dist/src/sync/simple.d.ts.map +1 -1
  31. package/dist/src/sync/simple.js +224 -74
  32. package/dist/src/sync/simple.js.map +1 -1
  33. package/package.json +17 -13
  34. package/src/checked-prune.ts +331 -0
  35. package/src/index.ts +429 -282
  36. package/src/sync/index.ts +11 -1
  37. package/src/sync/profile.ts +9 -0
  38. package/src/sync/rateless-iblt.ts +768 -128
  39. package/src/sync/simple.ts +256 -94
package/src/index.ts CHANGED
@@ -54,6 +54,8 @@ import {
54
54
  ACK_CONTROL_PRIORITY,
55
55
  AcknowledgeDelivery,
56
56
  AnyWhere,
57
+ BACKGROUND_MESSAGE_PRIORITY,
58
+ CONVERGENCE_MESSAGE_PRIORITY,
57
59
  createRequestTransportContext,
58
60
  DataMessage,
59
61
  MessageHeader,
@@ -72,6 +74,12 @@ import pDefer, { type DeferredPromise } from "p-defer";
72
74
  import PQueue from "p-queue";
73
75
  import { concat, fromString } from "uint8arrays";
74
76
  import { BlocksMessage } from "./blocks.js";
77
+ import {
78
+ CheckedPruneCoordinator,
79
+ type CheckedPruneEntry,
80
+ type CheckedPruneLeaderMap,
81
+ type CheckedPruneRetryState,
82
+ } from "./checked-prune.js";
75
83
  import { type CPUUsage, CPUUsageIntervalLag } from "./cpu.js";
76
84
  import {
77
85
  type DebouncedAccumulatorMap,
@@ -169,7 +177,11 @@ import type {
169
177
  Syncronizer,
170
178
  } from "./sync/index.js";
171
179
  import { RatelessIBLTSynchronizer } from "./sync/rateless-iblt.js";
172
- import { ConfirmEntriesMessage, SimpleSyncronizer } from "./sync/simple.js";
180
+ import {
181
+ ConfirmEntriesMessage,
182
+ SYNC_MESSAGE_PRIORITY,
183
+ SimpleSyncronizer,
184
+ } from "./sync/simple.js";
173
185
  import { groupByGid } from "./utils.js";
174
186
 
175
187
  const toLocalPublicSignKey = (
@@ -241,12 +253,6 @@ export { MAX_U32, MAX_U64, type NumberFromType };
241
253
  export const logger = loggerFn("peerbit:shared-log");
242
254
  const warn = logger.newScope("warn");
243
255
 
244
- type CheckedPruneLeaderMap = Map<string, { intersecting: boolean }>;
245
- type CheckedPruneEntry<T, R extends "u32" | "u64"> =
246
- | Entry<T>
247
- | ShallowEntry
248
- | EntryReplicated<R>;
249
-
250
256
  const getLatestEntry = (
251
257
  entries: (ShallowOrFullEntry<any> | EntryWithRefs<any>)[],
252
258
  ) => {
@@ -494,6 +500,7 @@ export const WAIT_FOR_PRUNE_DELAY = 0;
494
500
  const PRUNE_DEBOUNCE_INTERVAL = 500;
495
501
  const CHECKED_PRUNE_RESEND_INTERVAL_MIN_MS = 250;
496
502
  const CHECKED_PRUNE_RESEND_INTERVAL_MAX_MS = 5_000;
503
+ const CHECKED_PRUNE_BACKGROUND_TIMEOUT_MIN_MS = 120_000;
497
504
  const CHECKED_PRUNE_RETRY_MAX_ATTEMPTS = 3;
498
505
  const CHECKED_PRUNE_RETRY_MAX_DELAY_MS = 30_000;
499
506
 
@@ -799,15 +806,11 @@ export class SharedLog<
799
806
  SharedLogOptions<T, D, R>;
800
807
  private _closeController!: AbortController;
801
808
  private _respondToIHaveTimeout!: any;
802
- private _pendingDeletes!: Map<
803
- string,
804
- {
805
- promise: DeferredPromise<void>;
806
- clear: () => void;
807
- resolve: (publicKeyHash: string) => Promise<void> | void;
808
- reject(reason: any): Promise<void> | void;
809
- }
810
- >;
809
+ private _checkedPrune!: CheckedPruneCoordinator<T, R>;
810
+
811
+ private get _pendingDeletes() {
812
+ return this._checkedPrune.pendingDeletes;
813
+ }
811
814
 
812
815
  private _pendingIHave!: Map<
813
816
  string,
@@ -879,12 +882,15 @@ export class SharedLog<
879
882
  >
880
883
  >;
881
884
 
882
- private _requestIPruneSent!: Map<string, Set<string>>; // tracks entry hash to peer hash for requesting I prune messages
883
- private _requestIPruneResponseReplicatorSet!: Map<string, Set<string>>; // tracks entry hash to peer hash
884
- private _checkedPruneRetries!: Map<
885
- string,
886
- { attempts: number; timer?: ReturnType<typeof setTimeout> }
887
- >;
885
+ private get _requestIPruneSent() {
886
+ return this._checkedPrune.requestIPruneSent;
887
+ }
888
+ private get _requestIPruneResponseReplicatorSet() {
889
+ return this._checkedPrune.responseReplicatorSet;
890
+ }
891
+ private get _checkedPruneRetries() {
892
+ return this._checkedPrune.retries;
893
+ }
888
894
 
889
895
  private replicationChangeDebounceFn!: ReturnType<
890
896
  typeof debounceAggregationChanges<ReplicationRangeIndexable<R>>
@@ -1081,7 +1087,7 @@ export class SharedLog<
1081
1087
  header: new MessageHeader({
1082
1088
  session: 0,
1083
1089
  mode: new AnyWhere(),
1084
- priority: 0,
1090
+ priority: BACKGROUND_MESSAGE_PRIORITY,
1085
1091
  }),
1086
1092
  });
1087
1093
  contextMessage.header.timestamp = envelope.timestamp;
@@ -1113,7 +1119,7 @@ export class SharedLog<
1113
1119
  header: new MessageHeader({
1114
1120
  session: 0,
1115
1121
  mode: new AnyWhere(),
1116
- priority: 0,
1122
+ priority: BACKGROUND_MESSAGE_PRIORITY,
1117
1123
  }),
1118
1124
  });
1119
1125
  contextMessage.header.timestamp = detail.timestamp;
@@ -2015,7 +2021,7 @@ export class SharedLog<
2015
2021
  segmentIds: rangesToUnreplicate.map((x) => x.id),
2016
2022
  }),
2017
2023
  {
2018
- priority: 1,
2024
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
2019
2025
  },
2020
2026
  );
2021
2027
  }
@@ -2163,7 +2169,7 @@ export class SharedLog<
2163
2169
  this.node.identity.publicKey,
2164
2170
  );
2165
2171
  await this.rpc.send(new StoppedReplicating({ segmentIds }), {
2166
- priority: 1,
2172
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
2167
2173
  });
2168
2174
  }
2169
2175
 
@@ -2189,7 +2195,7 @@ export class SharedLog<
2189
2195
  // announce that we are no longer replicating
2190
2196
 
2191
2197
  await this.rpc.send(new AllReplicatingSegmentsMessage({ segments: [] }), {
2192
- priority: 1,
2198
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
2193
2199
  });
2194
2200
  }
2195
2201
 
@@ -2673,7 +2679,7 @@ export class SharedLog<
2673
2679
  return options.announce(message);
2674
2680
  } else {
2675
2681
  await this.rpc.send(message, {
2676
- priority: 1,
2682
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
2677
2683
  });
2678
2684
  }
2679
2685
  }
@@ -2902,7 +2908,7 @@ export class SharedLog<
2902
2908
  i + REPAIR_CONFIRMATION_HASH_BATCH_SIZE,
2903
2909
  );
2904
2910
  await this.rpc.send(new ConfirmEntriesMessage({ hashes: chunk }), {
2905
- priority: 1,
2911
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
2906
2912
  mode: new SilentDelivery({ to: [target], redundancy: 1 }),
2907
2913
  });
2908
2914
  }
@@ -2918,7 +2924,7 @@ export class SharedLog<
2918
2924
  )) {
2919
2925
  message.reserved[0] |= EXCHANGE_HEADS_REPAIR_HINT;
2920
2926
  await this.rpc.send(message, {
2921
- priority: 1,
2927
+ priority: SYNC_MESSAGE_PRIORITY,
2922
2928
  mode: new SilentDelivery({ to: [target], redundancy: 1 }),
2923
2929
  });
2924
2930
  }
@@ -3569,30 +3575,32 @@ export class SharedLog<
3569
3575
  if (this.keep && (await this.keep(args.value.entry))) {
3570
3576
  return false;
3571
3577
  }
3578
+ this._checkedPrune.trackCandidate(
3579
+ args.key,
3580
+ args.value.entry,
3581
+ args.value.leaders,
3582
+ );
3572
3583
  void this.pruneDebouncedFn.add(args);
3573
3584
  return true;
3574
3585
  }
3575
3586
 
3576
- private async cancelCheckedPruneForLocalLeader(hash: string) {
3587
+ private async cancelCheckedPruneForLocalLeader(
3588
+ hash: string,
3589
+ options?: { preserveRetry?: boolean },
3590
+ ) {
3577
3591
  this.pruneDebouncedFn.delete(hash);
3578
- this.clearCheckedPruneRetry(hash);
3579
- this.removePruneRequestSent(hash);
3580
- this._requestIPruneResponseReplicatorSet.delete(hash);
3581
- await this._pendingDeletes
3582
- .get(hash)
3583
- ?.reject(new Error("Failed to delete, is leader again"));
3592
+ const pendingDelete = this._checkedPrune.getPendingDelete(hash);
3593
+ this._checkedPrune.markCancelled(hash, {
3594
+ preserveRetry: options?.preserveRetry,
3595
+ });
3596
+ await pendingDelete?.reject(new Error("Failed to delete, is leader again"));
3584
3597
  }
3585
3598
 
3586
3599
  private hasActiveCheckedPruneWork(hash: string) {
3587
- return (
3588
- this._pendingDeletes.has(hash) ||
3589
- this._requestIPruneSent.has(hash) ||
3590
- this._requestIPruneResponseReplicatorSet.has(hash) ||
3591
- this._checkedPruneRetries.has(hash)
3592
- );
3600
+ return this._checkedPrune.hasActiveWork(hash);
3593
3601
  }
3594
3602
 
3595
- private async resolveCheckedPruneLeaders(args: {
3603
+ private async revalidateCheckedPruneOwnership(args: {
3596
3604
  hash: string;
3597
3605
  entry: CheckedPruneEntry<T, R>;
3598
3606
  leaders: CheckedPruneLeaderMap;
@@ -3660,7 +3668,7 @@ export class SharedLog<
3660
3668
  continue;
3661
3669
  }
3662
3670
 
3663
- if (this._pendingDeletes.has(entry.hash)) {
3671
+ if (this._checkedPrune.hasPendingDelete(entry.hash)) {
3664
3672
  continue;
3665
3673
  }
3666
3674
 
@@ -3676,7 +3684,9 @@ export class SharedLog<
3676
3684
  }
3677
3685
  }
3678
3686
 
3679
- private async pruneIndexedEntriesNoLongerLed() {
3687
+ private async pruneIndexedEntriesNoLongerLed(options?: {
3688
+ useDefaultRoleAge?: boolean;
3689
+ }) {
3680
3690
  const selfHash = this.node.identity.publicKey.hashcode();
3681
3691
  const iterator = this.entryCoordinatesIndex.iterate({});
3682
3692
  let enqueuedPrune = false;
@@ -3692,7 +3702,7 @@ export class SharedLog<
3692
3702
  const leaders = await this.findLeaders(
3693
3703
  entryReplicated.coordinates,
3694
3704
  entryReplicated,
3695
- { roleAge: 0 },
3705
+ options?.useDefaultRoleAge ? undefined : { roleAge: 0 },
3696
3706
  );
3697
3707
 
3698
3708
  if (leaders.has(selfHash)) {
@@ -3700,7 +3710,7 @@ export class SharedLog<
3700
3710
  continue;
3701
3711
  }
3702
3712
 
3703
- if (this._pendingDeletes.has(entryReplicated.hash)) {
3713
+ if (this._checkedPrune.hasPendingDelete(entryReplicated.hash)) {
3704
3714
  continue;
3705
3715
  }
3706
3716
 
@@ -3724,12 +3734,65 @@ export class SharedLog<
3724
3734
  }
3725
3735
  }
3726
3736
 
3727
- private clearCheckedPruneRetry(hash: string) {
3728
- const state = this._checkedPruneRetries.get(hash);
3729
- if (state?.timer) {
3730
- clearTimeout(state.timer);
3737
+ private async pruneCurrentHeadsNoLongerLed(options?: {
3738
+ useDefaultRoleAge?: boolean;
3739
+ }) {
3740
+ const selfHash = this.node.identity.publicKey.hashcode();
3741
+ const heads = await this.log.getHeads(true).all();
3742
+ let enqueuedPrune = false;
3743
+
3744
+ for (const head of heads) {
3745
+ if (this.closed) {
3746
+ break;
3747
+ }
3748
+
3749
+ const leaders = await this.findLeadersFromEntry(
3750
+ head,
3751
+ maxReplicas(this, [head]),
3752
+ options?.useDefaultRoleAge ? undefined : { roleAge: 0 },
3753
+ );
3754
+
3755
+ if (leaders.has(selfHash)) {
3756
+ await this.cancelCheckedPruneForLocalLeader(head.hash);
3757
+ continue;
3758
+ }
3759
+
3760
+ if (this._checkedPrune.hasPendingDelete(head.hash)) {
3761
+ continue;
3762
+ }
3763
+
3764
+ if (leaders.size === 0) {
3765
+ continue;
3766
+ }
3767
+
3768
+ enqueuedPrune =
3769
+ (await this.pruneDebouncedFnAddIfNotKeeping({
3770
+ key: head.hash,
3771
+ value: { entry: head, leaders },
3772
+ })) || enqueuedPrune;
3773
+ this.responseToPruneDebouncedFn.delete(head.hash);
3774
+ }
3775
+
3776
+ if (enqueuedPrune && !this.closed) {
3777
+ await this.pruneDebouncedFn.flush();
3778
+ }
3779
+ }
3780
+
3781
+ private checkedPruneLeadersToMap(
3782
+ leaders: CheckedPruneLeaderMap | Set<string>,
3783
+ ): CheckedPruneLeaderMap {
3784
+ if (leaders instanceof Map) {
3785
+ return new Map(leaders);
3786
+ }
3787
+ const leadersMap: CheckedPruneLeaderMap = new Map();
3788
+ for (const leader of leaders) {
3789
+ leadersMap.set(leader, { intersecting: true });
3731
3790
  }
3732
- this._checkedPruneRetries.delete(hash);
3791
+ return leadersMap;
3792
+ }
3793
+
3794
+ private clearCheckedPruneRetry(hash: string) {
3795
+ this._checkedPrune.clearRetry(hash);
3733
3796
  }
3734
3797
 
3735
3798
  private scheduleCheckedPruneRetry(args: {
@@ -3737,11 +3800,18 @@ export class SharedLog<
3737
3800
  leaders: CheckedPruneLeaderMap | Set<string>;
3738
3801
  }) {
3739
3802
  if (this.closed) return;
3740
- if (this._pendingDeletes.has(args.entry.hash)) return;
3803
+ if (this._checkedPrune.hasPendingDelete(args.entry.hash)) return;
3741
3804
 
3742
3805
  const hash = args.entry.hash;
3743
3806
  const state =
3744
- this._checkedPruneRetries.get(hash) ?? { attempts: 0 };
3807
+ this._checkedPrune.getRetry(hash) ??
3808
+ ({
3809
+ attempts: 0,
3810
+ entry: args.entry,
3811
+ leaders: args.leaders,
3812
+ } satisfies CheckedPruneRetryState<T, R>);
3813
+ state.entry = args.entry;
3814
+ state.leaders = args.leaders;
3745
3815
 
3746
3816
  if (state.timer) return;
3747
3817
  if (state.attempts >= CHECKED_PRUNE_RETRY_MAX_ATTEMPTS) {
@@ -3759,15 +3829,17 @@ export class SharedLog<
3759
3829
 
3760
3830
  state.attempts = attempt;
3761
3831
  state.timer = setTimeout(async () => {
3762
- const st = this._checkedPruneRetries.get(hash);
3832
+ const st = this._checkedPrune.getRetry(hash);
3763
3833
  if (st) st.timer = undefined;
3764
3834
  if (this.closed) return;
3765
- if (this._pendingDeletes.has(hash)) return;
3835
+ if (this._checkedPrune.hasPendingDelete(hash)) return;
3836
+ const retryEntry = st?.entry ?? args.entry;
3837
+ const retryLeaders = st?.leaders ?? args.leaders;
3766
3838
 
3767
3839
  let leadersMap: CheckedPruneLeaderMap | undefined;
3768
3840
  try {
3769
- const replicas = decodeReplicas(args.entry).getValue(this);
3770
- leadersMap = await this.findLeadersFromEntry(args.entry, replicas, {
3841
+ const replicas = decodeReplicas(retryEntry).getValue(this);
3842
+ leadersMap = await this.findLeadersFromEntry(retryEntry, replicas, {
3771
3843
  roleAge: 0,
3772
3844
  });
3773
3845
  } catch {
@@ -3775,14 +3847,7 @@ export class SharedLog<
3775
3847
  }
3776
3848
 
3777
3849
  if (!leadersMap || leadersMap.size === 0) {
3778
- if (args.leaders instanceof Map) {
3779
- leadersMap = args.leaders;
3780
- } else {
3781
- leadersMap = new Map<string, { intersecting: boolean }>();
3782
- for (const k of args.leaders) {
3783
- leadersMap.set(k, { intersecting: true });
3784
- }
3785
- }
3850
+ leadersMap = this.checkedPruneLeadersToMap(retryLeaders);
3786
3851
  }
3787
3852
 
3788
3853
  try {
@@ -3790,14 +3855,81 @@ export class SharedLog<
3790
3855
  leadersMap ?? new Map<string, { intersecting: boolean }>();
3791
3856
  await this.pruneDebouncedFnAddIfNotKeeping({
3792
3857
  key: hash,
3793
- value: { entry: args.entry, leaders: leadersForRetry },
3858
+ value: { entry: retryEntry, leaders: leadersForRetry },
3794
3859
  });
3795
3860
  } catch {
3796
3861
  // Best-effort only; pruning will be re-attempted on future changes.
3797
3862
  }
3798
3863
  }, delayMs);
3799
3864
  state.timer.unref?.();
3800
- this._checkedPruneRetries.set(hash, state);
3865
+ this._checkedPrune.setRetry(hash, state);
3866
+ }
3867
+
3868
+ private async recoverCheckedPruneFromLateResponses(
3869
+ hashes: string[],
3870
+ publicKeyHash: string,
3871
+ ) {
3872
+ if (this.closed) return;
3873
+ const selfHash = this.node.identity.publicKey.hashcode();
3874
+ const toPrune = new Map<
3875
+ string,
3876
+ {
3877
+ entry: CheckedPruneEntry<T, R>;
3878
+ leaders: CheckedPruneLeaderMap;
3879
+ }
3880
+ >();
3881
+ const responseStillApplies: string[] = [];
3882
+
3883
+ for (const hash of hashes) {
3884
+ if (this.closed) {
3885
+ break;
3886
+ }
3887
+ if (this._checkedPrune.hasPendingDelete(hash)) {
3888
+ continue;
3889
+ }
3890
+ const retry = this._checkedPrune.clearRetryTimer(hash);
3891
+ if (!retry) {
3892
+ continue;
3893
+ }
3894
+
3895
+ const entry = retry.entry;
3896
+ let leaders = this.checkedPruneLeadersToMap(retry.leaders);
3897
+ try {
3898
+ const currentLeaders = await this.findLeadersFromEntry(
3899
+ entry,
3900
+ decodeReplicas(entry).getValue(this),
3901
+ { roleAge: 0 },
3902
+ );
3903
+ if (currentLeaders.size > 0) {
3904
+ leaders = currentLeaders;
3905
+ }
3906
+ } catch {
3907
+ // Best-effort only; the stored retry leaders came from a previous
3908
+ // checked-prune decision for this exact entry.
3909
+ }
3910
+
3911
+ if (leaders.has(selfHash)) {
3912
+ await this.cancelCheckedPruneForLocalLeader(hash);
3913
+ continue;
3914
+ }
3915
+ if (leaders.size === 0) {
3916
+ continue;
3917
+ }
3918
+
3919
+ toPrune.set(hash, { entry, leaders });
3920
+ if (leaders.has(publicKeyHash)) {
3921
+ responseStillApplies.push(hash);
3922
+ }
3923
+ }
3924
+
3925
+ if (toPrune.size === 0) {
3926
+ return;
3927
+ }
3928
+
3929
+ void Promise.allSettled(this.prune(toPrune));
3930
+ for (const hash of responseStillApplies) {
3931
+ void this._checkedPrune.getPendingDelete(hash)?.resolve(publicKeyHash);
3932
+ }
3801
3933
  }
3802
3934
 
3803
3935
  async append(
@@ -3947,7 +4079,7 @@ export class SharedLog<
3947
4079
  this.domain.resolution,
3948
4080
  );
3949
4081
  this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 2e4;
3950
- this._pendingDeletes = new Map();
4082
+ this._checkedPrune = new CheckedPruneCoordinator<T, R>();
3951
4083
  this._pendingIHave = new Map();
3952
4084
  this.latestReplicationInfoMessage = new Map();
3953
4085
  this._replicationInfoBlockedPeers = new Set();
@@ -4139,10 +4271,6 @@ export class SharedLog<
4139
4271
  })) > 0;
4140
4272
 
4141
4273
  this._gidPeersHistory = new Map();
4142
- this._requestIPruneSent = new Map();
4143
- this._requestIPruneResponseReplicatorSet = new Map();
4144
- this._checkedPruneRetries = new Map();
4145
-
4146
4274
  this.replicationChangeDebounceFn = debounceAggregationChanges<
4147
4275
  ReplicationRangeIndexable<R>
4148
4276
  >(
@@ -4164,14 +4292,24 @@ export class SharedLog<
4164
4292
  >();
4165
4293
  const selfReplicating = await this.isReplicating();
4166
4294
  for (const [hash, value] of map) {
4167
- const checkedPruneLeaders = await this.resolveCheckedPruneLeaders({
4168
- hash,
4169
- entry: value.entry,
4170
- leaders: value.leaders,
4171
- selfReplicating,
4172
- });
4295
+ const checkedPruneLeaders =
4296
+ await this.revalidateCheckedPruneOwnership({
4297
+ hash,
4298
+ entry: value.entry,
4299
+ leaders: value.leaders,
4300
+ selfReplicating,
4301
+ });
4173
4302
  if (checkedPruneLeaders.localLeader) {
4174
- await this.cancelCheckedPruneForLocalLeader(hash);
4303
+ const preserveRetry = this._checkedPrune.hasRetry(hash);
4304
+ await this.cancelCheckedPruneForLocalLeader(hash, {
4305
+ preserveRetry,
4306
+ });
4307
+ if (preserveRetry) {
4308
+ this.scheduleCheckedPruneRetry({
4309
+ entry: value.entry,
4310
+ leaders: checkedPruneLeaders.leaders,
4311
+ });
4312
+ }
4175
4313
  continue;
4176
4314
  }
4177
4315
  current.set(hash, {
@@ -4217,7 +4355,7 @@ export class SharedLog<
4217
4355
  to: allRequestingPeers,
4218
4356
  redundancy: 1,
4219
4357
  }),
4220
- priority: 1,
4358
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
4221
4359
  });
4222
4360
  },
4223
4361
  () => {
@@ -4581,20 +4719,7 @@ export class SharedLog<
4581
4719
  this.cancelReplicationInfoRequests(peerHash);
4582
4720
  this._replicatorLivenessFailures.delete(peerHash);
4583
4721
  this._replicatorLastActivityAt.delete(peerHash);
4584
-
4585
- for (const [hash, peers] of this._requestIPruneSent) {
4586
- peers.delete(peerHash);
4587
- if (peers.size === 0) {
4588
- this._requestIPruneSent.delete(hash);
4589
- }
4590
- }
4591
-
4592
- for (const [hash, peers] of this._requestIPruneResponseReplicatorSet) {
4593
- peers.delete(peerHash);
4594
- if (peers.size === 0) {
4595
- this._requestIPruneResponseReplicatorSet.delete(hash);
4596
- }
4597
- }
4722
+ this._checkedPrune.cleanupPeer(peerHash);
4598
4723
  }
4599
4724
 
4600
4725
  private markReplicatorActivity(peerHash: string, now = Date.now()) {
@@ -4658,7 +4783,7 @@ export class SharedLog<
4658
4783
  const self = this.node.identity.publicKey.hashcode();
4659
4784
  const seed = hashToSeed32(hash);
4660
4785
 
4661
- const hinted = this._requestIPruneResponseReplicatorSet.get(hash);
4786
+ const hinted = this._checkedPrune.getConfirmedReplicators(hash);
4662
4787
  if (hinted && hinted.size > 0) {
4663
4788
  const peers = [...hinted].filter((p) => p !== self);
4664
4789
  return peers.length > 0
@@ -4666,7 +4791,7 @@ export class SharedLog<
4666
4791
  : undefined;
4667
4792
  }
4668
4793
 
4669
- const contacted = this._requestIPruneSent.get(hash);
4794
+ const contacted = this._checkedPrune.getContactedReplicators(hash);
4670
4795
  if (contacted && contacted.size > 0) {
4671
4796
  const peers = [...contacted].filter((p) => p !== self);
4672
4797
  return peers.length > 0
@@ -5084,25 +5209,15 @@ export class SharedLog<
5084
5209
  }
5085
5210
  this._appendBackfillPendingByTarget.clear();
5086
5211
 
5087
- for (const [_k, v] of this._pendingDeletes) {
5088
- v.clear();
5089
- v.promise.resolve(); // TODO or reject?
5090
- }
5091
5212
  for (const [_k, v] of this._pendingIHave) {
5092
5213
  v.clear();
5093
5214
  }
5094
- for (const [_k, v] of this._checkedPruneRetries) {
5095
- if (v.timer) clearTimeout(v.timer);
5096
- }
5215
+ this._checkedPrune.close();
5097
5216
 
5098
5217
  await this.remoteBlocks.stop();
5099
- this._pendingDeletes.clear();
5100
5218
  this._pendingIHave.clear();
5101
- this._checkedPruneRetries.clear();
5102
5219
  this.latestReplicationInfoMessage.clear();
5103
5220
  this._gidPeersHistory.clear();
5104
- this._requestIPruneSent.clear();
5105
- this._requestIPruneResponseReplicatorSet.clear();
5106
5221
  // Cancel any pending debounced timers so they can't fire after we've torn down
5107
5222
  // indexes/RPC state.
5108
5223
  this.rebalanceParticipationDebounced?.close();
@@ -5158,7 +5273,7 @@ export class SharedLog<
5158
5273
  try {
5159
5274
  await this.rpc
5160
5275
  .send(new AllReplicatingSegmentsMessage({ segments: [] }), {
5161
- priority: 1,
5276
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
5162
5277
  signal: abort.signal,
5163
5278
  })
5164
5279
  .catch(() => {});
@@ -5205,7 +5320,7 @@ export class SharedLog<
5205
5320
  try {
5206
5321
  await this.rpc
5207
5322
  .send(new AllReplicatingSegmentsMessage({ segments: [] }), {
5208
- priority: 1,
5323
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
5209
5324
  signal: abort.signal,
5210
5325
  })
5211
5326
  .catch(() => {});
@@ -5390,7 +5505,7 @@ export class SharedLog<
5390
5505
  for (const entry of entries) {
5391
5506
  this.pruneDebouncedFn.delete(entry.entry.hash);
5392
5507
  this.removePruneRequestSent(entry.entry.hash);
5393
- this._requestIPruneResponseReplicatorSet.delete(
5508
+ this._checkedPrune.clearConfirmedReplicators(
5394
5509
  entry.entry.hash,
5395
5510
  );
5396
5511
 
@@ -5513,44 +5628,51 @@ export class SharedLog<
5513
5628
 
5514
5629
  // if we expect the remote to be owner of this entry because we are to prune ourselves, then we need to remove the remote
5515
5630
  // this is due to that the remote has previously indicated to be a replicator to help us prune but now has changed their mind
5516
- const outGoingPrunes =
5517
- this._requestIPruneResponseReplicatorSet.get(hash);
5518
- if (outGoingPrunes) {
5519
- outGoingPrunes.delete(from);
5520
- }
5631
+ this._checkedPrune.removeConfirmedReplicator(hash, from);
5521
5632
 
5522
5633
  const indexedEntry = await this.log.entryIndex.getShallow(hash);
5523
5634
  let isLeader = false;
5524
5635
 
5525
- if (
5526
- indexedEntry &&
5527
- !this._pendingDeletes.has(hash) &&
5528
- (await this.log.blocks.has(hash))
5529
- ) {
5530
- this.removePeerFromGidPeerHistory(
5531
- context.from!.hashcode(),
5532
- indexedEntry!.value.meta.gid,
5533
- );
5636
+ if (indexedEntry && (await this.log.blocks.has(hash))) {
5637
+ const pendingDelete = this._checkedPrune.getPendingDelete(hash);
5638
+ if (pendingDelete) {
5639
+ const ownership =
5640
+ await this.revalidateCheckedPruneOwnership({
5641
+ hash,
5642
+ entry: indexedEntry.value,
5643
+ leaders: new Map(),
5644
+ });
5645
+ if (ownership.localLeader) {
5646
+ await this.cancelCheckedPruneForLocalLeader(hash);
5647
+ isLeader = true;
5648
+ }
5649
+ } else {
5650
+ this.removePeerFromGidPeerHistory(
5651
+ context.from!.hashcode(),
5652
+ indexedEntry!.value.meta.gid,
5653
+ );
5534
5654
 
5535
- await this._waitForReplicators(
5536
- await this.createCoordinates(
5655
+ await this._waitForReplicators(
5656
+ await this.createCoordinates(
5657
+ indexedEntry.value,
5658
+ decodeReplicas(indexedEntry.value).getValue(this),
5659
+ ),
5537
5660
  indexedEntry.value,
5538
- decodeReplicas(indexedEntry.value).getValue(this),
5539
- ),
5540
- indexedEntry.value,
5541
- [
5661
+ [
5662
+ {
5663
+ key: this.node.identity.publicKey.hashcode(),
5664
+ replicator: true,
5665
+ },
5666
+ ],
5542
5667
  {
5543
- key: this.node.identity.publicKey.hashcode(),
5544
- replicator: true,
5545
- },
5546
- ],
5547
- {
5548
- onLeader: (key) => {
5549
- isLeader =
5550
- isLeader || key === this.node.identity.publicKey.hashcode();
5668
+ onLeader: (key) => {
5669
+ isLeader =
5670
+ isLeader ||
5671
+ key === this.node.identity.publicKey.hashcode();
5672
+ },
5551
5673
  },
5552
- },
5553
- );
5674
+ );
5675
+ }
5554
5676
  }
5555
5677
 
5556
5678
  if (isLeader) {
@@ -5625,8 +5747,20 @@ export class SharedLog<
5625
5747
  }
5626
5748
  }
5627
5749
  } else if (msg instanceof ResponseIPrune) {
5750
+ const lateResponses: string[] = [];
5628
5751
  for (const hash of msg.hashes) {
5629
- this._pendingDeletes.get(hash)?.resolve(context.from.hashcode());
5752
+ const pendingDelete = this._checkedPrune.getPendingDelete(hash);
5753
+ if (pendingDelete) {
5754
+ void pendingDelete.resolve(context.from.hashcode());
5755
+ } else {
5756
+ lateResponses.push(hash);
5757
+ }
5758
+ }
5759
+ if (lateResponses.length > 0) {
5760
+ void this.recoverCheckedPruneFromLateResponses(
5761
+ lateResponses,
5762
+ context.from.hashcode(),
5763
+ ).catch((error) => logger.error(error.toString()));
5630
5764
  }
5631
5765
  } else if (msg instanceof ConfirmEntriesMessage) {
5632
5766
  this.markEntriesKnownByPeer(msg.hashes, context.from.hashcode());
@@ -6105,7 +6239,7 @@ export class SharedLog<
6105
6239
 
6106
6240
  if (messageToSend) {
6107
6241
  await this.rpc.send(messageToSend, {
6108
- priority: 1,
6242
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
6109
6243
  });
6110
6244
  }
6111
6245
  }
@@ -6892,11 +7026,12 @@ export class SharedLog<
6892
7026
  this._replicationInfoRequestByPeer.set(peerHash, state);
6893
7027
 
6894
7028
  const intervalMs = Math.max(50, this.waitForReplicatorRequestIntervalMs);
6895
- const maxAttempts = Math.min(
6896
- 5,
7029
+ const maxAttempts =
6897
7030
  this.waitForReplicatorRequestMaxAttempts ??
7031
+ Math.max(
6898
7032
  WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS,
6899
- );
7033
+ Math.ceil(this.waitForReplicatorTimeout / intervalMs),
7034
+ );
6900
7035
 
6901
7036
  const tick = () => {
6902
7037
  if (this.closed || this._closeController.signal.aborted) {
@@ -7022,17 +7157,7 @@ export class SharedLog<
7022
7157
  }
7023
7158
 
7024
7159
  private removePruneRequestSent(hash: string, to?: string) {
7025
- if (!to) {
7026
- this._requestIPruneSent.delete(hash);
7027
- } else {
7028
- let set = this._requestIPruneSent.get(hash);
7029
- if (set) {
7030
- set.delete(to);
7031
- if (set.size === 0) {
7032
- this._requestIPruneSent.delete(hash);
7033
- }
7034
- }
7035
- }
7160
+ this._checkedPrune.removeRequestSent(hash, to);
7036
7161
  }
7037
7162
 
7038
7163
  prune(
@@ -7049,7 +7174,7 @@ export class SharedLog<
7049
7174
  return [...entries.values()].map((x) => {
7050
7175
  this._gidPeersHistory.delete(x.entry.meta.gid);
7051
7176
  this.removePruneRequestSent(x.entry.hash);
7052
- this._requestIPruneResponseReplicatorSet.delete(x.entry.hash);
7177
+ this._checkedPrune.clearConfirmedReplicators(x.entry.hash);
7053
7178
  return this.log.remove(x.entry, {
7054
7179
  recursively: true,
7055
7180
  });
@@ -7086,7 +7211,7 @@ export class SharedLog<
7086
7211
  set.push(entry.hash);
7087
7212
  }
7088
7213
 
7089
- const pendingPrev = this._pendingDeletes.get(entry.hash);
7214
+ const pendingPrev = this._checkedPrune.getPendingDelete(entry.hash);
7090
7215
  if (pendingPrev) {
7091
7216
  // If a background prune is already in-flight, an explicit prune request should
7092
7217
  // still respect the caller's timeout. Otherwise, tests (and user calls) can
@@ -7122,62 +7247,77 @@ export class SharedLog<
7122
7247
  const deferredPromise: DeferredPromise<void> = pDefer();
7123
7248
 
7124
7249
  const clear = () => {
7125
- const pending = this._pendingDeletes.get(entry.hash);
7250
+ const pending = this._checkedPrune.getPendingDelete(entry.hash);
7126
7251
  if (pending?.promise === deferredPromise) {
7127
- this._pendingDeletes.delete(entry.hash);
7252
+ this._checkedPrune.deletePendingDelete(entry.hash, pending);
7128
7253
  }
7129
7254
  clearTimeout(timeout);
7130
7255
  };
7131
7256
 
7132
- const resolve = () => {
7133
- clear();
7134
- this.clearCheckedPruneRetry(entry.hash);
7135
- cleanupTimer.push(
7136
- setTimeout(async () => {
7137
- this._gidPeersHistory.delete(entry.meta.gid);
7138
- this.removePruneRequestSent(entry.hash);
7139
- this._requestIPruneResponseReplicatorSet.delete(entry.hash);
7140
-
7141
- if (
7142
- await this.isLeader({
7143
- entry,
7144
- replicas: minReplicas.getValue(this),
7145
- })
7146
- ) {
7147
- deferredPromise.reject(
7148
- new Error("Failed to delete, is leader again"),
7149
- );
7150
- return;
7151
- }
7152
-
7153
- return this.log
7154
- .remove(entry, {
7155
- recursively: true,
7156
- })
7157
- .then(() => {
7158
- deferredPromise.resolve();
7159
- })
7160
- .catch((e) => {
7161
- deferredPromise.reject(e);
7162
- })
7163
- .finally(async () => {
7257
+ const resolve = () => {
7258
+ clearTimeout(timeout);
7259
+ this.clearCheckedPruneRetry(entry.hash);
7260
+ cleanupTimer.push(
7261
+ setTimeout(async () => {
7164
7262
  this._gidPeersHistory.delete(entry.meta.gid);
7165
7263
  this.removePruneRequestSent(entry.hash);
7166
- this._requestIPruneResponseReplicatorSet.delete(entry.hash);
7167
- // TODO in the case we become leader again here we need to re-add the entry
7264
+ this._checkedPrune.clearConfirmedReplicators(entry.hash);
7168
7265
 
7169
- if (
7170
- await this.isLeader({
7266
+ const ownership =
7267
+ await this.revalidateCheckedPruneOwnership({
7268
+ hash: entry.hash,
7171
7269
  entry,
7172
- replicas: minReplicas.getValue(this),
7173
- })
7174
- ) {
7175
- logger.error("Unexpected: Is leader after delete");
7270
+ leaders: this.checkedPruneLeadersToMap(leaders),
7271
+ selfReplicating: true,
7272
+ });
7273
+ if (ownership.localLeader) {
7274
+ clear();
7275
+ if (!explicitTimeout) {
7276
+ this.scheduleCheckedPruneRetry({ entry, leaders });
7277
+ }
7278
+ deferredPromise.reject(
7279
+ new Error("Failed to delete, is leader again"),
7280
+ );
7281
+ return;
7176
7282
  }
7177
- });
7178
- }, this.waitForPruneDelay),
7179
- );
7180
- };
7283
+
7284
+ this._checkedPrune.markRemoving(entry.hash);
7285
+ return this.log
7286
+ .remove(entry, {
7287
+ recursively: true,
7288
+ })
7289
+ .then(() => {
7290
+ clear();
7291
+ this._checkedPrune.markDone(entry.hash);
7292
+ deferredPromise.resolve();
7293
+ })
7294
+ .catch((e) => {
7295
+ clear();
7296
+ this._checkedPrune.markCancelled(entry.hash, {
7297
+ preserveRetry: false,
7298
+ });
7299
+ deferredPromise.reject(e);
7300
+ })
7301
+ .finally(async () => {
7302
+ this._gidPeersHistory.delete(entry.meta.gid);
7303
+ this.removePruneRequestSent(entry.hash);
7304
+ this._checkedPrune.clearConfirmedReplicators(entry.hash);
7305
+ // TODO in the case we become leader again here we need to re-add the entry
7306
+
7307
+ const ownership =
7308
+ await this.revalidateCheckedPruneOwnership({
7309
+ hash: entry.hash,
7310
+ entry,
7311
+ leaders: this.checkedPruneLeadersToMap(leaders),
7312
+ selfReplicating: true,
7313
+ });
7314
+ if (ownership.localLeader) {
7315
+ logger.error("Unexpected: Is leader after delete");
7316
+ }
7317
+ });
7318
+ }, this.waitForPruneDelay),
7319
+ );
7320
+ };
7181
7321
 
7182
7322
  const reject = (e: any) => {
7183
7323
  clear();
@@ -7185,11 +7325,9 @@ export class SharedLog<
7185
7325
  e instanceof Error &&
7186
7326
  typeof e.message === "string" &&
7187
7327
  e.message.startsWith("Timeout for checked pruning");
7188
- if (explicitTimeout || !isCheckedPruneTimeout) {
7189
- this.clearCheckedPruneRetry(entry.hash);
7190
- }
7191
- this.removePruneRequestSent(entry.hash);
7192
- this._requestIPruneResponseReplicatorSet.delete(entry.hash);
7328
+ this._checkedPrune.markCancelled(entry.hash, {
7329
+ preserveRetry: !explicitTimeout && isCheckedPruneTimeout,
7330
+ });
7193
7331
  deferredPromise.reject(e);
7194
7332
  };
7195
7333
 
@@ -7203,70 +7341,65 @@ export class SharedLog<
7203
7341
  const checkedPruneTimeoutMs =
7204
7342
  options?.timeout ??
7205
7343
  Math.max(
7206
- 10_000,
7344
+ CHECKED_PRUNE_BACKGROUND_TIMEOUT_MIN_MS,
7207
7345
  Number(this._respondToIHaveTimeout ?? 0) +
7208
7346
  this.waitForReplicatorTimeout +
7209
7347
  PRUNE_DEBOUNCE_INTERVAL * 2,
7210
7348
  );
7211
7349
 
7212
- const timeout = setTimeout(() => {
7213
- // For internal/background prune flows (no explicit timeout), retry a few times
7214
- // to avoid "permanently prunable" entries when `_pendingIHave` expires under
7215
- // heavy load.
7216
- if (!explicitTimeout) {
7217
- this.scheduleCheckedPruneRetry({ entry, leaders });
7218
- }
7219
- reject(
7220
- new Error(
7221
- `Timeout for checked pruning after ${checkedPruneTimeoutMs}ms (closed=${this.closed})`,
7222
- ),
7223
- );
7224
- }, checkedPruneTimeoutMs);
7225
- timeout.unref?.();
7226
-
7227
- this._pendingDeletes.set(entry.hash, {
7228
- promise: deferredPromise,
7229
- clear,
7230
- reject,
7231
- resolve: async (publicKeyHash: string) => {
7232
- const minReplicasObj = this.getClampedReplicas(minReplicas);
7233
- const minReplicasValue = minReplicasObj.getValue(this);
7234
-
7235
- // TODO is this check necessary
7236
- if (
7237
- !(await this._waitForReplicators(
7238
- cursor ??
7239
- (cursor = await this.createCoordinates(
7240
- entry,
7241
- minReplicasValue,
7242
- )),
7243
- entry,
7244
- [
7245
- { key: publicKeyHash, replicator: true },
7350
+ const timeout = setTimeout(() => {
7351
+ // For internal/background prune flows (no explicit timeout), retry a few times
7352
+ // to avoid "permanently prunable" entries when `_pendingIHave` expires under
7353
+ // heavy load.
7354
+ if (!explicitTimeout) {
7355
+ this.scheduleCheckedPruneRetry({ entry, leaders });
7356
+ }
7357
+ reject(
7358
+ new Error(
7359
+ `Timeout for checked pruning after ${checkedPruneTimeoutMs}ms (closed=${this.closed})`,
7360
+ ),
7361
+ );
7362
+ }, checkedPruneTimeoutMs);
7363
+ timeout.unref?.();
7364
+
7365
+ this._checkedPrune.setPendingDelete(
7366
+ entry.hash,
7367
+ {
7368
+ promise: deferredPromise,
7369
+ clear,
7370
+ reject,
7371
+ resolve: async (publicKeyHash: string) => {
7372
+ const minReplicasObj = this.getClampedReplicas(minReplicas);
7373
+ const minReplicasValue = minReplicasObj.getValue(this);
7374
+
7375
+ // TODO is this check necessary
7376
+ if (
7377
+ !(await this._waitForReplicators(
7378
+ cursor ??
7379
+ (cursor = await this.createCoordinates(
7380
+ entry,
7381
+ minReplicasValue,
7382
+ )),
7383
+ entry,
7384
+ [
7385
+ { key: publicKeyHash, replicator: true },
7386
+ {
7387
+ key: this.node.identity.publicKey.hashcode(),
7388
+ replicator: false,
7389
+ },
7390
+ ],
7246
7391
  {
7247
- key: this.node.identity.publicKey.hashcode(),
7248
- replicator: false,
7392
+ persist: false,
7249
7393
  },
7250
- ],
7251
- {
7252
- persist: false,
7253
- },
7254
- ))
7255
- ) {
7256
- return;
7257
- }
7258
-
7259
- let existCounter = this._requestIPruneResponseReplicatorSet.get(
7260
- entry.hash,
7261
- );
7262
- if (!existCounter) {
7263
- existCounter = new Set();
7264
- this._requestIPruneResponseReplicatorSet.set(
7265
- entry.hash,
7266
- existCounter,
7267
- );
7394
+ ))
7395
+ ) {
7396
+ return;
7268
7397
  }
7269
- existCounter.add(publicKeyHash);
7398
+
7399
+ const existCounter = this._checkedPrune.addConfirmedReplicator(
7400
+ entry.hash,
7401
+ publicKeyHash,
7402
+ );
7270
7403
  // Seed provider hints so future remote reads can avoid extra round-trips.
7271
7404
  this.remoteBlocks.hintProviders(entry.hash, [publicKeyHash]);
7272
7405
 
@@ -7274,7 +7407,10 @@ export class SharedLog<
7274
7407
  resolve();
7275
7408
  }
7276
7409
  },
7277
- });
7410
+ },
7411
+ entry,
7412
+ leaders,
7413
+ );
7278
7414
 
7279
7415
  promises.push(deferredPromise.promise);
7280
7416
  }
@@ -7282,16 +7418,11 @@ export class SharedLog<
7282
7418
  const emitMessages = async (entries: string[], to: string) => {
7283
7419
  const filteredSet: string[] = [];
7284
7420
  for (const entry of entries) {
7285
- let set = this._requestIPruneSent.get(entry);
7286
- if (!set) {
7287
- set = new Set();
7288
- this._requestIPruneSent.set(entry, set);
7289
- }
7290
7421
  /* TODO why can we not have this statement?
7291
7422
  if (set.has(to)) {
7292
7423
  continue;
7293
7424
  } */
7294
- set.add(to);
7425
+ this._checkedPrune.addRequestSent(entry, to);
7295
7426
  filteredSet.push(entry);
7296
7427
  }
7297
7428
  if (filteredSet.length > 0) {
@@ -7304,7 +7435,7 @@ export class SharedLog<
7304
7435
  to: [to], // TODO group by peers?
7305
7436
  redundancy: 1,
7306
7437
  }),
7307
- priority: 1,
7438
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
7308
7439
  },
7309
7440
  );
7310
7441
  }
@@ -7333,7 +7464,9 @@ export class SharedLog<
7333
7464
 
7334
7465
  const pendingByPeer: [string, string[]][] = [];
7335
7466
  for (const [peer, hashes] of peerToEntries) {
7336
- const pending = hashes.filter((h) => this._pendingDeletes.has(h));
7467
+ const pending = hashes.filter((h) =>
7468
+ this._checkedPrune.hasPendingDelete(h),
7469
+ );
7337
7470
  if (pending.length > 0) {
7338
7471
  pendingByPeer.push([peer, pending]);
7339
7472
  }
@@ -7764,12 +7897,26 @@ export class SharedLog<
7764
7897
  flushUncheckedDeliverTarget(target);
7765
7898
  }
7766
7899
 
7767
- if (this._isAdaptiveReplicating && hasSelfRangeRemoval) {
7768
- // Adaptive shrink/replacement can make already-indexed local heads
7769
- // prunable even when the incremental rebalance scan missed them under
7770
- // churn or timing pressure. Re-scan after repair dispatches are flushed
7771
- // so checked prune work is enqueued before callers wait for idle.
7772
- await this.pruneIndexedEntriesNoLongerLed();
7900
+ if (
7901
+ this._isAdaptiveReplicating &&
7902
+ (hasSelfRangeRemoval ||
7903
+ changes.some(
7904
+ (change) =>
7905
+ change.type === "added" ||
7906
+ change.type === "removed" ||
7907
+ change.type === "replaced",
7908
+ ))
7909
+ ) {
7910
+ // Adaptive range changes can make already-indexed local heads prunable
7911
+ // even when the incremental rebalance scan misses them under churn or
7912
+ // timing pressure. Re-scan after repair dispatches are flushed using the
7913
+ // mature-role view, which matches the bounded pruning contract.
7914
+ await this.pruneIndexedEntriesNoLongerLed({
7915
+ useDefaultRoleAge: true,
7916
+ });
7917
+ await this.pruneCurrentHeadsNoLongerLed({
7918
+ useDefaultRoleAge: true,
7919
+ });
7773
7920
  }
7774
7921
 
7775
7922
  return changed;