@peerbit/shared-log 13.2.12 → 13.2.13

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.
@@ -240,15 +240,25 @@ export { RequestMaybeSyncCoordinateCapabilities };
240
240
  const canReceiveRawExchangeHeads = (message) => (message instanceof ResponseMaybeSyncCapabilities ||
241
241
  message instanceof RequestMaybeSyncCoordinateCapabilities) &&
242
242
  (message.capabilities & SIMPLE_SYNC_RAW_EXCHANGE_HEADS_CAPABILITY) !== 0;
243
- const getHashesFromSymbols = async (symbols, entryIndex, coordinateToHash, resolveHashesForSymbols, resolveHashListForSymbols) => {
243
+ const getHashesFromSymbols = async (symbols, entryIndex, coordinateToHash, resolveHashesForSymbols, resolveHashListForSymbols, maxHashes = 10_000) => {
244
244
  let queries = [];
245
245
  let batchSize = 128; // TODO arg
246
246
  let results = new Set();
247
247
  let missingSymbols = [];
248
+ const addHash = (hash) => {
249
+ if (results.has(hash)) {
250
+ return true;
251
+ }
252
+ if (results.size >= maxHashes) {
253
+ return false;
254
+ }
255
+ results.add(hash);
256
+ return true;
257
+ };
248
258
  const addMissingUnlessCached = (symbol) => {
249
259
  const fromCache = coordinateToHash.get(symbol);
250
260
  if (fromCache) {
251
- results.add(fromCache);
261
+ addHash(fromCache);
252
262
  return;
253
263
  }
254
264
  missingSymbols.push(symbol);
@@ -260,23 +270,45 @@ const getHashesFromSymbols = async (symbols, entryIndex, coordinateToHash, resol
260
270
  .all();
261
271
  queries = [];
262
272
  for (const entry of entries) {
263
- results.add(entry.value.hash);
273
+ if (!addHash(entry.value.hash)) {
274
+ break;
275
+ }
264
276
  coordinateToHash.add(entry.value.hashNumber, entry.value.hash);
277
+ if (results.size >= maxHashes) {
278
+ break;
279
+ }
265
280
  }
266
281
  }
267
282
  };
268
283
  if (resolveHashListForSymbols) {
269
284
  const resolvedHashes = await resolveHashListForSymbols(symbols);
270
285
  if (resolvedHashes) {
271
- const resolvedHashList = Array.isArray(resolvedHashes)
272
- ? resolvedHashes
273
- : [...resolvedHashes];
286
+ const resolvedHashList = [];
287
+ const iterator = resolvedHashes[Symbol.iterator]();
288
+ let exhausted = false;
289
+ try {
290
+ while (resolvedHashList.length < maxHashes) {
291
+ const next = iterator.next();
292
+ if (next.done) {
293
+ exhausted = true;
294
+ break;
295
+ }
296
+ resolvedHashList.push(next.value);
297
+ }
298
+ }
299
+ finally {
300
+ if (!exhausted) {
301
+ iterator.return?.();
302
+ }
303
+ }
274
304
  let mergedHashes;
275
305
  for (const symbol of symbols) {
276
306
  const fromCache = coordinateToHash.get(symbol);
277
307
  if (fromCache) {
278
308
  mergedHashes ??= new Set(resolvedHashList);
279
- mergedHashes.add(fromCache);
309
+ if (mergedHashes.size < maxHashes) {
310
+ mergedHashes.add(fromCache);
311
+ }
280
312
  }
281
313
  }
282
314
  return mergedHashes ?? resolvedHashList;
@@ -285,7 +317,11 @@ const getHashesFromSymbols = async (symbols, entryIndex, coordinateToHash, resol
285
317
  if (resolveHashesForSymbols) {
286
318
  const resolved = await resolveHashesForSymbols(symbols);
287
319
  if (resolved) {
320
+ let resolvedItemCount = 0;
288
321
  for (const symbol of symbols) {
322
+ if (resolvedItemCount >= maxHashes) {
323
+ break;
324
+ }
289
325
  const hashes = resolved.get(symbol);
290
326
  if (!hashes) {
291
327
  addMissingUnlessCached(symbol);
@@ -293,15 +329,39 @@ const getHashesFromSymbols = async (symbols, entryIndex, coordinateToHash, resol
293
329
  }
294
330
  let singleHash;
295
331
  let count = 0;
296
- for (const hash of hashes) {
297
- results.add(hash);
298
- singleHash = hash;
299
- count += 1;
332
+ let truncated = false;
333
+ const iterator = hashes[Symbol.iterator]();
334
+ let exhausted = false;
335
+ try {
336
+ while (resolvedItemCount < maxHashes) {
337
+ const next = iterator.next();
338
+ if (next.done) {
339
+ exhausted = true;
340
+ break;
341
+ }
342
+ resolvedItemCount += 1;
343
+ if (!addHash(next.value)) {
344
+ truncated = true;
345
+ break;
346
+ }
347
+ singleHash = next.value;
348
+ count += 1;
349
+ if (results.size >= maxHashes) {
350
+ truncated = true;
351
+ break;
352
+ }
353
+ }
354
+ }
355
+ finally {
356
+ if (!exhausted) {
357
+ truncated = true;
358
+ iterator.return?.();
359
+ }
300
360
  }
301
361
  if (count === 0) {
302
362
  addMissingUnlessCached(symbol);
303
363
  }
304
- else if (count === 1) {
364
+ else if (count === 1 && !truncated) {
305
365
  coordinateToHash.add(symbol, singleHash);
306
366
  }
307
367
  }
@@ -318,6 +378,9 @@ const getHashesFromSymbols = async (symbols, entryIndex, coordinateToHash, resol
318
378
  }
319
379
  }
320
380
  for (const symbol of missingSymbols) {
381
+ if (results.size >= maxHashes) {
382
+ break;
383
+ }
321
384
  const matchQuery = new IntegerCompare({
322
385
  key: "hashNumber",
323
386
  compare: Compare.Equal,
@@ -337,6 +400,10 @@ const SESSION_POLL_INTERVAL_MS = 100;
337
400
  const DEFAULT_MAX_HASHES_PER_MESSAGE = 1_024;
338
401
  const DEFAULT_MAX_COORDINATES_PER_MESSAGE = 1_024;
339
402
  const DEFAULT_MAX_CONVERGENT_TRACKED_HASHES = 4_096;
403
+ export const MAX_SIMPLE_COORDINATE_REQUEST_SYMBOLS = 1_024;
404
+ export const MAX_SIMPLE_COORDINATE_RESPONSE_HASHES = 10_000;
405
+ export const MAX_PENDING_SIMPLE_COORDINATE_RESPONSES_PER_PEER = 4;
406
+ export const MAX_PENDING_SIMPLE_COORDINATE_RESPONSES_GLOBAL = 32;
340
407
  // Keep convergence sync above the default/background lane. Dropping it to the
341
408
  // background priority lets repair traffic starve behind foreground work.
342
409
  export const SYNC_MESSAGE_PRIORITY = CONVERGENCE_MESSAGE_PRIORITY;
@@ -346,6 +413,38 @@ export const SYNC_MESSAGE_PRIORITY = CONVERGENCE_MESSAGE_PRIORITY;
346
413
  const SIMPLE_SYNC_RETRY_AFTER_MS = 10_000;
347
414
  const EXCHANGE_HEAD_RESPONSE_DEDUPE_TTL_MS = SIMPLE_SYNC_RETRY_AFTER_MS - 1_000;
348
415
  const RECENT_KNOWN_EXCHANGE_HEAD_SUPPRESSION_MS = 30_000;
416
+ const PENDING_MAYBE_SYNC_RESPONSE_TTL_MS = 30_000;
417
+ // An incoming maybe-sync claim keeps one retry candidate in both
418
+ // syncInFlightQueue and syncInFlightQueueInverted. Bound associations rather
419
+ // than only unique keys: otherwise many peers can grow the claimant array for
420
+ // the same keys without changing syncInFlightQueue.size.
421
+ // The per-peer allowance matches one full 10,000-hash response-authorization
422
+ // window; the global allowance lets four peers make full bounded progress.
423
+ export const MAX_PENDING_SIMPLE_SYNC_KEYS_PER_PEER = 10_000;
424
+ export const MAX_PENDING_SIMPLE_SYNC_KEYS_GLOBAL = 40_000;
425
+ // Storage presence resolution is not universally abortable. Keep the number of
426
+ // live resolver calls small even when requests use only one key each.
427
+ export const MAX_PENDING_SIMPLE_SYNC_LOOKUPS_PER_PEER = 4;
428
+ export const MAX_PENDING_SIMPLE_SYNC_LOOKUPS_GLOBAL = 32;
429
+ // Retry scanning can touch persistent indexes. Bound each pass so a full
430
+ // adversarial queue cannot force 40,000 lookups in one event-loop turn.
431
+ export const MAX_SIMPLE_SYNC_RETRY_KEYS_PER_TICK = 4_096;
432
+ // Late coordinate-to-hash cache fills are discovered incrementally. Keep this
433
+ // independent of retained queue size so an empty or repeated request cannot
434
+ // force an O(global pending keys) reverse-alias rebuild.
435
+ const MAX_PENDING_SIMPLE_SYNC_ALIAS_REFRESH_PER_MESSAGE = 128;
436
+ const QUEUED_SYNC_ALIAS_REFRESH_PENDING = Symbol("queued-sync-alias-refresh-pending");
437
+ // This is an absolute first-seen lifetime. Repeated claims and additional peers
438
+ // deliberately do not slide the deadline.
439
+ export const PENDING_SIMPLE_SYNC_KEY_TTL_MS = 60_000;
440
+ // Bound retained request/response associations globally. Ten thousand hashes
441
+ // covers several full default-size request batches while keeping adversarial or
442
+ // abandoned requests to a small, predictable amount of heap.
443
+ const MAX_PENDING_MAYBE_SYNC_RESPONSE_HASHES = 10_000;
444
+ export const MAX_ACTIVE_SIMPLE_SYNC_RESPONSES_PER_PEER = 4;
445
+ export const MAX_ACTIVE_SIMPLE_SYNC_RESPONSES_GLOBAL = 32;
446
+ const MAX_PENDING_MAYBE_SYNC_RESPONSE_WAITER_BYPASSES = 32;
447
+ const MAX_PENDING_MAYBE_SYNC_RESPONSE_WAITERS = 10_000;
349
448
  const createDeferred = () => {
350
449
  let resolve;
351
450
  let reject;
@@ -359,8 +458,35 @@ export class SimpleSyncronizer {
359
458
  // map of hash to public keys that we can ask for entries
360
459
  syncInFlightQueue;
361
460
  syncInFlightQueueInverted;
461
+ syncInFlightQueueExpiresAt;
462
+ syncInFlightQueueExpiryTimer;
463
+ pendingSyncExpiryHeap;
464
+ pendingSyncKeyExpiryNodes;
465
+ pendingSyncAdmissionExpiryNodes;
466
+ syncInFlightRetryIterator;
467
+ syncInFlightRetryRemaining = 0;
468
+ syncInFlightQueueClaimants;
469
+ syncInFlightQueueClaimantIndexes;
470
+ syncInFlightQueueRoundRobinCursor;
471
+ syncInFlightQueuedCoordinates;
472
+ syncInFlightQueuedHashByCoordinate;
473
+ syncInFlightQueuedCoordinatesByHash;
474
+ syncInFlightQueuedCoordinateRefreshIterator;
475
+ pendingSyncClaimCount;
476
+ pendingSyncAdmissionCount;
477
+ pendingSyncActiveAdmissionReservations;
478
+ pendingSyncAdmissionCountByPeer;
479
+ pendingSyncAdmissionIdentitiesByPeer;
480
+ pendingSyncAdmissionReservations;
481
+ pendingSyncAdmissionReservationsByPeer;
482
+ pendingSyncAdmissionReservationsByIdentity;
483
+ pendingCoordinateLookupCount;
484
+ pendingCoordinateLookupCountByPeer;
485
+ pendingCoordinateResponseCount;
486
+ pendingCoordinateResponseCountByPeer;
362
487
  // map of hash to public keys that we have asked for entries
363
488
  syncInFlight;
489
+ syncInFlightTargetsByKey;
364
490
  rpc;
365
491
  log;
366
492
  entryIndex;
@@ -371,6 +497,24 @@ export class SimpleSyncronizer {
371
497
  isEntryRecentlyKnownByPeer;
372
498
  sendRawExchangeHeads;
373
499
  recentlySentExchangeHeads;
500
+ pendingMaybeSyncResponses;
501
+ pendingMaybeSyncResponseCount;
502
+ pendingMaybeSyncResponseWaiters;
503
+ pendingMaybeSyncResponseWaiterHeap;
504
+ pendingMaybeSyncResponseWaiterFitHeap;
505
+ pendingMaybeSyncResponseWaiterOrder;
506
+ pendingMaybeSyncResponseWaiterAssociationCount;
507
+ pendingMaybeSyncResponseWakeScheduled;
508
+ pendingMaybeSyncResponseConflictWaiterCount;
509
+ pendingMaybeSyncResponseBatches;
510
+ pendingMaybeSyncResponseExpiryTimer;
511
+ pendingMaybeSyncResponseExpiryHeap;
512
+ activeMaybeSyncResponseCount;
513
+ activeMaybeSyncResponseCountByPeer;
514
+ syncDispatchLifecycleController;
515
+ syncDispatchTargetEpochCounter;
516
+ syncDispatchTargetEpochs;
517
+ syncDispatchTargets;
374
518
  repairSessionCounter;
375
519
  repairSessions;
376
520
  // Syncing and dedeplucation work
@@ -379,7 +523,30 @@ export class SimpleSyncronizer {
379
523
  constructor(properties) {
380
524
  this.syncInFlightQueue = new Map();
381
525
  this.syncInFlightQueueInverted = new Map();
526
+ this.syncInFlightQueueExpiresAt = new Map();
527
+ this.pendingSyncExpiryHeap = [];
528
+ this.pendingSyncKeyExpiryNodes = new Map();
529
+ this.pendingSyncAdmissionExpiryNodes = new Map();
530
+ this.syncInFlightQueueClaimants = new Map();
531
+ this.syncInFlightQueueClaimantIndexes = new Map();
532
+ this.syncInFlightQueueRoundRobinCursor = new Map();
533
+ this.syncInFlightQueuedCoordinates = new Set();
534
+ this.syncInFlightQueuedHashByCoordinate = new Map();
535
+ this.syncInFlightQueuedCoordinatesByHash = new Map();
536
+ this.pendingSyncClaimCount = 0;
537
+ this.pendingSyncAdmissionCount = 0;
538
+ this.pendingSyncActiveAdmissionReservations = 0;
539
+ this.pendingSyncAdmissionCountByPeer = new Map();
540
+ this.pendingSyncAdmissionIdentitiesByPeer = new Map();
541
+ this.pendingSyncAdmissionReservations = new Set();
542
+ this.pendingSyncAdmissionReservationsByPeer = new Map();
543
+ this.pendingSyncAdmissionReservationsByIdentity = new Map();
544
+ this.pendingCoordinateLookupCount = 0;
545
+ this.pendingCoordinateLookupCountByPeer = new Map();
546
+ this.pendingCoordinateResponseCount = 0;
547
+ this.pendingCoordinateResponseCountByPeer = new Map();
382
548
  this.syncInFlight = new Map();
549
+ this.syncInFlightTargetsByKey = new Map();
383
550
  this.rpc = properties.rpc;
384
551
  this.log = properties.log;
385
552
  this.entryIndex = properties.entryIndex;
@@ -390,6 +557,23 @@ export class SimpleSyncronizer {
390
557
  this.isEntryRecentlyKnownByPeer = properties.isEntryRecentlyKnownByPeer;
391
558
  this.sendRawExchangeHeads = properties.sendRawExchangeHeads;
392
559
  this.recentlySentExchangeHeads = new Map();
560
+ this.pendingMaybeSyncResponses = new Map();
561
+ this.pendingMaybeSyncResponseCount = 0;
562
+ this.pendingMaybeSyncResponseWaiters = new Set();
563
+ this.pendingMaybeSyncResponseWaiterHeap = [];
564
+ this.pendingMaybeSyncResponseWaiterFitHeap = [];
565
+ this.pendingMaybeSyncResponseWaiterOrder = 0;
566
+ this.pendingMaybeSyncResponseWaiterAssociationCount = 0;
567
+ this.pendingMaybeSyncResponseWakeScheduled = false;
568
+ this.pendingMaybeSyncResponseConflictWaiterCount = 0;
569
+ this.pendingMaybeSyncResponseBatches = new Set();
570
+ this.pendingMaybeSyncResponseExpiryHeap = [];
571
+ this.activeMaybeSyncResponseCount = 0;
572
+ this.activeMaybeSyncResponseCountByPeer = new Map();
573
+ this.syncDispatchLifecycleController = new AbortController();
574
+ this.syncDispatchTargetEpochCounter = 0;
575
+ this.syncDispatchTargetEpochs = new Map();
576
+ this.syncDispatchTargets = new Map();
393
577
  this.repairSessionCounter = 0;
394
578
  this.repairSessions = new Map();
395
579
  }
@@ -427,19 +611,19 @@ export class SimpleSyncronizer {
427
611
  get maxHashesPerMessage() {
428
612
  const value = this.syncOptions?.maxSimpleHashesPerMessage;
429
613
  return value && Number.isFinite(value) && value > 0
430
- ? Math.floor(value)
614
+ ? Math.max(1, Math.floor(value))
431
615
  : DEFAULT_MAX_HASHES_PER_MESSAGE;
432
616
  }
433
617
  get maxCoordinatesPerMessage() {
434
618
  const value = this.syncOptions?.maxSimpleCoordinatesPerMessage;
435
619
  return value && Number.isFinite(value) && value > 0
436
- ? Math.floor(value)
620
+ ? Math.min(MAX_SIMPLE_COORDINATE_REQUEST_SYMBOLS, Math.max(1, Math.floor(value)))
437
621
  : DEFAULT_MAX_COORDINATES_PER_MESSAGE;
438
622
  }
439
623
  get maxConvergentTrackedHashes() {
440
624
  const value = this.syncOptions?.maxConvergentTrackedHashes;
441
625
  return value && Number.isFinite(value) && value > 0
442
- ? Math.floor(value)
626
+ ? Math.max(1, Math.floor(value))
443
627
  : DEFAULT_MAX_CONVERGENT_TRACKED_HASHES;
444
628
  }
445
629
  chunk(values, size) {
@@ -483,494 +667,2441 @@ export class SimpleSyncronizer {
483
667
  }
484
668
  return out;
485
669
  }
486
- isRepairSessionComplete(session) {
487
- for (const state of session.targets.values()) {
488
- if (state.unresolved.size > 0) {
489
- return false;
490
- }
670
+ forgetRecentlySentExchangeHeads(hashes, peer) {
671
+ const recentlySent = this.recentlySentExchangeHeads.get(peer.hashcode());
672
+ if (!recentlySent) {
673
+ return;
674
+ }
675
+ for (const hash of hashes) {
676
+ recentlySent.delete(hash);
677
+ }
678
+ if (recentlySent.size === 0) {
679
+ this.recentlySentExchangeHeads.delete(peer.hashcode());
491
680
  }
492
- return true;
493
681
  }
494
- buildRepairSessionResult(session, completed) {
495
- const durationMs = Date.now() - session.startedAt;
496
- const out = [];
497
- for (const [target, state] of session.targets) {
498
- const unresolved = [...state.unresolved];
499
- out.push({
682
+ getOrCreateSyncDispatchTargetEpoch(target) {
683
+ let epoch = this.syncDispatchTargetEpochs.get(target);
684
+ if (!epoch) {
685
+ epoch = { id: ++this.syncDispatchTargetEpochCounter };
686
+ this.syncDispatchTargetEpochs.set(target, epoch);
687
+ }
688
+ return epoch;
689
+ }
690
+ captureSyncDispatchLifecycle(targets, callerSignal, options) {
691
+ const ownershipLifecycleController = options?.ownershipLifecycleController ??
692
+ this.syncDispatchLifecycleController;
693
+ const lifecycle = {
694
+ ownershipLifecycleController,
695
+ callerSignal,
696
+ controller: new AbortController(),
697
+ targets: new Map(),
698
+ retainedWork: 0,
699
+ onOwnerOrCallerAbort: () => {
700
+ const reason = callerSignal?.aborted === true
701
+ ? callerSignal.reason
702
+ : ownershipLifecycleController.signal.reason;
703
+ this.abortSyncDispatchLifecycle(lifecycle, reason);
704
+ },
705
+ dispatchFinished: false,
706
+ disposed: false,
707
+ abortAllOnTargetDisconnect: options?.abortAllOnTargetDisconnect === true,
708
+ };
709
+ for (const target of [...new Set(targets)]) {
710
+ const expectedEpoch = options?.targetEpochs?.get(target);
711
+ const currentEpoch = this.syncDispatchTargetEpochs.get(target);
712
+ const epoch = expectedEpoch ??
713
+ currentEpoch ??
714
+ (options?.createTargetEpochs === false
715
+ ? undefined
716
+ : this.getOrCreateSyncDispatchTargetEpoch(target));
717
+ if (!epoch) {
718
+ continue;
719
+ }
720
+ const targetLifecycle = {
721
+ lifecycle,
500
722
  target,
501
- requested: state.requestedCount,
502
- resolved: state.requestedCount - unresolved.length,
503
- unresolved,
504
- attempts: state.attempts,
505
- durationMs,
506
- completed,
507
- requestedTotal: state.requestedTotalCount,
508
- truncated: session.truncated,
723
+ epoch,
724
+ controller: new AbortController(),
725
+ batches: new Set(),
726
+ responseLeases: 0,
727
+ activeWaiters: 0,
728
+ };
729
+ lifecycle.targets.set(target, targetLifecycle);
730
+ let activeForTarget = this.syncDispatchTargets.get(target);
731
+ if (!activeForTarget) {
732
+ activeForTarget = new Set();
733
+ this.syncDispatchTargets.set(target, activeForTarget);
734
+ }
735
+ activeForTarget.add(targetLifecycle);
736
+ if (this.syncDispatchTargetEpochs.get(target) !== epoch) {
737
+ this.abortSyncDispatchTarget(targetLifecycle, new Error("sync target lifecycle is stale"));
738
+ }
739
+ }
740
+ ownershipLifecycleController.signal.addEventListener("abort", lifecycle.onOwnerOrCallerAbort, { once: true });
741
+ if (callerSignal && callerSignal !== ownershipLifecycleController.signal) {
742
+ callerSignal.addEventListener("abort", lifecycle.onOwnerOrCallerAbort, {
743
+ once: true,
509
744
  });
510
745
  }
511
- return out;
746
+ if (this.closed === true ||
747
+ ownershipLifecycleController !== this.syncDispatchLifecycleController ||
748
+ ownershipLifecycleController.signal.aborted ||
749
+ callerSignal?.aborted) {
750
+ lifecycle.onOwnerOrCallerAbort();
751
+ }
752
+ return lifecycle;
512
753
  }
513
- finalizeRepairSession(sessionId, completed) {
514
- const session = this.repairSessions.get(sessionId);
515
- if (!session) {
516
- return;
754
+ abortSyncDispatchTarget(targetLifecycle, reason) {
755
+ if (!targetLifecycle.controller.signal.aborted) {
756
+ targetLifecycle.controller.abort(reason);
517
757
  }
518
- this.repairSessions.delete(sessionId);
519
- session.cancelled = true;
520
- if (session.timer) {
521
- clearTimeout(session.timer);
758
+ for (const batch of [...targetLifecycle.batches]) {
759
+ this.removePendingMaybeSyncResponseBatch(batch);
522
760
  }
523
- session.deferred.resolve(this.buildRepairSessionResult(session, completed));
761
+ this.maybeDisposeSyncDispatchLifecycle(targetLifecycle.lifecycle);
524
762
  }
525
- async refreshRepairSessionState(sessionId) {
526
- const session = this.repairSessions.get(sessionId);
527
- if (!session) {
763
+ abortSyncDispatchLifecycle(lifecycle, reason) {
764
+ if (!lifecycle.controller.signal.aborted) {
765
+ lifecycle.controller.abort(reason);
766
+ }
767
+ for (const targetLifecycle of lifecycle.targets.values()) {
768
+ this.abortSyncDispatchTarget(targetLifecycle, reason);
769
+ }
770
+ this.maybeDisposeSyncDispatchLifecycle(lifecycle);
771
+ }
772
+ finishSyncDispatchLifecycle(lifecycle) {
773
+ lifecycle.dispatchFinished = true;
774
+ this.maybeDisposeSyncDispatchLifecycle(lifecycle);
775
+ }
776
+ maybeDisposeSyncDispatchLifecycle(lifecycle) {
777
+ if (lifecycle.disposed ||
778
+ !lifecycle.dispatchFinished ||
779
+ lifecycle.retainedWork > 0) {
528
780
  return;
529
781
  }
530
- for (const state of session.targets.values()) {
531
- const resolved = typeof this.log.hasMany === "function"
532
- ? await this.log.hasMany(state.unresolved)
533
- : await this.getExistingRepairHashes(state.unresolved);
534
- for (const hash of resolved) {
535
- state.unresolved.delete(hash);
782
+ lifecycle.disposed = true;
783
+ lifecycle.ownershipLifecycleController.signal.removeEventListener("abort", lifecycle.onOwnerOrCallerAbort);
784
+ if (lifecycle.callerSignal &&
785
+ lifecycle.callerSignal !== lifecycle.ownershipLifecycleController.signal) {
786
+ lifecycle.callerSignal.removeEventListener("abort", lifecycle.onOwnerOrCallerAbort);
787
+ }
788
+ for (const targetLifecycle of lifecycle.targets.values()) {
789
+ const activeForTarget = this.syncDispatchTargets.get(targetLifecycle.target);
790
+ activeForTarget?.delete(targetLifecycle);
791
+ if (activeForTarget?.size === 0) {
792
+ this.syncDispatchTargets.delete(targetLifecycle.target);
536
793
  }
537
794
  }
538
795
  }
539
- async getExistingRepairHashes(hashes) {
540
- const resolved = new Set();
541
- for (const hash of hashes) {
542
- if (await this.log.has(hash)) {
543
- resolved.add(hash);
544
- }
796
+ isSyncDispatchLifecycleActive(lifecycle, target) {
797
+ if (this.closed === true ||
798
+ lifecycle.disposed ||
799
+ lifecycle.ownershipLifecycleController !==
800
+ this.syncDispatchLifecycleController ||
801
+ lifecycle.ownershipLifecycleController.signal.aborted ||
802
+ lifecycle.callerSignal?.aborted ||
803
+ lifecycle.controller.signal.aborted) {
804
+ return false;
805
+ }
806
+ if (target === undefined) {
807
+ return true;
545
808
  }
546
- return resolved;
809
+ const targetLifecycle = lifecycle.targets.get(target);
810
+ return (targetLifecycle !== undefined &&
811
+ !targetLifecycle.controller.signal.aborted &&
812
+ this.syncDispatchTargetEpochs.get(target) === targetLifecycle.epoch);
547
813
  }
548
- markRepairSessionResolvedHashes(hashes) {
549
- if (hashes.length === 0 || this.repairSessions.size === 0) {
814
+ getSyncDispatchSignal(lifecycle, target) {
815
+ return (lifecycle.targets.get(target)?.controller.signal ??
816
+ lifecycle.controller.signal);
817
+ }
818
+ pendingMaybeSyncResponseWaiterBefore(left, right) {
819
+ return left.order < right.order;
820
+ }
821
+ swapPendingMaybeSyncResponseWaiters(left, right) {
822
+ const leftWaiter = this.pendingMaybeSyncResponseWaiterHeap[left];
823
+ const rightWaiter = this.pendingMaybeSyncResponseWaiterHeap[right];
824
+ this.pendingMaybeSyncResponseWaiterHeap[left] = rightWaiter;
825
+ this.pendingMaybeSyncResponseWaiterHeap[right] = leftWaiter;
826
+ rightWaiter.heapIndex = left;
827
+ leftWaiter.heapIndex = right;
828
+ }
829
+ pushPendingMaybeSyncResponseWaiter(waiter) {
830
+ waiter.heapIndex = this.pendingMaybeSyncResponseWaiterHeap.length;
831
+ this.pendingMaybeSyncResponseWaiterHeap.push(waiter);
832
+ let index = waiter.heapIndex;
833
+ while (index > 0) {
834
+ const parent = Math.floor((index - 1) / 2);
835
+ if (this.pendingMaybeSyncResponseWaiterBefore(this.pendingMaybeSyncResponseWaiterHeap[parent], this.pendingMaybeSyncResponseWaiterHeap[index])) {
836
+ break;
837
+ }
838
+ this.swapPendingMaybeSyncResponseWaiters(parent, index);
839
+ index = parent;
840
+ }
841
+ this.pushPendingMaybeSyncResponseFitWaiter(waiter);
842
+ }
843
+ removePendingMaybeSyncResponseWaiter(waiter) {
844
+ this.removePendingMaybeSyncResponseFitWaiter(waiter);
845
+ const index = waiter.heapIndex;
846
+ if (index < 0 ||
847
+ index >= this.pendingMaybeSyncResponseWaiterHeap.length ||
848
+ this.pendingMaybeSyncResponseWaiterHeap[index] !== waiter) {
550
849
  return;
551
850
  }
552
- for (const [sessionId, session] of this.repairSessions) {
553
- for (const state of session.targets.values()) {
554
- for (const hash of hashes) {
555
- state.unresolved.delete(hash);
556
- }
851
+ const last = this.pendingMaybeSyncResponseWaiterHeap.pop();
852
+ waiter.heapIndex = -1;
853
+ if (index >= this.pendingMaybeSyncResponseWaiterHeap.length) {
854
+ return;
855
+ }
856
+ this.pendingMaybeSyncResponseWaiterHeap[index] = last;
857
+ last.heapIndex = index;
858
+ let current = index;
859
+ while (current > 0) {
860
+ const parent = Math.floor((current - 1) / 2);
861
+ if (this.pendingMaybeSyncResponseWaiterBefore(this.pendingMaybeSyncResponseWaiterHeap[parent], this.pendingMaybeSyncResponseWaiterHeap[current])) {
862
+ break;
557
863
  }
558
- if (this.isRepairSessionComplete(session)) {
559
- this.finalizeRepairSession(sessionId, true);
864
+ this.swapPendingMaybeSyncResponseWaiters(parent, current);
865
+ current = parent;
866
+ }
867
+ for (;;) {
868
+ const left = current * 2 + 1;
869
+ const right = left + 1;
870
+ let smallest = current;
871
+ if (left < this.pendingMaybeSyncResponseWaiterHeap.length &&
872
+ this.pendingMaybeSyncResponseWaiterBefore(this.pendingMaybeSyncResponseWaiterHeap[left], this.pendingMaybeSyncResponseWaiterHeap[smallest])) {
873
+ smallest = left;
874
+ }
875
+ if (right < this.pendingMaybeSyncResponseWaiterHeap.length &&
876
+ this.pendingMaybeSyncResponseWaiterBefore(this.pendingMaybeSyncResponseWaiterHeap[right], this.pendingMaybeSyncResponseWaiterHeap[smallest])) {
877
+ smallest = right;
560
878
  }
879
+ if (smallest === current) {
880
+ break;
881
+ }
882
+ this.swapPendingMaybeSyncResponseWaiters(current, smallest);
883
+ current = smallest;
561
884
  }
562
885
  }
563
- markRepairSessionResolvedHash(hash) {
564
- if (this.repairSessions.size === 0) {
886
+ pendingMaybeSyncResponseFitWaiterBefore(left, right) {
887
+ return (left.required < right.required ||
888
+ (left.required === right.required && left.order < right.order));
889
+ }
890
+ swapPendingMaybeSyncResponseFitWaiters(left, right) {
891
+ const leftWaiter = this.pendingMaybeSyncResponseWaiterFitHeap[left];
892
+ const rightWaiter = this.pendingMaybeSyncResponseWaiterFitHeap[right];
893
+ this.pendingMaybeSyncResponseWaiterFitHeap[left] = rightWaiter;
894
+ this.pendingMaybeSyncResponseWaiterFitHeap[right] = leftWaiter;
895
+ rightWaiter.fitHeapIndex = left;
896
+ leftWaiter.fitHeapIndex = right;
897
+ }
898
+ pushPendingMaybeSyncResponseFitWaiter(waiter) {
899
+ waiter.fitHeapIndex = this.pendingMaybeSyncResponseWaiterFitHeap.length;
900
+ this.pendingMaybeSyncResponseWaiterFitHeap.push(waiter);
901
+ let index = waiter.fitHeapIndex;
902
+ while (index > 0) {
903
+ const parent = Math.floor((index - 1) / 2);
904
+ if (this.pendingMaybeSyncResponseFitWaiterBefore(this.pendingMaybeSyncResponseWaiterFitHeap[parent], this.pendingMaybeSyncResponseWaiterFitHeap[index])) {
905
+ break;
906
+ }
907
+ this.swapPendingMaybeSyncResponseFitWaiters(parent, index);
908
+ index = parent;
909
+ }
910
+ }
911
+ removePendingMaybeSyncResponseFitWaiter(waiter) {
912
+ const index = waiter.fitHeapIndex;
913
+ if (index < 0 ||
914
+ index >= this.pendingMaybeSyncResponseWaiterFitHeap.length ||
915
+ this.pendingMaybeSyncResponseWaiterFitHeap[index] !== waiter) {
565
916
  return;
566
917
  }
567
- for (const [sessionId, session] of this.repairSessions) {
568
- for (const state of session.targets.values()) {
569
- state.unresolved.delete(hash);
918
+ const last = this.pendingMaybeSyncResponseWaiterFitHeap.pop();
919
+ waiter.fitHeapIndex = -1;
920
+ if (index >= this.pendingMaybeSyncResponseWaiterFitHeap.length) {
921
+ return;
922
+ }
923
+ this.pendingMaybeSyncResponseWaiterFitHeap[index] = last;
924
+ last.fitHeapIndex = index;
925
+ let current = index;
926
+ while (current > 0) {
927
+ const parent = Math.floor((current - 1) / 2);
928
+ if (this.pendingMaybeSyncResponseFitWaiterBefore(this.pendingMaybeSyncResponseWaiterFitHeap[parent], this.pendingMaybeSyncResponseWaiterFitHeap[current])) {
929
+ break;
570
930
  }
571
- if (this.isRepairSessionComplete(session)) {
572
- this.finalizeRepairSession(sessionId, true);
931
+ this.swapPendingMaybeSyncResponseFitWaiters(parent, current);
932
+ current = parent;
933
+ }
934
+ for (;;) {
935
+ const left = current * 2 + 1;
936
+ const right = left + 1;
937
+ let smallest = current;
938
+ if (left < this.pendingMaybeSyncResponseWaiterFitHeap.length &&
939
+ this.pendingMaybeSyncResponseFitWaiterBefore(this.pendingMaybeSyncResponseWaiterFitHeap[left], this.pendingMaybeSyncResponseWaiterFitHeap[smallest])) {
940
+ smallest = left;
573
941
  }
942
+ if (right < this.pendingMaybeSyncResponseWaiterFitHeap.length &&
943
+ this.pendingMaybeSyncResponseFitWaiterBefore(this.pendingMaybeSyncResponseWaiterFitHeap[right], this.pendingMaybeSyncResponseWaiterFitHeap[smallest])) {
944
+ smallest = right;
945
+ }
946
+ if (smallest === current) {
947
+ break;
948
+ }
949
+ this.swapPendingMaybeSyncResponseFitWaiters(current, smallest);
950
+ current = smallest;
574
951
  }
575
952
  }
576
- async runRepairSession(sessionId) {
577
- const session = this.repairSessions.get(sessionId);
578
- if (!session) {
953
+ notifyPendingMaybeSyncResponseWaiter() {
954
+ const waiter = this.pendingMaybeSyncResponseWaiterHeap[0];
955
+ const available = MAX_PENDING_MAYBE_SYNC_RESPONSE_HASHES -
956
+ this.pendingMaybeSyncResponseCount;
957
+ if (!waiter) {
579
958
  return;
580
959
  }
581
- let previousDelay = 0;
582
- for (const delayMs of session.retryIntervalsMs) {
583
- if (!this.repairSessions.has(sessionId) || this.closed) {
584
- return;
960
+ if (waiter.required <= available) {
961
+ waiter.wake();
962
+ return;
963
+ }
964
+ if (waiter.bypasses >= MAX_PENDING_MAYBE_SYNC_RESPONSE_WAITER_BYPASSES) {
965
+ // Reserve newly freed capacity for the oldest large request after a
966
+ // bounded number of smaller requests have bypassed it.
967
+ return;
968
+ }
969
+ const candidate = this.pendingMaybeSyncResponseWaiterFitHeap[0];
970
+ if (candidate && candidate.required <= available) {
971
+ waiter.bypasses += 1;
972
+ candidate.wake();
973
+ }
974
+ }
975
+ schedulePendingMaybeSyncResponseWaiter() {
976
+ if (this.pendingMaybeSyncResponseWakeScheduled) {
977
+ return;
978
+ }
979
+ this.pendingMaybeSyncResponseWakeScheduled = true;
980
+ queueMicrotask(() => {
981
+ this.pendingMaybeSyncResponseWakeScheduled = false;
982
+ this.notifyPendingMaybeSyncResponseWaiter();
983
+ });
984
+ }
985
+ swapPendingMaybeSyncResponseExpiry(left, right) {
986
+ const leftBatch = this.pendingMaybeSyncResponseExpiryHeap[left];
987
+ const rightBatch = this.pendingMaybeSyncResponseExpiryHeap[right];
988
+ this.pendingMaybeSyncResponseExpiryHeap[left] = rightBatch;
989
+ this.pendingMaybeSyncResponseExpiryHeap[right] = leftBatch;
990
+ rightBatch.heapIndex = left;
991
+ leftBatch.heapIndex = right;
992
+ }
993
+ pushPendingMaybeSyncResponseExpiry(batch) {
994
+ batch.heapIndex = this.pendingMaybeSyncResponseExpiryHeap.length;
995
+ this.pendingMaybeSyncResponseExpiryHeap.push(batch);
996
+ let index = batch.heapIndex;
997
+ while (index > 0) {
998
+ const parent = Math.floor((index - 1) / 2);
999
+ if (this.pendingMaybeSyncResponseExpiryHeap[parent].expiresAt <=
1000
+ this.pendingMaybeSyncResponseExpiryHeap[index].expiresAt) {
1001
+ break;
1002
+ }
1003
+ this.swapPendingMaybeSyncResponseExpiry(parent, index);
1004
+ index = parent;
1005
+ }
1006
+ }
1007
+ removePendingMaybeSyncResponseExpiry(batch) {
1008
+ const index = batch.heapIndex;
1009
+ if (index < 0 ||
1010
+ index >= this.pendingMaybeSyncResponseExpiryHeap.length ||
1011
+ this.pendingMaybeSyncResponseExpiryHeap[index] !== batch) {
1012
+ return;
1013
+ }
1014
+ const last = this.pendingMaybeSyncResponseExpiryHeap.pop();
1015
+ batch.heapIndex = -1;
1016
+ if (index >= this.pendingMaybeSyncResponseExpiryHeap.length) {
1017
+ return;
1018
+ }
1019
+ this.pendingMaybeSyncResponseExpiryHeap[index] = last;
1020
+ last.heapIndex = index;
1021
+ let current = index;
1022
+ while (current > 0) {
1023
+ const parent = Math.floor((current - 1) / 2);
1024
+ if (this.pendingMaybeSyncResponseExpiryHeap[parent].expiresAt <=
1025
+ this.pendingMaybeSyncResponseExpiryHeap[current].expiresAt) {
1026
+ break;
585
1027
  }
586
- const waitMs = Math.max(0, delayMs - previousDelay);
587
- previousDelay = delayMs;
588
- if (waitMs > 0) {
589
- await new Promise((resolve) => {
590
- const timer = setTimeout(resolve, waitMs);
591
- timer.unref?.();
592
- });
1028
+ this.swapPendingMaybeSyncResponseExpiry(parent, current);
1029
+ current = parent;
1030
+ }
1031
+ for (;;) {
1032
+ const left = current * 2 + 1;
1033
+ const right = left + 1;
1034
+ let smallest = current;
1035
+ if (left < this.pendingMaybeSyncResponseExpiryHeap.length &&
1036
+ this.pendingMaybeSyncResponseExpiryHeap[left].expiresAt <
1037
+ this.pendingMaybeSyncResponseExpiryHeap[smallest].expiresAt) {
1038
+ smallest = left;
593
1039
  }
594
- if (!this.repairSessions.has(sessionId) || this.closed) {
595
- return;
1040
+ if (right < this.pendingMaybeSyncResponseExpiryHeap.length &&
1041
+ this.pendingMaybeSyncResponseExpiryHeap[right].expiresAt <
1042
+ this.pendingMaybeSyncResponseExpiryHeap[smallest].expiresAt) {
1043
+ smallest = right;
596
1044
  }
597
- await this.refreshRepairSessionState(sessionId);
598
- const current = this.repairSessions.get(sessionId);
599
- if (!current) {
600
- return;
1045
+ if (smallest === current) {
1046
+ break;
601
1047
  }
602
- if (this.isRepairSessionComplete(current)) {
603
- this.finalizeRepairSession(sessionId, true);
604
- return;
1048
+ this.swapPendingMaybeSyncResponseExpiry(current, smallest);
1049
+ current = smallest;
1050
+ }
1051
+ }
1052
+ schedulePendingMaybeSyncResponseExpiry() {
1053
+ if (this.pendingMaybeSyncResponseExpiryTimer ||
1054
+ this.pendingMaybeSyncResponseExpiryHeap.length === 0) {
1055
+ return;
1056
+ }
1057
+ const earliest = this.pendingMaybeSyncResponseExpiryHeap[0].expiresAt;
1058
+ this.pendingMaybeSyncResponseExpiryTimer = setTimeout(() => {
1059
+ this.pendingMaybeSyncResponseExpiryTimer = undefined;
1060
+ this.expirePendingMaybeSyncResponses();
1061
+ this.schedulePendingMaybeSyncResponseExpiry();
1062
+ }, Math.max(0, earliest - Date.now()));
1063
+ this.pendingMaybeSyncResponseExpiryTimer.unref?.();
1064
+ }
1065
+ expirePendingMaybeSyncResponses(now = Date.now()) {
1066
+ for (;;) {
1067
+ const batch = this.pendingMaybeSyncResponseExpiryHeap[0];
1068
+ if (!batch || batch.expiresAt > now) {
1069
+ break;
605
1070
  }
606
- for (const [target, state] of current.targets) {
607
- if (state.unresolved.size === 0) {
608
- continue;
1071
+ this.removePendingMaybeSyncResponseBatch(batch);
1072
+ }
1073
+ }
1074
+ settlePendingMaybeSyncResponseAuthorization(authorization, fulfilled) {
1075
+ if (authorization.settled) {
1076
+ return;
1077
+ }
1078
+ authorization.settled = fulfilled ? "fulfilled" : "released";
1079
+ const waiters = [...authorization.waiters];
1080
+ authorization.waiters.clear();
1081
+ for (const waiter of waiters) {
1082
+ waiter(fulfilled ? "fulfilled" : "released");
1083
+ }
1084
+ }
1085
+ doesPendingMaybeSyncResponseScopeMatch(authorization, lifecycle, target) {
1086
+ const owner = authorization.batch.targetLifecycle;
1087
+ return (owner.epoch === lifecycle.targets.get(target)?.epoch &&
1088
+ owner.lifecycle.ownershipLifecycleController ===
1089
+ lifecycle.ownershipLifecycleController &&
1090
+ owner.lifecycle.callerSignal === lifecycle.callerSignal);
1091
+ }
1092
+ waitForPendingMaybeSyncResponseConflicts(conflicts, lifecycle, target, retainedAssociations) {
1093
+ const unique = [...new Set(conflicts)];
1094
+ if (this.pendingMaybeSyncResponseWaiterAssociationCount +
1095
+ retainedAssociations >
1096
+ MAX_PENDING_MAYBE_SYNC_RESPONSE_HASHES) {
1097
+ return Promise.resolve([]);
1098
+ }
1099
+ const available = MAX_PENDING_MAYBE_SYNC_RESPONSE_HASHES -
1100
+ this.pendingMaybeSyncResponseConflictWaiterCount;
1101
+ const admitted = unique.slice(0, Math.max(0, available));
1102
+ if (admitted.length === 0) {
1103
+ return Promise.resolve([]);
1104
+ }
1105
+ this.pendingMaybeSyncResponseConflictWaiterCount += admitted.length;
1106
+ this.pendingMaybeSyncResponseWaiterAssociationCount += retainedAssociations;
1107
+ const targetSignal = lifecycle.targets.get(target)?.controller.signal ??
1108
+ lifecycle.controller.signal;
1109
+ return new Promise((resolve) => {
1110
+ const retry = new Set();
1111
+ const callbacks = new Map();
1112
+ let remaining = admitted.length;
1113
+ let groupSettled = false;
1114
+ const finishGroup = () => {
1115
+ if (groupSettled || remaining !== 0) {
1116
+ return;
609
1117
  }
610
- state.attempts += 1;
611
- try {
612
- await this.requestSync([...state.unresolved], [target]);
1118
+ groupSettled = true;
1119
+ lifecycle.controller.signal.removeEventListener("abort", abort);
1120
+ if (targetSignal !== lifecycle.controller.signal) {
1121
+ targetSignal.removeEventListener("abort", abort);
613
1122
  }
614
- catch {
615
- // Best-effort: keep unresolved and let retries/timeout determine outcome.
1123
+ this.pendingMaybeSyncResponseWaiterAssociationCount -=
1124
+ retainedAssociations;
1125
+ resolve([...retry]);
1126
+ };
1127
+ const finishOne = (authorization, event) => {
1128
+ if (event === "delivered" &&
1129
+ (authorization.active === true ||
1130
+ !this.doesPendingMaybeSyncResponseScopeMatch(authorization, lifecycle, target))) {
1131
+ return;
1132
+ }
1133
+ const callback = callbacks.get(authorization);
1134
+ if (!callback) {
1135
+ return;
1136
+ }
1137
+ callbacks.delete(authorization);
1138
+ authorization.waiters.delete(callback);
1139
+ this.pendingMaybeSyncResponseConflictWaiterCount -= 1;
1140
+ if (event === "released" &&
1141
+ this.isSyncDispatchLifecycleActive(lifecycle, target)) {
1142
+ retry.add(authorization.hash);
1143
+ }
1144
+ remaining -= 1;
1145
+ finishGroup();
1146
+ };
1147
+ const abort = () => {
1148
+ for (const authorization of [...callbacks.keys()]) {
1149
+ finishOne(authorization, "fulfilled");
1150
+ }
1151
+ };
1152
+ lifecycle.controller.signal.addEventListener("abort", abort, {
1153
+ once: true,
1154
+ });
1155
+ if (targetSignal !== lifecycle.controller.signal) {
1156
+ targetSignal.addEventListener("abort", abort, { once: true });
1157
+ }
1158
+ for (const authorization of admitted) {
1159
+ const callback = (event) => finishOne(authorization, event);
1160
+ callbacks.set(authorization, callback);
1161
+ authorization.waiters.add(callback);
1162
+ if (authorization.settled) {
1163
+ callback(authorization.settled);
1164
+ }
1165
+ else if (authorization.requestDelivered === true &&
1166
+ authorization.active !== true) {
1167
+ callback("delivered");
616
1168
  }
617
1169
  }
618
- await this.refreshRepairSessionState(sessionId);
619
- const afterSend = this.repairSessions.get(sessionId);
620
- if (!afterSend) {
621
- return;
1170
+ if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
1171
+ abort();
622
1172
  }
623
- if (this.isRepairSessionComplete(afterSend)) {
624
- this.finalizeRepairSession(sessionId, true);
625
- return;
1173
+ });
1174
+ }
1175
+ removePendingMaybeSyncResponseBatch(batch) {
1176
+ const pendingForTarget = this.pendingMaybeSyncResponses.get(batch.target);
1177
+ let removed = 0;
1178
+ if (pendingForTarget) {
1179
+ for (const hash of batch.hashes) {
1180
+ const authorization = pendingForTarget.get(hash);
1181
+ if (authorization?.batch !== batch) {
1182
+ continue;
1183
+ }
1184
+ this.settlePendingMaybeSyncResponseAuthorization(authorization, false);
1185
+ pendingForTarget.delete(hash);
1186
+ if (authorization.deliveryInFlight !== true) {
1187
+ removed += 1;
1188
+ }
626
1189
  }
627
- if (afterSend.mode === "best-effort") {
628
- this.finalizeRepairSession(sessionId, false);
629
- return;
1190
+ if (pendingForTarget.size === 0) {
1191
+ this.pendingMaybeSyncResponses.delete(batch.target);
630
1192
  }
631
1193
  }
632
- for (;;) {
633
- if (!this.repairSessions.has(sessionId) || this.closed) {
634
- return;
635
- }
636
- await this.refreshRepairSessionState(sessionId);
637
- const current = this.repairSessions.get(sessionId);
638
- if (!current) {
639
- return;
640
- }
641
- if (this.isRepairSessionComplete(current)) {
642
- this.finalizeRepairSession(sessionId, true);
643
- return;
644
- }
645
- await new Promise((resolve) => {
646
- const timer = setTimeout(resolve, SESSION_POLL_INTERVAL_MS);
647
- timer.unref?.();
648
- });
1194
+ this.pendingMaybeSyncResponseCount -= removed;
1195
+ this.pendingMaybeSyncResponseBatches.delete(batch);
1196
+ this.removePendingMaybeSyncResponseExpiry(batch);
1197
+ if (batch.targetLifecycle.batches.delete(batch)) {
1198
+ batch.targetLifecycle.lifecycle.retainedWork -= 1;
649
1199
  }
1200
+ batch.hashes.clear();
1201
+ if (this.pendingMaybeSyncResponseExpiryHeap.length === 0 &&
1202
+ this.pendingMaybeSyncResponseExpiryTimer) {
1203
+ clearTimeout(this.pendingMaybeSyncResponseExpiryTimer);
1204
+ this.pendingMaybeSyncResponseExpiryTimer = undefined;
1205
+ }
1206
+ if (removed > 0) {
1207
+ this.schedulePendingMaybeSyncResponseWaiter();
1208
+ }
1209
+ this.maybeDisposeSyncDispatchLifecycle(batch.targetLifecycle.lifecycle);
650
1210
  }
651
- getQueuedSyncKey(key) {
652
- if (this.syncInFlightQueue.has(key)) {
653
- return key;
1211
+ clearPendingMaybeSyncResponses(target) {
1212
+ const batches = target === undefined
1213
+ ? [...this.pendingMaybeSyncResponseBatches]
1214
+ : [
1215
+ ...new Set([
1216
+ ...(this.pendingMaybeSyncResponses.get(target)?.values() ?? []),
1217
+ ].map((authorization) => authorization.batch)),
1218
+ ];
1219
+ for (const batch of batches) {
1220
+ this.removePendingMaybeSyncResponseBatch(batch);
654
1221
  }
655
- if (typeof key === "string") {
656
- for (const queuedKey of this.syncInFlightQueue.keys()) {
657
- if (typeof queuedKey === "bigint" &&
658
- this.coordinateToHash.get(queuedKey) === key) {
659
- return queuedKey;
1222
+ }
1223
+ tryReservePendingMaybeSyncResponse(properties) {
1224
+ // Timers are only a cleanup aid. Enforce absolute deadlines at the
1225
+ // admission boundary as well, including for unrelated fresh hashes.
1226
+ this.expirePendingMaybeSyncResponses();
1227
+ const hashes = [...new Set(properties.hashes)];
1228
+ const targets = [...new Set(properties.targets)];
1229
+ if (!this.isSyncDispatchLifecycleActive(properties.lifecycle) ||
1230
+ targets.some((target) => !this.isSyncDispatchLifecycleActive(properties.lifecycle, target))) {
1231
+ return { kind: "inactive" };
1232
+ }
1233
+ const hashesToAddByTarget = new Map();
1234
+ const conflicts = [];
1235
+ let required = 0;
1236
+ for (const target of targets) {
1237
+ const hashesToAdd = [];
1238
+ for (const hash of hashes) {
1239
+ let existing = this.pendingMaybeSyncResponses.get(target)?.get(hash);
1240
+ if (existing &&
1241
+ existing.active !== true &&
1242
+ (existing.batch.expiresAt <= Date.now() ||
1243
+ !this.isSyncDispatchLifecycleActive(existing.batch.targetLifecycle.lifecycle, target))) {
1244
+ this.removePendingMaybeSyncResponseBatch(existing.batch);
1245
+ existing = undefined;
1246
+ }
1247
+ if (existing) {
1248
+ const existingTarget = existing.batch.targetLifecycle;
1249
+ if (existingTarget.lifecycle === properties.lifecycle ||
1250
+ (existing.active !== true &&
1251
+ existing.requestDelivered === true &&
1252
+ this.doesPendingMaybeSyncResponseScopeMatch(existing, properties.lifecycle, target))) {
1253
+ continue;
1254
+ }
1255
+ // Another live caller already owns the authorization for this
1256
+ // exact target/hash. Send unrelated hashes now, then wait for this
1257
+ // authorization to be fulfilled or released before deciding
1258
+ // whether this caller must retry it.
1259
+ conflicts.push(existing);
1260
+ continue;
660
1261
  }
1262
+ hashesToAdd.push(hash);
1263
+ }
1264
+ if (hashesToAdd.length > 0) {
1265
+ hashesToAddByTarget.set(target, hashesToAdd);
1266
+ required += hashesToAdd.length;
661
1267
  }
662
- return undefined;
663
1268
  }
664
- const hash = this.coordinateToHash.get(key);
665
- return hash && this.syncInFlightQueue.has(hash) ? hash : undefined;
1269
+ if (required >
1270
+ MAX_PENDING_MAYBE_SYNC_RESPONSE_HASHES -
1271
+ this.pendingMaybeSyncResponseCount) {
1272
+ return { kind: "capacity", required };
1273
+ }
1274
+ const addedBatches = [];
1275
+ const addedAuthorizations = [];
1276
+ for (const [target, hashesToAdd] of hashesToAddByTarget) {
1277
+ const targetLifecycle = properties.lifecycle.targets.get(target);
1278
+ const batch = {
1279
+ hashes: new Set(hashesToAdd),
1280
+ target,
1281
+ targetLifecycle,
1282
+ // Start the response deadline only after rpc.send succeeds. Keeping
1283
+ // pre-delivery work charged prevents a slow/non-abortable transport
1284
+ // from rolling over an unbounded number of sends.
1285
+ expiresAt: Infinity,
1286
+ heapIndex: -1,
1287
+ };
1288
+ let pendingForTarget = this.pendingMaybeSyncResponses.get(target);
1289
+ if (!pendingForTarget) {
1290
+ pendingForTarget = new Map();
1291
+ this.pendingMaybeSyncResponses.set(target, pendingForTarget);
1292
+ }
1293
+ for (const hash of hashesToAdd) {
1294
+ const authorization = {
1295
+ batch,
1296
+ hash,
1297
+ waiters: new Set(),
1298
+ };
1299
+ pendingForTarget.set(hash, authorization);
1300
+ addedAuthorizations.push(authorization);
1301
+ }
1302
+ this.pendingMaybeSyncResponseCount += hashesToAdd.length;
1303
+ this.pendingMaybeSyncResponseBatches.add(batch);
1304
+ targetLifecycle.batches.add(batch);
1305
+ targetLifecycle.lifecycle.retainedWork += 1;
1306
+ addedBatches.push(batch);
1307
+ }
1308
+ this.schedulePendingMaybeSyncResponseExpiry();
1309
+ if (this.pendingMaybeSyncResponseWaiters.size > 0) {
1310
+ this.schedulePendingMaybeSyncResponseWaiter();
1311
+ }
1312
+ let released = false;
1313
+ const release = () => {
1314
+ if (released) {
1315
+ return;
1316
+ }
1317
+ released = true;
1318
+ for (const batch of addedBatches) {
1319
+ this.removePendingMaybeSyncResponseBatch(batch);
1320
+ }
1321
+ };
1322
+ const retained = () => this.isSyncDispatchLifecycleActive(properties.lifecycle) &&
1323
+ addedBatches.every((batch) => this.pendingMaybeSyncResponseBatches.has(batch) &&
1324
+ batch.expiresAt > Date.now() &&
1325
+ batch.hashes.size > 0);
1326
+ if (!this.isSyncDispatchLifecycleActive(properties.lifecycle)) {
1327
+ release();
1328
+ return { kind: "inactive" };
1329
+ }
1330
+ return {
1331
+ kind: "reserved",
1332
+ reservation: {
1333
+ release,
1334
+ beginDelivery: () => {
1335
+ for (const authorization of addedAuthorizations) {
1336
+ if (!authorization.settled) {
1337
+ authorization.deliveryInFlight = true;
1338
+ }
1339
+ }
1340
+ },
1341
+ finishDelivery: () => {
1342
+ let releasedCount = 0;
1343
+ for (const authorization of addedAuthorizations) {
1344
+ if (authorization.deliveryInFlight !== true) {
1345
+ continue;
1346
+ }
1347
+ authorization.deliveryInFlight = false;
1348
+ if (authorization.settled ||
1349
+ this.pendingMaybeSyncResponses
1350
+ .get(authorization.batch.target)
1351
+ ?.get(authorization.hash) !== authorization) {
1352
+ releasedCount += 1;
1353
+ }
1354
+ }
1355
+ if (releasedCount > 0) {
1356
+ this.pendingMaybeSyncResponseCount -= releasedCount;
1357
+ this.schedulePendingMaybeSyncResponseWaiter();
1358
+ }
1359
+ },
1360
+ markDelivered: () => {
1361
+ const delivered = [];
1362
+ const deliveredBatches = new Set();
1363
+ for (const authorization of addedAuthorizations) {
1364
+ if (authorization.settled ||
1365
+ authorization.active === true ||
1366
+ this.pendingMaybeSyncResponses
1367
+ .get(authorization.batch.target)
1368
+ ?.get(authorization.hash) !== authorization ||
1369
+ !this.pendingMaybeSyncResponseBatches.has(authorization.batch)) {
1370
+ continue;
1371
+ }
1372
+ authorization.requestDelivered = true;
1373
+ delivered.push(authorization);
1374
+ deliveredBatches.add(authorization.batch);
1375
+ }
1376
+ const expiresAt = Date.now() + PENDING_MAYBE_SYNC_RESPONSE_TTL_MS;
1377
+ for (const batch of deliveredBatches) {
1378
+ if (batch.heapIndex >= 0) {
1379
+ this.removePendingMaybeSyncResponseExpiry(batch);
1380
+ }
1381
+ batch.expiresAt = expiresAt;
1382
+ this.pushPendingMaybeSyncResponseExpiry(batch);
1383
+ }
1384
+ this.schedulePendingMaybeSyncResponseExpiry();
1385
+ for (const authorization of delivered) {
1386
+ if (authorization.settled || authorization.active === true) {
1387
+ continue;
1388
+ }
1389
+ for (const waiter of [...authorization.waiters]) {
1390
+ waiter("delivered");
1391
+ }
1392
+ }
1393
+ },
1394
+ newlyAuthorizedByTarget: hashesToAddByTarget,
1395
+ retained,
1396
+ signal: properties.lifecycle.controller.signal,
1397
+ },
1398
+ conflicts,
1399
+ };
666
1400
  }
667
- startRepairSession(properties) {
668
- const mode = properties.mode ?? "best-effort";
669
- const startedAt = Date.now();
670
- const timeoutMs = Math.max(1, Math.floor(properties.timeoutMs ??
671
- (mode === "convergent"
672
- ? DEFAULT_CONVERGENT_REPAIR_TIMEOUT_MS
673
- : DEFAULT_CONVERGENT_REPAIR_TIMEOUT_MS)));
674
- const retryIntervalsMs = this.normalizeRetryIntervals(mode, properties.retryIntervalsMs);
675
- const allHashes = this.getPrioritizedHashes(properties.entries);
676
- const trackedHashes = mode === "convergent" &&
677
- allHashes.length > this.maxConvergentTrackedHashes
678
- ? allHashes.slice(0, this.maxConvergentTrackedHashes)
679
- : allHashes;
680
- const truncated = trackedHashes.length < allHashes.length;
1401
+ waitForPendingMaybeSyncResponseChange(lifecycle, targets, required, associations) {
1402
+ if (!this.isSyncDispatchLifecycleActive(lifecycle) ||
1403
+ targets.some((target) => !this.isSyncDispatchLifecycleActive(lifecycle, target))) {
1404
+ return Promise.resolve();
1405
+ }
1406
+ const targetLifecycles = targets
1407
+ .map((target) => lifecycle.targets.get(target))
1408
+ .filter((target) => target !== undefined);
1409
+ for (const targetLifecycle of targetLifecycles) {
1410
+ targetLifecycle.activeWaiters += 1;
1411
+ targetLifecycle.lifecycle.retainedWork += 1;
1412
+ }
1413
+ return new Promise((resolve) => {
1414
+ let settled = false;
1415
+ let waiter;
1416
+ const abortSignals = [
1417
+ lifecycle.controller.signal,
1418
+ ...targetLifecycles.map((targetLifecycle) => targetLifecycle.controller.signal),
1419
+ ];
1420
+ const wake = () => {
1421
+ if (settled) {
1422
+ return;
1423
+ }
1424
+ const advanceWaiters = !this.isSyncDispatchLifecycleActive(lifecycle) ||
1425
+ targets.some((target) => !this.isSyncDispatchLifecycleActive(lifecycle, target));
1426
+ settled = true;
1427
+ this.pendingMaybeSyncResponseWaiters.delete(waiter);
1428
+ this.removePendingMaybeSyncResponseWaiter(waiter);
1429
+ this.pendingMaybeSyncResponseWaiterAssociationCount -=
1430
+ waiter.associations;
1431
+ for (const signal of abortSignals) {
1432
+ signal.removeEventListener("abort", wake);
1433
+ }
1434
+ for (const targetLifecycle of targetLifecycles) {
1435
+ targetLifecycle.activeWaiters -= 1;
1436
+ targetLifecycle.lifecycle.retainedWork -= 1;
1437
+ }
1438
+ resolve();
1439
+ this.maybeDisposeSyncDispatchLifecycle(lifecycle);
1440
+ if (advanceWaiters) {
1441
+ this.schedulePendingMaybeSyncResponseWaiter();
1442
+ }
1443
+ };
1444
+ waiter = {
1445
+ required,
1446
+ associations,
1447
+ order: ++this.pendingMaybeSyncResponseWaiterOrder,
1448
+ bypasses: 0,
1449
+ heapIndex: -1,
1450
+ fitHeapIndex: -1,
1451
+ wake,
1452
+ };
1453
+ this.pendingMaybeSyncResponseWaiters.add(waiter);
1454
+ this.pendingMaybeSyncResponseWaiterAssociationCount += associations;
1455
+ this.pushPendingMaybeSyncResponseWaiter(waiter);
1456
+ // This exact request does not fit, but an older/stale requirement may
1457
+ // have changed and another smaller waiter can still use the capacity.
1458
+ this.schedulePendingMaybeSyncResponseWaiter();
1459
+ for (const signal of abortSignals) {
1460
+ signal.addEventListener("abort", wake, { once: true });
1461
+ }
1462
+ if (!this.isSyncDispatchLifecycleActive(lifecycle) ||
1463
+ targets.some((target) => !this.isSyncDispatchLifecycleActive(lifecycle, target))) {
1464
+ wake();
1465
+ }
1466
+ });
1467
+ }
1468
+ async reservePendingMaybeSyncResponse(properties) {
1469
+ const hashes = [...new Set(properties.hashes)];
681
1470
  const targets = [...new Set(properties.targets)];
682
- const id = `repair-${++this.repairSessionCounter}`;
683
- const deferred = createDeferred();
684
- const targetStates = new Map();
685
- for (const target of targets) {
686
- targetStates.set(target, {
687
- unresolved: new Set(trackedHashes),
688
- requestedCount: trackedHashes.length,
689
- requestedTotalCount: allHashes.length,
690
- attempts: 0,
1471
+ const associations = Math.max(1, Math.min(MAX_PENDING_MAYBE_SYNC_RESPONSE_HASHES, hashes.length) *
1472
+ Math.min(MAX_PENDING_MAYBE_SYNC_RESPONSE_HASHES, targets.length));
1473
+ while (this.isSyncDispatchLifecycleActive(properties.lifecycle) &&
1474
+ targets.every((target) => this.isSyncDispatchLifecycleActive(properties.lifecycle, target))) {
1475
+ const attempt = this.tryReservePendingMaybeSyncResponse({
1476
+ hashes,
1477
+ targets,
1478
+ lifecycle: properties.lifecycle,
691
1479
  });
1480
+ if (attempt.kind === "reserved") {
1481
+ return {
1482
+ reservation: attempt.reservation,
1483
+ conflicts: attempt.conflicts,
1484
+ };
1485
+ }
1486
+ if (attempt.kind !== "capacity") {
1487
+ this.schedulePendingMaybeSyncResponseWaiter();
1488
+ return undefined;
1489
+ }
1490
+ if (this.pendingMaybeSyncResponseWaiters.size >=
1491
+ MAX_PENDING_MAYBE_SYNC_RESPONSE_WAITERS ||
1492
+ this.pendingMaybeSyncResponseWaiterAssociationCount + associations >
1493
+ MAX_PENDING_MAYBE_SYNC_RESPONSE_HASHES) {
1494
+ return undefined;
1495
+ }
1496
+ await this.waitForPendingMaybeSyncResponseChange(properties.lifecycle, targets, attempt.required, associations);
692
1497
  }
693
- const session = {
694
- id,
695
- mode,
696
- startedAt,
697
- timeoutMs,
698
- retryIntervalsMs,
699
- targets: targetStates,
700
- truncated,
701
- deferred,
702
- cancelled: false,
703
- };
704
- if (allHashes.length === 0 || targets.length === 0) {
705
- deferred.resolve(this.buildRepairSessionResult(session, true));
1498
+ return undefined;
1499
+ }
1500
+ expectMaybeSyncResponse(properties) {
1501
+ const targets = [...new Set(properties.targets)];
1502
+ const lifecycle = this.captureSyncDispatchLifecycle(targets, properties.signal, { abortAllOnTargetDisconnect: true });
1503
+ const attempt = this.tryReservePendingMaybeSyncResponse({
1504
+ hashes: properties.hashes,
1505
+ targets,
1506
+ lifecycle,
1507
+ });
1508
+ const reservation = attempt.kind === "reserved" ? attempt.reservation : undefined;
1509
+ let retainedReservation;
1510
+ if (reservation) {
1511
+ // This synchronous helper represents an already-issued request.
1512
+ reservation.markDelivered();
1513
+ const leasedTargets = [...lifecycle.targets.values()];
1514
+ for (const targetLifecycle of leasedTargets) {
1515
+ targetLifecycle.responseLeases += 1;
1516
+ targetLifecycle.lifecycle.retainedWork += 1;
1517
+ }
1518
+ let released = false;
1519
+ retainedReservation = {
1520
+ ...reservation,
1521
+ release: () => {
1522
+ if (released) {
1523
+ return;
1524
+ }
1525
+ released = true;
1526
+ reservation.release();
1527
+ for (const targetLifecycle of leasedTargets) {
1528
+ targetLifecycle.responseLeases -= 1;
1529
+ targetLifecycle.lifecycle.retainedWork -= 1;
1530
+ }
1531
+ this.maybeDisposeSyncDispatchLifecycle(lifecycle);
1532
+ },
1533
+ };
1534
+ }
1535
+ this.finishSyncDispatchLifecycle(lifecycle);
1536
+ return retainedReservation;
1537
+ }
1538
+ consumeAuthorizedMaybeSyncResponse(hashes, from) {
1539
+ const fromHash = from.hashcode();
1540
+ const pendingForTarget = this.pendingMaybeSyncResponses.get(fromHash);
1541
+ if (!pendingForTarget) {
1542
+ return [];
1543
+ }
1544
+ if (this.activeMaybeSyncResponseCount >=
1545
+ MAX_ACTIVE_SIMPLE_SYNC_RESPONSES_GLOBAL ||
1546
+ (this.activeMaybeSyncResponseCountByPeer.get(fromHash) ?? 0) >=
1547
+ MAX_ACTIVE_SIMPLE_SYNC_RESPONSES_PER_PEER) {
1548
+ return [];
1549
+ }
1550
+ const acceptedByLifecycle = new Map();
1551
+ const seen = new Set();
1552
+ let inspected = 0;
1553
+ const iterator = hashes[Symbol.iterator]();
1554
+ let exhausted = false;
1555
+ try {
1556
+ while (inspected < MAX_PENDING_MAYBE_SYNC_RESPONSE_HASHES &&
1557
+ pendingForTarget.size > 0) {
1558
+ const next = iterator.next();
1559
+ if (next.done) {
1560
+ exhausted = true;
1561
+ break;
1562
+ }
1563
+ inspected += 1;
1564
+ const hash = next.value;
1565
+ if (seen.has(hash)) {
1566
+ continue;
1567
+ }
1568
+ seen.add(hash);
1569
+ const authorization = pendingForTarget.get(hash);
1570
+ if (!authorization || authorization.active === true) {
1571
+ continue;
1572
+ }
1573
+ const batch = authorization.batch;
1574
+ const targetLifecycle = batch.targetLifecycle;
1575
+ if (batch.expiresAt <= Date.now() ||
1576
+ !this.isSyncDispatchLifecycleActive(targetLifecycle.lifecycle, fromHash)) {
1577
+ this.removePendingMaybeSyncResponseBatch(batch);
1578
+ continue;
1579
+ }
1580
+ if (pendingForTarget.get(hash)?.batch !== batch) {
1581
+ continue;
1582
+ }
1583
+ let accepted = acceptedByLifecycle.get(targetLifecycle);
1584
+ if (!accepted) {
1585
+ accepted = { hashes: [], authorizations: [] };
1586
+ acceptedByLifecycle.set(targetLifecycle, accepted);
1587
+ targetLifecycle.responseLeases += 1;
1588
+ targetLifecycle.lifecycle.retainedWork += 1;
1589
+ }
1590
+ authorization.active = true;
1591
+ batch.hashes.delete(hash);
1592
+ accepted.hashes.push(hash);
1593
+ accepted.authorizations.push(authorization);
1594
+ if (batch.hashes.size === 0) {
1595
+ this.removePendingMaybeSyncResponseBatch(batch);
1596
+ }
1597
+ }
1598
+ }
1599
+ finally {
1600
+ if (!exhausted) {
1601
+ iterator.return?.();
1602
+ }
1603
+ }
1604
+ if (pendingForTarget.size === 0) {
1605
+ this.pendingMaybeSyncResponses.delete(fromHash);
1606
+ }
1607
+ if (acceptedByLifecycle.size === 0) {
1608
+ return [];
1609
+ }
1610
+ this.activeMaybeSyncResponseCount += 1;
1611
+ this.activeMaybeSyncResponseCountByPeer.set(fromHash, (this.activeMaybeSyncResponseCountByPeer.get(fromHash) ?? 0) + 1);
1612
+ let remainingLeases = acceptedByLifecycle.size;
1613
+ return [...acceptedByLifecycle].map(([targetLifecycle, { hashes: acceptedHashes, authorizations }]) => {
1614
+ let released = false;
706
1615
  return {
707
- id,
708
- done: deferred.promise,
709
- cancel: () => {
710
- // no-op
1616
+ hashes: acceptedHashes,
1617
+ signal: targetLifecycle.controller.signal,
1618
+ release: (options) => {
1619
+ if (released) {
1620
+ return;
1621
+ }
1622
+ released = true;
1623
+ const pendingForTarget = this.pendingMaybeSyncResponses.get(fromHash);
1624
+ for (const authorization of authorizations) {
1625
+ this.settlePendingMaybeSyncResponseAuthorization(authorization, options?.fulfilled === true);
1626
+ if (pendingForTarget?.get(authorization.hash) === authorization) {
1627
+ pendingForTarget.delete(authorization.hash);
1628
+ }
1629
+ }
1630
+ if (pendingForTarget?.size === 0) {
1631
+ this.pendingMaybeSyncResponses.delete(fromHash);
1632
+ }
1633
+ this.pendingMaybeSyncResponseCount -= authorizations.filter((authorization) => authorization.deliveryInFlight !== true).length;
1634
+ targetLifecycle.responseLeases -= 1;
1635
+ targetLifecycle.lifecycle.retainedWork -= 1;
1636
+ remainingLeases -= 1;
1637
+ if (remainingLeases === 0) {
1638
+ this.activeMaybeSyncResponseCount -= 1;
1639
+ const activeForPeer = (this.activeMaybeSyncResponseCountByPeer.get(fromHash) ?? 1) -
1640
+ 1;
1641
+ if (activeForPeer === 0) {
1642
+ this.activeMaybeSyncResponseCountByPeer.delete(fromHash);
1643
+ }
1644
+ else {
1645
+ this.activeMaybeSyncResponseCountByPeer.set(fromHash, activeForPeer);
1646
+ }
1647
+ }
1648
+ this.schedulePendingMaybeSyncResponseWaiter();
1649
+ this.maybeDisposeSyncDispatchLifecycle(targetLifecycle.lifecycle);
711
1650
  },
712
1651
  };
1652
+ });
1653
+ }
1654
+ isRepairSessionComplete(session) {
1655
+ for (const state of session.targets.values()) {
1656
+ if (state.unresolved.size > 0) {
1657
+ return false;
1658
+ }
713
1659
  }
714
- // For capped convergent sessions, still dispatch the full set once so large
715
- // repairs are not limited to tracked hashes.
716
- if (mode === "convergent" && truncated) {
717
- void this.onMaybeMissingEntries({
718
- entries: properties.entries,
719
- targets,
720
- }).catch(() => {
721
- // Best-effort: retries on tracked hashes continue via runRepairSession.
1660
+ return true;
1661
+ }
1662
+ buildRepairSessionResult(session, completed) {
1663
+ const durationMs = Date.now() - session.startedAt;
1664
+ const out = [];
1665
+ for (const [target, state] of session.targets) {
1666
+ const unresolved = [...state.unresolved];
1667
+ out.push({
1668
+ target,
1669
+ requested: state.requestedCount,
1670
+ resolved: state.requestedCount - unresolved.length,
1671
+ unresolved,
1672
+ attempts: state.attempts,
1673
+ durationMs,
1674
+ completed,
1675
+ requestedTotal: state.requestedTotalCount,
1676
+ truncated: session.truncated,
722
1677
  });
723
1678
  }
724
- session.timer = setTimeout(() => {
725
- this.finalizeRepairSession(id, false);
726
- }, timeoutMs);
727
- session.timer.unref?.();
728
- this.repairSessions.set(id, session);
729
- void this.runRepairSession(id).catch(() => {
730
- this.finalizeRepairSession(id, false);
731
- });
732
- return {
733
- id,
734
- done: deferred.promise,
735
- cancel: () => {
736
- this.finalizeRepairSession(id, false);
737
- },
738
- };
1679
+ return out;
739
1680
  }
740
- async onMaybeMissingEntries(properties) {
741
- await this.onMaybeMissingHashes({
742
- hashes: this.getPrioritizedHashes(properties.entries),
743
- targets: properties.targets,
744
- });
1681
+ finalizeRepairSession(sessionId, completed) {
1682
+ const session = this.repairSessions.get(sessionId);
1683
+ if (!session) {
1684
+ return;
1685
+ }
1686
+ this.repairSessions.delete(sessionId);
1687
+ session.cancelled = true;
1688
+ if (session.timer) {
1689
+ clearTimeout(session.timer);
1690
+ }
1691
+ session.deferred.resolve(this.buildRepairSessionResult(session, completed));
745
1692
  }
746
- async onMaybeMissingHashes(properties) {
747
- const profile = this.syncOptions?.profile;
748
- const startedAt = syncProfileStart(profile);
749
- const hashes = [...properties.hashes];
750
- const chunks = this.chunk(hashes, this.maxHashesPerMessage);
751
- try {
752
- await chunks.reduce((promise, chunk) => promise.then(() => this.rpc.send(new RequestMaybeSync({ hashes: chunk }), {
753
- priority: SYNC_MESSAGE_PRIORITY,
754
- mode: new SilentDelivery({
755
- to: properties.targets,
756
- redundancy: 1,
757
- }),
758
- })), Promise.resolve());
1693
+ async refreshRepairSessionState(sessionId) {
1694
+ const session = this.repairSessions.get(sessionId);
1695
+ if (!session) {
1696
+ return;
1697
+ }
1698
+ for (const state of session.targets.values()) {
1699
+ const resolved = typeof this.log.hasMany === "function"
1700
+ ? await this.log.hasMany(state.unresolved)
1701
+ : await this.getExistingRepairHashes(state.unresolved);
1702
+ for (const hash of resolved) {
1703
+ state.unresolved.delete(hash);
1704
+ }
1705
+ }
1706
+ }
1707
+ async getExistingRepairHashes(hashes) {
1708
+ const resolved = new Set();
1709
+ for (const hash of hashes) {
1710
+ if (await this.log.has(hash)) {
1711
+ resolved.add(hash);
1712
+ }
1713
+ }
1714
+ return resolved;
1715
+ }
1716
+ markRepairSessionResolvedHashes(hashes) {
1717
+ if (hashes.length === 0 || this.repairSessions.size === 0) {
1718
+ return;
1719
+ }
1720
+ for (const [sessionId, session] of this.repairSessions) {
1721
+ for (const state of session.targets.values()) {
1722
+ for (const hash of hashes) {
1723
+ state.unresolved.delete(hash);
1724
+ }
1725
+ }
1726
+ if (this.isRepairSessionComplete(session)) {
1727
+ this.finalizeRepairSession(sessionId, true);
1728
+ }
1729
+ }
1730
+ }
1731
+ markRepairSessionResolvedHash(hash) {
1732
+ if (this.repairSessions.size === 0) {
1733
+ return;
1734
+ }
1735
+ for (const [sessionId, session] of this.repairSessions) {
1736
+ for (const state of session.targets.values()) {
1737
+ state.unresolved.delete(hash);
1738
+ }
1739
+ if (this.isRepairSessionComplete(session)) {
1740
+ this.finalizeRepairSession(sessionId, true);
1741
+ }
1742
+ }
1743
+ }
1744
+ async runRepairSession(sessionId) {
1745
+ const session = this.repairSessions.get(sessionId);
1746
+ if (!session) {
1747
+ return;
1748
+ }
1749
+ let previousDelay = 0;
1750
+ for (const delayMs of session.retryIntervalsMs) {
1751
+ if (!this.repairSessions.has(sessionId) || this.closed) {
1752
+ return;
1753
+ }
1754
+ const waitMs = Math.max(0, delayMs - previousDelay);
1755
+ previousDelay = delayMs;
1756
+ if (waitMs > 0) {
1757
+ await new Promise((resolve) => {
1758
+ const timer = setTimeout(resolve, waitMs);
1759
+ timer.unref?.();
1760
+ });
1761
+ }
1762
+ if (!this.repairSessions.has(sessionId) || this.closed) {
1763
+ return;
1764
+ }
1765
+ await this.refreshRepairSessionState(sessionId);
1766
+ const current = this.repairSessions.get(sessionId);
1767
+ if (!current) {
1768
+ return;
1769
+ }
1770
+ if (this.isRepairSessionComplete(current)) {
1771
+ this.finalizeRepairSession(sessionId, true);
1772
+ return;
1773
+ }
1774
+ for (const [target, state] of current.targets) {
1775
+ if (state.unresolved.size === 0) {
1776
+ continue;
1777
+ }
1778
+ state.attempts += 1;
1779
+ try {
1780
+ await this.requestSync([...state.unresolved], [target], {
1781
+ targetEpochs: new Map([[target, state.targetEpoch]]),
1782
+ createTargetEpochs: false,
1783
+ });
1784
+ }
1785
+ catch {
1786
+ // Best-effort: keep unresolved and let retries/timeout determine outcome.
1787
+ }
1788
+ }
1789
+ await this.refreshRepairSessionState(sessionId);
1790
+ const afterSend = this.repairSessions.get(sessionId);
1791
+ if (!afterSend) {
1792
+ return;
1793
+ }
1794
+ if (this.isRepairSessionComplete(afterSend)) {
1795
+ this.finalizeRepairSession(sessionId, true);
1796
+ return;
1797
+ }
1798
+ if (afterSend.mode === "best-effort") {
1799
+ this.finalizeRepairSession(sessionId, false);
1800
+ return;
1801
+ }
1802
+ }
1803
+ for (;;) {
1804
+ if (!this.repairSessions.has(sessionId) || this.closed) {
1805
+ return;
1806
+ }
1807
+ await this.refreshRepairSessionState(sessionId);
1808
+ const current = this.repairSessions.get(sessionId);
1809
+ if (!current) {
1810
+ return;
1811
+ }
1812
+ if (this.isRepairSessionComplete(current)) {
1813
+ this.finalizeRepairSession(sessionId, true);
1814
+ return;
1815
+ }
1816
+ await new Promise((resolve) => {
1817
+ const timer = setTimeout(resolve, SESSION_POLL_INTERVAL_MS);
1818
+ timer.unref?.();
1819
+ });
1820
+ }
1821
+ }
1822
+ getQueuedSyncKey(key) {
1823
+ if (this.syncInFlightQueue.has(key)) {
1824
+ return key;
1825
+ }
1826
+ if (typeof key === "string") {
1827
+ for (const queuedKey of this.syncInFlightQueue.keys()) {
1828
+ if (typeof queuedKey === "bigint" &&
1829
+ this.coordinateToHash.get(queuedKey) === key) {
1830
+ return queuedKey;
1831
+ }
1832
+ }
1833
+ return undefined;
1834
+ }
1835
+ const hash = this.coordinateToHash.get(key);
1836
+ return hash && this.syncInFlightQueue.has(hash) ? hash : undefined;
1837
+ }
1838
+ startRepairSession(properties) {
1839
+ const mode = properties.mode ?? "best-effort";
1840
+ const startedAt = Date.now();
1841
+ const timeoutMs = Math.max(1, Math.floor(properties.timeoutMs ??
1842
+ (mode === "convergent"
1843
+ ? DEFAULT_CONVERGENT_REPAIR_TIMEOUT_MS
1844
+ : DEFAULT_CONVERGENT_REPAIR_TIMEOUT_MS)));
1845
+ const retryIntervalsMs = this.normalizeRetryIntervals(mode, properties.retryIntervalsMs);
1846
+ const allHashes = this.getPrioritizedHashes(properties.entries);
1847
+ const trackedHashes = mode === "convergent" &&
1848
+ allHashes.length > this.maxConvergentTrackedHashes
1849
+ ? allHashes.slice(0, this.maxConvergentTrackedHashes)
1850
+ : allHashes;
1851
+ const truncated = trackedHashes.length < allHashes.length;
1852
+ const targets = [...new Set(properties.targets)];
1853
+ const id = `repair-${++this.repairSessionCounter}`;
1854
+ const deferred = createDeferred();
1855
+ const targetStates = new Map();
1856
+ for (const target of targets) {
1857
+ targetStates.set(target, {
1858
+ unresolved: new Set(trackedHashes),
1859
+ requestedCount: trackedHashes.length,
1860
+ requestedTotalCount: allHashes.length,
1861
+ attempts: 0,
1862
+ targetEpoch: this.getOrCreateSyncDispatchTargetEpoch(target),
1863
+ });
1864
+ }
1865
+ const session = {
1866
+ id,
1867
+ mode,
1868
+ startedAt,
1869
+ timeoutMs,
1870
+ retryIntervalsMs,
1871
+ targets: targetStates,
1872
+ truncated,
1873
+ deferred,
1874
+ cancelled: false,
1875
+ };
1876
+ if (allHashes.length === 0 || targets.length === 0) {
1877
+ deferred.resolve(this.buildRepairSessionResult(session, true));
1878
+ return {
1879
+ id,
1880
+ done: deferred.promise,
1881
+ cancel: () => {
1882
+ // no-op
1883
+ },
1884
+ };
1885
+ }
1886
+ // For capped convergent sessions, still dispatch the full set once so large
1887
+ // repairs are not limited to tracked hashes.
1888
+ if (mode === "convergent" && truncated) {
1889
+ void this.onMaybeMissingEntries({
1890
+ entries: properties.entries,
1891
+ targets,
1892
+ }).catch(() => {
1893
+ // Best-effort: retries on tracked hashes continue via runRepairSession.
1894
+ });
1895
+ }
1896
+ session.timer = setTimeout(() => {
1897
+ this.finalizeRepairSession(id, false);
1898
+ }, timeoutMs);
1899
+ session.timer.unref?.();
1900
+ this.repairSessions.set(id, session);
1901
+ void this.runRepairSession(id).catch(() => {
1902
+ this.finalizeRepairSession(id, false);
1903
+ });
1904
+ return {
1905
+ id,
1906
+ done: deferred.promise,
1907
+ cancel: () => {
1908
+ this.finalizeRepairSession(id, false);
1909
+ },
1910
+ };
1911
+ }
1912
+ async onMaybeMissingEntries(properties) {
1913
+ if (properties.signal?.aborted) {
1914
+ return;
1915
+ }
1916
+ await this.onMaybeMissingHashes({
1917
+ hashes: this.getPrioritizedHashes(properties.entries),
1918
+ targets: properties.targets,
1919
+ signal: properties.signal,
1920
+ });
1921
+ }
1922
+ async onMaybeMissingHashes(properties) {
1923
+ const targets = [...new Set(properties.targets)];
1924
+ const lifecycle = this.captureSyncDispatchLifecycle(targets, properties.signal);
1925
+ if (!this.isSyncDispatchLifecycleActive(lifecycle)) {
1926
+ this.finishSyncDispatchLifecycle(lifecycle);
1927
+ return;
1928
+ }
1929
+ const profile = this.syncOptions?.profile;
1930
+ const startedAt = syncProfileStart(profile);
1931
+ const hashes = [...new Set(properties.hashes)];
1932
+ const targetsPerAuthorizationWindow = Math.max(1, Math.min(targets.length, MAX_PENDING_MAYBE_SYNC_RESPONSE_HASHES));
1933
+ const chunks = this.chunk(hashes, Math.max(1, Math.min(this.maxHashesPerMessage, Math.floor(MAX_PENDING_MAYBE_SYNC_RESPONSE_HASHES /
1934
+ targetsPerAuthorizationWindow))));
1935
+ let messages = 0;
1936
+ try {
1937
+ for (const chunk of chunks) {
1938
+ if (!this.isSyncDispatchLifecycleActive(lifecycle)) {
1939
+ break;
1940
+ }
1941
+ for (const target of targets) {
1942
+ if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
1943
+ continue;
1944
+ }
1945
+ let hashesToAuthorize = chunk;
1946
+ while (hashesToAuthorize.length > 0 &&
1947
+ this.isSyncDispatchLifecycleActive(lifecycle, target)) {
1948
+ const reserved = await this.reservePendingMaybeSyncResponse({
1949
+ hashes: hashesToAuthorize,
1950
+ targets: [target],
1951
+ lifecycle,
1952
+ });
1953
+ if (!reserved ||
1954
+ !this.isSyncDispatchLifecycleActive(lifecycle, target)) {
1955
+ break;
1956
+ }
1957
+ const { reservation, conflicts } = reserved;
1958
+ const hashesToSend = reservation.newlyAuthorizedByTarget.get(target) ?? [];
1959
+ if (!reservation.retained()) {
1960
+ reservation.release();
1961
+ break;
1962
+ }
1963
+ if (hashesToSend.length > 0) {
1964
+ reservation.beginDelivery();
1965
+ try {
1966
+ await this.rpc.send(new RequestMaybeSync({ hashes: hashesToSend }), {
1967
+ priority: SYNC_MESSAGE_PRIORITY,
1968
+ mode: new SilentDelivery({
1969
+ to: [target],
1970
+ redundancy: 1,
1971
+ }),
1972
+ signal: this.getSyncDispatchSignal(lifecycle, target),
1973
+ });
1974
+ reservation.markDelivered();
1975
+ messages += 1;
1976
+ }
1977
+ catch (error) {
1978
+ reservation.release();
1979
+ if (!this.isSyncDispatchLifecycleActive(lifecycle) ||
1980
+ !this.isSyncDispatchLifecycleActive(lifecycle, target)) {
1981
+ break;
1982
+ }
1983
+ throw error;
1984
+ }
1985
+ finally {
1986
+ reservation.finishDelivery();
1987
+ }
1988
+ }
1989
+ hashesToAuthorize =
1990
+ await this.waitForPendingMaybeSyncResponseConflicts(conflicts, lifecycle, target, hashesToAuthorize.length);
1991
+ }
1992
+ }
1993
+ if (!this.isSyncDispatchLifecycleActive(lifecycle)) {
1994
+ break;
1995
+ }
1996
+ }
1997
+ }
1998
+ finally {
1999
+ this.finishSyncDispatchLifecycle(lifecycle);
2000
+ if (profile) {
2001
+ emitSyncProfileDuration(profile, startedAt, {
2002
+ name: "simple.onMaybeMissingEntries",
2003
+ entries: hashes.length,
2004
+ messages,
2005
+ targets: targets.length,
2006
+ details: !this.isSyncDispatchLifecycleActive(lifecycle)
2007
+ ? { cancelled: true }
2008
+ : undefined,
2009
+ });
2010
+ }
2011
+ }
2012
+ }
2013
+ /**
2014
+ * Ship exchange heads to one peer: the fused native raw path when the
2015
+ * peer advertised raw capability and the shared log provided a fused
2016
+ * sender, otherwise the TS message path (raw or plain by capability).
2017
+ * Returns the number of messages sent and whether the fused path ran.
2018
+ */
2019
+ async shipExchangeHeads(hashes, to, canReceiveRaw, signal) {
2020
+ if (signal?.aborted) {
2021
+ return { messages: 0, fused: false };
2022
+ }
2023
+ if (canReceiveRaw && this.sendRawExchangeHeads) {
2024
+ let sentMessages;
2025
+ try {
2026
+ sentMessages = await this.sendRawExchangeHeads(hashes, [to.hashcode()], { signal });
2027
+ }
2028
+ catch (error) {
2029
+ if (signal?.aborted) {
2030
+ return { messages: 0, fused: true };
2031
+ }
2032
+ throw error;
2033
+ }
2034
+ if (sentMessages !== undefined) {
2035
+ return { messages: sentMessages, fused: true };
2036
+ }
2037
+ }
2038
+ let messages = 0;
2039
+ const messageGenerator = canReceiveRaw
2040
+ ? createRawExchangeHeadsMessages(this.log, hashes, this.syncOptions?.profile)
2041
+ : createExchangeHeadsMessages(this.log, hashes);
2042
+ for await (const message of messageGenerator) {
2043
+ if (signal?.aborted) {
2044
+ break;
2045
+ }
2046
+ try {
2047
+ await this.rpc.send(message, {
2048
+ mode: new SilentDelivery({ to: [to], redundancy: 1 }),
2049
+ signal,
2050
+ });
2051
+ messages += 1;
2052
+ }
2053
+ catch (error) {
2054
+ if (signal?.aborted) {
2055
+ break;
2056
+ }
2057
+ throw error;
2058
+ }
2059
+ }
2060
+ return { messages, fused: false };
2061
+ }
2062
+ /**
2063
+ * Ships hashes that the caller has already authorized for this exact sender.
2064
+ *
2065
+ * This deliberately does not consult or extend Simple's bounded pending-response
2066
+ * window. Rateless sync owns a separate, target-scoped authorization lifecycle
2067
+ * and uses this helper only after intersecting the response with that process's
2068
+ * exact advertised hash set.
2069
+ */
2070
+ async shipAuthorizedMaybeSyncResponse(properties) {
2071
+ const hashes = this.filterRecentlySentExchangeHeads(properties.hashes, properties.from);
2072
+ try {
2073
+ const shipped = await this.shipExchangeHeads(hashes, properties.from, canReceiveRawExchangeHeads(properties.response), properties.signal);
2074
+ return {
2075
+ ...shipped,
2076
+ entries: hashes.length,
2077
+ };
2078
+ }
2079
+ catch (error) {
2080
+ this.forgetRecentlySentExchangeHeads(hashes, properties.from);
2081
+ throw error;
2082
+ }
2083
+ finally {
2084
+ if (properties.signal.aborted) {
2085
+ this.forgetRecentlySentExchangeHeads(hashes, properties.from);
2086
+ }
2087
+ }
2088
+ }
2089
+ async shipAuthorizedMaybeSyncResponseLeases(properties) {
2090
+ if (properties.leases.length === 0) {
2091
+ return { messages: 0, fused: false, entries: 0 };
2092
+ }
2093
+ const profile = this.syncOptions?.profile;
2094
+ const startedAt = syncProfileStart(profile);
2095
+ let messages = 0;
2096
+ let fused = false;
2097
+ let entries = 0;
2098
+ let firstError;
2099
+ for (const lease of properties.leases) {
2100
+ let fulfilled = false;
2101
+ try {
2102
+ const shipped = await this.shipAuthorizedMaybeSyncResponse({
2103
+ hashes: lease.hashes,
2104
+ from: properties.from,
2105
+ response: properties.response,
2106
+ signal: lease.signal,
2107
+ });
2108
+ messages += shipped.messages;
2109
+ fused ||= shipped.fused;
2110
+ entries += shipped.entries;
2111
+ fulfilled = !lease.signal.aborted;
2112
+ }
2113
+ catch (error) {
2114
+ firstError ??= error;
2115
+ }
2116
+ finally {
2117
+ lease.release({ fulfilled });
2118
+ }
2119
+ }
2120
+ if (profile) {
2121
+ emitSyncProfileDuration(profile, startedAt, {
2122
+ name: "simple.exchangeHeads",
2123
+ entries,
2124
+ messages,
2125
+ targets: 1,
2126
+ details: {
2127
+ source: properties.source ?? "responseMaybeSync",
2128
+ fused,
2129
+ },
2130
+ });
2131
+ }
2132
+ if (firstError !== undefined) {
2133
+ throw firstError;
2134
+ }
2135
+ return { messages, fused, entries };
2136
+ }
2137
+ async onMessage(msg, context) {
2138
+ const from = context.from;
2139
+ if (msg instanceof RequestMaybeSync) {
2140
+ await this.queueSync(msg.hashes, from);
2141
+ return true;
2142
+ }
2143
+ else if (msg instanceof ResponseMaybeSync ||
2144
+ msg instanceof ResponseMaybeSyncCapabilities) {
2145
+ // TODO perhaps send less messages to more receivers for performance reasons?
2146
+ // TODO wait for previous send to target before trying to send more?
2147
+ const pending = this.consumeAuthorizedMaybeSyncResponse(msg.hashes, from);
2148
+ if (pending.length === 0) {
2149
+ return true;
2150
+ }
2151
+ await this.shipAuthorizedMaybeSyncResponseLeases({
2152
+ leases: pending,
2153
+ from,
2154
+ response: msg,
2155
+ });
2156
+ return true;
2157
+ }
2158
+ else if (msg instanceof RequestMaybeSyncCoordinate ||
2159
+ msg instanceof RequestMaybeSyncCoordinateCapabilities) {
2160
+ if (msg.hashNumbers.length === 0 ||
2161
+ msg.hashNumbers.length > MAX_SIMPLE_COORDINATE_REQUEST_SYMBOLS) {
2162
+ return true;
2163
+ }
2164
+ const target = from.hashcode();
2165
+ const releaseLookup = this.tryAcquireCoordinateLookup(target);
2166
+ if (!releaseLookup) {
2167
+ return true;
2168
+ }
2169
+ const lifecycle = this.captureSyncDispatchLifecycle([target]);
2170
+ let lookupReleased = false;
2171
+ const finishLookup = () => {
2172
+ if (lookupReleased) {
2173
+ return;
2174
+ }
2175
+ lookupReleased = true;
2176
+ releaseLookup();
2177
+ };
2178
+ try {
2179
+ if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
2180
+ return true;
2181
+ }
2182
+ const symbols = [...new Set(msg.hashNumbers)];
2183
+ const profile = this.syncOptions?.profile;
2184
+ const lookupStartedAt = syncProfileStart(profile);
2185
+ const hashes = await getHashesFromSymbols(symbols, this.entryIndex, this.coordinateToHash, this.resolveHashesForSymbols, this.resolveHashListForSymbols, MAX_SIMPLE_COORDINATE_RESPONSE_HASHES);
2186
+ finishLookup();
2187
+ if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
2188
+ return true;
2189
+ }
2190
+ if (profile) {
2191
+ emitSyncProfileDuration(profile, lookupStartedAt, {
2192
+ name: "simple.coordinateLookup",
2193
+ entries: hashLookupResultSize(hashes),
2194
+ symbols: symbols.length,
2195
+ });
2196
+ }
2197
+ const releaseResponse = this.tryAcquireCoordinateResponse(target);
2198
+ if (!releaseResponse) {
2199
+ return true;
2200
+ }
2201
+ const exchangeStartedAt = syncProfileStart(profile);
2202
+ let hashesToSend = [];
2203
+ let messages = 0;
2204
+ let fused = false;
2205
+ try {
2206
+ hashesToSend = this.filterRecentlySentExchangeHeads(hashes, from);
2207
+ // dont set priority 1 here because this will block other messages that should higher priority
2208
+ ({ messages, fused } = await this.shipExchangeHeads(hashesToSend, context.from, canReceiveRawExchangeHeads(msg), this.getSyncDispatchSignal(lifecycle, target)));
2209
+ }
2210
+ finally {
2211
+ releaseResponse();
2212
+ if (profile) {
2213
+ emitSyncProfileDuration(profile, exchangeStartedAt, {
2214
+ name: "simple.exchangeHeads",
2215
+ entries: hashesToSend.length,
2216
+ messages,
2217
+ targets: 1,
2218
+ details: {
2219
+ source: "requestMaybeSyncCoordinate",
2220
+ fused,
2221
+ },
2222
+ });
2223
+ }
2224
+ }
2225
+ return true;
2226
+ }
2227
+ finally {
2228
+ finishLookup();
2229
+ this.finishSyncDispatchLifecycle(lifecycle);
2230
+ }
2231
+ }
2232
+ else {
2233
+ return false; // no message was consumed
2234
+ }
2235
+ }
2236
+ onReceivedEntries(properties) {
2237
+ return this.onReceivedEntryHashes({
2238
+ hashes: properties.entries.map((entry) => entry.entry.hash),
2239
+ from: properties.from,
2240
+ });
2241
+ }
2242
+ onReceivedEntryHashes(properties) {
2243
+ this.clearSyncInFlightForPeerHashes(properties.from.hashcode(), properties.hashes);
2244
+ this.markRepairSessionResolvedHashes(properties.hashes);
2245
+ }
2246
+ getPendingSyncKeyIdentity(key) {
2247
+ if (typeof key === "string") {
2248
+ return key;
2249
+ }
2250
+ const hash = this.coordinateToHash.get(key);
2251
+ return hash ?? key;
2252
+ }
2253
+ removeQueuedSyncCoordinateAlias(key) {
2254
+ this.syncInFlightQueuedCoordinates.delete(key);
2255
+ if (this.syncInFlightQueuedCoordinates.size === 0) {
2256
+ this.syncInFlightQueuedCoordinateRefreshIterator = undefined;
2257
+ }
2258
+ const previousHash = this.syncInFlightQueuedHashByCoordinate.get(key);
2259
+ this.syncInFlightQueuedHashByCoordinate.delete(key);
2260
+ if (previousHash != null) {
2261
+ const coordinates = this.syncInFlightQueuedCoordinatesByHash.get(previousHash);
2262
+ coordinates?.delete(key);
2263
+ if (coordinates?.size === 0) {
2264
+ this.syncInFlightQueuedCoordinatesByHash.delete(previousHash);
2265
+ }
2266
+ }
2267
+ }
2268
+ refreshQueuedSyncCoordinateAlias(key) {
2269
+ if (!this.syncInFlightQueue.has(key)) {
2270
+ this.removeQueuedSyncCoordinateAlias(key);
2271
+ return;
2272
+ }
2273
+ this.syncInFlightQueuedCoordinates.add(key);
2274
+ const hash = this.coordinateToHash.get(key) ?? undefined;
2275
+ const previousHash = this.syncInFlightQueuedHashByCoordinate.get(key);
2276
+ if (previousHash !== hash) {
2277
+ if (previousHash != null) {
2278
+ const coordinates = this.syncInFlightQueuedCoordinatesByHash.get(previousHash);
2279
+ coordinates?.delete(key);
2280
+ if (coordinates?.size === 0) {
2281
+ this.syncInFlightQueuedCoordinatesByHash.delete(previousHash);
2282
+ }
2283
+ }
2284
+ if (hash == null) {
2285
+ this.syncInFlightQueuedHashByCoordinate.delete(key);
2286
+ }
2287
+ else {
2288
+ this.syncInFlightQueuedHashByCoordinate.set(key, hash);
2289
+ }
2290
+ }
2291
+ if (hash != null) {
2292
+ let coordinates = this.syncInFlightQueuedCoordinatesByHash.get(hash);
2293
+ if (!coordinates) {
2294
+ coordinates = new Set();
2295
+ this.syncInFlightQueuedCoordinatesByHash.set(hash, coordinates);
2296
+ }
2297
+ coordinates.add(key);
2298
+ this.reconcileQueuedSyncCoordinateAlias(key, hash);
2299
+ }
2300
+ }
2301
+ reconcileQueuedSyncCoordinateAlias(coordinate, hash) {
2302
+ const now = Date.now();
2303
+ const coordinateExpiresAt = this.syncInFlightQueueExpiresAt.get(coordinate);
2304
+ if (coordinateExpiresAt != null && coordinateExpiresAt <= now) {
2305
+ this.clearSyncProcessKey(coordinate);
2306
+ return;
2307
+ }
2308
+ const hashExpiresAt = this.syncInFlightQueueExpiresAt.get(hash);
2309
+ if (hashExpiresAt != null && hashExpiresAt <= now) {
2310
+ this.clearSyncProcessKey(hash);
2311
+ return;
2312
+ }
2313
+ const hashClaimants = this.syncInFlightQueue.get(hash);
2314
+ if (!hashClaimants || !this.syncInFlightQueue.has(coordinate)) {
2315
+ return;
2316
+ }
2317
+ const expiresAt = Math.min(this.syncInFlightQueueExpiresAt.get(coordinate) ?? Infinity, this.syncInFlightQueueExpiresAt.get(hash) ?? Infinity);
2318
+ for (const claimant of [...hashClaimants]) {
2319
+ this.addPendingSyncClaim(coordinate, claimant, expiresAt);
2320
+ }
2321
+ if (Number.isFinite(expiresAt)) {
2322
+ this.movePendingSyncKeyExpiryEarlier(coordinate, expiresAt);
2323
+ }
2324
+ for (const target of [...(this.syncInFlightTargetsByKey.get(hash) ?? [])]) {
2325
+ const state = this.syncInFlight.get(target)?.get(hash);
2326
+ if (state) {
2327
+ this.setSyncInFlightTargetKey(target, coordinate, state.timestamp);
2328
+ }
2329
+ }
2330
+ this.clearSyncProcessKey(hash);
2331
+ }
2332
+ refreshQueuedSyncCoordinateAliases() {
2333
+ if (this.syncInFlightQueuedCoordinates.size === 0 &&
2334
+ this.syncInFlightQueueClaimants.size < this.syncInFlightQueue.size) {
2335
+ // Defensive compatibility for callers/tests that seed the public queue
2336
+ // directly. Keep hydration bounded; internal writes register coordinates
2337
+ // when the key is first admitted.
2338
+ let inspected = 0;
2339
+ for (const key of this.syncInFlightQueue.keys()) {
2340
+ if (typeof key === "bigint") {
2341
+ this.syncInFlightQueuedCoordinates.add(key);
2342
+ }
2343
+ inspected += 1;
2344
+ if (inspected >= MAX_PENDING_SIMPLE_SYNC_ALIAS_REFRESH_PER_MESSAGE) {
2345
+ break;
2346
+ }
2347
+ }
2348
+ }
2349
+ if (this.syncInFlightQueuedCoordinates.size === 0) {
2350
+ this.syncInFlightQueuedCoordinateRefreshIterator = undefined;
2351
+ return;
2352
+ }
2353
+ this.syncInFlightQueuedCoordinateRefreshIterator ??=
2354
+ this.syncInFlightQueuedCoordinates.values();
2355
+ for (let refreshed = 0; refreshed < MAX_PENDING_SIMPLE_SYNC_ALIAS_REFRESH_PER_MESSAGE; refreshed += 1) {
2356
+ const next = this.syncInFlightQueuedCoordinateRefreshIterator.next();
2357
+ if (next.done) {
2358
+ this.syncInFlightQueuedCoordinateRefreshIterator = undefined;
2359
+ break;
2360
+ }
2361
+ this.refreshQueuedSyncCoordinateAlias(next.value);
2362
+ }
2363
+ }
2364
+ getQueuedSyncKeyForAdmission(key) {
2365
+ const getValidQueuedKey = (candidate) => {
2366
+ if (!this.syncInFlightQueue.has(candidate)) {
2367
+ return undefined;
2368
+ }
2369
+ const expiresAt = this.syncInFlightQueueExpiresAt.get(candidate);
2370
+ if (expiresAt != null && expiresAt <= Date.now()) {
2371
+ this.clearSyncProcessKey(candidate);
2372
+ return undefined;
2373
+ }
2374
+ return candidate;
2375
+ };
2376
+ if (getValidQueuedKey(key) != null) {
2377
+ if (typeof key === "bigint") {
2378
+ this.refreshQueuedSyncCoordinateAlias(key);
2379
+ return getValidQueuedKey(key);
2380
+ }
2381
+ return key;
2382
+ }
2383
+ if (typeof key === "string") {
2384
+ const aliases = this.syncInFlightQueuedCoordinatesByHash.get(key);
2385
+ if (aliases) {
2386
+ let inspected = 0;
2387
+ for (const alias of aliases) {
2388
+ if (inspected >= MAX_PENDING_SIMPLE_SYNC_ALIAS_REFRESH_PER_MESSAGE) {
2389
+ return QUEUED_SYNC_ALIAS_REFRESH_PENDING;
2390
+ }
2391
+ inspected += 1;
2392
+ this.refreshQueuedSyncCoordinateAlias(alias);
2393
+ if (this.syncInFlightQueuedCoordinatesByHash.get(key)?.has(alias) ===
2394
+ true) {
2395
+ const validAlias = getValidQueuedKey(alias);
2396
+ if (validAlias != null) {
2397
+ return validAlias;
2398
+ }
2399
+ }
2400
+ }
2401
+ if ((this.syncInFlightQueuedCoordinatesByHash.get(key)?.size ?? 0) > 0) {
2402
+ return QUEUED_SYNC_ALIAS_REFRESH_PENDING;
2403
+ }
2404
+ }
2405
+ return undefined;
2406
+ }
2407
+ const hash = this.coordinateToHash.get(key);
2408
+ return hash != null ? getValidQueuedKey(hash) : undefined;
2409
+ }
2410
+ addPendingSyncClaim(key, from, expiresAt) {
2411
+ const fromHash = from.hashcode();
2412
+ let peers = this.syncInFlightQueue.get(key);
2413
+ let claimants = this.syncInFlightQueueClaimants.get(key);
2414
+ let claimantIndexes = this.syncInFlightQueueClaimantIndexes.get(key);
2415
+ if (!peers) {
2416
+ peers = [];
2417
+ this.syncInFlightQueue.set(key, peers);
2418
+ claimants = new Set();
2419
+ this.syncInFlightQueueClaimants.set(key, claimants);
2420
+ claimantIndexes = new Map();
2421
+ this.syncInFlightQueueClaimantIndexes.set(key, claimantIndexes);
2422
+ const deadline = expiresAt ?? Date.now() + PENDING_SIMPLE_SYNC_KEY_TTL_MS;
2423
+ this.syncInFlightQueueExpiresAt.set(key, deadline);
2424
+ const expiryNode = {
2425
+ kind: "key",
2426
+ key,
2427
+ expiresAt: deadline,
2428
+ heapIndex: -1,
2429
+ };
2430
+ this.pendingSyncKeyExpiryNodes.set(key, expiryNode);
2431
+ this.pushPendingSyncExpiry(expiryNode);
2432
+ if (typeof key === "bigint") {
2433
+ this.refreshQueuedSyncCoordinateAlias(key);
2434
+ }
2435
+ }
2436
+ else if (!claimants) {
2437
+ // Defensive compatibility for callers/tests that seed the public queue
2438
+ // maps directly. Internally every retained key gets this set at creation.
2439
+ claimants = new Set(peers.map((peer) => peer.hashcode()));
2440
+ this.syncInFlightQueueClaimants.set(key, claimants);
2441
+ this.pendingSyncClaimCount += claimants.size;
2442
+ }
2443
+ if (!claimantIndexes) {
2444
+ claimantIndexes = new Map();
2445
+ for (let index = 0; index < peers.length; index += 1) {
2446
+ claimantIndexes.set(peers[index].hashcode(), index);
2447
+ }
2448
+ this.syncInFlightQueueClaimantIndexes.set(key, claimantIndexes);
2449
+ }
2450
+ if (claimants.has(fromHash)) {
2451
+ return false;
2452
+ }
2453
+ claimantIndexes.set(fromHash, peers.length);
2454
+ peers.push(from);
2455
+ claimants.add(fromHash);
2456
+ let inverted = this.syncInFlightQueueInverted.get(fromHash);
2457
+ if (!inverted) {
2458
+ inverted = new Set();
2459
+ this.syncInFlightQueueInverted.set(fromHash, inverted);
2460
+ }
2461
+ inverted.add(key);
2462
+ this.pendingSyncClaimCount += 1;
2463
+ this.schedulePendingSyncKeyExpiry();
2464
+ return true;
2465
+ }
2466
+ hasPendingSyncClaim(key, peer) {
2467
+ const claimants = this.syncInFlightQueueClaimants.get(key);
2468
+ if (claimants) {
2469
+ return claimants.has(peer);
2470
+ }
2471
+ const peers = this.syncInFlightQueue.get(key);
2472
+ if (!peers) {
2473
+ return false;
2474
+ }
2475
+ const hydrated = new Set(peers.map((candidate) => candidate.hashcode()));
2476
+ this.syncInFlightQueueClaimants.set(key, hydrated);
2477
+ const indexes = new Map();
2478
+ for (let index = 0; index < peers.length; index += 1) {
2479
+ indexes.set(peers[index].hashcode(), index);
2480
+ }
2481
+ this.syncInFlightQueueClaimantIndexes.set(key, indexes);
2482
+ this.pendingSyncClaimCount += hydrated.size;
2483
+ return hydrated.has(peer);
2484
+ }
2485
+ filterDispatchablePendingSyncClaims(keys, peer, epoch) {
2486
+ if (this.syncDispatchTargetEpochs.get(peer) !== epoch) {
2487
+ return [];
2488
+ }
2489
+ const now = Date.now();
2490
+ const dispatchable = [];
2491
+ for (const key of keys) {
2492
+ const expiresAt = this.syncInFlightQueueExpiresAt.get(key);
2493
+ if (expiresAt != null && expiresAt <= now) {
2494
+ this.clearSyncProcessKey(key);
2495
+ continue;
2496
+ }
2497
+ if (this.syncInFlightQueue.has(key) &&
2498
+ this.hasPendingSyncClaim(key, peer)) {
2499
+ dispatchable.push(key);
2500
+ }
2501
+ }
2502
+ return dispatchable;
2503
+ }
2504
+ canStartPendingSyncLookup(peer) {
2505
+ return (this.pendingSyncAdmissionReservations.size +
2506
+ this.pendingCoordinateLookupCount <
2507
+ MAX_PENDING_SIMPLE_SYNC_LOOKUPS_GLOBAL &&
2508
+ (this.pendingSyncAdmissionReservationsByPeer.get(peer)?.size ?? 0) +
2509
+ (this.pendingCoordinateLookupCountByPeer.get(peer) ?? 0) <
2510
+ MAX_PENDING_SIMPLE_SYNC_LOOKUPS_PER_PEER);
2511
+ }
2512
+ tryAcquireCoordinateLookup(peer) {
2513
+ if (!this.canStartPendingSyncLookup(peer)) {
2514
+ return undefined;
2515
+ }
2516
+ this.pendingCoordinateLookupCount += 1;
2517
+ this.pendingCoordinateLookupCountByPeer.set(peer, (this.pendingCoordinateLookupCountByPeer.get(peer) ?? 0) + 1);
2518
+ let released = false;
2519
+ return () => {
2520
+ if (released) {
2521
+ return;
2522
+ }
2523
+ released = true;
2524
+ this.pendingCoordinateLookupCount -= 1;
2525
+ const remaining = (this.pendingCoordinateLookupCountByPeer.get(peer) ?? 1) - 1;
2526
+ if (remaining === 0) {
2527
+ this.pendingCoordinateLookupCountByPeer.delete(peer);
2528
+ }
2529
+ else {
2530
+ this.pendingCoordinateLookupCountByPeer.set(peer, remaining);
2531
+ }
2532
+ };
2533
+ }
2534
+ tryAcquireCoordinateResponse(peer) {
2535
+ if (this.pendingCoordinateResponseCount >=
2536
+ MAX_PENDING_SIMPLE_COORDINATE_RESPONSES_GLOBAL ||
2537
+ (this.pendingCoordinateResponseCountByPeer.get(peer) ?? 0) >=
2538
+ MAX_PENDING_SIMPLE_COORDINATE_RESPONSES_PER_PEER) {
2539
+ return undefined;
2540
+ }
2541
+ this.pendingCoordinateResponseCount += 1;
2542
+ this.pendingCoordinateResponseCountByPeer.set(peer, (this.pendingCoordinateResponseCountByPeer.get(peer) ?? 0) + 1);
2543
+ let released = false;
2544
+ return () => {
2545
+ if (released) {
2546
+ return;
2547
+ }
2548
+ released = true;
2549
+ this.pendingCoordinateResponseCount -= 1;
2550
+ const remaining = (this.pendingCoordinateResponseCountByPeer.get(peer) ?? 1) - 1;
2551
+ if (remaining === 0) {
2552
+ this.pendingCoordinateResponseCountByPeer.delete(peer);
2553
+ }
2554
+ else {
2555
+ this.pendingCoordinateResponseCountByPeer.set(peer, remaining);
2556
+ }
2557
+ };
2558
+ }
2559
+ reservePendingSyncAdmission(peer, identities) {
2560
+ const count = identities.length;
2561
+ if (count <= 0) {
2562
+ return undefined;
2563
+ }
2564
+ const reservation = {
2565
+ peer,
2566
+ remaining: count,
2567
+ active: true,
2568
+ released: false,
2569
+ expiresAt: Date.now() + PENDING_SIMPLE_SYNC_KEY_TTL_MS,
2570
+ identities: new Set(identities),
2571
+ retainedSettled: 0,
2572
+ };
2573
+ this.pendingSyncAdmissionReservations.add(reservation);
2574
+ let peerReservations = this.pendingSyncAdmissionReservationsByPeer.get(peer);
2575
+ if (!peerReservations) {
2576
+ peerReservations = new Set();
2577
+ this.pendingSyncAdmissionReservationsByPeer.set(peer, peerReservations);
2578
+ }
2579
+ peerReservations.add(reservation);
2580
+ this.pendingSyncActiveAdmissionReservations += 1;
2581
+ this.pendingSyncAdmissionCount += count;
2582
+ this.pendingSyncAdmissionCountByPeer.set(peer, (this.pendingSyncAdmissionCountByPeer.get(peer) ?? 0) + count);
2583
+ let reservedIdentities = this.pendingSyncAdmissionIdentitiesByPeer.get(peer);
2584
+ if (!reservedIdentities) {
2585
+ reservedIdentities = new Set();
2586
+ this.pendingSyncAdmissionIdentitiesByPeer.set(peer, reservedIdentities);
2587
+ }
2588
+ for (const identity of identities) {
2589
+ reservedIdentities.add(identity);
2590
+ let reservationsForIdentity = this.pendingSyncAdmissionReservationsByIdentity.get(identity);
2591
+ if (!reservationsForIdentity) {
2592
+ reservationsForIdentity = new Set();
2593
+ this.pendingSyncAdmissionReservationsByIdentity.set(identity, reservationsForIdentity);
2594
+ }
2595
+ reservationsForIdentity.add(reservation);
2596
+ }
2597
+ const expiryNode = {
2598
+ kind: "admission",
2599
+ reservation,
2600
+ expiresAt: reservation.expiresAt,
2601
+ heapIndex: -1,
2602
+ };
2603
+ this.pendingSyncAdmissionExpiryNodes.set(reservation, expiryNode);
2604
+ this.pushPendingSyncExpiry(expiryNode);
2605
+ this.schedulePendingSyncKeyExpiry();
2606
+ return reservation;
2607
+ }
2608
+ removePendingSyncAdmissionIdentity(reservation, identity, options) {
2609
+ if (!reservation.identities.delete(identity)) {
2610
+ return false;
2611
+ }
2612
+ if (options?.retainQuota === true) {
2613
+ reservation.retainedSettled += 1;
2614
+ }
2615
+ else {
2616
+ reservation.remaining -= 1;
2617
+ this.pendingSyncAdmissionCount -= 1;
2618
+ const peerCount = (this.pendingSyncAdmissionCountByPeer.get(reservation.peer) ?? 0) - 1;
2619
+ if (peerCount === 0) {
2620
+ this.pendingSyncAdmissionCountByPeer.delete(reservation.peer);
2621
+ }
2622
+ else {
2623
+ this.pendingSyncAdmissionCountByPeer.set(reservation.peer, peerCount);
2624
+ }
2625
+ }
2626
+ const reservedIdentities = this.pendingSyncAdmissionIdentitiesByPeer.get(reservation.peer);
2627
+ reservedIdentities?.delete(identity);
2628
+ if (reservedIdentities?.size === 0) {
2629
+ this.pendingSyncAdmissionIdentitiesByPeer.delete(reservation.peer);
2630
+ }
2631
+ const reservationsForIdentity = this.pendingSyncAdmissionReservationsByIdentity.get(identity);
2632
+ reservationsForIdentity?.delete(reservation);
2633
+ if (reservationsForIdentity?.size === 0) {
2634
+ this.pendingSyncAdmissionReservationsByIdentity.delete(identity);
2635
+ }
2636
+ return true;
2637
+ }
2638
+ clearPendingSyncAdmissionIdentity(identity) {
2639
+ const reservations = this.pendingSyncAdmissionReservationsByIdentity.get(identity);
2640
+ if (!reservations) {
2641
+ return;
2642
+ }
2643
+ for (const reservation of [...reservations]) {
2644
+ this.removePendingSyncAdmissionIdentity(reservation, identity, {
2645
+ retainQuota: true,
2646
+ });
2647
+ }
2648
+ }
2649
+ consumePendingSyncAdmission(reservation, identity) {
2650
+ if (!reservation.active || reservation.expiresAt <= Date.now()) {
2651
+ if (reservation.expiresAt <= Date.now()) {
2652
+ this.invalidatePendingSyncAdmission(reservation);
2653
+ }
2654
+ return "invalid";
2655
+ }
2656
+ if (!reservation.identities.has(identity)) {
2657
+ return "settled";
2658
+ }
2659
+ this.removePendingSyncAdmissionIdentity(reservation, identity);
2660
+ if (reservation.remaining === 0) {
2661
+ this.removePendingSyncAdmissionExpiry(reservation);
2662
+ reservation.active = false;
2663
+ reservation.released = true;
2664
+ this.pendingSyncActiveAdmissionReservations -= 1;
2665
+ this.pendingSyncAdmissionReservations.delete(reservation);
2666
+ const peerReservations = this.pendingSyncAdmissionReservationsByPeer.get(reservation.peer);
2667
+ peerReservations?.delete(reservation);
2668
+ if (peerReservations?.size === 0) {
2669
+ this.pendingSyncAdmissionReservationsByPeer.delete(reservation.peer);
2670
+ }
2671
+ this.clearPendingSyncExpiryTimerIfIdle();
2672
+ }
2673
+ return "consumed";
2674
+ }
2675
+ transferPendingSyncAdmissionIdentity(peer, identity) {
2676
+ const reservations = this.pendingSyncAdmissionReservationsByIdentity.get(identity);
2677
+ if (!reservations) {
2678
+ return undefined;
2679
+ }
2680
+ for (const reservation of reservations) {
2681
+ if (reservation.peer !== peer ||
2682
+ reservation.released ||
2683
+ reservation.expiresAt <= Date.now() ||
2684
+ !this.removePendingSyncAdmissionIdentity(reservation, identity, {
2685
+ retainQuota: true,
2686
+ })) {
2687
+ continue;
2688
+ }
2689
+ // The original resolver may be non-abortable and still retains its input
2690
+ // arrays. Keep that reservation charged until its queueSync finally
2691
+ // settles; the queued claim is counted separately and can disappear
2692
+ // without returning the resolver's quota early.
2693
+ return reservation.expiresAt;
2694
+ }
2695
+ return undefined;
2696
+ }
2697
+ invalidatePendingSyncAdmission(reservation) {
2698
+ if (!reservation || reservation.released || !reservation.active) {
2699
+ return;
2700
+ }
2701
+ // Expiry/disconnect invalidates late lookup results, but it must not return
2702
+ // the quota slot while the underlying storage/index work is still alive.
2703
+ // Those lookups are not generally abortable; only queueSync's finally block
2704
+ // may release their active-work accounting.
2705
+ this.removePendingSyncAdmissionExpiry(reservation);
2706
+ reservation.active = false;
2707
+ this.pendingSyncActiveAdmissionReservations -= 1;
2708
+ this.clearPendingSyncExpiryTimerIfIdle();
2709
+ }
2710
+ releasePendingSyncAdmission(reservation) {
2711
+ if (!reservation || reservation.released) {
2712
+ return;
2713
+ }
2714
+ this.removePendingSyncAdmissionExpiry(reservation);
2715
+ for (const identity of [...reservation.identities]) {
2716
+ this.removePendingSyncAdmissionIdentity(reservation, identity);
2717
+ }
2718
+ if (reservation.retainedSettled > 0) {
2719
+ const retainedSettled = reservation.retainedSettled;
2720
+ reservation.retainedSettled = 0;
2721
+ reservation.remaining -= retainedSettled;
2722
+ this.pendingSyncAdmissionCount -= retainedSettled;
2723
+ const peerCount = (this.pendingSyncAdmissionCountByPeer.get(reservation.peer) ?? 0) -
2724
+ retainedSettled;
2725
+ if (peerCount === 0) {
2726
+ this.pendingSyncAdmissionCountByPeer.delete(reservation.peer);
2727
+ }
2728
+ else {
2729
+ this.pendingSyncAdmissionCountByPeer.set(reservation.peer, peerCount);
2730
+ }
2731
+ }
2732
+ if (reservation.active) {
2733
+ this.pendingSyncActiveAdmissionReservations -= 1;
2734
+ }
2735
+ reservation.active = false;
2736
+ reservation.released = true;
2737
+ this.pendingSyncAdmissionReservations.delete(reservation);
2738
+ const peerReservations = this.pendingSyncAdmissionReservationsByPeer.get(reservation.peer);
2739
+ peerReservations?.delete(reservation);
2740
+ if (peerReservations?.size === 0) {
2741
+ this.pendingSyncAdmissionReservationsByPeer.delete(reservation.peer);
2742
+ }
2743
+ this.clearPendingSyncExpiryTimerIfIdle();
2744
+ }
2745
+ clearPendingSyncAdmissions(peer) {
2746
+ const reservations = peer == null
2747
+ ? this.pendingSyncAdmissionReservations
2748
+ : this.pendingSyncAdmissionReservationsByPeer.get(peer);
2749
+ if (!reservations) {
2750
+ return;
2751
+ }
2752
+ for (const reservation of [...reservations]) {
2753
+ this.invalidatePendingSyncAdmission(reservation);
2754
+ }
2755
+ }
2756
+ swapPendingSyncExpiry(left, right) {
2757
+ const leftNode = this.pendingSyncExpiryHeap[left];
2758
+ const rightNode = this.pendingSyncExpiryHeap[right];
2759
+ this.pendingSyncExpiryHeap[left] = rightNode;
2760
+ this.pendingSyncExpiryHeap[right] = leftNode;
2761
+ rightNode.heapIndex = left;
2762
+ leftNode.heapIndex = right;
2763
+ }
2764
+ pushPendingSyncExpiry(node) {
2765
+ node.heapIndex = this.pendingSyncExpiryHeap.length;
2766
+ this.pendingSyncExpiryHeap.push(node);
2767
+ let index = node.heapIndex;
2768
+ while (index > 0) {
2769
+ const parent = Math.floor((index - 1) / 2);
2770
+ if (this.pendingSyncExpiryHeap[parent].expiresAt <=
2771
+ this.pendingSyncExpiryHeap[index].expiresAt) {
2772
+ break;
2773
+ }
2774
+ this.swapPendingSyncExpiry(parent, index);
2775
+ index = parent;
2776
+ }
2777
+ }
2778
+ removePendingSyncExpiry(node) {
2779
+ const index = node.heapIndex;
2780
+ if (index < 0 ||
2781
+ index >= this.pendingSyncExpiryHeap.length ||
2782
+ this.pendingSyncExpiryHeap[index] !== node) {
2783
+ return;
759
2784
  }
760
- finally {
761
- if (profile) {
762
- emitSyncProfileDuration(profile, startedAt, {
763
- name: "simple.onMaybeMissingEntries",
764
- entries: hashes.length,
765
- messages: chunks.length,
766
- targets: properties.targets.length,
767
- });
2785
+ const last = this.pendingSyncExpiryHeap.pop();
2786
+ node.heapIndex = -1;
2787
+ if (index >= this.pendingSyncExpiryHeap.length) {
2788
+ return;
2789
+ }
2790
+ this.pendingSyncExpiryHeap[index] = last;
2791
+ last.heapIndex = index;
2792
+ let current = index;
2793
+ while (current > 0) {
2794
+ const parent = Math.floor((current - 1) / 2);
2795
+ if (this.pendingSyncExpiryHeap[parent].expiresAt <=
2796
+ this.pendingSyncExpiryHeap[current].expiresAt) {
2797
+ break;
768
2798
  }
2799
+ this.swapPendingSyncExpiry(parent, current);
2800
+ current = parent;
769
2801
  }
770
- }
771
- /**
772
- * Ship exchange heads to one peer: the fused native raw path when the
773
- * peer advertised raw capability and the shared log provided a fused
774
- * sender, otherwise the TS message path (raw or plain by capability).
775
- * Returns the number of messages sent and whether the fused path ran.
776
- */
777
- async shipExchangeHeads(hashes, to, canReceiveRaw) {
778
- if (canReceiveRaw && this.sendRawExchangeHeads) {
779
- const sentMessages = await this.sendRawExchangeHeads(hashes, [
780
- to.hashcode(),
781
- ]);
782
- if (sentMessages !== undefined) {
783
- return { messages: sentMessages, fused: true };
2802
+ for (;;) {
2803
+ const left = current * 2 + 1;
2804
+ const right = left + 1;
2805
+ let smallest = current;
2806
+ if (left < this.pendingSyncExpiryHeap.length &&
2807
+ this.pendingSyncExpiryHeap[left].expiresAt <
2808
+ this.pendingSyncExpiryHeap[smallest].expiresAt) {
2809
+ smallest = left;
2810
+ }
2811
+ if (right < this.pendingSyncExpiryHeap.length &&
2812
+ this.pendingSyncExpiryHeap[right].expiresAt <
2813
+ this.pendingSyncExpiryHeap[smallest].expiresAt) {
2814
+ smallest = right;
784
2815
  }
2816
+ if (smallest === current) {
2817
+ break;
2818
+ }
2819
+ this.swapPendingSyncExpiry(current, smallest);
2820
+ current = smallest;
785
2821
  }
786
- let messages = 0;
787
- const messageGenerator = canReceiveRaw
788
- ? createRawExchangeHeadsMessages(this.log, hashes, this.syncOptions?.profile)
789
- : createExchangeHeadsMessages(this.log, hashes);
790
- for await (const message of messageGenerator) {
791
- messages += 1;
792
- await this.rpc.send(message, {
793
- mode: new SilentDelivery({ to: [to], redundancy: 1 }),
794
- });
2822
+ }
2823
+ removePendingSyncKeyExpiry(key) {
2824
+ const node = this.pendingSyncKeyExpiryNodes.get(key);
2825
+ if (!node) {
2826
+ return;
795
2827
  }
796
- return { messages, fused: false };
2828
+ this.pendingSyncKeyExpiryNodes.delete(key);
2829
+ this.removePendingSyncExpiry(node);
797
2830
  }
798
- async onMessage(msg, context) {
799
- const from = context.from;
800
- if (msg instanceof RequestMaybeSync) {
801
- await this.queueSync(msg.hashes, from);
802
- return true;
2831
+ movePendingSyncKeyExpiryEarlier(key, expiresAt) {
2832
+ const current = this.syncInFlightQueueExpiresAt.get(key);
2833
+ if (current == null || current <= expiresAt) {
2834
+ return;
803
2835
  }
804
- else if (msg instanceof ResponseMaybeSync ||
805
- msg instanceof ResponseMaybeSyncCapabilities) {
806
- // TODO perhaps send less messages to more receivers for performance reasons?
807
- // TODO wait for previous send to target before trying to send more?
808
- const profile = this.syncOptions?.profile;
809
- const startedAt = syncProfileStart(profile);
810
- const hashes = this.filterRecentlySentExchangeHeads(msg.hashes, from);
811
- let messages = 0;
812
- let fused = false;
813
- try {
814
- ({ messages, fused } = await this.shipExchangeHeads(hashes, context.from, canReceiveRawExchangeHeads(msg)));
815
- }
816
- finally {
817
- if (profile) {
818
- emitSyncProfileDuration(profile, startedAt, {
819
- name: "simple.exchangeHeads",
820
- entries: hashes.length,
821
- messages,
822
- targets: 1,
823
- details: { source: "responseMaybeSync", fused },
824
- });
825
- }
826
- }
827
- return true;
2836
+ this.removePendingSyncKeyExpiry(key);
2837
+ this.syncInFlightQueueExpiresAt.set(key, expiresAt);
2838
+ const node = {
2839
+ kind: "key",
2840
+ key,
2841
+ expiresAt,
2842
+ heapIndex: -1,
2843
+ };
2844
+ this.pendingSyncKeyExpiryNodes.set(key, node);
2845
+ this.pushPendingSyncExpiry(node);
2846
+ this.schedulePendingSyncKeyExpiry();
2847
+ }
2848
+ removePendingSyncAdmissionExpiry(reservation) {
2849
+ const node = this.pendingSyncAdmissionExpiryNodes.get(reservation);
2850
+ if (!node) {
2851
+ return;
828
2852
  }
829
- else if (msg instanceof RequestMaybeSyncCoordinate ||
830
- msg instanceof RequestMaybeSyncCoordinateCapabilities) {
831
- const profile = this.syncOptions?.profile;
832
- const lookupStartedAt = syncProfileStart(profile);
833
- const hashes = await getHashesFromSymbols(msg.hashNumbers, this.entryIndex, this.coordinateToHash, this.resolveHashesForSymbols, this.resolveHashListForSymbols);
834
- if (profile) {
835
- emitSyncProfileDuration(profile, lookupStartedAt, {
836
- name: "simple.coordinateLookup",
837
- entries: hashLookupResultSize(hashes),
838
- symbols: msg.hashNumbers.length,
839
- });
2853
+ this.pendingSyncAdmissionExpiryNodes.delete(reservation);
2854
+ this.removePendingSyncExpiry(node);
2855
+ }
2856
+ expirePendingSyncKeys(now = Date.now()) {
2857
+ for (;;) {
2858
+ const node = this.pendingSyncExpiryHeap[0];
2859
+ if (!node || node.expiresAt > now) {
2860
+ break;
840
2861
  }
841
- const exchangeStartedAt = syncProfileStart(profile);
842
- const hashesToSend = this.filterRecentlySentExchangeHeads(hashes, from);
843
- let messages = 0;
844
- let fused = false;
845
- try {
846
- // dont set priority 1 here because this will block other messages that should higher priority
847
- ({ messages, fused } = await this.shipExchangeHeads(hashesToSend, context.from, canReceiveRawExchangeHeads(msg)));
2862
+ this.removePendingSyncExpiry(node);
2863
+ if (node.kind === "key") {
2864
+ if (this.pendingSyncKeyExpiryNodes.get(node.key) !== node) {
2865
+ continue;
2866
+ }
2867
+ this.pendingSyncKeyExpiryNodes.delete(node.key);
2868
+ this.clearSyncProcessKey(node.key);
848
2869
  }
849
- finally {
850
- if (profile) {
851
- emitSyncProfileDuration(profile, exchangeStartedAt, {
852
- name: "simple.exchangeHeads",
853
- entries: hashesToSend.length,
854
- messages,
855
- targets: 1,
856
- details: { source: "requestMaybeSyncCoordinate", fused },
857
- });
2870
+ else {
2871
+ if (this.pendingSyncAdmissionExpiryNodes.get(node.reservation) !== node) {
2872
+ continue;
858
2873
  }
2874
+ this.pendingSyncAdmissionExpiryNodes.delete(node.reservation);
2875
+ this.invalidatePendingSyncAdmission(node.reservation);
859
2876
  }
860
- return true;
861
- }
862
- else {
863
- return false; // no message was consumed
864
2877
  }
865
2878
  }
866
- onReceivedEntries(properties) {
867
- return this.onReceivedEntryHashes({
868
- hashes: properties.entries.map((entry) => entry.entry.hash),
869
- from: properties.from,
870
- });
2879
+ clearPendingSyncExpiryTimerIfIdle() {
2880
+ if (this.pendingSyncExpiryHeap.length === 0 &&
2881
+ this.syncInFlightQueueExpiryTimer != null) {
2882
+ clearTimeout(this.syncInFlightQueueExpiryTimer);
2883
+ this.syncInFlightQueueExpiryTimer = undefined;
2884
+ }
871
2885
  }
872
- onReceivedEntryHashes(properties) {
873
- this.clearSyncInFlightForPeerHashes(properties.from.hashcode(), properties.hashes);
874
- this.markRepairSessionResolvedHashes(properties.hashes);
2886
+ schedulePendingSyncKeyExpiry() {
2887
+ if (this.syncInFlightQueueExpiryTimer != null ||
2888
+ this.pendingSyncExpiryHeap.length === 0) {
2889
+ return;
2890
+ }
2891
+ const expiresAt = this.pendingSyncExpiryHeap[0].expiresAt;
2892
+ this.syncInFlightQueueExpiryTimer = setTimeout(() => {
2893
+ this.syncInFlightQueueExpiryTimer = undefined;
2894
+ this.expirePendingSyncKeys();
2895
+ this.schedulePendingSyncKeyExpiry();
2896
+ }, Math.max(0, expiresAt - Date.now()));
2897
+ this.syncInFlightQueueExpiryTimer.unref?.();
875
2898
  }
876
2899
  async queueSync(keys, from, options) {
2900
+ if (this.closed === true || keys.length === 0) {
2901
+ return;
2902
+ }
2903
+ // A delayed timer must not let expired claims or admission reservations
2904
+ // keep the exact per-peer/global quota closed to fresh work.
2905
+ this.expirePendingSyncKeys();
2906
+ this.clearPendingSyncExpiryTimerIfIdle();
2907
+ const fromHash = from.hashcode();
2908
+ const canStartLookup = options?.skipCheck === true || this.canStartPendingSyncLookup(fromHash);
2909
+ const peerClaimCount = this.syncInFlightQueueInverted.get(fromHash)?.size ?? 0;
2910
+ let availableClaims = Math.max(0, Math.min(MAX_PENDING_SIMPLE_SYNC_KEYS_PER_PEER -
2911
+ peerClaimCount -
2912
+ (this.pendingSyncAdmissionCountByPeer.get(fromHash) ?? 0), MAX_PENDING_SIMPLE_SYNC_KEYS_GLOBAL -
2913
+ this.pendingSyncClaimCount -
2914
+ this.pendingSyncAdmissionCount));
2915
+ const targetEpoch = this.getOrCreateSyncDispatchTargetEpoch(fromHash);
2916
+ const ownershipLifecycleController = this.syncDispatchLifecycleController;
2917
+ const isCapturedLifecycleActive = () => this.closed !== true &&
2918
+ this.syncDispatchLifecycleController === ownershipLifecycleController &&
2919
+ !ownershipLifecycleController.signal.aborted &&
2920
+ this.syncDispatchTargetEpochs.get(fromHash) === targetEpoch;
877
2921
  const requestHashes = [];
2922
+ const existingRequestHashes = [];
878
2923
  const profile = this.syncOptions?.profile;
879
2924
  const startedAt = syncProfileStart(profile);
880
- const resolveKnownStartedAt = syncProfileStart(profile);
881
- const knownKeys = options?.skipCheck === true
882
- ? undefined
883
- : await this.resolveKnownSyncKeys(keys);
884
- if (profile) {
885
- emitSyncProfileDuration(profile, resolveKnownStartedAt, {
886
- name: "simple.queueSync.resolveKnown",
887
- entries: keys.length,
888
- count: knownKeys?.keys.size ?? 0,
889
- details: {
890
- checkedCoordinates: knownKeys?.checkedCoordinates === true,
891
- checkedHashes: knownKeys?.checkedHashes === true,
892
- skipCheck: options?.skipCheck === true,
893
- },
894
- });
2925
+ if (!isCapturedLifecycleActive()) {
2926
+ return;
895
2927
  }
896
- const fromHash = from.hashcode();
897
- let queuedHashAliases;
898
- const getQueuedSyncKeyForBatch = (key) => {
899
- if (this.syncInFlightQueue.has(key)) {
900
- return key;
901
- }
902
- if (typeof key === "string") {
903
- if (!queuedHashAliases) {
904
- queuedHashAliases = new Map();
905
- for (const queuedKey of this.syncInFlightQueue.keys()) {
906
- if (typeof queuedKey !== "bigint") {
907
- continue;
908
- }
909
- const hash = this.coordinateToHash.get(queuedKey);
910
- if (hash) {
911
- queuedHashAliases.set(hash, queuedKey);
912
- }
2928
+ if (availableClaims === 0) {
2929
+ return;
2930
+ }
2931
+ this.refreshQueuedSyncCoordinateAliases();
2932
+ const keysToCheck = [];
2933
+ const identitiesToCheck = [];
2934
+ const seen = new Set();
2935
+ const pendingAdmissionIdentities = this.pendingSyncAdmissionIdentitiesByPeer.get(fromHash);
2936
+ // Default senders use 1,024-key messages. Capping examined input at the
2937
+ // entire per-peer allowance prevents a duplicate-filled oversized vector
2938
+ // from turning admission itself into unbounded work.
2939
+ const inspectionLimit = canStartLookup
2940
+ ? MAX_PENDING_SIMPLE_SYNC_KEYS_PER_PEER
2941
+ : Math.min(DEFAULT_MAX_HASHES_PER_MESSAGE, keys.length);
2942
+ for (let index = 0; index < keys.length && index < inspectionLimit; index += 1) {
2943
+ const key = keys[index];
2944
+ const queuedKeyResult = this.getQueuedSyncKeyForAdmission(key);
2945
+ if (queuedKeyResult === QUEUED_SYNC_ALIAS_REFRESH_PENDING) {
2946
+ continue;
2947
+ }
2948
+ const queuedKey = queuedKeyResult;
2949
+ const coordinateOrHash = queuedKey ?? key;
2950
+ const identity = this.getPendingSyncKeyIdentity(coordinateOrHash);
2951
+ if (seen.has(identity)) {
2952
+ continue;
2953
+ }
2954
+ seen.add(identity);
2955
+ if (queuedKey != null) {
2956
+ let transferredDeadline;
2957
+ if (availableClaims > 0 &&
2958
+ pendingAdmissionIdentities?.has(identity) &&
2959
+ !this.hasPendingSyncClaim(queuedKey, fromHash)) {
2960
+ transferredDeadline = this.transferPendingSyncAdmissionIdentity(fromHash, identity);
2961
+ if (transferredDeadline != null) {
2962
+ this.movePendingSyncKeyExpiryEarlier(queuedKey, transferredDeadline);
913
2963
  }
914
2964
  }
915
- return queuedHashAliases.get(key);
2965
+ if (availableClaims === 0) {
2966
+ continue;
2967
+ }
2968
+ if (this.addPendingSyncClaim(queuedKey, from)) {
2969
+ availableClaims -= 1;
2970
+ existingRequestHashes.push(queuedKey);
2971
+ }
2972
+ continue;
916
2973
  }
917
- const hash = this.coordinateToHash.get(key);
918
- return hash && this.syncInFlightQueue.has(hash) ? hash : undefined;
919
- };
2974
+ if (pendingAdmissionIdentities?.has(identity) ||
2975
+ !canStartLookup ||
2976
+ availableClaims === 0) {
2977
+ continue;
2978
+ }
2979
+ keysToCheck.push(key);
2980
+ identitiesToCheck.push(identity);
2981
+ availableClaims -= 1;
2982
+ }
2983
+ const admission = this.reservePendingSyncAdmission(fromHash, identitiesToCheck);
2984
+ const dispatchableExistingRequestHashes = this.filterDispatchablePendingSyncClaims(existingRequestHashes, fromHash, targetEpoch);
2985
+ const existingRequest = dispatchableExistingRequestHashes.length > 0
2986
+ ? this.requestSync(dispatchableExistingRequestHashes, [fromHash], {
2987
+ ownershipLifecycleController,
2988
+ targetEpochs: new Map([[fromHash, targetEpoch]]),
2989
+ createTargetEpochs: false,
2990
+ })
2991
+ : undefined;
2992
+ // Existing queued claims must not wait behind a potentially blocked storage
2993
+ // lookup for unrelated new keys. Observe this eagerly-started request even
2994
+ // when a later lifecycle check returns before joining it.
2995
+ void existingRequest?.catch(() => { });
2996
+ const resolveKnownStartedAt = syncProfileStart(profile);
920
2997
  try {
2998
+ const knownKeys = options?.skipCheck === true || keysToCheck.length === 0
2999
+ ? undefined
3000
+ : await this.resolveKnownSyncKeys(keysToCheck);
3001
+ if (!isCapturedLifecycleActive()) {
3002
+ return;
3003
+ }
3004
+ if (profile) {
3005
+ emitSyncProfileDuration(profile, resolveKnownStartedAt, {
3006
+ name: "simple.queueSync.resolveKnown",
3007
+ entries: keysToCheck.length,
3008
+ count: knownKeys?.keys.size ?? 0,
3009
+ details: {
3010
+ checkedCoordinates: knownKeys?.checkedCoordinates === true,
3011
+ checkedHashes: knownKeys?.checkedHashes === true,
3012
+ skipCheck: options?.skipCheck === true,
3013
+ },
3014
+ });
3015
+ }
3016
+ if (keysToCheck.length > 0) {
3017
+ // A resolver/index lookup may have populated coordinateToHash while
3018
+ // it yielded. Refresh another fixed-size slice, never the full queue.
3019
+ this.refreshQueuedSyncCoordinateAliases();
3020
+ }
921
3021
  const loopStartedAt = syncProfileStart(profile);
922
- for (const key of keys) {
923
- const coordinateOrHash = getQueuedSyncKeyForBatch(key) ?? key;
3022
+ for (let index = 0; index < keysToCheck.length; index += 1) {
3023
+ const key = keysToCheck[index];
3024
+ const identity = identitiesToCheck[index];
3025
+ if (!isCapturedLifecycleActive()) {
3026
+ return;
3027
+ }
3028
+ const queuedKeyResult = this.getQueuedSyncKeyForAdmission(key);
3029
+ if (queuedKeyResult === QUEUED_SYNC_ALIAS_REFRESH_PENDING) {
3030
+ const consumption = this.consumePendingSyncAdmission(admission, identity);
3031
+ if (consumption === "invalid") {
3032
+ return;
3033
+ }
3034
+ continue;
3035
+ }
3036
+ const coordinateOrHash = queuedKeyResult ?? key;
924
3037
  const inFlight = this.syncInFlightQueue.get(coordinateOrHash);
925
3038
  if (inFlight) {
926
- if (!inFlight.find((x) => x.hashcode() === fromHash)) {
927
- inFlight.push(from);
928
- let inverted = this.syncInFlightQueueInverted.get(fromHash);
929
- if (!inverted) {
930
- inverted = new Set();
931
- this.syncInFlightQueueInverted.set(fromHash, inverted);
3039
+ if (!this.hasPendingSyncClaim(coordinateOrHash, fromHash)) {
3040
+ const consumption = this.consumePendingSyncAdmission(admission, identity);
3041
+ if (consumption === "invalid") {
3042
+ return;
3043
+ }
3044
+ if (consumption === "settled") {
3045
+ continue;
3046
+ }
3047
+ this.movePendingSyncKeyExpiryEarlier(coordinateOrHash, admission.expiresAt);
3048
+ const added = this.addPendingSyncClaim(coordinateOrHash, from, admission.expiresAt);
3049
+ if (added) {
3050
+ requestHashes.push(coordinateOrHash);
932
3051
  }
933
- inverted.add(coordinateOrHash);
934
3052
  }
935
3053
  }
936
- else if (options?.skipCheck ||
937
- !(await this.checkHasCoordinateOrHash(coordinateOrHash, knownKeys))) {
938
- // Track the initial sender so we can retry if the first request is lost.
939
- this.syncInFlightQueue.set(coordinateOrHash, [from]);
940
- let inverted = this.syncInFlightQueueInverted.get(fromHash);
941
- if (!inverted) {
942
- inverted = new Set();
943
- this.syncInFlightQueueInverted.set(fromHash, inverted);
3054
+ else {
3055
+ const has = options?.skipCheck !== true &&
3056
+ (await this.checkHasCoordinateOrHash(coordinateOrHash, knownKeys));
3057
+ if (!isCapturedLifecycleActive()) {
3058
+ return;
944
3059
  }
945
- inverted.add(coordinateOrHash);
946
- requestHashes.push(coordinateOrHash); // request immediately (first time we have seen this hash)
947
- if (queuedHashAliases &&
948
- typeof coordinateOrHash === "bigint") {
949
- const hash = this.coordinateToHash.get(coordinateOrHash);
950
- if (hash) {
951
- queuedHashAliases.set(hash, coordinateOrHash);
952
- }
3060
+ if (has) {
3061
+ this.clearPendingSyncAdmissionIdentity(identity);
3062
+ continue;
3063
+ }
3064
+ const consumption = this.consumePendingSyncAdmission(admission, identity);
3065
+ if (consumption === "invalid") {
3066
+ return;
3067
+ }
3068
+ if (consumption === "settled") {
3069
+ continue;
953
3070
  }
3071
+ // Track the initial sender so we can retry if the first request is lost.
3072
+ this.addPendingSyncClaim(coordinateOrHash, from, admission.expiresAt);
3073
+ requestHashes.push(coordinateOrHash); // request immediately (first time we have seen this hash)
954
3074
  }
955
3075
  }
956
3076
  if (profile) {
957
3077
  emitSyncProfileDuration(profile, loopStartedAt, {
958
3078
  name: "simple.queueSync.plan",
959
- entries: keys.length,
3079
+ entries: keysToCheck.length,
960
3080
  count: requestHashes.length,
961
3081
  targets: 1,
962
3082
  });
963
3083
  }
964
- requestHashes.length > 0 &&
965
- (await this.requestSync(requestHashes, [fromHash]));
3084
+ // Persistent admission work is complete. Do not let an unrelated
3085
+ // blocked transport send retain unused quota.
3086
+ this.releasePendingSyncAdmission(admission);
3087
+ const dispatchableRequestHashes = this.filterDispatchablePendingSyncClaims(requestHashes, fromHash, targetEpoch);
3088
+ dispatchableRequestHashes.length > 0 &&
3089
+ (await this.requestSync(dispatchableRequestHashes, [fromHash], {
3090
+ ownershipLifecycleController,
3091
+ targetEpochs: new Map([[fromHash, targetEpoch]]),
3092
+ createTargetEpochs: false,
3093
+ }));
3094
+ await existingRequest;
966
3095
  }
967
3096
  finally {
3097
+ this.releasePendingSyncAdmission(admission);
968
3098
  if (profile) {
969
3099
  emitSyncProfileDuration(profile, startedAt, {
970
3100
  name: "simple.queueSync",
971
3101
  entries: keys.length,
972
3102
  targets: 1,
973
3103
  details: {
3104
+ admitted: keysToCheck.length,
974
3105
  requested: requestHashes.length,
975
3106
  skipCheck: options?.skipCheck === true,
976
3107
  },
@@ -978,10 +3109,16 @@ export class SimpleSyncronizer {
978
3109
  }
979
3110
  }
980
3111
  }
981
- async requestSync(hashes, to) {
982
- if (hashes.length === 0) {
3112
+ async requestSync(hashes, to, options) {
3113
+ const targets = [...new Set(to)];
3114
+ if (hashes.length === 0 || targets.length === 0) {
983
3115
  return;
984
3116
  }
3117
+ const lifecycle = this.captureSyncDispatchLifecycle(targets, undefined, {
3118
+ ownershipLifecycleController: options?.ownershipLifecycleController,
3119
+ targetEpochs: options?.targetEpochs,
3120
+ createTargetEpochs: options?.createTargetEpochs,
3121
+ });
985
3122
  const profile = this.syncOptions?.profile;
986
3123
  const startedAt = syncProfileStart(profile);
987
3124
  let coordinateMessages = 0;
@@ -990,14 +3127,12 @@ export class SimpleSyncronizer {
990
3127
  let stringHashCount = 0;
991
3128
  try {
992
3129
  const now = +new Date();
993
- for (const node of to) {
994
- let map = this.syncInFlight.get(node);
995
- if (!map) {
996
- map = new Map();
997
- this.syncInFlight.set(node, map);
3130
+ for (const node of targets) {
3131
+ if (!this.isSyncDispatchLifecycleActive(lifecycle, node)) {
3132
+ continue;
998
3133
  }
999
3134
  for (const hash of hashes) {
1000
- map.set(hash, { timestamp: now });
3135
+ this.setSyncInFlightTargetKey(node, hash, now);
1001
3136
  }
1002
3137
  }
1003
3138
  const coordinateHashes = [];
@@ -1014,38 +3149,77 @@ export class SimpleSyncronizer {
1014
3149
  stringHashCount = stringHashes.length;
1015
3150
  if (coordinateHashes.length > 0) {
1016
3151
  const chunks = this.chunk(coordinateHashes, this.maxCoordinatesPerMessage);
1017
- coordinateMessages = chunks.length;
1018
- for (const chunk of chunks) {
1019
- await this.rpc.send(this.syncOptions?.rawExchangeHeads
1020
- ? new RequestMaybeSyncCoordinateCapabilities({
1021
- hashNumbers: chunk,
1022
- })
1023
- : new RequestMaybeSyncCoordinate({ hashNumbers: chunk }), {
1024
- mode: new SilentDelivery({ to, redundancy: 1 }),
1025
- priority: SYNC_MESSAGE_PRIORITY,
1026
- });
3152
+ for (const target of targets) {
3153
+ for (const chunk of chunks) {
3154
+ if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
3155
+ break;
3156
+ }
3157
+ try {
3158
+ await this.rpc.send(this.syncOptions?.rawExchangeHeads
3159
+ ? new RequestMaybeSyncCoordinateCapabilities({
3160
+ hashNumbers: chunk,
3161
+ })
3162
+ : new RequestMaybeSyncCoordinate({
3163
+ hashNumbers: chunk,
3164
+ }), {
3165
+ mode: new SilentDelivery({
3166
+ to: [target],
3167
+ redundancy: 1,
3168
+ }),
3169
+ priority: SYNC_MESSAGE_PRIORITY,
3170
+ signal: this.getSyncDispatchSignal(lifecycle, target),
3171
+ });
3172
+ coordinateMessages += 1;
3173
+ }
3174
+ catch (error) {
3175
+ if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
3176
+ break;
3177
+ }
3178
+ throw error;
3179
+ }
3180
+ }
1027
3181
  }
1028
3182
  }
1029
3183
  if (stringHashes.length > 0) {
1030
3184
  const chunks = this.chunk(stringHashes, this.maxHashesPerMessage);
1031
- stringMessages = chunks.length;
1032
- for (const chunk of chunks) {
1033
- await this.rpc.send(this.syncOptions?.rawExchangeHeads
1034
- ? new ResponseMaybeSyncCapabilities({ hashes: chunk })
1035
- : new ResponseMaybeSync({ hashes: chunk }), {
1036
- mode: new SilentDelivery({ to, redundancy: 1 }),
1037
- priority: SYNC_MESSAGE_PRIORITY,
1038
- });
3185
+ for (const target of targets) {
3186
+ for (const chunk of chunks) {
3187
+ if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
3188
+ break;
3189
+ }
3190
+ try {
3191
+ await this.rpc.send(this.syncOptions?.rawExchangeHeads
3192
+ ? new ResponseMaybeSyncCapabilities({
3193
+ hashes: chunk,
3194
+ })
3195
+ : new ResponseMaybeSync({ hashes: chunk }), {
3196
+ mode: new SilentDelivery({
3197
+ to: [target],
3198
+ redundancy: 1,
3199
+ }),
3200
+ priority: SYNC_MESSAGE_PRIORITY,
3201
+ signal: this.getSyncDispatchSignal(lifecycle, target),
3202
+ });
3203
+ stringMessages += 1;
3204
+ }
3205
+ catch (error) {
3206
+ if (!this.isSyncDispatchLifecycleActive(lifecycle, target)) {
3207
+ break;
3208
+ }
3209
+ throw error;
3210
+ }
3211
+ }
1039
3212
  }
1040
3213
  }
1041
3214
  }
1042
3215
  finally {
3216
+ this.finishSyncDispatchLifecycle(lifecycle);
1043
3217
  if (profile) {
1044
3218
  emitSyncProfileDuration(profile, startedAt, {
1045
3219
  name: "simple.requestSync",
1046
3220
  entries: hashes.length,
1047
3221
  messages: coordinateMessages + stringMessages,
1048
- targets: Array.isArray(to) ? to.length : to.size,
3222
+ targets: targets.length,
1049
3223
  details: {
1050
3224
  coordinateHashes: coordinateHashCount,
1051
3225
  stringHashes: stringHashCount,
@@ -1095,6 +3269,12 @@ export class SimpleSyncronizer {
1095
3269
  return known.checkedCoordinates || known.checkedHashes ? known : undefined;
1096
3270
  }
1097
3271
  async checkHasCoordinateOrHash(key, knownKeys) {
3272
+ if (typeof key === "bigint") {
3273
+ const mappedHash = this.coordinateToHash.get(key);
3274
+ if (mappedHash != null && (await this.log.has(mappedHash))) {
3275
+ return true;
3276
+ }
3277
+ }
1098
3278
  if (knownKeys) {
1099
3279
  if (knownKeys.keys.has(key)) {
1100
3280
  return true;
@@ -1111,8 +3291,30 @@ export class SimpleSyncronizer {
1111
3291
  : this.log.has(key);
1112
3292
  }
1113
3293
  async open() {
3294
+ this.syncDispatchLifecycleController.abort();
3295
+ this.clearPendingMaybeSyncResponses();
3296
+ this.syncDispatchTargetEpochs.clear();
3297
+ this.syncDispatchLifecycleController = new AbortController();
3298
+ this.syncInFlightRetryIterator = undefined;
3299
+ this.syncInFlightRetryRemaining = 0;
3300
+ const openLifecycleController = this.syncDispatchLifecycleController;
1114
3301
  this.closed = false;
1115
- const requestSyncLoop = async () => {
3302
+ const isOpenLifecycleActive = () => this.closed !== true &&
3303
+ this.syncDispatchLifecycleController === openLifecycleController &&
3304
+ !openLifecycleController.signal.aborted;
3305
+ let requestSyncLoop;
3306
+ const scheduleRequestSyncLoop = () => {
3307
+ if (!isOpenLifecycleActive()) {
3308
+ return;
3309
+ }
3310
+ this.syncMoreInterval = setTimeout(() => {
3311
+ void requestSyncLoop().catch(() => {
3312
+ // The loop is best-effort. Observe all failures so a transport or
3313
+ // storage rejection cannot become an unhandled process rejection.
3314
+ });
3315
+ }, 3e3);
3316
+ };
3317
+ requestSyncLoop = async () => {
1116
3318
  /**
1117
3319
  * This method fetches entries that we potentially want.
1118
3320
  * In a case in which we become replicator of a segment,
@@ -1120,68 +3322,165 @@ export class SimpleSyncronizer {
1120
3322
  * This method makes sure that we only request on entry from the remotes at a time
1121
3323
  * so we don't get flooded with the same entry
1122
3324
  */
1123
- const requestHashes = [];
1124
- const from = new Set();
1125
- const now = Date.now();
1126
- for (const [key, value] of this.syncInFlightQueue) {
1127
- if (this.closed) {
1128
- return;
3325
+ if (!isOpenLifecycleActive()) {
3326
+ return;
3327
+ }
3328
+ try {
3329
+ const requestHashesByEpoch = new Map();
3330
+ const now = Date.now();
3331
+ if (!this.syncInFlightRetryIterator) {
3332
+ this.syncInFlightRetryIterator = this.syncInFlightQueue.entries();
3333
+ this.syncInFlightRetryRemaining = this.syncInFlightQueue.size;
1129
3334
  }
1130
- const has = await this.checkHasCoordinateOrHash(key);
1131
- if (!has) {
1132
- if (value.length === 0) {
1133
- // No remaining peers to ask; drop the pending key to avoid leaking.
3335
+ for (let inspected = 0; inspected < MAX_SIMPLE_SYNC_RETRY_KEYS_PER_TICK &&
3336
+ this.syncInFlightRetryRemaining > 0; inspected += 1) {
3337
+ const next = this.syncInFlightRetryIterator.next();
3338
+ if (next.done) {
3339
+ this.syncInFlightRetryIterator = undefined;
3340
+ this.syncInFlightRetryRemaining = 0;
3341
+ break;
3342
+ }
3343
+ this.syncInFlightRetryRemaining -= 1;
3344
+ const [key] = next.value;
3345
+ if (!isOpenLifecycleActive()) {
3346
+ return;
3347
+ }
3348
+ const expiresAt = this.syncInFlightQueueExpiresAt.get(key);
3349
+ if (expiresAt != null && expiresAt <= Date.now()) {
3350
+ this.clearSyncProcessKey(key);
3351
+ continue;
3352
+ }
3353
+ const has = await this.checkHasCoordinateOrHash(key);
3354
+ if (!isOpenLifecycleActive()) {
3355
+ return;
3356
+ }
3357
+ const value = this.syncInFlightQueue.get(key);
3358
+ if (!value) {
3359
+ continue;
3360
+ }
3361
+ const currentExpiresAt = this.syncInFlightQueueExpiresAt.get(key);
3362
+ if (currentExpiresAt != null && currentExpiresAt <= Date.now()) {
1134
3363
  this.clearSyncProcessKey(key);
1135
3364
  continue;
1136
3365
  }
1137
- // Ask one peer per key per loop. If a previous request is still considered
1138
- // "recent", wait until the retry window elapses.
1139
- const candidate = value[0];
1140
- const publicKeyHash = candidate.hashcode();
1141
- const inflightTimestamp = this.syncInFlight
1142
- .get(publicKeyHash)
1143
- ?.get(key)?.timestamp;
1144
- if (inflightTimestamp == null ||
1145
- now - inflightTimestamp >= SIMPLE_SYNC_RETRY_AFTER_MS) {
1146
- requestHashes.push(key);
1147
- from.add(publicKeyHash);
1148
- if (value.length > 1) {
1149
- // Rotate for fairness across multiple possible sources.
1150
- value.push(value.shift());
3366
+ if (!has) {
3367
+ if (value.length === 0) {
3368
+ this.clearSyncProcessKey(key);
3369
+ continue;
1151
3370
  }
3371
+ const cursor = (this.syncInFlightQueueRoundRobinCursor.get(key) ?? 0) %
3372
+ value.length;
3373
+ const candidate = value[cursor];
3374
+ const publicKeyHash = candidate.hashcode();
3375
+ const inflightTimestamp = this.syncInFlight
3376
+ .get(publicKeyHash)
3377
+ ?.get(key)?.timestamp;
3378
+ if (inflightTimestamp == null ||
3379
+ now - inflightTimestamp >= SIMPLE_SYNC_RETRY_AFTER_MS) {
3380
+ const epoch = this.syncDispatchTargetEpochs.get(publicKeyHash);
3381
+ if (!epoch) {
3382
+ continue;
3383
+ }
3384
+ let request = requestHashesByEpoch.get(epoch);
3385
+ if (!request) {
3386
+ request = { target: publicKeyHash, hashes: [] };
3387
+ requestHashesByEpoch.set(epoch, request);
3388
+ }
3389
+ request.hashes.push(key);
3390
+ if (value.length > 1) {
3391
+ this.syncInFlightQueueRoundRobinCursor.set(key, (cursor + 1) % value.length);
3392
+ }
3393
+ }
3394
+ }
3395
+ else {
3396
+ this.clearSyncProcessKey(key);
1152
3397
  }
1153
3398
  }
1154
- else {
1155
- this.clearSyncProcessKey(key);
3399
+ if (this.syncInFlightRetryRemaining === 0) {
3400
+ this.syncInFlightRetryIterator = undefined;
1156
3401
  }
1157
- }
1158
- const nowMin10s = +new Date() - 2e4;
1159
- for (const [key, map] of this.syncInFlight) {
1160
- // cleanup "old" missing syncs
1161
- for (const [hash, { timestamp }] of map) {
1162
- if (timestamp < nowMin10s) {
1163
- map.delete(hash);
3402
+ if (!isOpenLifecycleActive()) {
3403
+ return;
3404
+ }
3405
+ const nowMin10s = +new Date() - 2e4;
3406
+ for (const [target, map] of this.syncInFlight) {
3407
+ for (const [key, { timestamp }] of map) {
3408
+ if (timestamp < nowMin10s) {
3409
+ this.removeSyncInFlightTargetKey(target, key);
3410
+ }
3411
+ }
3412
+ }
3413
+ if (!isOpenLifecycleActive()) {
3414
+ return;
3415
+ }
3416
+ for (const [epoch, request] of requestHashesByEpoch) {
3417
+ if (!isOpenLifecycleActive()) {
3418
+ return;
3419
+ }
3420
+ const requestHashes = this.filterDispatchablePendingSyncClaims(request.hashes, request.target, epoch);
3421
+ if (requestHashes.length === 0) {
3422
+ continue;
3423
+ }
3424
+ try {
3425
+ await this.requestSync(requestHashes, [request.target], {
3426
+ ownershipLifecycleController: openLifecycleController,
3427
+ targetEpochs: new Map([[request.target, epoch]]),
3428
+ createTargetEpochs: false,
3429
+ });
3430
+ }
3431
+ catch {
3432
+ if (!isOpenLifecycleActive()) {
3433
+ return;
3434
+ }
3435
+ // A failed target must not prevent bounded retries for unrelated
3436
+ // peers in this pass.
1164
3437
  }
1165
3438
  }
1166
- if (map.size === 0) {
1167
- this.syncInFlight.delete(key);
3439
+ if (!isOpenLifecycleActive()) {
3440
+ return;
1168
3441
  }
1169
3442
  }
1170
- this.requestSync(requestHashes, from).finally(() => {
1171
- if (this.closed) {
3443
+ catch {
3444
+ if (!isOpenLifecycleActive()) {
1172
3445
  return;
1173
3446
  }
1174
- this.syncMoreInterval = setTimeout(requestSyncLoop, 3e3);
1175
- });
3447
+ // Retry on the next interval; the rejection is deliberately observed.
3448
+ }
3449
+ scheduleRequestSyncLoop();
1176
3450
  };
1177
- requestSyncLoop();
3451
+ void requestSyncLoop().catch(() => {
3452
+ // Defensive observation for failures outside the loop's best-effort body.
3453
+ });
1178
3454
  }
1179
3455
  async close() {
1180
3456
  this.closed = true;
3457
+ this.syncDispatchLifecycleController.abort();
3458
+ this.syncDispatchTargetEpochs.clear();
3459
+ this.clearPendingSyncAdmissions();
1181
3460
  this.syncInFlightQueue.clear();
1182
3461
  this.syncInFlightQueueInverted.clear();
3462
+ this.syncInFlightRetryIterator = undefined;
3463
+ this.syncInFlightRetryRemaining = 0;
3464
+ this.syncInFlightQueueExpiresAt.clear();
3465
+ this.pendingSyncExpiryHeap.length = 0;
3466
+ this.pendingSyncKeyExpiryNodes.clear();
3467
+ this.pendingSyncAdmissionExpiryNodes.clear();
3468
+ this.syncInFlightQueueClaimants.clear();
3469
+ this.syncInFlightQueueClaimantIndexes.clear();
3470
+ this.syncInFlightQueueRoundRobinCursor.clear();
3471
+ this.syncInFlightQueuedCoordinates.clear();
3472
+ this.syncInFlightQueuedHashByCoordinate.clear();
3473
+ this.syncInFlightQueuedCoordinatesByHash.clear();
3474
+ this.syncInFlightQueuedCoordinateRefreshIterator = undefined;
3475
+ this.pendingSyncClaimCount = 0;
3476
+ if (this.syncInFlightQueueExpiryTimer != null) {
3477
+ clearTimeout(this.syncInFlightQueueExpiryTimer);
3478
+ this.syncInFlightQueueExpiryTimer = undefined;
3479
+ }
1183
3480
  this.syncInFlight.clear();
3481
+ this.syncInFlightTargetsByKey.clear();
1184
3482
  this.recentlySentExchangeHeads.clear();
3483
+ this.clearPendingMaybeSyncResponses();
1185
3484
  for (const sessionId of [...this.repairSessions.keys()]) {
1186
3485
  this.finalizeRepairSession(sessionId, false);
1187
3486
  }
@@ -1194,6 +3493,9 @@ export class SimpleSyncronizer {
1194
3493
  if (hashes.length === 0 || !this.hasEntryAddedState()) {
1195
3494
  return;
1196
3495
  }
3496
+ for (const hash of hashes) {
3497
+ this.clearPendingSyncAdmissionIdentity(hash);
3498
+ }
1197
3499
  this.clearSyncProcesses(hashes);
1198
3500
  this.markRepairSessionResolvedHashes(hashes);
1199
3501
  }
@@ -1201,6 +3503,7 @@ export class SimpleSyncronizer {
1201
3503
  if (!this.hasEntryAddedState()) {
1202
3504
  return;
1203
3505
  }
3506
+ this.clearPendingSyncAdmissionIdentity(hash);
1204
3507
  this.clearSyncProcess(hash);
1205
3508
  this.markRepairSessionResolvedHash(hash);
1206
3509
  }
@@ -1222,6 +3525,7 @@ export class SimpleSyncronizer {
1222
3525
  hasSyncProcessState() {
1223
3526
  return (this.syncInFlightQueue.size > 0 ||
1224
3527
  this.syncInFlightQueueInverted.size > 0 ||
3528
+ this.pendingSyncAdmissionCount > 0 ||
1225
3529
  this.syncInFlight.size > 0);
1226
3530
  }
1227
3531
  clearSyncProcessKey(key) {
@@ -1236,35 +3540,149 @@ export class SimpleSyncronizer {
1236
3540
  }
1237
3541
  }
1238
3542
  }
3543
+ const trackedClaimants = this.syncInFlightQueueClaimants.get(key);
3544
+ this.pendingSyncClaimCount = Math.max(0, this.pendingSyncClaimCount -
3545
+ (trackedClaimants?.size ?? inflight.length));
1239
3546
  this.syncInFlightQueue.delete(key);
1240
3547
  }
3548
+ this.syncInFlightQueueClaimants.delete(key);
3549
+ this.syncInFlightQueueClaimantIndexes.delete(key);
3550
+ this.syncInFlightQueueRoundRobinCursor.delete(key);
3551
+ if (typeof key === "bigint") {
3552
+ this.removeQueuedSyncCoordinateAlias(key);
3553
+ }
3554
+ this.removePendingSyncKeyExpiry(key);
3555
+ this.syncInFlightQueueExpiresAt.delete(key);
3556
+ this.clearPendingSyncExpiryTimerIfIdle();
1241
3557
  this.clearSyncInFlightKey(key);
1242
3558
  }
1243
- clearSyncInFlightKey(key) {
1244
- for (const [peer, map] of this.syncInFlight) {
1245
- map.delete(key);
1246
- if (map.size === 0) {
1247
- this.syncInFlight.delete(peer);
3559
+ removePendingSyncClaim(key, peer) {
3560
+ const inflight = this.syncInFlightQueue.get(key);
3561
+ if (!inflight) {
3562
+ return;
3563
+ }
3564
+ let claimants = this.syncInFlightQueueClaimants.get(key);
3565
+ let claimantIndexes = this.syncInFlightQueueClaimantIndexes.get(key);
3566
+ if (!claimants || !claimantIndexes) {
3567
+ claimants = new Set();
3568
+ claimantIndexes = new Map();
3569
+ for (let index = 0; index < inflight.length; index += 1) {
3570
+ const claimant = inflight[index].hashcode();
3571
+ claimants.add(claimant);
3572
+ claimantIndexes.set(claimant, index);
1248
3573
  }
3574
+ if (!this.syncInFlightQueueClaimants.has(key)) {
3575
+ this.pendingSyncClaimCount += claimants.size;
3576
+ }
3577
+ this.syncInFlightQueueClaimants.set(key, claimants);
3578
+ this.syncInFlightQueueClaimantIndexes.set(key, claimantIndexes);
3579
+ }
3580
+ const index = claimantIndexes.get(peer);
3581
+ if (index == null) {
3582
+ return;
3583
+ }
3584
+ const lastIndex = inflight.length - 1;
3585
+ if (index !== lastIndex) {
3586
+ const lastClaimant = inflight[lastIndex];
3587
+ const lastClaimantHash = lastClaimant.hashcode();
3588
+ inflight[index] = lastClaimant;
3589
+ claimantIndexes.set(lastClaimantHash, index);
3590
+ }
3591
+ inflight.pop();
3592
+ claimantIndexes.delete(peer);
3593
+ claimants.delete(peer);
3594
+ this.pendingSyncClaimCount = Math.max(0, this.pendingSyncClaimCount - 1);
3595
+ const inverted = this.syncInFlightQueueInverted.get(peer);
3596
+ inverted?.delete(key);
3597
+ if (inverted?.size === 0) {
3598
+ this.syncInFlightQueueInverted.delete(peer);
3599
+ }
3600
+ if (inflight.length > 0) {
3601
+ const cursor = this.syncInFlightQueueRoundRobinCursor.get(key) ?? 0;
3602
+ this.syncInFlightQueueRoundRobinCursor.set(key, cursor === lastIndex
3603
+ ? index % inflight.length
3604
+ : cursor % inflight.length);
3605
+ return;
3606
+ }
3607
+ this.syncInFlightQueue.delete(key);
3608
+ this.syncInFlightQueueClaimants.delete(key);
3609
+ this.syncInFlightQueueClaimantIndexes.delete(key);
3610
+ this.syncInFlightQueueRoundRobinCursor.delete(key);
3611
+ if (typeof key === "bigint") {
3612
+ this.removeQueuedSyncCoordinateAlias(key);
1249
3613
  }
3614
+ this.removePendingSyncKeyExpiry(key);
3615
+ this.syncInFlightQueueExpiresAt.delete(key);
3616
+ this.clearSyncInFlightKey(key);
3617
+ this.clearPendingSyncExpiryTimerIfIdle();
1250
3618
  }
1251
- forEachKnownAlias(hash, callback) {
1252
- callback(hash);
1253
- if (this.syncInFlightQueue.size === 0 && this.syncInFlight.size === 0) {
3619
+ removeSyncInFlightTargetKey(peer, key) {
3620
+ const map = this.syncInFlight.get(peer);
3621
+ if (!map?.delete(key)) {
3622
+ return;
3623
+ }
3624
+ if (map.size === 0) {
3625
+ this.syncInFlight.delete(peer);
3626
+ }
3627
+ const targets = this.syncInFlightTargetsByKey.get(key);
3628
+ targets?.delete(peer);
3629
+ if (targets?.size === 0) {
3630
+ this.syncInFlightTargetsByKey.delete(key);
3631
+ }
3632
+ }
3633
+ setSyncInFlightTargetKey(peer, key, timestamp) {
3634
+ let map = this.syncInFlight.get(peer);
3635
+ if (!map) {
3636
+ map = new Map();
3637
+ this.syncInFlight.set(peer, map);
3638
+ }
3639
+ const existing = map.get(key);
3640
+ if (!existing || existing.timestamp < timestamp) {
3641
+ map.set(key, { timestamp });
3642
+ }
3643
+ let targets = this.syncInFlightTargetsByKey.get(key);
3644
+ if (!targets) {
3645
+ targets = new Set();
3646
+ this.syncInFlightTargetsByKey.set(key, targets);
3647
+ }
3648
+ targets.add(peer);
3649
+ }
3650
+ clearSyncInFlightTarget(peer) {
3651
+ const map = this.syncInFlight.get(peer);
3652
+ if (!map) {
1254
3653
  return;
1255
3654
  }
1256
- for (const key of this.syncInFlightQueue.keys()) {
1257
- if (typeof key === "bigint" && this.coordinateToHash.get(key) === hash) {
1258
- callback(key);
3655
+ for (const key of map.keys()) {
3656
+ const targets = this.syncInFlightTargetsByKey.get(key);
3657
+ targets?.delete(peer);
3658
+ if (targets?.size === 0) {
3659
+ this.syncInFlightTargetsByKey.delete(key);
1259
3660
  }
1260
3661
  }
1261
- for (const map of this.syncInFlight.values()) {
1262
- for (const key of map.keys()) {
1263
- if (typeof key === "bigint" &&
1264
- this.coordinateToHash.get(key) === hash) {
1265
- callback(key);
3662
+ this.syncInFlight.delete(peer);
3663
+ }
3664
+ clearSyncInFlightKey(key) {
3665
+ const targets = this.syncInFlightTargetsByKey.get(key);
3666
+ if (!targets) {
3667
+ // Defensive compatibility for tests or integrations that seed the public
3668
+ // syncInFlight map directly. Internal writes always populate the index.
3669
+ for (const [peer, map] of this.syncInFlight) {
3670
+ if (map.has(key)) {
3671
+ this.removeSyncInFlightTargetKey(peer, key);
1266
3672
  }
1267
3673
  }
3674
+ return;
3675
+ }
3676
+ for (const peer of [...targets]) {
3677
+ this.removeSyncInFlightTargetKey(peer, key);
3678
+ }
3679
+ }
3680
+ forEachKnownAlias(hash, callback) {
3681
+ callback(hash);
3682
+ for (const coordinate of [
3683
+ ...(this.syncInFlightQueuedCoordinatesByHash.get(hash) ?? []),
3684
+ ]) {
3685
+ callback(coordinate);
1268
3686
  }
1269
3687
  }
1270
3688
  clearSyncInFlightForPeer(publicKeyHash, hash) {
@@ -1272,59 +3690,31 @@ export class SimpleSyncronizer {
1272
3690
  if (!map) {
1273
3691
  return;
1274
3692
  }
1275
- this.forEachKnownAlias(hash, (key) => map.delete(key));
1276
- if (map.size === 0) {
1277
- this.syncInFlight.delete(publicKeyHash);
1278
- }
3693
+ this.refreshQueuedSyncCoordinateAliases();
3694
+ this.forEachKnownAlias(hash, (key) => this.removeSyncInFlightTargetKey(publicKeyHash, key));
1279
3695
  }
1280
3696
  clearSyncInFlightForPeerHashes(publicKeyHash, hashes) {
1281
3697
  const map = this.syncInFlight.get(publicKeyHash);
1282
3698
  if (!map || hashes.length === 0) {
1283
3699
  return;
1284
3700
  }
1285
- const keys = new Set(hashes);
1286
- const hashSet = new Set(hashes);
1287
- for (const key of map.keys()) {
1288
- if (typeof key !== "bigint") {
1289
- continue;
1290
- }
1291
- const hash = this.coordinateToHash.get(key);
1292
- if (hash != null && hashSet.has(hash)) {
1293
- keys.add(key);
1294
- }
1295
- }
1296
- for (const key of keys) {
1297
- map.delete(key);
1298
- }
1299
- if (map.size === 0) {
1300
- this.syncInFlight.delete(publicKeyHash);
3701
+ this.refreshQueuedSyncCoordinateAliases();
3702
+ for (const hash of hashes) {
3703
+ this.forEachKnownAlias(hash, (key) => this.removeSyncInFlightTargetKey(publicKeyHash, key));
1301
3704
  }
1302
3705
  }
1303
3706
  clearSyncProcess(hash) {
3707
+ this.refreshQueuedSyncCoordinateAliases();
1304
3708
  this.forEachKnownAlias(hash, (key) => this.clearSyncProcessKey(key));
1305
3709
  }
1306
3710
  clearSyncProcesses(hashes) {
1307
3711
  if (hashes.length === 0) {
1308
3712
  return;
1309
3713
  }
1310
- const keys = new Set(hashes);
1311
- const hashSet = new Set(hashes);
1312
- const maybeAddAlias = (key) => {
1313
- if (typeof key !== "bigint") {
1314
- return;
1315
- }
1316
- const hash = this.coordinateToHash.get(key);
1317
- if (hash != null && hashSet.has(hash)) {
1318
- keys.add(key);
1319
- }
1320
- };
1321
- for (const key of this.syncInFlightQueue.keys()) {
1322
- maybeAddAlias(key);
1323
- }
1324
- for (const map of this.syncInFlight.values()) {
1325
- for (const key of map.keys()) {
1326
- maybeAddAlias(key);
1327
- }
3714
+ this.refreshQueuedSyncCoordinateAliases();
3715
+ const keys = new Set();
3716
+ for (const hash of hashes) {
3717
+ this.forEachKnownAlias(hash, (key) => keys.add(key));
1328
3718
  }
1329
3719
  for (const key of keys) {
1330
3720
  this.clearSyncProcessKey(key);
@@ -1335,21 +3725,25 @@ export class SimpleSyncronizer {
1335
3725
  return this.clearSyncProcessPublicKeyHash(publicKeyHash);
1336
3726
  }
1337
3727
  clearSyncProcessPublicKeyHash(publicKeyHash) {
1338
- this.syncInFlight.delete(publicKeyHash);
3728
+ this.syncDispatchTargetEpochs.delete(publicKeyHash);
3729
+ this.clearPendingSyncAdmissions(publicKeyHash);
3730
+ for (const targetLifecycle of [
3731
+ ...(this.syncDispatchTargets.get(publicKeyHash) ?? []),
3732
+ ]) {
3733
+ if (targetLifecycle.lifecycle.abortAllOnTargetDisconnect) {
3734
+ this.abortSyncDispatchLifecycle(targetLifecycle.lifecycle, new Error("sync target disconnected"));
3735
+ }
3736
+ else {
3737
+ this.abortSyncDispatchTarget(targetLifecycle, new Error("sync target disconnected"));
3738
+ }
3739
+ }
3740
+ this.clearSyncInFlightTarget(publicKeyHash);
1339
3741
  this.recentlySentExchangeHeads.delete(publicKeyHash);
3742
+ this.clearPendingMaybeSyncResponses(publicKeyHash);
1340
3743
  const map = this.syncInFlightQueueInverted.get(publicKeyHash);
1341
3744
  if (map) {
1342
- for (const hash of map) {
1343
- const arr = this.syncInFlightQueue.get(hash);
1344
- if (arr) {
1345
- const filtered = arr.filter((x) => x.hashcode() !== publicKeyHash);
1346
- if (filtered.length > 0) {
1347
- this.syncInFlightQueue.set(hash, filtered);
1348
- }
1349
- else {
1350
- this.syncInFlightQueue.delete(hash);
1351
- }
1352
- }
3745
+ for (const hash of [...map]) {
3746
+ this.removePendingSyncClaim(hash, publicKeyHash);
1353
3747
  }
1354
3748
  this.syncInFlightQueueInverted.delete(publicKeyHash);
1355
3749
  }