@peerbit/shared-log 13.0.23 → 13.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -495,6 +495,7 @@ const RECALCULATE_PARTICIPATION_MIN_RELATIVE_CHANGE = 0.01;
495
495
  const RECALCULATE_PARTICIPATION_MIN_RELATIVE_CHANGE_WITH_CPU_LIMIT = 0.005;
496
496
  const RECALCULATE_PARTICIPATION_MIN_RELATIVE_CHANGE_WITH_MEMORY_LIMIT = 0.001;
497
497
  const RECALCULATE_PARTICIPATION_RELATIVE_DENOMINATOR_FLOOR = 1e-3;
498
+ const TOPIC_SUBSCRIBERS_CACHE_TTL_MS = 250;
498
499
  const ADAPTIVE_REBALANCE_IDLE_INTERVAL_MULTIPLIER = 5;
499
500
  const ADAPTIVE_REBALANCE_MIN_IDLE_AFTER_LOCAL_APPEND_MS = 10_000;
500
501
 
@@ -752,6 +753,10 @@ export class SharedLog<
752
753
  private _repairSweepRunning!: boolean;
753
754
  private _repairSweepForceFreshPending!: boolean;
754
755
  private _repairSweepAddedPeersPending!: Set<string>;
756
+ private _topicSubscribersCache!: Map<
757
+ string,
758
+ { expiresAt: number; keys: PublicSignKey[] }
759
+ >;
755
760
 
756
761
  // regular distribution checks
757
762
  private distributeQueue?: PQueue;
@@ -1364,14 +1369,26 @@ export class SharedLog<
1364
1369
  private async _getTopicSubscribers(
1365
1370
  topic: string,
1366
1371
  ): Promise<PublicSignKey[] | undefined> {
1372
+ const cached = this._topicSubscribersCache.get(topic);
1373
+ if (cached && cached.expiresAt > Date.now()) {
1374
+ return cached.keys.slice();
1375
+ }
1376
+
1367
1377
  const maxPeers = 64;
1378
+ const cache = (keys: PublicSignKey[]) => {
1379
+ this._topicSubscribersCache.set(topic, {
1380
+ expiresAt: Date.now() + TOPIC_SUBSCRIBERS_CACHE_TTL_MS,
1381
+ keys,
1382
+ });
1383
+ return keys.slice();
1384
+ };
1368
1385
 
1369
1386
  // Prefer the bounded peer set we already know from the fanout overlay.
1370
1387
  if (this._fanoutChannel && (topic === this.topic || topic === this.rpc.topic)) {
1371
1388
  const hashes = this._fanoutChannel
1372
1389
  .getPeerHashes({ includeSelf: false })
1373
1390
  .slice(0, maxPeers);
1374
- if (hashes.length === 0) return [];
1391
+ if (hashes.length === 0) return cache([]);
1375
1392
 
1376
1393
  const keys = await Promise.all(
1377
1394
  hashes.map((hash) => this._resolvePublicKeyFromHash(hash)),
@@ -1387,7 +1404,7 @@ export class SharedLog<
1387
1404
  seen.add(hash);
1388
1405
  uniqueKeys.push(key);
1389
1406
  }
1390
- return uniqueKeys;
1407
+ return cache(uniqueKeys);
1391
1408
  }
1392
1409
 
1393
1410
  const selfHash = this.node.identity.publicKey.hashcode();
@@ -1444,7 +1461,7 @@ export class SharedLog<
1444
1461
  }
1445
1462
  }
1446
1463
 
1447
- if (hashes.length === 0) return [];
1464
+ if (hashes.length === 0) return cache([]);
1448
1465
 
1449
1466
  const uniqueHashes: string[] = [];
1450
1467
  const seen = new Set<string>();
@@ -1465,7 +1482,18 @@ export class SharedLog<
1465
1482
  if (hash === selfHash) continue;
1466
1483
  uniqueKeys.push(key);
1467
1484
  }
1468
- return uniqueKeys;
1485
+ return cache(uniqueKeys);
1486
+ }
1487
+
1488
+ private invalidateTopicSubscribersCache(...topics: (string | undefined)[]) {
1489
+ for (const topic of topics) {
1490
+ if (!topic) continue;
1491
+ this._topicSubscribersCache.delete(topic);
1492
+ }
1493
+ }
1494
+
1495
+ private invalidateSharedLogTopicSubscribersCache() {
1496
+ this.invalidateTopicSubscribersCache(this.topic, this.rpc.topic);
1469
1497
  }
1470
1498
 
1471
1499
  // @deprecated
@@ -2639,15 +2667,16 @@ export class SharedLog<
2639
2667
 
2640
2668
  const iterator = this.entryCoordinatesIndex.iterate({});
2641
2669
  try {
2642
- while (!this.closed && !iterator.done()) {
2643
- const entries = await iterator.next(REPAIR_SWEEP_ENTRY_BATCH_SIZE);
2644
- for (const entry of entries) {
2645
- const entryReplicated = entry.value;
2646
- const currentPeers = await this.findLeaders(
2647
- entryReplicated.coordinates,
2648
- entryReplicated,
2649
- { roleAge: 0 },
2650
- );
2670
+ while (!this.closed && !iterator.done()) {
2671
+ const entries = await iterator.next(REPAIR_SWEEP_ENTRY_BATCH_SIZE);
2672
+ for (const entry of entries) {
2673
+ const entryReplicated = entry.value;
2674
+ const knownPeers = this._gidPeersHistory.get(entryReplicated.gid);
2675
+ const currentPeers = await this.findLeaders(
2676
+ entryReplicated.coordinates,
2677
+ entryReplicated,
2678
+ { roleAge: 0 },
2679
+ );
2651
2680
  if (forceFreshDelivery) {
2652
2681
  for (const [currentPeer] of currentPeers) {
2653
2682
  if (currentPeer === this.node.identity.publicKey.hashcode()) {
@@ -2656,14 +2685,14 @@ export class SharedLog<
2656
2685
  queueEntryForTarget(currentPeer, entryReplicated);
2657
2686
  }
2658
2687
  }
2659
- if (addedPeers.size > 0) {
2660
- for (const peer of addedPeers) {
2661
- if (currentPeers.has(peer)) {
2662
- queueEntryForTarget(peer, entryReplicated);
2688
+ if (addedPeers.size > 0) {
2689
+ for (const peer of addedPeers) {
2690
+ if (currentPeers.has(peer) && !knownPeers?.has(peer)) {
2691
+ queueEntryForTarget(peer, entryReplicated);
2692
+ }
2663
2693
  }
2664
2694
  }
2665
2695
  }
2666
- }
2667
2696
  }
2668
2697
  } finally {
2669
2698
  await iterator.close();
@@ -2934,6 +2963,7 @@ export class SharedLog<
2934
2963
  this._repairSweepRunning = false;
2935
2964
  this._repairSweepForceFreshPending = false;
2936
2965
  this._repairSweepAddedPeersPending = new Set();
2966
+ this._topicSubscribersCache = new Map();
2937
2967
  this.coordinateToHash = new Cache<string>({ max: 1e6, ttl: 1e4 });
2938
2968
  this.recentlyRebalanced = new Cache<string>({ max: 1e4, ttl: 1e5 });
2939
2969
 
@@ -3049,6 +3079,18 @@ export class SharedLog<
3049
3079
  })) ?? []
3050
3080
  );
3051
3081
  },
3082
+ watchProviders: fanoutService
3083
+ ? (cid, opts) =>
3084
+ fanoutService.watchProviders(blockProviderNamespace(cid), {
3085
+ signal: opts.signal,
3086
+ want: 8,
3087
+ ttlMs: 10_000,
3088
+ renewIntervalMs: 5_000,
3089
+ bootstrapMaxPeers: 2,
3090
+ onProviders: (providers) =>
3091
+ opts.onProviders(providers.map((provider) => provider.hash)),
3092
+ })
3093
+ : undefined,
3052
3094
  onPut: async (cid) => {
3053
3095
  // Best-effort directory announce for "get without remote.from" workflows.
3054
3096
  try {
@@ -3958,39 +4000,40 @@ export class SharedLog<
3958
4000
  this.coordinateToHash.clear();
3959
4001
  this.recentlyRebalanced.clear();
3960
4002
  this.uniqueReplicators.clear();
3961
- this._closeController.abort();
4003
+ this._topicSubscribersCache.clear();
4004
+ this._closeController.abort();
3962
4005
 
3963
- clearInterval(this.interval);
3964
- this.stopReplicatorLivenessSweep();
4006
+ clearInterval(this.interval);
4007
+ this.stopReplicatorLivenessSweep();
3965
4008
 
3966
- this.node.services.pubsub.removeEventListener(
3967
- "subscribe",
3968
- this._onSubscriptionFn,
4009
+ this.node.services.pubsub.removeEventListener(
4010
+ "subscribe",
4011
+ this._onSubscriptionFn,
3969
4012
  );
3970
4013
 
3971
4014
  this.node.services.pubsub.removeEventListener(
3972
4015
  "unsubscribe",
3973
4016
  this._onUnsubscriptionFn,
3974
4017
  );
3975
- for (const timer of this._repairRetryTimers) {
3976
- clearTimeout(timer);
3977
- }
3978
- this._repairRetryTimers.clear();
3979
- this._recentRepairDispatch.clear();
3980
- this._repairSweepRunning = false;
3981
- this._repairSweepForceFreshPending = false;
3982
- this._repairSweepAddedPeersPending.clear();
4018
+ for (const timer of this._repairRetryTimers) {
4019
+ clearTimeout(timer);
4020
+ }
4021
+ this._repairRetryTimers.clear();
4022
+ this._recentRepairDispatch.clear();
4023
+ this._repairSweepRunning = false;
4024
+ this._repairSweepForceFreshPending = false;
4025
+ this._repairSweepAddedPeersPending.clear();
3983
4026
 
3984
4027
  for (const [_k, v] of this._pendingDeletes) {
3985
4028
  v.clear();
3986
4029
  v.promise.resolve(); // TODO or reject?
3987
4030
  }
3988
- for (const [_k, v] of this._pendingIHave) {
3989
- v.clear();
3990
- }
3991
- for (const [_k, v] of this._checkedPruneRetries) {
3992
- if (v.timer) clearTimeout(v.timer);
3993
- }
4031
+ for (const [_k, v] of this._pendingIHave) {
4032
+ v.clear();
4033
+ }
4034
+ for (const [_k, v] of this._checkedPruneRetries) {
4035
+ if (v.timer) clearTimeout(v.timer);
4036
+ }
3994
4037
 
3995
4038
  await this.remoteBlocks.stop();
3996
4039
  this._pendingDeletes.clear();
@@ -4846,6 +4889,23 @@ export class SharedLog<
4846
4889
  options?.replicate &&
4847
4890
  typeof options.replicate !== "boolean" &&
4848
4891
  options.replicate.assumeSynced;
4892
+ const seedAssumeSyncedPeerHistory = async (entry: Entry<T>) => {
4893
+ if (!assumeSynced) {
4894
+ return;
4895
+ }
4896
+
4897
+ const minReplicas = decodeReplicas(entry).getValue(this);
4898
+ const leaders = await this.findLeaders(
4899
+ await this.createCoordinates(entry, minReplicas),
4900
+ entry,
4901
+ {
4902
+ roleAge: 0,
4903
+ persist: false,
4904
+ },
4905
+ );
4906
+
4907
+ this.addPeersToGidPeerHistory(entry.meta.gid, leaders.keys());
4908
+ };
4849
4909
  const persistCoordinate = async (entry: Entry<T>) => {
4850
4910
  const minReplicas = decodeReplicas(entry).getValue(this);
4851
4911
  const leaders = await this.findLeaders(
@@ -4887,6 +4947,12 @@ export class SharedLog<
4887
4947
  if (options?.replicate) {
4888
4948
  let messageToSend: AddedReplicationSegmentMessage | undefined = undefined;
4889
4949
 
4950
+ if (assumeSynced) {
4951
+ for (const entry of entriesToReplicate) {
4952
+ await seedAssumeSyncedPeerHistory(entry);
4953
+ }
4954
+ }
4955
+
4890
4956
  await this.replicate(entriesToReplicate, {
4891
4957
  rebalance: assumeSynced ? false : true,
4892
4958
  checkDuplicates: true,
@@ -5390,6 +5456,7 @@ export class SharedLog<
5390
5456
  entry: Entry<T> | EntryReplicated<R> | ShallowEntry,
5391
5457
  options?: {
5392
5458
  roleAge?: number;
5459
+ candidates?: Iterable<string>;
5393
5460
  onLeader?: (key: string) => void;
5394
5461
  // persist even if not leader
5395
5462
  persist?:
@@ -5433,6 +5500,7 @@ export class SharedLog<
5433
5500
  },
5434
5501
  options?: {
5435
5502
  roleAge?: number;
5503
+ candidates?: Iterable<string>;
5436
5504
  onLeader?: (key: string) => void;
5437
5505
  // persist even if not leader
5438
5506
  persist?:
@@ -5458,6 +5526,7 @@ export class SharedLog<
5458
5526
  cursors: NumberFromType<R>[],
5459
5527
  options?: {
5460
5528
  roleAge?: number;
5529
+ candidates?: Iterable<string>;
5461
5530
  },
5462
5531
  ): Promise<Map<string, { intersecting: boolean }>> {
5463
5532
  const roleAge = options?.roleAge ?? (await this.getDefaultMinRoleAge()); // TODO -500 as is added so that i f someone else is just as new as us, then we treat them as mature as us. without -500 we might be slower syncing if two nodes starts almost at the same time
@@ -5467,44 +5536,48 @@ export class SharedLog<
5467
5536
  // If it is still warming up (for example, only contains self), supplement with
5468
5537
  // current subscribers until we have enough candidates for this decision.
5469
5538
  let peerFilter: Set<string> | undefined = undefined;
5470
- const selfReplicating = await this.isReplicating();
5471
- if (this.uniqueReplicators.size > 0) {
5472
- peerFilter = new Set(this.uniqueReplicators);
5473
- if (selfReplicating) {
5474
- peerFilter.add(selfHash);
5475
- } else {
5476
- peerFilter.delete(selfHash);
5477
- }
5539
+ if (options?.candidates) {
5540
+ peerFilter = new Set(options.candidates);
5541
+ } else {
5542
+ const selfReplicating = await this.isReplicating();
5543
+ if (this.uniqueReplicators.size > 0) {
5544
+ peerFilter = new Set(this.uniqueReplicators);
5545
+ if (selfReplicating) {
5546
+ peerFilter.add(selfHash);
5547
+ } else {
5548
+ peerFilter.delete(selfHash);
5549
+ }
5478
5550
 
5479
- try {
5480
- const subscribers = await this._getTopicSubscribers(this.topic);
5481
- if (subscribers && subscribers.length > 0) {
5482
- for (const subscriber of subscribers) {
5483
- peerFilter.add(subscriber.hashcode());
5484
- }
5485
- if (selfReplicating) {
5486
- peerFilter.add(selfHash);
5487
- } else {
5488
- peerFilter.delete(selfHash);
5551
+ try {
5552
+ const subscribers = await this._getTopicSubscribers(this.topic);
5553
+ if (subscribers && subscribers.length > 0) {
5554
+ for (const subscriber of subscribers) {
5555
+ peerFilter.add(subscriber.hashcode());
5556
+ }
5557
+ if (selfReplicating) {
5558
+ peerFilter.add(selfHash);
5559
+ } else {
5560
+ peerFilter.delete(selfHash);
5561
+ }
5489
5562
  }
5563
+ } catch {
5564
+ // Best-effort only; keep current peerFilter.
5490
5565
  }
5491
- } catch {
5492
- // Best-effort only; keep current peerFilter.
5493
- }
5494
- } else {
5495
- try {
5496
- const subscribers =
5497
- (await this._getTopicSubscribers(this.topic)) ?? undefined;
5498
- if (subscribers && subscribers.length > 0) {
5499
- peerFilter = new Set(subscribers.map((key) => key.hashcode()));
5500
- if (selfReplicating) {
5501
- peerFilter.add(selfHash);
5502
- } else {
5503
- peerFilter.delete(selfHash);
5566
+ } else {
5567
+ try {
5568
+ const subscribers =
5569
+ (await this._getTopicSubscribers(this.topic)) ?? undefined;
5570
+ if (subscribers && subscribers.length > 0) {
5571
+ peerFilter = new Set(subscribers.map((key) => key.hashcode()));
5572
+ if (selfReplicating) {
5573
+ peerFilter.add(selfHash);
5574
+ } else {
5575
+ peerFilter.delete(selfHash);
5576
+ }
5504
5577
  }
5578
+ } catch {
5579
+ // Best-effort only; if pubsub isn't ready, do a full scan.
5505
5580
  }
5506
- } catch {
5507
- // Best-effort only; if pubsub isn't ready, do a full scan.
5508
5581
  }
5509
5582
  }
5510
5583
  return getSamples<R>(
@@ -6156,30 +6229,48 @@ export class SharedLog<
6156
6229
  }
6157
6230
  }
6158
6231
 
6159
- const changed = false;
6160
- const replacedPeers = new Set<string>();
6161
- for (const change of changes) {
6162
- if (change.type === "replaced" && change.range.hash !== selfHash) {
6163
- replacedPeers.add(change.range.hash);
6164
- }
6165
- }
6166
- const addedPeers = new Set<string>();
6167
- for (const change of changes) {
6168
- if (change.type === "added" || change.type === "replaced") {
6169
- const hash = change.range.hash;
6170
- if (hash !== selfHash) {
6171
- // Range updates can reassign entries to an existing peer shortly after it
6172
- // already received a subset. Avoid suppressing legitimate follow-up repair.
6173
- this._recentRepairDispatch.delete(hash);
6232
+ const changed = false;
6233
+ const addedPeers = new Set<string>();
6234
+ const warmupPeers = new Set<string>();
6235
+ const hasSelfWarmupChange = changes.some(
6236
+ (change) =>
6237
+ change.range.hash === selfHash &&
6238
+ (change.type === "added" || change.type === "replaced"),
6239
+ );
6240
+ for (const change of changes) {
6241
+ if (change.type === "added" || change.type === "replaced") {
6242
+ const hash = change.range.hash;
6243
+ if (hash !== selfHash) {
6244
+ // Range updates can reassign entries to an existing peer shortly after it
6245
+ // already received a subset. Avoid suppressing legitimate follow-up repair.
6246
+ this._recentRepairDispatch.delete(hash);
6247
+ }
6174
6248
  }
6175
- }
6176
- if (change.type === "added") {
6177
- const hash = change.range.hash;
6178
- if (hash !== selfHash && !replacedPeers.has(hash)) {
6179
- addedPeers.add(hash);
6249
+ if (change.type === "added") {
6250
+ const hash = change.range.hash;
6251
+ if (hash !== selfHash) {
6252
+ addedPeers.add(hash);
6253
+ warmupPeers.add(hash);
6254
+ }
6180
6255
  }
6181
6256
  }
6182
- }
6257
+ const hasAdaptiveStorageLimit =
6258
+ this._isAdaptiveReplicating &&
6259
+ this.replicationController?.maxMemoryLimit != null;
6260
+ const useJoinWarmupFastPath =
6261
+ !forceFreshDelivery &&
6262
+ warmupPeers.size > 0 &&
6263
+ !hasSelfWarmupChange &&
6264
+ !hasAdaptiveStorageLimit;
6265
+ const immediateRebalanceChanges = useJoinWarmupFastPath
6266
+ ? changes.filter(
6267
+ (change) =>
6268
+ !(
6269
+ change.range.hash === selfHash &&
6270
+ (change.type === "added" || change.type === "replaced")
6271
+ ),
6272
+ )
6273
+ : changes;
6183
6274
 
6184
6275
  try {
6185
6276
  const uncheckedDeliver: Map<
@@ -6191,15 +6282,15 @@ export class SharedLog<
6191
6282
  if (!entries || entries.size === 0) {
6192
6283
  return;
6193
6284
  }
6194
- const isJoinWarmupTarget = addedPeers.has(target);
6195
- const bypassRecentDedupe = isJoinWarmupTarget || forceFreshDelivery;
6196
- this.dispatchMaybeMissingEntries(target, entries, {
6197
- bypassRecentDedupe,
6198
- retryScheduleMs: isJoinWarmupTarget
6199
- ? JOIN_WARMUP_RETRY_SCHEDULE_MS
6200
- : undefined,
6201
- forceFreshDelivery,
6202
- });
6285
+ const isWarmupTarget = warmupPeers.has(target);
6286
+ const bypassRecentDedupe = isWarmupTarget || forceFreshDelivery;
6287
+ this.dispatchMaybeMissingEntries(target, entries, {
6288
+ bypassRecentDedupe,
6289
+ retryScheduleMs: isWarmupTarget
6290
+ ? JOIN_WARMUP_RETRY_SCHEDULE_MS
6291
+ : undefined,
6292
+ forceFreshDelivery,
6293
+ });
6203
6294
  uncheckedDeliver.delete(target);
6204
6295
  };
6205
6296
  const queueUncheckedDeliver = (
@@ -6220,18 +6311,85 @@ export class SharedLog<
6220
6311
  }
6221
6312
  };
6222
6313
 
6223
- for await (const entryReplicated of toRebalance<R>(
6224
- changes,
6225
- this.entryCoordinatesIndex,
6226
- this.recentlyRebalanced,
6227
- { forceFresh: forceFreshDelivery },
6228
- )) {
6314
+ if (immediateRebalanceChanges.length > 0) {
6315
+ for await (const entryReplicated of toRebalance<R>(
6316
+ immediateRebalanceChanges,
6317
+ this.entryCoordinatesIndex,
6318
+ this.recentlyRebalanced,
6319
+ {
6320
+ forceFresh: forceFreshDelivery || useJoinWarmupFastPath,
6321
+ },
6322
+ )) {
6229
6323
  if (this.closed) {
6230
6324
  break;
6231
6325
  }
6232
6326
 
6233
- let oldPeersSet: Set<string> | undefined;
6234
- if (!forceFreshDelivery) {
6327
+ if (useJoinWarmupFastPath) {
6328
+ let oldPeersSet: Set<string> | undefined;
6329
+ const gid = entryReplicated.gid;
6330
+ oldPeersSet = gidPeersHistorySnapshot.get(gid);
6331
+ if (!gidPeersHistorySnapshot.has(gid)) {
6332
+ const existing = this._gidPeersHistory.get(gid);
6333
+ oldPeersSet = existing ? new Set(existing) : undefined;
6334
+ gidPeersHistorySnapshot.set(gid, oldPeersSet);
6335
+ }
6336
+
6337
+ for (const target of warmupPeers) {
6338
+ queueUncheckedDeliver(target, entryReplicated);
6339
+ }
6340
+
6341
+ const candidatePeers = new Set<string>([selfHash]);
6342
+ for (const target of warmupPeers) {
6343
+ candidatePeers.add(target);
6344
+ }
6345
+ if (oldPeersSet) {
6346
+ for (const oldPeer of oldPeersSet) {
6347
+ candidatePeers.add(oldPeer);
6348
+ }
6349
+ }
6350
+
6351
+ const currentPeers = await this.findLeaders(
6352
+ entryReplicated.coordinates,
6353
+ entryReplicated,
6354
+ {
6355
+ roleAge: 0,
6356
+ candidates: candidatePeers,
6357
+ persist: false,
6358
+ },
6359
+ );
6360
+
6361
+ if (oldPeersSet) {
6362
+ for (const oldPeer of oldPeersSet) {
6363
+ if (!currentPeers.has(oldPeer)) {
6364
+ this.removePruneRequestSent(entryReplicated.hash);
6365
+ }
6366
+ }
6367
+ }
6368
+
6369
+ this.addPeersToGidPeerHistory(
6370
+ entryReplicated.gid,
6371
+ currentPeers.keys(),
6372
+ true,
6373
+ );
6374
+
6375
+ if (!currentPeers.has(selfHash)) {
6376
+ this.pruneDebouncedFnAddIfNotKeeping({
6377
+ key: entryReplicated.hash,
6378
+ value: { entry: entryReplicated, leaders: currentPeers },
6379
+ });
6380
+
6381
+ this.responseToPruneDebouncedFn.delete(entryReplicated.hash);
6382
+ } else {
6383
+ this.pruneDebouncedFn.delete(entryReplicated.hash);
6384
+ await this._pendingDeletes
6385
+ .get(entryReplicated.hash)
6386
+ ?.reject(new Error("Failed to delete, is leader again"));
6387
+ this.removePruneRequestSent(entryReplicated.hash);
6388
+ }
6389
+ continue;
6390
+ }
6391
+
6392
+ let oldPeersSet: Set<string> | undefined;
6235
6393
  const gid = entryReplicated.gid;
6236
6394
  oldPeersSet = gidPeersHistorySnapshot.get(gid);
6237
6395
  if (!gidPeersHistorySnapshot.has(gid)) {
@@ -6239,18 +6397,18 @@ export class SharedLog<
6239
6397
  oldPeersSet = existing ? new Set(existing) : undefined;
6240
6398
  gidPeersHistorySnapshot.set(gid, oldPeersSet);
6241
6399
  }
6242
- }
6243
- let isLeader = false;
6244
6400
 
6245
- let currentPeers = await this.findLeaders(
6246
- entryReplicated.coordinates,
6247
- entryReplicated,
6248
- {
6249
- // we do this to make sure new replicators get data even though they are not mature so they can figure out if they want to replicate more or less
6250
- // TODO make this smarter because if a new replicator is not mature and want to replicate too much data the syncing overhead can be bad
6251
- roleAge: 0,
6252
- },
6253
- );
6401
+ let isLeader = false;
6402
+ const currentPeers = await this.findLeaders(
6403
+ entryReplicated.coordinates,
6404
+ entryReplicated,
6405
+ {
6406
+ // We do this to make sure new replicators get data even though
6407
+ // they are not mature so they can figure out if they want to
6408
+ // replicate more or less.
6409
+ roleAge: 0,
6410
+ },
6411
+ );
6254
6412
 
6255
6413
  for (const [currentPeer] of currentPeers) {
6256
6414
  if (currentPeer === this.node.identity.publicKey.hashcode()) {
@@ -6263,41 +6421,63 @@ export class SharedLog<
6263
6421
  }
6264
6422
  }
6265
6423
 
6266
- if (oldPeersSet) {
6267
- for (const oldPeer of oldPeersSet) {
6268
- if (!currentPeers.has(oldPeer)) {
6269
- this.removePruneRequestSent(entryReplicated.hash);
6424
+ if (oldPeersSet) {
6425
+ for (const oldPeer of oldPeersSet) {
6426
+ if (!currentPeers.has(oldPeer)) {
6427
+ this.removePruneRequestSent(entryReplicated.hash);
6428
+ }
6270
6429
  }
6271
6430
  }
6272
- }
6273
6431
 
6274
- this.addPeersToGidPeerHistory(
6275
- entryReplicated.gid,
6276
- currentPeers.keys(),
6277
- true,
6278
- );
6432
+ this.addPeersToGidPeerHistory(
6433
+ entryReplicated.gid,
6434
+ currentPeers.keys(),
6435
+ true,
6436
+ );
6279
6437
 
6280
- if (!isLeader) {
6281
- this.pruneDebouncedFnAddIfNotKeeping({
6282
- key: entryReplicated.hash,
6283
- value: { entry: entryReplicated, leaders: currentPeers },
6284
- });
6438
+ if (!isLeader) {
6439
+ this.pruneDebouncedFnAddIfNotKeeping({
6440
+ key: entryReplicated.hash,
6441
+ value: { entry: entryReplicated, leaders: currentPeers },
6442
+ });
6285
6443
 
6286
- this.responseToPruneDebouncedFn.delete(entryReplicated.hash); // don't allow others to prune because of expecting me to replicating this entry
6287
- } else {
6288
- this.pruneDebouncedFn.delete(entryReplicated.hash);
6289
- await this._pendingDeletes
6290
- .get(entryReplicated.hash)
6291
- ?.reject(new Error("Failed to delete, is leader again"));
6292
- this.removePruneRequestSent(entryReplicated.hash);
6444
+ this.responseToPruneDebouncedFn.delete(entryReplicated.hash); // don't allow others to prune because of expecting me to replicating this entry
6445
+ } else {
6446
+ this.pruneDebouncedFn.delete(entryReplicated.hash);
6447
+ await this._pendingDeletes
6448
+ .get(entryReplicated.hash)
6449
+ ?.reject(new Error("Failed to delete, is leader again"));
6450
+ this.removePruneRequestSent(entryReplicated.hash);
6451
+ }
6452
+ }
6293
6453
  }
6294
- }
6295
6454
 
6296
- if (forceFreshDelivery || addedPeers.size > 0) {
6297
- // Schedule a coalesced background sweep for churn/join windows instead of
6298
- // scanning the whole index synchronously on each replication change.
6299
- this.scheduleRepairSweep({ forceFreshDelivery, addedPeers });
6300
- }
6455
+ if (forceFreshDelivery) {
6456
+ // Removed/shrunk ranges still need the authoritative background pass.
6457
+ this.scheduleRepairSweep({ forceFreshDelivery, addedPeers });
6458
+ } else if (useJoinWarmupFastPath) {
6459
+ // Pure join warmup uses the cheap immediate maybe-missing dispatch above,
6460
+ // then defers the authoritative sweep so it does not compete with the
6461
+ // write burst itself.
6462
+ const peers = new Set(addedPeers);
6463
+ const timer = setTimeout(() => {
6464
+ this._repairRetryTimers.delete(timer);
6465
+ if (this.closed) {
6466
+ return;
6467
+ }
6468
+ this.scheduleRepairSweep({
6469
+ forceFreshDelivery: false,
6470
+ addedPeers: peers,
6471
+ });
6472
+ }, 250);
6473
+ timer.unref?.();
6474
+ this._repairRetryTimers.add(timer);
6475
+ } else if (addedPeers.size > 0) {
6476
+ this.scheduleRepairSweep({
6477
+ forceFreshDelivery: false,
6478
+ addedPeers,
6479
+ });
6480
+ }
6301
6481
 
6302
6482
  for (const target of [...uncheckedDeliver.keys()]) {
6303
6483
  flushUncheckedDeliverTarget(target);
@@ -6336,6 +6516,7 @@ export class SharedLog<
6336
6516
  if (!prev || prev < now) {
6337
6517
  this.latestReplicationInfoMessage.set(fromHash, now);
6338
6518
  }
6519
+ this.invalidateSharedLogTopicSubscribersCache();
6339
6520
 
6340
6521
  return this.handleSubscriptionChange(
6341
6522
  evt.detail.from,
@@ -6356,6 +6537,7 @@ export class SharedLog<
6356
6537
 
6357
6538
  this.remoteBlocks.onReachable(evt.detail.from);
6358
6539
  this._replicationInfoBlockedPeers.delete(evt.detail.from.hashcode());
6540
+ this.invalidateSharedLogTopicSubscribersCache();
6359
6541
 
6360
6542
  await this.handleSubscriptionChange(
6361
6543
  evt.detail.from,