@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/dist/src/index.js CHANGED
@@ -44,12 +44,13 @@ import { ClosedError, Program } from "@peerbit/program";
44
44
  import { FanoutChannel, waitForSubscribers, } from "@peerbit/pubsub";
45
45
  import { SubscriptionEvent, UnsubcriptionEvent, } from "@peerbit/pubsub-interface";
46
46
  import { RPC } from "@peerbit/rpc";
47
- import { ACK_CONTROL_PRIORITY, AcknowledgeDelivery, AnyWhere, createRequestTransportContext, DataMessage, MessageHeader, NotStartedError, SilentDelivery, } from "@peerbit/stream-interface";
47
+ import { ACK_CONTROL_PRIORITY, AcknowledgeDelivery, AnyWhere, BACKGROUND_MESSAGE_PRIORITY, CONVERGENCE_MESSAGE_PRIORITY, createRequestTransportContext, DataMessage, MessageHeader, NotStartedError, SilentDelivery, } from "@peerbit/stream-interface";
48
48
  import { AbortError, TimeoutError, debounceAccumulator, debounceFixedInterval, waitFor, } from "@peerbit/time";
49
49
  import pDefer, {} from "p-defer";
50
50
  import PQueue from "p-queue";
51
- import { concat } from "uint8arrays";
51
+ import { concat, fromString } from "uint8arrays";
52
52
  import { BlocksMessage } from "./blocks.js";
53
+ import { CheckedPruneCoordinator, } from "./checked-prune.js";
53
54
  import { CPUUsageIntervalLag } from "./cpu.js";
54
55
  import { debouncedAccumulatorMap, } from "./debounce.js";
55
56
  import { NoPeersError } from "./errors.js";
@@ -66,7 +67,7 @@ import {} from "./replication-domain.js";
66
67
  import { AbsoluteReplicas, AddedReplicationSegmentMessage, AllReplicatingSegmentsMessage, MinReplicas, ReplicationPingMessage, ReplicationError, RequestReplicationInfoMessage, ResponseRoleMessage, StoppedReplicating, decodeReplicas, encodeReplicas, maxReplicas, } from "./replication.js";
67
68
  import { Observer, Replicator } from "./role.js";
68
69
  import { RatelessIBLTSynchronizer } from "./sync/rateless-iblt.js";
69
- import { ConfirmEntriesMessage, SimpleSyncronizer } from "./sync/simple.js";
70
+ import { ConfirmEntriesMessage, SYNC_MESSAGE_PRIORITY, SimpleSyncronizer, } from "./sync/simple.js";
70
71
  import { groupByGid } from "./utils.js";
71
72
  const toLocalPublicSignKey = (key) => {
72
73
  if (typeof key === "string") {
@@ -238,6 +239,7 @@ export const WAIT_FOR_PRUNE_DELAY = 0;
238
239
  const PRUNE_DEBOUNCE_INTERVAL = 500;
239
240
  const CHECKED_PRUNE_RESEND_INTERVAL_MIN_MS = 250;
240
241
  const CHECKED_PRUNE_RESEND_INTERVAL_MAX_MS = 5_000;
242
+ const CHECKED_PRUNE_BACKGROUND_TIMEOUT_MIN_MS = 120_000;
241
243
  const CHECKED_PRUNE_RETRY_MAX_ATTEMPTS = 3;
242
244
  const CHECKED_PRUNE_RETRY_MAX_DELAY_MS = 30_000;
243
245
  // DONT SET THIS ANY LOWER, because it will make the pid controller unstable as the system responses are not fast enough to updates from the pid controller
@@ -426,7 +428,10 @@ let SharedLog = (() => {
426
428
  _logProperties;
427
429
  _closeController;
428
430
  _respondToIHaveTimeout;
429
- _pendingDeletes;
431
+ _checkedPrune;
432
+ get _pendingDeletes() {
433
+ return this._checkedPrune.pendingDeletes;
434
+ }
430
435
  _pendingIHave;
431
436
  // public key hash to range id to range
432
437
  pendingMaturity; // map of peerId to timeout
@@ -453,9 +458,15 @@ let SharedLog = (() => {
453
458
  // A fn for debouncing the calls for pruning
454
459
  pruneDebouncedFn;
455
460
  responseToPruneDebouncedFn;
456
- _requestIPruneSent; // tracks entry hash to peer hash for requesting I prune messages
457
- _requestIPruneResponseReplicatorSet; // tracks entry hash to peer hash
458
- _checkedPruneRetries;
461
+ get _requestIPruneSent() {
462
+ return this._checkedPrune.requestIPruneSent;
463
+ }
464
+ get _requestIPruneResponseReplicatorSet() {
465
+ return this._checkedPrune.responseReplicatorSet;
466
+ }
467
+ get _checkedPruneRetries() {
468
+ return this._checkedPrune.retries;
469
+ }
459
470
  replicationChangeDebounceFn;
460
471
  _repairRetryTimers;
461
472
  _recentRepairDispatch;
@@ -606,7 +617,7 @@ let SharedLog = (() => {
606
617
  header: new MessageHeader({
607
618
  session: 0,
608
619
  mode: new AnyWhere(),
609
- priority: 0,
620
+ priority: BACKGROUND_MESSAGE_PRIORITY,
610
621
  }),
611
622
  });
612
623
  contextMessage.header.timestamp = envelope.timestamp;
@@ -634,7 +645,7 @@ let SharedLog = (() => {
634
645
  header: new MessageHeader({
635
646
  session: 0,
636
647
  mode: new AnyWhere(),
637
- priority: 0,
648
+ priority: BACKGROUND_MESSAGE_PRIORITY,
638
649
  }),
639
650
  });
640
651
  contextMessage.header.timestamp = detail.timestamp;
@@ -1347,7 +1358,7 @@ let SharedLog = (() => {
1347
1358
  await this.rpc.send(new StoppedReplicating({
1348
1359
  segmentIds: rangesToUnreplicate.map((x) => x.id),
1349
1360
  }), {
1350
- priority: 1,
1361
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
1351
1362
  });
1352
1363
  }
1353
1364
  return rangesToReplicate;
@@ -1374,12 +1385,18 @@ let SharedLog = (() => {
1374
1385
  this.setupRebalanceDebounceFunction(options?.limits?.interval);
1375
1386
  }
1376
1387
  async replicate(rangeOrEntry, options) {
1388
+ const entryRangeId = (entry) => sha256Sync(concat([
1389
+ this.log.id,
1390
+ fromString(entry.hash),
1391
+ fromString(this.node.identity.publicKey.hashcode()),
1392
+ ]));
1377
1393
  let range = undefined;
1378
1394
  if (rangeOrEntry instanceof ReplicationRangeMessage) {
1379
1395
  range = rangeOrEntry;
1380
1396
  }
1381
1397
  else if (rangeOrEntry instanceof Entry) {
1382
1398
  range = {
1399
+ id: entryRangeId(rangeOrEntry),
1383
1400
  factor: 1,
1384
1401
  offset: await this.domain.fromEntry(rangeOrEntry),
1385
1402
  normalized: false,
@@ -1390,6 +1407,7 @@ let SharedLog = (() => {
1390
1407
  for (const entry of rangeOrEntry) {
1391
1408
  if (entry instanceof Entry) {
1392
1409
  ranges.push({
1410
+ id: entryRangeId(entry),
1393
1411
  factor: 1,
1394
1412
  offset: await this.domain.fromEntry(entry),
1395
1413
  normalized: false,
@@ -1452,7 +1470,7 @@ let SharedLog = (() => {
1452
1470
  const rangesToRemove = await this.resolveReplicationRangesFromIdsAndKey(segmentIds, this.node.identity.publicKey);
1453
1471
  await this.removeReplicationRanges(rangesToRemove, this.node.identity.publicKey);
1454
1472
  await this.rpc.send(new StoppedReplicating({ segmentIds }), {
1455
- priority: 1,
1473
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
1456
1474
  });
1457
1475
  }
1458
1476
  async removeReplicator(key, options) {
@@ -1470,7 +1488,7 @@ let SharedLog = (() => {
1470
1488
  if (isMe) {
1471
1489
  // announce that we are no longer replicating
1472
1490
  await this.rpc.send(new AllReplicatingSegmentsMessage({ segments: [] }), {
1473
- priority: 1,
1491
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
1474
1492
  });
1475
1493
  }
1476
1494
  if (options?.noEvent !== true) {
@@ -1833,7 +1851,7 @@ let SharedLog = (() => {
1833
1851
  }
1834
1852
  else {
1835
1853
  await this.rpc.send(message, {
1836
- priority: 1,
1854
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
1837
1855
  });
1838
1856
  }
1839
1857
  }
@@ -2020,7 +2038,7 @@ let SharedLog = (() => {
2020
2038
  for (let i = 0; i < uniqueHashes.length; i += REPAIR_CONFIRMATION_HASH_BATCH_SIZE) {
2021
2039
  const chunk = uniqueHashes.slice(i, i + REPAIR_CONFIRMATION_HASH_BATCH_SIZE);
2022
2040
  await this.rpc.send(new ConfirmEntriesMessage({ hashes: chunk }), {
2023
- priority: 1,
2041
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
2024
2042
  mode: new SilentDelivery({ to: [target], redundancy: 1 }),
2025
2043
  });
2026
2044
  }
@@ -2029,7 +2047,7 @@ let SharedLog = (() => {
2029
2047
  for await (const message of createExchangeHeadsMessages(this.log, [...entries.keys()])) {
2030
2048
  message.reserved[0] |= EXCHANGE_HEADS_REPAIR_HINT;
2031
2049
  await this.rpc.send(message, {
2032
- priority: 1,
2050
+ priority: SYNC_MESSAGE_PRIORITY,
2033
2051
  mode: new SilentDelivery({ to: [target], redundancy: 1 }),
2034
2052
  });
2035
2053
  }
@@ -2504,28 +2522,19 @@ let SharedLog = (() => {
2504
2522
  const frontierTargets = this._repairFrontierByMode.get(mode);
2505
2523
  for (const target of pendingPeersByMode.get(mode) ?? []) {
2506
2524
  const replacement = nextTargets.get(target);
2507
- if (mode === "join-authoritative") {
2508
- // Authoritative join repair is receipt-driven: a later sweep can have a
2509
- // narrower transient leader view, but it must not forget unconfirmed
2510
- // hashes that were already queued for this joiner.
2511
- if (replacement && replacement.size > 0) {
2512
- const existing = frontierTargets?.get(target);
2513
- if (existing && existing.size > 0) {
2514
- for (const [hash, entry] of replacement) {
2515
- existing.set(hash, entry);
2516
- }
2517
- }
2518
- else {
2519
- frontierTargets?.set(target, replacement);
2525
+ // These repairs are receipt-driven: a later sweep can have a narrower
2526
+ // transient leader view, but it must not forget unconfirmed hashes
2527
+ // that were already queued for this target.
2528
+ if (replacement && replacement.size > 0) {
2529
+ const existing = frontierTargets?.get(target);
2530
+ if (existing && existing.size > 0) {
2531
+ for (const [hash, entry] of replacement) {
2532
+ existing.set(hash, entry);
2520
2533
  }
2521
2534
  }
2522
- continue;
2523
- }
2524
- if (replacement && replacement.size > 0) {
2525
- frontierTargets?.set(target, replacement);
2526
- }
2527
- else {
2528
- frontierTargets?.delete(target);
2535
+ else {
2536
+ frontierTargets?.set(target, replacement);
2537
+ }
2529
2538
  }
2530
2539
  }
2531
2540
  }
@@ -2553,25 +2562,22 @@ let SharedLog = (() => {
2553
2562
  if (this.keep && (await this.keep(args.value.entry))) {
2554
2563
  return false;
2555
2564
  }
2565
+ this._checkedPrune.trackCandidate(args.key, args.value.entry, args.value.leaders);
2556
2566
  void this.pruneDebouncedFn.add(args);
2557
2567
  return true;
2558
2568
  }
2559
- async cancelCheckedPruneForLocalLeader(hash) {
2569
+ async cancelCheckedPruneForLocalLeader(hash, options) {
2560
2570
  this.pruneDebouncedFn.delete(hash);
2561
- this.clearCheckedPruneRetry(hash);
2562
- this.removePruneRequestSent(hash);
2563
- this._requestIPruneResponseReplicatorSet.delete(hash);
2564
- await this._pendingDeletes
2565
- .get(hash)
2566
- ?.reject(new Error("Failed to delete, is leader again"));
2571
+ const pendingDelete = this._checkedPrune.getPendingDelete(hash);
2572
+ this._checkedPrune.markCancelled(hash, {
2573
+ preserveRetry: options?.preserveRetry,
2574
+ });
2575
+ await pendingDelete?.reject(new Error("Failed to delete, is leader again"));
2567
2576
  }
2568
2577
  hasActiveCheckedPruneWork(hash) {
2569
- return (this._pendingDeletes.has(hash) ||
2570
- this._requestIPruneSent.has(hash) ||
2571
- this._requestIPruneResponseReplicatorSet.has(hash) ||
2572
- this._checkedPruneRetries.has(hash));
2578
+ return this._checkedPrune.hasActiveWork(hash);
2573
2579
  }
2574
- async resolveCheckedPruneLeaders(args) {
2580
+ async revalidateCheckedPruneOwnership(args) {
2575
2581
  const selfHash = this.node.identity.publicKey.hashcode();
2576
2582
  if (args.leaders.has(selfHash)) {
2577
2583
  if (args.selfReplicating === false) {
@@ -2617,7 +2623,7 @@ let SharedLog = (() => {
2617
2623
  await this.cancelCheckedPruneForLocalLeader(entry.hash);
2618
2624
  continue;
2619
2625
  }
2620
- if (this._pendingDeletes.has(entry.hash)) {
2626
+ if (this._checkedPrune.hasPendingDelete(entry.hash)) {
2621
2627
  continue;
2622
2628
  }
2623
2629
  if (leaders.size === 0) {
@@ -2630,7 +2636,7 @@ let SharedLog = (() => {
2630
2636
  this.responseToPruneDebouncedFn.delete(entry.hash);
2631
2637
  }
2632
2638
  }
2633
- async pruneIndexedEntriesNoLongerLed() {
2639
+ async pruneIndexedEntriesNoLongerLed(options) {
2634
2640
  const selfHash = this.node.identity.publicKey.hashcode();
2635
2641
  const iterator = this.entryCoordinatesIndex.iterate({});
2636
2642
  let enqueuedPrune = false;
@@ -2642,12 +2648,12 @@ let SharedLog = (() => {
2642
2648
  if (this.closed) {
2643
2649
  continue;
2644
2650
  }
2645
- const leaders = await this.findLeaders(entryReplicated.coordinates, entryReplicated, { roleAge: 0 });
2651
+ const leaders = await this.findLeaders(entryReplicated.coordinates, entryReplicated, options?.useDefaultRoleAge ? undefined : { roleAge: 0 });
2646
2652
  if (leaders.has(selfHash)) {
2647
2653
  await this.cancelCheckedPruneForLocalLeader(entryReplicated.hash);
2648
2654
  continue;
2649
2655
  }
2650
- if (this._pendingDeletes.has(entryReplicated.hash)) {
2656
+ if (this._checkedPrune.hasPendingDelete(entryReplicated.hash)) {
2651
2657
  continue;
2652
2658
  }
2653
2659
  if (leaders.size === 0) {
@@ -2669,20 +2675,63 @@ let SharedLog = (() => {
2669
2675
  await this.pruneDebouncedFn.flush();
2670
2676
  }
2671
2677
  }
2672
- clearCheckedPruneRetry(hash) {
2673
- const state = this._checkedPruneRetries.get(hash);
2674
- if (state?.timer) {
2675
- clearTimeout(state.timer);
2678
+ async pruneCurrentHeadsNoLongerLed(options) {
2679
+ const selfHash = this.node.identity.publicKey.hashcode();
2680
+ const heads = await this.log.getHeads(true).all();
2681
+ let enqueuedPrune = false;
2682
+ for (const head of heads) {
2683
+ if (this.closed) {
2684
+ break;
2685
+ }
2686
+ const leaders = await this.findLeadersFromEntry(head, maxReplicas(this, [head]), options?.useDefaultRoleAge ? undefined : { roleAge: 0 });
2687
+ if (leaders.has(selfHash)) {
2688
+ await this.cancelCheckedPruneForLocalLeader(head.hash);
2689
+ continue;
2690
+ }
2691
+ if (this._checkedPrune.hasPendingDelete(head.hash)) {
2692
+ continue;
2693
+ }
2694
+ if (leaders.size === 0) {
2695
+ continue;
2696
+ }
2697
+ enqueuedPrune =
2698
+ (await this.pruneDebouncedFnAddIfNotKeeping({
2699
+ key: head.hash,
2700
+ value: { entry: head, leaders },
2701
+ })) || enqueuedPrune;
2702
+ this.responseToPruneDebouncedFn.delete(head.hash);
2703
+ }
2704
+ if (enqueuedPrune && !this.closed) {
2705
+ await this.pruneDebouncedFn.flush();
2706
+ }
2707
+ }
2708
+ checkedPruneLeadersToMap(leaders) {
2709
+ if (leaders instanceof Map) {
2710
+ return new Map(leaders);
2711
+ }
2712
+ const leadersMap = new Map();
2713
+ for (const leader of leaders) {
2714
+ leadersMap.set(leader, { intersecting: true });
2676
2715
  }
2677
- this._checkedPruneRetries.delete(hash);
2716
+ return leadersMap;
2717
+ }
2718
+ clearCheckedPruneRetry(hash) {
2719
+ this._checkedPrune.clearRetry(hash);
2678
2720
  }
2679
2721
  scheduleCheckedPruneRetry(args) {
2680
2722
  if (this.closed)
2681
2723
  return;
2682
- if (this._pendingDeletes.has(args.entry.hash))
2724
+ if (this._checkedPrune.hasPendingDelete(args.entry.hash))
2683
2725
  return;
2684
2726
  const hash = args.entry.hash;
2685
- const state = this._checkedPruneRetries.get(hash) ?? { attempts: 0 };
2727
+ const state = this._checkedPrune.getRetry(hash) ??
2728
+ {
2729
+ attempts: 0,
2730
+ entry: args.entry,
2731
+ leaders: args.leaders,
2732
+ };
2733
+ state.entry = args.entry;
2734
+ state.leaders = args.leaders;
2686
2735
  if (state.timer)
2687
2736
  return;
2688
2737
  if (state.attempts >= CHECKED_PRUNE_RETRY_MAX_ATTEMPTS) {
@@ -2695,17 +2744,19 @@ let SharedLog = (() => {
2695
2744
  const delayMs = Math.min(CHECKED_PRUNE_RETRY_MAX_DELAY_MS, 1_000 * 2 ** (attempt - 1) + jitterMs);
2696
2745
  state.attempts = attempt;
2697
2746
  state.timer = setTimeout(async () => {
2698
- const st = this._checkedPruneRetries.get(hash);
2747
+ const st = this._checkedPrune.getRetry(hash);
2699
2748
  if (st)
2700
2749
  st.timer = undefined;
2701
2750
  if (this.closed)
2702
2751
  return;
2703
- if (this._pendingDeletes.has(hash))
2752
+ if (this._checkedPrune.hasPendingDelete(hash))
2704
2753
  return;
2754
+ const retryEntry = st?.entry ?? args.entry;
2755
+ const retryLeaders = st?.leaders ?? args.leaders;
2705
2756
  let leadersMap;
2706
2757
  try {
2707
- const replicas = decodeReplicas(args.entry).getValue(this);
2708
- leadersMap = await this.findLeadersFromEntry(args.entry, replicas, {
2758
+ const replicas = decodeReplicas(retryEntry).getValue(this);
2759
+ leadersMap = await this.findLeadersFromEntry(retryEntry, replicas, {
2709
2760
  roleAge: 0,
2710
2761
  });
2711
2762
  }
@@ -2713,21 +2764,13 @@ let SharedLog = (() => {
2713
2764
  // Best-effort only.
2714
2765
  }
2715
2766
  if (!leadersMap || leadersMap.size === 0) {
2716
- if (args.leaders instanceof Map) {
2717
- leadersMap = args.leaders;
2718
- }
2719
- else {
2720
- leadersMap = new Map();
2721
- for (const k of args.leaders) {
2722
- leadersMap.set(k, { intersecting: true });
2723
- }
2724
- }
2767
+ leadersMap = this.checkedPruneLeadersToMap(retryLeaders);
2725
2768
  }
2726
2769
  try {
2727
2770
  const leadersForRetry = leadersMap ?? new Map();
2728
2771
  await this.pruneDebouncedFnAddIfNotKeeping({
2729
2772
  key: hash,
2730
- value: { entry: args.entry, leaders: leadersForRetry },
2773
+ value: { entry: retryEntry, leaders: leadersForRetry },
2731
2774
  });
2732
2775
  }
2733
2776
  catch {
@@ -2735,7 +2778,56 @@ let SharedLog = (() => {
2735
2778
  }
2736
2779
  }, delayMs);
2737
2780
  state.timer.unref?.();
2738
- this._checkedPruneRetries.set(hash, state);
2781
+ this._checkedPrune.setRetry(hash, state);
2782
+ }
2783
+ async recoverCheckedPruneFromLateResponses(hashes, publicKeyHash) {
2784
+ if (this.closed)
2785
+ return;
2786
+ const selfHash = this.node.identity.publicKey.hashcode();
2787
+ const toPrune = new Map();
2788
+ const responseStillApplies = [];
2789
+ for (const hash of hashes) {
2790
+ if (this.closed) {
2791
+ break;
2792
+ }
2793
+ if (this._checkedPrune.hasPendingDelete(hash)) {
2794
+ continue;
2795
+ }
2796
+ const retry = this._checkedPrune.clearRetryTimer(hash);
2797
+ if (!retry) {
2798
+ continue;
2799
+ }
2800
+ const entry = retry.entry;
2801
+ let leaders = this.checkedPruneLeadersToMap(retry.leaders);
2802
+ try {
2803
+ const currentLeaders = await this.findLeadersFromEntry(entry, decodeReplicas(entry).getValue(this), { roleAge: 0 });
2804
+ if (currentLeaders.size > 0) {
2805
+ leaders = currentLeaders;
2806
+ }
2807
+ }
2808
+ catch {
2809
+ // Best-effort only; the stored retry leaders came from a previous
2810
+ // checked-prune decision for this exact entry.
2811
+ }
2812
+ if (leaders.has(selfHash)) {
2813
+ await this.cancelCheckedPruneForLocalLeader(hash);
2814
+ continue;
2815
+ }
2816
+ if (leaders.size === 0) {
2817
+ continue;
2818
+ }
2819
+ toPrune.set(hash, { entry, leaders });
2820
+ if (leaders.has(publicKeyHash)) {
2821
+ responseStillApplies.push(hash);
2822
+ }
2823
+ }
2824
+ if (toPrune.size === 0) {
2825
+ return;
2826
+ }
2827
+ void Promise.allSettled(this.prune(toPrune));
2828
+ for (const hash of responseStillApplies) {
2829
+ void this._checkedPrune.getPendingDelete(hash)?.resolve(publicKeyHash);
2830
+ }
2739
2831
  }
2740
2832
  async append(data, options) {
2741
2833
  if (this._isAdaptiveReplicating) {
@@ -2843,7 +2935,7 @@ let SharedLog = (() => {
2843
2935
  : createReplicationDomainHash(options?.compatibility && options?.compatibility < 10 ? "u32" : "u64")(this);
2844
2936
  this.indexableDomain = createIndexableDomainFromResolution(this.domain.resolution);
2845
2937
  this._respondToIHaveTimeout = options?.respondToIHaveTimeout ?? 2e4;
2846
- this._pendingDeletes = new Map();
2938
+ this._checkedPrune = new CheckedPruneCoordinator();
2847
2939
  this._pendingIHave = new Map();
2848
2940
  this.latestReplicationInfoMessage = new Map();
2849
2941
  this._replicationInfoBlockedPeers = new Set();
@@ -3001,22 +3093,28 @@ let SharedLog = (() => {
3001
3093
  ],
3002
3094
  })) > 0;
3003
3095
  this._gidPeersHistory = new Map();
3004
- this._requestIPruneSent = new Map();
3005
- this._requestIPruneResponseReplicatorSet = new Map();
3006
- this._checkedPruneRetries = new Map();
3007
3096
  this.replicationChangeDebounceFn = debounceAggregationChanges((change) => this.onReplicationChange(change).then(() => this.rebalanceParticipationDebounced?.call()), this.distributionDebounceTime);
3008
3097
  this.pruneDebouncedFn = debouncedAccumulatorMap(async (map) => {
3009
3098
  const current = new Map();
3010
3099
  const selfReplicating = await this.isReplicating();
3011
3100
  for (const [hash, value] of map) {
3012
- const checkedPruneLeaders = await this.resolveCheckedPruneLeaders({
3101
+ const checkedPruneLeaders = await this.revalidateCheckedPruneOwnership({
3013
3102
  hash,
3014
3103
  entry: value.entry,
3015
3104
  leaders: value.leaders,
3016
3105
  selfReplicating,
3017
3106
  });
3018
3107
  if (checkedPruneLeaders.localLeader) {
3019
- await this.cancelCheckedPruneForLocalLeader(hash);
3108
+ const preserveRetry = this._checkedPrune.hasRetry(hash);
3109
+ await this.cancelCheckedPruneForLocalLeader(hash, {
3110
+ preserveRetry,
3111
+ });
3112
+ if (preserveRetry) {
3113
+ this.scheduleCheckedPruneRetry({
3114
+ entry: value.entry,
3115
+ leaders: checkedPruneLeaders.leaders,
3116
+ });
3117
+ }
3020
3118
  continue;
3021
3119
  }
3022
3120
  current.set(hash, {
@@ -3050,7 +3148,7 @@ let SharedLog = (() => {
3050
3148
  to: allRequestingPeers,
3051
3149
  redundancy: 1,
3052
3150
  }),
3053
- priority: 1,
3151
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
3054
3152
  });
3055
3153
  }, () => {
3056
3154
  let accumulator = new Map();
@@ -3339,18 +3437,7 @@ let SharedLog = (() => {
3339
3437
  this.cancelReplicationInfoRequests(peerHash);
3340
3438
  this._replicatorLivenessFailures.delete(peerHash);
3341
3439
  this._replicatorLastActivityAt.delete(peerHash);
3342
- for (const [hash, peers] of this._requestIPruneSent) {
3343
- peers.delete(peerHash);
3344
- if (peers.size === 0) {
3345
- this._requestIPruneSent.delete(hash);
3346
- }
3347
- }
3348
- for (const [hash, peers] of this._requestIPruneResponseReplicatorSet) {
3349
- peers.delete(peerHash);
3350
- if (peers.size === 0) {
3351
- this._requestIPruneResponseReplicatorSet.delete(hash);
3352
- }
3353
- }
3440
+ this._checkedPrune.cleanupPeer(peerHash);
3354
3441
  }
3355
3442
  markReplicatorActivity(peerHash, now = Date.now()) {
3356
3443
  this._replicatorLastActivityAt.set(peerHash, now);
@@ -3396,14 +3483,14 @@ let SharedLog = (() => {
3396
3483
  const maxPeers = options?.maxPeers ?? 8;
3397
3484
  const self = this.node.identity.publicKey.hashcode();
3398
3485
  const seed = hashToSeed32(hash);
3399
- const hinted = this._requestIPruneResponseReplicatorSet.get(hash);
3486
+ const hinted = this._checkedPrune.getConfirmedReplicators(hash);
3400
3487
  if (hinted && hinted.size > 0) {
3401
3488
  const peers = [...hinted].filter((p) => p !== self);
3402
3489
  return peers.length > 0
3403
3490
  ? pickDeterministicSubset(peers, seed, maxPeers)
3404
3491
  : undefined;
3405
3492
  }
3406
- const contacted = this._requestIPruneSent.get(hash);
3493
+ const contacted = this._checkedPrune.getContactedReplicators(hash);
3407
3494
  if (contacted && contacted.size > 0) {
3408
3495
  const peers = [...contacted].filter((p) => p !== self);
3409
3496
  return peers.length > 0
@@ -3760,25 +3847,14 @@ let SharedLog = (() => {
3760
3847
  this._appendBackfillTimer = undefined;
3761
3848
  }
3762
3849
  this._appendBackfillPendingByTarget.clear();
3763
- for (const [_k, v] of this._pendingDeletes) {
3764
- v.clear();
3765
- v.promise.resolve(); // TODO or reject?
3766
- }
3767
3850
  for (const [_k, v] of this._pendingIHave) {
3768
3851
  v.clear();
3769
3852
  }
3770
- for (const [_k, v] of this._checkedPruneRetries) {
3771
- if (v.timer)
3772
- clearTimeout(v.timer);
3773
- }
3853
+ this._checkedPrune.close();
3774
3854
  await this.remoteBlocks.stop();
3775
- this._pendingDeletes.clear();
3776
3855
  this._pendingIHave.clear();
3777
- this._checkedPruneRetries.clear();
3778
3856
  this.latestReplicationInfoMessage.clear();
3779
3857
  this._gidPeersHistory.clear();
3780
- this._requestIPruneSent.clear();
3781
- this._requestIPruneResponseReplicatorSet.clear();
3782
3858
  // Cancel any pending debounced timers so they can't fire after we've torn down
3783
3859
  // indexes/RPC state.
3784
3860
  this.rebalanceParticipationDebounced?.close();
@@ -3829,7 +3905,7 @@ let SharedLog = (() => {
3829
3905
  try {
3830
3906
  await this.rpc
3831
3907
  .send(new AllReplicatingSegmentsMessage({ segments: [] }), {
3832
- priority: 1,
3908
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
3833
3909
  signal: abort.signal,
3834
3910
  })
3835
3911
  .catch(() => { });
@@ -3873,7 +3949,7 @@ let SharedLog = (() => {
3873
3949
  try {
3874
3950
  await this.rpc
3875
3951
  .send(new AllReplicatingSegmentsMessage({ segments: [] }), {
3876
- priority: 1,
3952
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
3877
3953
  signal: abort.signal,
3878
3954
  })
3879
3955
  .catch(() => { });
@@ -4016,7 +4092,7 @@ let SharedLog = (() => {
4016
4092
  for (const entry of entries) {
4017
4093
  this.pruneDebouncedFn.delete(entry.entry.hash);
4018
4094
  this.removePruneRequestSent(entry.entry.hash);
4019
- this._requestIPruneResponseReplicatorSet.delete(entry.entry.hash);
4095
+ this._checkedPrune.clearConfirmedReplicators(entry.entry.hash);
4020
4096
  if (fromIsLeader) {
4021
4097
  this.addPeersToGidPeerHistory(gid, [
4022
4098
  context.from.hashcode(),
@@ -4112,27 +4188,37 @@ let SharedLog = (() => {
4112
4188
  this.removeEntriesKnownByPeer([hash], from);
4113
4189
  // if we expect the remote to be owner of this entry because we are to prune ourselves, then we need to remove the remote
4114
4190
  // this is due to that the remote has previously indicated to be a replicator to help us prune but now has changed their mind
4115
- const outGoingPrunes = this._requestIPruneResponseReplicatorSet.get(hash);
4116
- if (outGoingPrunes) {
4117
- outGoingPrunes.delete(from);
4118
- }
4191
+ this._checkedPrune.removeConfirmedReplicator(hash, from);
4119
4192
  const indexedEntry = await this.log.entryIndex.getShallow(hash);
4120
4193
  let isLeader = false;
4121
- if (indexedEntry &&
4122
- !this._pendingDeletes.has(hash) &&
4123
- (await this.log.blocks.has(hash))) {
4124
- this.removePeerFromGidPeerHistory(context.from.hashcode(), indexedEntry.value.meta.gid);
4125
- await this._waitForReplicators(await this.createCoordinates(indexedEntry.value, decodeReplicas(indexedEntry.value).getValue(this)), indexedEntry.value, [
4126
- {
4127
- key: this.node.identity.publicKey.hashcode(),
4128
- replicator: true,
4129
- },
4130
- ], {
4131
- onLeader: (key) => {
4132
- isLeader =
4133
- isLeader || key === this.node.identity.publicKey.hashcode();
4134
- },
4135
- });
4194
+ if (indexedEntry && (await this.log.blocks.has(hash))) {
4195
+ const pendingDelete = this._checkedPrune.getPendingDelete(hash);
4196
+ if (pendingDelete) {
4197
+ const ownership = await this.revalidateCheckedPruneOwnership({
4198
+ hash,
4199
+ entry: indexedEntry.value,
4200
+ leaders: new Map(),
4201
+ });
4202
+ if (ownership.localLeader) {
4203
+ await this.cancelCheckedPruneForLocalLeader(hash);
4204
+ isLeader = true;
4205
+ }
4206
+ }
4207
+ else {
4208
+ this.removePeerFromGidPeerHistory(context.from.hashcode(), indexedEntry.value.meta.gid);
4209
+ await this._waitForReplicators(await this.createCoordinates(indexedEntry.value, decodeReplicas(indexedEntry.value).getValue(this)), indexedEntry.value, [
4210
+ {
4211
+ key: this.node.identity.publicKey.hashcode(),
4212
+ replicator: true,
4213
+ },
4214
+ ], {
4215
+ onLeader: (key) => {
4216
+ isLeader =
4217
+ isLeader ||
4218
+ key === this.node.identity.publicKey.hashcode();
4219
+ },
4220
+ });
4221
+ }
4136
4222
  }
4137
4223
  if (isLeader) {
4138
4224
  hasAndIsLeader.push(hash);
@@ -4195,8 +4281,18 @@ let SharedLog = (() => {
4195
4281
  }
4196
4282
  }
4197
4283
  else if (msg instanceof ResponseIPrune) {
4284
+ const lateResponses = [];
4198
4285
  for (const hash of msg.hashes) {
4199
- this._pendingDeletes.get(hash)?.resolve(context.from.hashcode());
4286
+ const pendingDelete = this._checkedPrune.getPendingDelete(hash);
4287
+ if (pendingDelete) {
4288
+ void pendingDelete.resolve(context.from.hashcode());
4289
+ }
4290
+ else {
4291
+ lateResponses.push(hash);
4292
+ }
4293
+ }
4294
+ if (lateResponses.length > 0) {
4295
+ void this.recoverCheckedPruneFromLateResponses(lateResponses, context.from.hashcode()).catch((error) => logger.error(error.toString()));
4200
4296
  }
4201
4297
  }
4202
4298
  else if (msg instanceof ConfirmEntriesMessage) {
@@ -4539,7 +4635,7 @@ let SharedLog = (() => {
4539
4635
  }
4540
4636
  await this.replicate(entriesToReplicate, {
4541
4637
  rebalance: assumeSynced ? false : true,
4542
- checkDuplicates: true,
4638
+ checkDuplicates: assumeSynced ? false : true,
4543
4639
  mergeSegments: typeof options.replicate !== "boolean" && options.replicate
4544
4640
  ? options.replicate.mergeSegments
4545
4641
  : false,
@@ -4566,7 +4662,7 @@ let SharedLog = (() => {
4566
4662
  }
4567
4663
  if (messageToSend) {
4568
4664
  await this.rpc.send(messageToSend, {
4569
- priority: 1,
4665
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
4570
4666
  });
4571
4667
  }
4572
4668
  }
@@ -5125,8 +5221,8 @@ let SharedLog = (() => {
5125
5221
  };
5126
5222
  this._replicationInfoRequestByPeer.set(peerHash, state);
5127
5223
  const intervalMs = Math.max(50, this.waitForReplicatorRequestIntervalMs);
5128
- const maxAttempts = Math.min(5, this.waitForReplicatorRequestMaxAttempts ??
5129
- WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS);
5224
+ const maxAttempts = this.waitForReplicatorRequestMaxAttempts ??
5225
+ Math.max(WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS, Math.ceil(this.waitForReplicatorTimeout / intervalMs));
5130
5226
  const tick = () => {
5131
5227
  if (this.closed || this._closeController.signal.aborted) {
5132
5228
  this.cancelReplicationInfoRequests(peerHash);
@@ -5223,25 +5319,14 @@ let SharedLog = (() => {
5223
5319
  return new AbsoluteReplicas(maxValue);
5224
5320
  }
5225
5321
  removePruneRequestSent(hash, to) {
5226
- if (!to) {
5227
- this._requestIPruneSent.delete(hash);
5228
- }
5229
- else {
5230
- let set = this._requestIPruneSent.get(hash);
5231
- if (set) {
5232
- set.delete(to);
5233
- if (set.size === 0) {
5234
- this._requestIPruneSent.delete(hash);
5235
- }
5236
- }
5237
- }
5322
+ this._checkedPrune.removeRequestSent(hash, to);
5238
5323
  }
5239
5324
  prune(entries, options) {
5240
5325
  if (options?.unchecked) {
5241
5326
  return [...entries.values()].map((x) => {
5242
5327
  this._gidPeersHistory.delete(x.entry.meta.gid);
5243
5328
  this.removePruneRequestSent(x.entry.hash);
5244
- this._requestIPruneResponseReplicatorSet.delete(x.entry.hash);
5329
+ this._checkedPrune.clearConfirmedReplicators(x.entry.hash);
5245
5330
  return this.log.remove(x.entry, {
5246
5331
  recursively: true,
5247
5332
  });
@@ -5269,7 +5354,7 @@ let SharedLog = (() => {
5269
5354
  }
5270
5355
  set.push(entry.hash);
5271
5356
  }
5272
- const pendingPrev = this._pendingDeletes.get(entry.hash);
5357
+ const pendingPrev = this._checkedPrune.getPendingDelete(entry.hash);
5273
5358
  if (pendingPrev) {
5274
5359
  // If a background prune is already in-flight, an explicit prune request should
5275
5360
  // still respect the caller's timeout. Otherwise, tests (and user calls) can
@@ -5298,45 +5383,62 @@ let SharedLog = (() => {
5298
5383
  const minReplicas = decodeReplicas(entry);
5299
5384
  const deferredPromise = pDefer();
5300
5385
  const clear = () => {
5301
- const pending = this._pendingDeletes.get(entry.hash);
5386
+ const pending = this._checkedPrune.getPendingDelete(entry.hash);
5302
5387
  if (pending?.promise === deferredPromise) {
5303
- this._pendingDeletes.delete(entry.hash);
5388
+ this._checkedPrune.deletePendingDelete(entry.hash, pending);
5304
5389
  }
5305
5390
  clearTimeout(timeout);
5306
5391
  };
5307
5392
  const resolve = () => {
5308
- clear();
5393
+ clearTimeout(timeout);
5309
5394
  this.clearCheckedPruneRetry(entry.hash);
5310
5395
  cleanupTimer.push(setTimeout(async () => {
5311
5396
  this._gidPeersHistory.delete(entry.meta.gid);
5312
5397
  this.removePruneRequestSent(entry.hash);
5313
- this._requestIPruneResponseReplicatorSet.delete(entry.hash);
5314
- if (await this.isLeader({
5398
+ this._checkedPrune.clearConfirmedReplicators(entry.hash);
5399
+ const ownership = await this.revalidateCheckedPruneOwnership({
5400
+ hash: entry.hash,
5315
5401
  entry,
5316
- replicas: minReplicas.getValue(this),
5317
- })) {
5402
+ leaders: this.checkedPruneLeadersToMap(leaders),
5403
+ selfReplicating: true,
5404
+ });
5405
+ if (ownership.localLeader) {
5406
+ clear();
5407
+ if (!explicitTimeout) {
5408
+ this.scheduleCheckedPruneRetry({ entry, leaders });
5409
+ }
5318
5410
  deferredPromise.reject(new Error("Failed to delete, is leader again"));
5319
5411
  return;
5320
5412
  }
5413
+ this._checkedPrune.markRemoving(entry.hash);
5321
5414
  return this.log
5322
5415
  .remove(entry, {
5323
5416
  recursively: true,
5324
5417
  })
5325
5418
  .then(() => {
5419
+ clear();
5420
+ this._checkedPrune.markDone(entry.hash);
5326
5421
  deferredPromise.resolve();
5327
5422
  })
5328
5423
  .catch((e) => {
5424
+ clear();
5425
+ this._checkedPrune.markCancelled(entry.hash, {
5426
+ preserveRetry: false,
5427
+ });
5329
5428
  deferredPromise.reject(e);
5330
5429
  })
5331
5430
  .finally(async () => {
5332
5431
  this._gidPeersHistory.delete(entry.meta.gid);
5333
5432
  this.removePruneRequestSent(entry.hash);
5334
- this._requestIPruneResponseReplicatorSet.delete(entry.hash);
5433
+ this._checkedPrune.clearConfirmedReplicators(entry.hash);
5335
5434
  // TODO in the case we become leader again here we need to re-add the entry
5336
- if (await this.isLeader({
5435
+ const ownership = await this.revalidateCheckedPruneOwnership({
5436
+ hash: entry.hash,
5337
5437
  entry,
5338
- replicas: minReplicas.getValue(this),
5339
- })) {
5438
+ leaders: this.checkedPruneLeadersToMap(leaders),
5439
+ selfReplicating: true,
5440
+ });
5441
+ if (ownership.localLeader) {
5340
5442
  logger.error("Unexpected: Is leader after delete");
5341
5443
  }
5342
5444
  });
@@ -5347,11 +5449,9 @@ let SharedLog = (() => {
5347
5449
  const isCheckedPruneTimeout = e instanceof Error &&
5348
5450
  typeof e.message === "string" &&
5349
5451
  e.message.startsWith("Timeout for checked pruning");
5350
- if (explicitTimeout || !isCheckedPruneTimeout) {
5351
- this.clearCheckedPruneRetry(entry.hash);
5352
- }
5353
- this.removePruneRequestSent(entry.hash);
5354
- this._requestIPruneResponseReplicatorSet.delete(entry.hash);
5452
+ this._checkedPrune.markCancelled(entry.hash, {
5453
+ preserveRetry: !explicitTimeout && isCheckedPruneTimeout,
5454
+ });
5355
5455
  deferredPromise.reject(e);
5356
5456
  };
5357
5457
  let cursor = undefined;
@@ -5361,7 +5461,7 @@ let SharedLog = (() => {
5361
5461
  // If we time out too early we can end up with permanently prunable heads that never
5362
5462
  // get retried (a common CI flake in "prune before join" tests).
5363
5463
  const checkedPruneTimeoutMs = options?.timeout ??
5364
- Math.max(10_000, Number(this._respondToIHaveTimeout ?? 0) +
5464
+ Math.max(CHECKED_PRUNE_BACKGROUND_TIMEOUT_MIN_MS, Number(this._respondToIHaveTimeout ?? 0) +
5365
5465
  this.waitForReplicatorTimeout +
5366
5466
  PRUNE_DEBOUNCE_INTERVAL * 2);
5367
5467
  const timeout = setTimeout(() => {
@@ -5374,7 +5474,7 @@ let SharedLog = (() => {
5374
5474
  reject(new Error(`Timeout for checked pruning after ${checkedPruneTimeoutMs}ms (closed=${this.closed})`));
5375
5475
  }, checkedPruneTimeoutMs);
5376
5476
  timeout.unref?.();
5377
- this._pendingDeletes.set(entry.hash, {
5477
+ this._checkedPrune.setPendingDelete(entry.hash, {
5378
5478
  promise: deferredPromise,
5379
5479
  clear,
5380
5480
  reject,
@@ -5394,34 +5494,24 @@ let SharedLog = (() => {
5394
5494
  }))) {
5395
5495
  return;
5396
5496
  }
5397
- let existCounter = this._requestIPruneResponseReplicatorSet.get(entry.hash);
5398
- if (!existCounter) {
5399
- existCounter = new Set();
5400
- this._requestIPruneResponseReplicatorSet.set(entry.hash, existCounter);
5401
- }
5402
- existCounter.add(publicKeyHash);
5497
+ const existCounter = this._checkedPrune.addConfirmedReplicator(entry.hash, publicKeyHash);
5403
5498
  // Seed provider hints so future remote reads can avoid extra round-trips.
5404
5499
  this.remoteBlocks.hintProviders(entry.hash, [publicKeyHash]);
5405
5500
  if (minReplicasValue <= existCounter.size) {
5406
5501
  resolve();
5407
5502
  }
5408
5503
  },
5409
- });
5504
+ }, entry, leaders);
5410
5505
  promises.push(deferredPromise.promise);
5411
5506
  }
5412
5507
  const emitMessages = async (entries, to) => {
5413
5508
  const filteredSet = [];
5414
5509
  for (const entry of entries) {
5415
- let set = this._requestIPruneSent.get(entry);
5416
- if (!set) {
5417
- set = new Set();
5418
- this._requestIPruneSent.set(entry, set);
5419
- }
5420
5510
  /* TODO why can we not have this statement?
5421
5511
  if (set.has(to)) {
5422
5512
  continue;
5423
5513
  } */
5424
- set.add(to);
5514
+ this._checkedPrune.addRequestSent(entry, to);
5425
5515
  filteredSet.push(entry);
5426
5516
  }
5427
5517
  if (filteredSet.length > 0) {
@@ -5432,7 +5522,7 @@ let SharedLog = (() => {
5432
5522
  to: [to], // TODO group by peers?
5433
5523
  redundancy: 1,
5434
5524
  }),
5435
- priority: 1,
5525
+ priority: CONVERGENCE_MESSAGE_PRIORITY,
5436
5526
  });
5437
5527
  }
5438
5528
  };
@@ -5453,7 +5543,7 @@ let SharedLog = (() => {
5453
5543
  return;
5454
5544
  const pendingByPeer = [];
5455
5545
  for (const [peer, hashes] of peerToEntries) {
5456
- const pending = hashes.filter((h) => this._pendingDeletes.has(h));
5546
+ const pending = hashes.filter((h) => this._checkedPrune.hasPendingDelete(h));
5457
5547
  if (pending.length > 0) {
5458
5548
  pendingByPeer.push([peer, pending]);
5459
5549
  }
@@ -5779,12 +5869,21 @@ let SharedLog = (() => {
5779
5869
  for (const target of [...uncheckedDeliver.keys()]) {
5780
5870
  flushUncheckedDeliverTarget(target);
5781
5871
  }
5782
- if (this._isAdaptiveReplicating && hasSelfRangeRemoval) {
5783
- // Adaptive shrink/replacement can make already-indexed local heads
5784
- // prunable even when the incremental rebalance scan missed them under
5785
- // churn or timing pressure. Re-scan after repair dispatches are flushed
5786
- // so checked prune work is enqueued before callers wait for idle.
5787
- await this.pruneIndexedEntriesNoLongerLed();
5872
+ if (this._isAdaptiveReplicating &&
5873
+ (hasSelfRangeRemoval ||
5874
+ changes.some((change) => change.type === "added" ||
5875
+ change.type === "removed" ||
5876
+ change.type === "replaced"))) {
5877
+ // Adaptive range changes can make already-indexed local heads prunable
5878
+ // even when the incremental rebalance scan misses them under churn or
5879
+ // timing pressure. Re-scan after repair dispatches are flushed using the
5880
+ // mature-role view, which matches the bounded pruning contract.
5881
+ await this.pruneIndexedEntriesNoLongerLed({
5882
+ useDefaultRoleAge: true,
5883
+ });
5884
+ await this.pruneCurrentHeadsNoLongerLed({
5885
+ useDefaultRoleAge: true,
5886
+ });
5788
5887
  }
5789
5888
  return changed;
5790
5889
  }