@peerbit/shared-log 13.1.15 → 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 +285 -186
  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 +16 -12
  34. package/src/checked-prune.ts +331 -0
  35. package/src/index.ts +451 -302
  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,
@@ -70,8 +72,14 @@ import {
70
72
  } from "@peerbit/time";
71
73
  import pDefer, { type DeferredPromise } from "p-defer";
72
74
  import PQueue from "p-queue";
73
- import { concat } from "uint8arrays";
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
  }
@@ -2065,6 +2071,14 @@ export class SharedLog<
2065
2071
  ) => void;
2066
2072
  },
2067
2073
  ) {
2074
+ const entryRangeId = (entry: Entry<T>) =>
2075
+ sha256Sync(
2076
+ concat([
2077
+ this.log.id,
2078
+ fromString(entry.hash),
2079
+ fromString(this.node.identity.publicKey.hashcode()),
2080
+ ]),
2081
+ );
2068
2082
  let range:
2069
2083
  | ReplicationRangeMessage<any>[]
2070
2084
  | ReplicationOptions<R>
@@ -2074,6 +2088,7 @@ export class SharedLog<
2074
2088
  range = rangeOrEntry;
2075
2089
  } else if (rangeOrEntry instanceof Entry) {
2076
2090
  range = {
2091
+ id: entryRangeId(rangeOrEntry),
2077
2092
  factor: 1,
2078
2093
  offset: await this.domain.fromEntry(rangeOrEntry),
2079
2094
  normalized: false,
@@ -2084,6 +2099,7 @@ export class SharedLog<
2084
2099
  for (const entry of rangeOrEntry) {
2085
2100
  if (entry instanceof Entry) {
2086
2101
  ranges.push({
2102
+ id: entryRangeId(entry),
2087
2103
  factor: 1,
2088
2104
  offset: await this.domain.fromEntry(entry),
2089
2105
  normalized: false,
@@ -2153,7 +2169,7 @@ export class SharedLog<
2153
2169
  this.node.identity.publicKey,
2154
2170
  );
2155
2171
  await this.rpc.send(new StoppedReplicating({ segmentIds }), {
2156
- priority: 1,
2172
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
2157
2173
  });
2158
2174
  }
2159
2175
 
@@ -2179,7 +2195,7 @@ export class SharedLog<
2179
2195
  // announce that we are no longer replicating
2180
2196
 
2181
2197
  await this.rpc.send(new AllReplicatingSegmentsMessage({ segments: [] }), {
2182
- priority: 1,
2198
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
2183
2199
  });
2184
2200
  }
2185
2201
 
@@ -2663,7 +2679,7 @@ export class SharedLog<
2663
2679
  return options.announce(message);
2664
2680
  } else {
2665
2681
  await this.rpc.send(message, {
2666
- priority: 1,
2682
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
2667
2683
  });
2668
2684
  }
2669
2685
  }
@@ -2892,7 +2908,7 @@ export class SharedLog<
2892
2908
  i + REPAIR_CONFIRMATION_HASH_BATCH_SIZE,
2893
2909
  );
2894
2910
  await this.rpc.send(new ConfirmEntriesMessage({ hashes: chunk }), {
2895
- priority: 1,
2911
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
2896
2912
  mode: new SilentDelivery({ to: [target], redundancy: 1 }),
2897
2913
  });
2898
2914
  }
@@ -2908,7 +2924,7 @@ export class SharedLog<
2908
2924
  )) {
2909
2925
  message.reserved[0] |= EXCHANGE_HEADS_REPAIR_HINT;
2910
2926
  await this.rpc.send(message, {
2911
- priority: 1,
2927
+ priority: SYNC_MESSAGE_PRIORITY,
2912
2928
  mode: new SilentDelivery({ to: [target], redundancy: 1 }),
2913
2929
  });
2914
2930
  }
@@ -3514,26 +3530,18 @@ export class SharedLog<
3514
3530
  const frontierTargets = this._repairFrontierByMode.get(mode);
3515
3531
  for (const target of pendingPeersByMode.get(mode) ?? []) {
3516
3532
  const replacement = nextTargets.get(target);
3517
- if (mode === "join-authoritative") {
3518
- // Authoritative join repair is receipt-driven: a later sweep can have a
3519
- // narrower transient leader view, but it must not forget unconfirmed
3520
- // hashes that were already queued for this joiner.
3521
- if (replacement && replacement.size > 0) {
3522
- const existing = frontierTargets?.get(target);
3523
- if (existing && existing.size > 0) {
3524
- for (const [hash, entry] of replacement) {
3525
- existing.set(hash, entry);
3526
- }
3527
- } else {
3528
- frontierTargets?.set(target, replacement);
3533
+ // These repairs are receipt-driven: a later sweep can have a narrower
3534
+ // transient leader view, but it must not forget unconfirmed hashes
3535
+ // that were already queued for this target.
3536
+ if (replacement && replacement.size > 0) {
3537
+ const existing = frontierTargets?.get(target);
3538
+ if (existing && existing.size > 0) {
3539
+ for (const [hash, entry] of replacement) {
3540
+ existing.set(hash, entry);
3529
3541
  }
3542
+ } else {
3543
+ frontierTargets?.set(target, replacement);
3530
3544
  }
3531
- continue;
3532
- }
3533
- if (replacement && replacement.size > 0) {
3534
- frontierTargets?.set(target, replacement);
3535
- } else {
3536
- frontierTargets?.delete(target);
3537
3545
  }
3538
3546
  }
3539
3547
  }
@@ -3567,30 +3575,32 @@ export class SharedLog<
3567
3575
  if (this.keep && (await this.keep(args.value.entry))) {
3568
3576
  return false;
3569
3577
  }
3578
+ this._checkedPrune.trackCandidate(
3579
+ args.key,
3580
+ args.value.entry,
3581
+ args.value.leaders,
3582
+ );
3570
3583
  void this.pruneDebouncedFn.add(args);
3571
3584
  return true;
3572
3585
  }
3573
3586
 
3574
- private async cancelCheckedPruneForLocalLeader(hash: string) {
3587
+ private async cancelCheckedPruneForLocalLeader(
3588
+ hash: string,
3589
+ options?: { preserveRetry?: boolean },
3590
+ ) {
3575
3591
  this.pruneDebouncedFn.delete(hash);
3576
- this.clearCheckedPruneRetry(hash);
3577
- this.removePruneRequestSent(hash);
3578
- this._requestIPruneResponseReplicatorSet.delete(hash);
3579
- await this._pendingDeletes
3580
- .get(hash)
3581
- ?.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"));
3582
3597
  }
3583
3598
 
3584
3599
  private hasActiveCheckedPruneWork(hash: string) {
3585
- return (
3586
- this._pendingDeletes.has(hash) ||
3587
- this._requestIPruneSent.has(hash) ||
3588
- this._requestIPruneResponseReplicatorSet.has(hash) ||
3589
- this._checkedPruneRetries.has(hash)
3590
- );
3600
+ return this._checkedPrune.hasActiveWork(hash);
3591
3601
  }
3592
3602
 
3593
- private async resolveCheckedPruneLeaders(args: {
3603
+ private async revalidateCheckedPruneOwnership(args: {
3594
3604
  hash: string;
3595
3605
  entry: CheckedPruneEntry<T, R>;
3596
3606
  leaders: CheckedPruneLeaderMap;
@@ -3658,7 +3668,7 @@ export class SharedLog<
3658
3668
  continue;
3659
3669
  }
3660
3670
 
3661
- if (this._pendingDeletes.has(entry.hash)) {
3671
+ if (this._checkedPrune.hasPendingDelete(entry.hash)) {
3662
3672
  continue;
3663
3673
  }
3664
3674
 
@@ -3674,7 +3684,9 @@ export class SharedLog<
3674
3684
  }
3675
3685
  }
3676
3686
 
3677
- private async pruneIndexedEntriesNoLongerLed() {
3687
+ private async pruneIndexedEntriesNoLongerLed(options?: {
3688
+ useDefaultRoleAge?: boolean;
3689
+ }) {
3678
3690
  const selfHash = this.node.identity.publicKey.hashcode();
3679
3691
  const iterator = this.entryCoordinatesIndex.iterate({});
3680
3692
  let enqueuedPrune = false;
@@ -3690,7 +3702,7 @@ export class SharedLog<
3690
3702
  const leaders = await this.findLeaders(
3691
3703
  entryReplicated.coordinates,
3692
3704
  entryReplicated,
3693
- { roleAge: 0 },
3705
+ options?.useDefaultRoleAge ? undefined : { roleAge: 0 },
3694
3706
  );
3695
3707
 
3696
3708
  if (leaders.has(selfHash)) {
@@ -3698,7 +3710,7 @@ export class SharedLog<
3698
3710
  continue;
3699
3711
  }
3700
3712
 
3701
- if (this._pendingDeletes.has(entryReplicated.hash)) {
3713
+ if (this._checkedPrune.hasPendingDelete(entryReplicated.hash)) {
3702
3714
  continue;
3703
3715
  }
3704
3716
 
@@ -3722,12 +3734,65 @@ export class SharedLog<
3722
3734
  }
3723
3735
  }
3724
3736
 
3725
- private clearCheckedPruneRetry(hash: string) {
3726
- const state = this._checkedPruneRetries.get(hash);
3727
- if (state?.timer) {
3728
- 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);
3729
3774
  }
3730
- this._checkedPruneRetries.delete(hash);
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 });
3790
+ }
3791
+ return leadersMap;
3792
+ }
3793
+
3794
+ private clearCheckedPruneRetry(hash: string) {
3795
+ this._checkedPrune.clearRetry(hash);
3731
3796
  }
3732
3797
 
3733
3798
  private scheduleCheckedPruneRetry(args: {
@@ -3735,11 +3800,18 @@ export class SharedLog<
3735
3800
  leaders: CheckedPruneLeaderMap | Set<string>;
3736
3801
  }) {
3737
3802
  if (this.closed) return;
3738
- if (this._pendingDeletes.has(args.entry.hash)) return;
3803
+ if (this._checkedPrune.hasPendingDelete(args.entry.hash)) return;
3739
3804
 
3740
3805
  const hash = args.entry.hash;
3741
3806
  const state =
3742
- 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;
3743
3815
 
3744
3816
  if (state.timer) return;
3745
3817
  if (state.attempts >= CHECKED_PRUNE_RETRY_MAX_ATTEMPTS) {
@@ -3757,15 +3829,17 @@ export class SharedLog<
3757
3829
 
3758
3830
  state.attempts = attempt;
3759
3831
  state.timer = setTimeout(async () => {
3760
- const st = this._checkedPruneRetries.get(hash);
3832
+ const st = this._checkedPrune.getRetry(hash);
3761
3833
  if (st) st.timer = undefined;
3762
3834
  if (this.closed) return;
3763
- 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;
3764
3838
 
3765
3839
  let leadersMap: CheckedPruneLeaderMap | undefined;
3766
3840
  try {
3767
- const replicas = decodeReplicas(args.entry).getValue(this);
3768
- leadersMap = await this.findLeadersFromEntry(args.entry, replicas, {
3841
+ const replicas = decodeReplicas(retryEntry).getValue(this);
3842
+ leadersMap = await this.findLeadersFromEntry(retryEntry, replicas, {
3769
3843
  roleAge: 0,
3770
3844
  });
3771
3845
  } catch {
@@ -3773,14 +3847,7 @@ export class SharedLog<
3773
3847
  }
3774
3848
 
3775
3849
  if (!leadersMap || leadersMap.size === 0) {
3776
- if (args.leaders instanceof Map) {
3777
- leadersMap = args.leaders;
3778
- } else {
3779
- leadersMap = new Map<string, { intersecting: boolean }>();
3780
- for (const k of args.leaders) {
3781
- leadersMap.set(k, { intersecting: true });
3782
- }
3783
- }
3850
+ leadersMap = this.checkedPruneLeadersToMap(retryLeaders);
3784
3851
  }
3785
3852
 
3786
3853
  try {
@@ -3788,14 +3855,81 @@ export class SharedLog<
3788
3855
  leadersMap ?? new Map<string, { intersecting: boolean }>();
3789
3856
  await this.pruneDebouncedFnAddIfNotKeeping({
3790
3857
  key: hash,
3791
- value: { entry: args.entry, leaders: leadersForRetry },
3858
+ value: { entry: retryEntry, leaders: leadersForRetry },
3792
3859
  });
3793
3860
  } catch {
3794
3861
  // Best-effort only; pruning will be re-attempted on future changes.
3795
3862
  }
3796
3863
  }, delayMs);
3797
3864
  state.timer.unref?.();
3798
- 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
+ }
3799
3933
  }
3800
3934
 
3801
3935
  async append(
@@ -3945,7 +4079,7 @@ export class SharedLog<
3945
4079
  this.domain.resolution,
3946
4080
  );
3947
4081
  this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 2e4;
3948
- this._pendingDeletes = new Map();
4082
+ this._checkedPrune = new CheckedPruneCoordinator<T, R>();
3949
4083
  this._pendingIHave = new Map();
3950
4084
  this.latestReplicationInfoMessage = new Map();
3951
4085
  this._replicationInfoBlockedPeers = new Set();
@@ -4137,10 +4271,6 @@ export class SharedLog<
4137
4271
  })) > 0;
4138
4272
 
4139
4273
  this._gidPeersHistory = new Map();
4140
- this._requestIPruneSent = new Map();
4141
- this._requestIPruneResponseReplicatorSet = new Map();
4142
- this._checkedPruneRetries = new Map();
4143
-
4144
4274
  this.replicationChangeDebounceFn = debounceAggregationChanges<
4145
4275
  ReplicationRangeIndexable<R>
4146
4276
  >(
@@ -4162,14 +4292,24 @@ export class SharedLog<
4162
4292
  >();
4163
4293
  const selfReplicating = await this.isReplicating();
4164
4294
  for (const [hash, value] of map) {
4165
- const checkedPruneLeaders = await this.resolveCheckedPruneLeaders({
4166
- hash,
4167
- entry: value.entry,
4168
- leaders: value.leaders,
4169
- selfReplicating,
4170
- });
4295
+ const checkedPruneLeaders =
4296
+ await this.revalidateCheckedPruneOwnership({
4297
+ hash,
4298
+ entry: value.entry,
4299
+ leaders: value.leaders,
4300
+ selfReplicating,
4301
+ });
4171
4302
  if (checkedPruneLeaders.localLeader) {
4172
- 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
+ }
4173
4313
  continue;
4174
4314
  }
4175
4315
  current.set(hash, {
@@ -4215,7 +4355,7 @@ export class SharedLog<
4215
4355
  to: allRequestingPeers,
4216
4356
  redundancy: 1,
4217
4357
  }),
4218
- priority: 1,
4358
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
4219
4359
  });
4220
4360
  },
4221
4361
  () => {
@@ -4579,20 +4719,7 @@ export class SharedLog<
4579
4719
  this.cancelReplicationInfoRequests(peerHash);
4580
4720
  this._replicatorLivenessFailures.delete(peerHash);
4581
4721
  this._replicatorLastActivityAt.delete(peerHash);
4582
-
4583
- for (const [hash, peers] of this._requestIPruneSent) {
4584
- peers.delete(peerHash);
4585
- if (peers.size === 0) {
4586
- this._requestIPruneSent.delete(hash);
4587
- }
4588
- }
4589
-
4590
- for (const [hash, peers] of this._requestIPruneResponseReplicatorSet) {
4591
- peers.delete(peerHash);
4592
- if (peers.size === 0) {
4593
- this._requestIPruneResponseReplicatorSet.delete(hash);
4594
- }
4595
- }
4722
+ this._checkedPrune.cleanupPeer(peerHash);
4596
4723
  }
4597
4724
 
4598
4725
  private markReplicatorActivity(peerHash: string, now = Date.now()) {
@@ -4656,7 +4783,7 @@ export class SharedLog<
4656
4783
  const self = this.node.identity.publicKey.hashcode();
4657
4784
  const seed = hashToSeed32(hash);
4658
4785
 
4659
- const hinted = this._requestIPruneResponseReplicatorSet.get(hash);
4786
+ const hinted = this._checkedPrune.getConfirmedReplicators(hash);
4660
4787
  if (hinted && hinted.size > 0) {
4661
4788
  const peers = [...hinted].filter((p) => p !== self);
4662
4789
  return peers.length > 0
@@ -4664,7 +4791,7 @@ export class SharedLog<
4664
4791
  : undefined;
4665
4792
  }
4666
4793
 
4667
- const contacted = this._requestIPruneSent.get(hash);
4794
+ const contacted = this._checkedPrune.getContactedReplicators(hash);
4668
4795
  if (contacted && contacted.size > 0) {
4669
4796
  const peers = [...contacted].filter((p) => p !== self);
4670
4797
  return peers.length > 0
@@ -5082,25 +5209,15 @@ export class SharedLog<
5082
5209
  }
5083
5210
  this._appendBackfillPendingByTarget.clear();
5084
5211
 
5085
- for (const [_k, v] of this._pendingDeletes) {
5086
- v.clear();
5087
- v.promise.resolve(); // TODO or reject?
5088
- }
5089
5212
  for (const [_k, v] of this._pendingIHave) {
5090
5213
  v.clear();
5091
5214
  }
5092
- for (const [_k, v] of this._checkedPruneRetries) {
5093
- if (v.timer) clearTimeout(v.timer);
5094
- }
5215
+ this._checkedPrune.close();
5095
5216
 
5096
5217
  await this.remoteBlocks.stop();
5097
- this._pendingDeletes.clear();
5098
5218
  this._pendingIHave.clear();
5099
- this._checkedPruneRetries.clear();
5100
5219
  this.latestReplicationInfoMessage.clear();
5101
5220
  this._gidPeersHistory.clear();
5102
- this._requestIPruneSent.clear();
5103
- this._requestIPruneResponseReplicatorSet.clear();
5104
5221
  // Cancel any pending debounced timers so they can't fire after we've torn down
5105
5222
  // indexes/RPC state.
5106
5223
  this.rebalanceParticipationDebounced?.close();
@@ -5156,7 +5273,7 @@ export class SharedLog<
5156
5273
  try {
5157
5274
  await this.rpc
5158
5275
  .send(new AllReplicatingSegmentsMessage({ segments: [] }), {
5159
- priority: 1,
5276
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
5160
5277
  signal: abort.signal,
5161
5278
  })
5162
5279
  .catch(() => {});
@@ -5203,7 +5320,7 @@ export class SharedLog<
5203
5320
  try {
5204
5321
  await this.rpc
5205
5322
  .send(new AllReplicatingSegmentsMessage({ segments: [] }), {
5206
- priority: 1,
5323
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
5207
5324
  signal: abort.signal,
5208
5325
  })
5209
5326
  .catch(() => {});
@@ -5388,7 +5505,7 @@ export class SharedLog<
5388
5505
  for (const entry of entries) {
5389
5506
  this.pruneDebouncedFn.delete(entry.entry.hash);
5390
5507
  this.removePruneRequestSent(entry.entry.hash);
5391
- this._requestIPruneResponseReplicatorSet.delete(
5508
+ this._checkedPrune.clearConfirmedReplicators(
5392
5509
  entry.entry.hash,
5393
5510
  );
5394
5511
 
@@ -5511,44 +5628,51 @@ export class SharedLog<
5511
5628
 
5512
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
5513
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
5514
- const outGoingPrunes =
5515
- this._requestIPruneResponseReplicatorSet.get(hash);
5516
- if (outGoingPrunes) {
5517
- outGoingPrunes.delete(from);
5518
- }
5631
+ this._checkedPrune.removeConfirmedReplicator(hash, from);
5519
5632
 
5520
5633
  const indexedEntry = await this.log.entryIndex.getShallow(hash);
5521
5634
  let isLeader = false;
5522
5635
 
5523
- if (
5524
- indexedEntry &&
5525
- !this._pendingDeletes.has(hash) &&
5526
- (await this.log.blocks.has(hash))
5527
- ) {
5528
- this.removePeerFromGidPeerHistory(
5529
- context.from!.hashcode(),
5530
- indexedEntry!.value.meta.gid,
5531
- );
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
+ );
5532
5654
 
5533
- await this._waitForReplicators(
5534
- await this.createCoordinates(
5655
+ await this._waitForReplicators(
5656
+ await this.createCoordinates(
5657
+ indexedEntry.value,
5658
+ decodeReplicas(indexedEntry.value).getValue(this),
5659
+ ),
5535
5660
  indexedEntry.value,
5536
- decodeReplicas(indexedEntry.value).getValue(this),
5537
- ),
5538
- indexedEntry.value,
5539
- [
5661
+ [
5662
+ {
5663
+ key: this.node.identity.publicKey.hashcode(),
5664
+ replicator: true,
5665
+ },
5666
+ ],
5540
5667
  {
5541
- key: this.node.identity.publicKey.hashcode(),
5542
- replicator: true,
5543
- },
5544
- ],
5545
- {
5546
- onLeader: (key) => {
5547
- isLeader =
5548
- isLeader || key === this.node.identity.publicKey.hashcode();
5668
+ onLeader: (key) => {
5669
+ isLeader =
5670
+ isLeader ||
5671
+ key === this.node.identity.publicKey.hashcode();
5672
+ },
5549
5673
  },
5550
- },
5551
- );
5674
+ );
5675
+ }
5552
5676
  }
5553
5677
 
5554
5678
  if (isLeader) {
@@ -5623,8 +5747,20 @@ export class SharedLog<
5623
5747
  }
5624
5748
  }
5625
5749
  } else if (msg instanceof ResponseIPrune) {
5750
+ const lateResponses: string[] = [];
5626
5751
  for (const hash of msg.hashes) {
5627
- 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()));
5628
5764
  }
5629
5765
  } else if (msg instanceof ConfirmEntriesMessage) {
5630
5766
  this.markEntriesKnownByPeer(msg.hashes, context.from.hashcode());
@@ -6072,7 +6208,7 @@ export class SharedLog<
6072
6208
 
6073
6209
  await this.replicate(entriesToReplicate, {
6074
6210
  rebalance: assumeSynced ? false : true,
6075
- checkDuplicates: true,
6211
+ checkDuplicates: assumeSynced ? false : true,
6076
6212
  mergeSegments:
6077
6213
  typeof options.replicate !== "boolean" && options.replicate
6078
6214
  ? options.replicate.mergeSegments
@@ -6103,7 +6239,7 @@ export class SharedLog<
6103
6239
 
6104
6240
  if (messageToSend) {
6105
6241
  await this.rpc.send(messageToSend, {
6106
- priority: 1,
6242
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
6107
6243
  });
6108
6244
  }
6109
6245
  }
@@ -6890,11 +7026,12 @@ export class SharedLog<
6890
7026
  this._replicationInfoRequestByPeer.set(peerHash, state);
6891
7027
 
6892
7028
  const intervalMs = Math.max(50, this.waitForReplicatorRequestIntervalMs);
6893
- const maxAttempts = Math.min(
6894
- 5,
7029
+ const maxAttempts =
6895
7030
  this.waitForReplicatorRequestMaxAttempts ??
7031
+ Math.max(
6896
7032
  WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS,
6897
- );
7033
+ Math.ceil(this.waitForReplicatorTimeout / intervalMs),
7034
+ );
6898
7035
 
6899
7036
  const tick = () => {
6900
7037
  if (this.closed || this._closeController.signal.aborted) {
@@ -7020,17 +7157,7 @@ export class SharedLog<
7020
7157
  }
7021
7158
 
7022
7159
  private removePruneRequestSent(hash: string, to?: string) {
7023
- if (!to) {
7024
- this._requestIPruneSent.delete(hash);
7025
- } else {
7026
- let set = this._requestIPruneSent.get(hash);
7027
- if (set) {
7028
- set.delete(to);
7029
- if (set.size === 0) {
7030
- this._requestIPruneSent.delete(hash);
7031
- }
7032
- }
7033
- }
7160
+ this._checkedPrune.removeRequestSent(hash, to);
7034
7161
  }
7035
7162
 
7036
7163
  prune(
@@ -7047,7 +7174,7 @@ export class SharedLog<
7047
7174
  return [...entries.values()].map((x) => {
7048
7175
  this._gidPeersHistory.delete(x.entry.meta.gid);
7049
7176
  this.removePruneRequestSent(x.entry.hash);
7050
- this._requestIPruneResponseReplicatorSet.delete(x.entry.hash);
7177
+ this._checkedPrune.clearConfirmedReplicators(x.entry.hash);
7051
7178
  return this.log.remove(x.entry, {
7052
7179
  recursively: true,
7053
7180
  });
@@ -7084,7 +7211,7 @@ export class SharedLog<
7084
7211
  set.push(entry.hash);
7085
7212
  }
7086
7213
 
7087
- const pendingPrev = this._pendingDeletes.get(entry.hash);
7214
+ const pendingPrev = this._checkedPrune.getPendingDelete(entry.hash);
7088
7215
  if (pendingPrev) {
7089
7216
  // If a background prune is already in-flight, an explicit prune request should
7090
7217
  // still respect the caller's timeout. Otherwise, tests (and user calls) can
@@ -7120,62 +7247,77 @@ export class SharedLog<
7120
7247
  const deferredPromise: DeferredPromise<void> = pDefer();
7121
7248
 
7122
7249
  const clear = () => {
7123
- const pending = this._pendingDeletes.get(entry.hash);
7250
+ const pending = this._checkedPrune.getPendingDelete(entry.hash);
7124
7251
  if (pending?.promise === deferredPromise) {
7125
- this._pendingDeletes.delete(entry.hash);
7252
+ this._checkedPrune.deletePendingDelete(entry.hash, pending);
7126
7253
  }
7127
7254
  clearTimeout(timeout);
7128
7255
  };
7129
7256
 
7130
- const resolve = () => {
7131
- clear();
7132
- this.clearCheckedPruneRetry(entry.hash);
7133
- cleanupTimer.push(
7134
- setTimeout(async () => {
7135
- this._gidPeersHistory.delete(entry.meta.gid);
7136
- this.removePruneRequestSent(entry.hash);
7137
- this._requestIPruneResponseReplicatorSet.delete(entry.hash);
7138
-
7139
- if (
7140
- await this.isLeader({
7141
- entry,
7142
- replicas: minReplicas.getValue(this),
7143
- })
7144
- ) {
7145
- deferredPromise.reject(
7146
- new Error("Failed to delete, is leader again"),
7147
- );
7148
- return;
7149
- }
7150
-
7151
- return this.log
7152
- .remove(entry, {
7153
- recursively: true,
7154
- })
7155
- .then(() => {
7156
- deferredPromise.resolve();
7157
- })
7158
- .catch((e) => {
7159
- deferredPromise.reject(e);
7160
- })
7161
- .finally(async () => {
7257
+ const resolve = () => {
7258
+ clearTimeout(timeout);
7259
+ this.clearCheckedPruneRetry(entry.hash);
7260
+ cleanupTimer.push(
7261
+ setTimeout(async () => {
7162
7262
  this._gidPeersHistory.delete(entry.meta.gid);
7163
7263
  this.removePruneRequestSent(entry.hash);
7164
- this._requestIPruneResponseReplicatorSet.delete(entry.hash);
7165
- // TODO in the case we become leader again here we need to re-add the entry
7264
+ this._checkedPrune.clearConfirmedReplicators(entry.hash);
7166
7265
 
7167
- if (
7168
- await this.isLeader({
7266
+ const ownership =
7267
+ await this.revalidateCheckedPruneOwnership({
7268
+ hash: entry.hash,
7169
7269
  entry,
7170
- replicas: minReplicas.getValue(this),
7171
- })
7172
- ) {
7173
- 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;
7174
7282
  }
7175
- });
7176
- }, this.waitForPruneDelay),
7177
- );
7178
- };
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
+ };
7179
7321
 
7180
7322
  const reject = (e: any) => {
7181
7323
  clear();
@@ -7183,11 +7325,9 @@ export class SharedLog<
7183
7325
  e instanceof Error &&
7184
7326
  typeof e.message === "string" &&
7185
7327
  e.message.startsWith("Timeout for checked pruning");
7186
- if (explicitTimeout || !isCheckedPruneTimeout) {
7187
- this.clearCheckedPruneRetry(entry.hash);
7188
- }
7189
- this.removePruneRequestSent(entry.hash);
7190
- this._requestIPruneResponseReplicatorSet.delete(entry.hash);
7328
+ this._checkedPrune.markCancelled(entry.hash, {
7329
+ preserveRetry: !explicitTimeout && isCheckedPruneTimeout,
7330
+ });
7191
7331
  deferredPromise.reject(e);
7192
7332
  };
7193
7333
 
@@ -7201,70 +7341,65 @@ export class SharedLog<
7201
7341
  const checkedPruneTimeoutMs =
7202
7342
  options?.timeout ??
7203
7343
  Math.max(
7204
- 10_000,
7344
+ CHECKED_PRUNE_BACKGROUND_TIMEOUT_MIN_MS,
7205
7345
  Number(this._respondToIHaveTimeout ?? 0) +
7206
7346
  this.waitForReplicatorTimeout +
7207
7347
  PRUNE_DEBOUNCE_INTERVAL * 2,
7208
7348
  );
7209
7349
 
7210
- const timeout = setTimeout(() => {
7211
- // For internal/background prune flows (no explicit timeout), retry a few times
7212
- // to avoid "permanently prunable" entries when `_pendingIHave` expires under
7213
- // heavy load.
7214
- if (!explicitTimeout) {
7215
- this.scheduleCheckedPruneRetry({ entry, leaders });
7216
- }
7217
- reject(
7218
- new Error(
7219
- `Timeout for checked pruning after ${checkedPruneTimeoutMs}ms (closed=${this.closed})`,
7220
- ),
7221
- );
7222
- }, checkedPruneTimeoutMs);
7223
- timeout.unref?.();
7224
-
7225
- this._pendingDeletes.set(entry.hash, {
7226
- promise: deferredPromise,
7227
- clear,
7228
- reject,
7229
- resolve: async (publicKeyHash: string) => {
7230
- const minReplicasObj = this.getClampedReplicas(minReplicas);
7231
- const minReplicasValue = minReplicasObj.getValue(this);
7232
-
7233
- // TODO is this check necessary
7234
- if (
7235
- !(await this._waitForReplicators(
7236
- cursor ??
7237
- (cursor = await this.createCoordinates(
7238
- entry,
7239
- minReplicasValue,
7240
- )),
7241
- entry,
7242
- [
7243
- { 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
+ ],
7244
7391
  {
7245
- key: this.node.identity.publicKey.hashcode(),
7246
- replicator: false,
7392
+ persist: false,
7247
7393
  },
7248
- ],
7249
- {
7250
- persist: false,
7251
- },
7252
- ))
7253
- ) {
7254
- return;
7255
- }
7256
-
7257
- let existCounter = this._requestIPruneResponseReplicatorSet.get(
7258
- entry.hash,
7259
- );
7260
- if (!existCounter) {
7261
- existCounter = new Set();
7262
- this._requestIPruneResponseReplicatorSet.set(
7263
- entry.hash,
7264
- existCounter,
7265
- );
7394
+ ))
7395
+ ) {
7396
+ return;
7266
7397
  }
7267
- existCounter.add(publicKeyHash);
7398
+
7399
+ const existCounter = this._checkedPrune.addConfirmedReplicator(
7400
+ entry.hash,
7401
+ publicKeyHash,
7402
+ );
7268
7403
  // Seed provider hints so future remote reads can avoid extra round-trips.
7269
7404
  this.remoteBlocks.hintProviders(entry.hash, [publicKeyHash]);
7270
7405
 
@@ -7272,7 +7407,10 @@ export class SharedLog<
7272
7407
  resolve();
7273
7408
  }
7274
7409
  },
7275
- });
7410
+ },
7411
+ entry,
7412
+ leaders,
7413
+ );
7276
7414
 
7277
7415
  promises.push(deferredPromise.promise);
7278
7416
  }
@@ -7280,16 +7418,11 @@ export class SharedLog<
7280
7418
  const emitMessages = async (entries: string[], to: string) => {
7281
7419
  const filteredSet: string[] = [];
7282
7420
  for (const entry of entries) {
7283
- let set = this._requestIPruneSent.get(entry);
7284
- if (!set) {
7285
- set = new Set();
7286
- this._requestIPruneSent.set(entry, set);
7287
- }
7288
7421
  /* TODO why can we not have this statement?
7289
7422
  if (set.has(to)) {
7290
7423
  continue;
7291
7424
  } */
7292
- set.add(to);
7425
+ this._checkedPrune.addRequestSent(entry, to);
7293
7426
  filteredSet.push(entry);
7294
7427
  }
7295
7428
  if (filteredSet.length > 0) {
@@ -7302,7 +7435,7 @@ export class SharedLog<
7302
7435
  to: [to], // TODO group by peers?
7303
7436
  redundancy: 1,
7304
7437
  }),
7305
- priority: 1,
7438
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
7306
7439
  },
7307
7440
  );
7308
7441
  }
@@ -7331,7 +7464,9 @@ export class SharedLog<
7331
7464
 
7332
7465
  const pendingByPeer: [string, string[]][] = [];
7333
7466
  for (const [peer, hashes] of peerToEntries) {
7334
- const pending = hashes.filter((h) => this._pendingDeletes.has(h));
7467
+ const pending = hashes.filter((h) =>
7468
+ this._checkedPrune.hasPendingDelete(h),
7469
+ );
7335
7470
  if (pending.length > 0) {
7336
7471
  pendingByPeer.push([peer, pending]);
7337
7472
  }
@@ -7762,12 +7897,26 @@ export class SharedLog<
7762
7897
  flushUncheckedDeliverTarget(target);
7763
7898
  }
7764
7899
 
7765
- if (this._isAdaptiveReplicating && hasSelfRangeRemoval) {
7766
- // Adaptive shrink/replacement can make already-indexed local heads
7767
- // prunable even when the incremental rebalance scan missed them under
7768
- // churn or timing pressure. Re-scan after repair dispatches are flushed
7769
- // so checked prune work is enqueued before callers wait for idle.
7770
- 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
+ });
7771
7920
  }
7772
7921
 
7773
7922
  return changed;