@peerbit/shared-log 13.2.24 → 13.2.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/src/checked-prune.d.ts +12 -0
  2. package/dist/src/checked-prune.d.ts.map +1 -1
  3. package/dist/src/checked-prune.js +33 -0
  4. package/dist/src/checked-prune.js.map +1 -1
  5. package/dist/src/coordinate-persistence.d.ts +211 -0
  6. package/dist/src/coordinate-persistence.d.ts.map +1 -0
  7. package/dist/src/coordinate-persistence.js +1133 -0
  8. package/dist/src/coordinate-persistence.js.map +1 -0
  9. package/dist/src/errors.d.ts +1 -0
  10. package/dist/src/errors.d.ts.map +1 -1
  11. package/dist/src/errors.js +19 -0
  12. package/dist/src/errors.js.map +1 -1
  13. package/dist/src/index.d.ts +127 -133
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/index.js +1400 -4269
  16. package/dist/src/index.js.map +1 -1
  17. package/dist/src/instance-lifecycle.d.ts +117 -0
  18. package/dist/src/instance-lifecycle.d.ts.map +1 -0
  19. package/dist/src/instance-lifecycle.js +254 -0
  20. package/dist/src/instance-lifecycle.js.map +1 -0
  21. package/dist/src/join-warmup.d.ts +86 -0
  22. package/dist/src/join-warmup.d.ts.map +1 -0
  23. package/dist/src/join-warmup.js +351 -0
  24. package/dist/src/join-warmup.js.map +1 -0
  25. package/dist/src/native-write-through-block-store.d.ts +123 -0
  26. package/dist/src/native-write-through-block-store.d.ts.map +1 -0
  27. package/dist/src/native-write-through-block-store.js +989 -0
  28. package/dist/src/native-write-through-block-store.js.map +1 -0
  29. package/dist/src/peer-session.d.ts +117 -0
  30. package/dist/src/peer-session.d.ts.map +1 -0
  31. package/dist/src/peer-session.js +259 -0
  32. package/dist/src/peer-session.js.map +1 -0
  33. package/dist/src/replication-announcement.d.ts +116 -0
  34. package/dist/src/replication-announcement.d.ts.map +1 -0
  35. package/dist/src/replication-announcement.js +515 -0
  36. package/dist/src/replication-announcement.js.map +1 -0
  37. package/dist/src/replicator-liveness.d.ts +59 -0
  38. package/dist/src/replicator-liveness.d.ts.map +1 -0
  39. package/dist/src/replicator-liveness.js +304 -0
  40. package/dist/src/replicator-liveness.js.map +1 -0
  41. package/dist/src/sync/dispatch-lifecycle.d.ts +48 -0
  42. package/dist/src/sync/dispatch-lifecycle.d.ts.map +1 -0
  43. package/dist/src/sync/dispatch-lifecycle.js +141 -0
  44. package/dist/src/sync/dispatch-lifecycle.js.map +1 -0
  45. package/dist/src/sync/factory.d.ts +29 -0
  46. package/dist/src/sync/factory.d.ts.map +1 -0
  47. package/dist/src/sync/factory.js +87 -0
  48. package/dist/src/sync/factory.js.map +1 -0
  49. package/dist/src/sync/pending-sync-store.d.ts +105 -0
  50. package/dist/src/sync/pending-sync-store.d.ts.map +1 -0
  51. package/dist/src/sync/pending-sync-store.js +877 -0
  52. package/dist/src/sync/pending-sync-store.js.map +1 -0
  53. package/dist/src/sync/rateless-iblt.d.ts +4 -1
  54. package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
  55. package/dist/src/sync/rateless-iblt.js +102 -106
  56. package/dist/src/sync/rateless-iblt.js.map +1 -1
  57. package/dist/src/sync/simple.d.ts +10 -41
  58. package/dist/src/sync/simple.d.ts.map +1 -1
  59. package/dist/src/sync/simple.js +259 -952
  60. package/dist/src/sync/simple.js.map +1 -1
  61. package/dist/src/sync/sync-peer-state.d.ts +16 -0
  62. package/dist/src/sync/sync-peer-state.d.ts.map +1 -0
  63. package/dist/src/sync/sync-peer-state.js +82 -0
  64. package/dist/src/sync/sync-peer-state.js.map +1 -0
  65. package/package.json +12 -12
  66. package/src/checked-prune.ts +35 -0
  67. package/src/coordinate-persistence.ts +1796 -0
  68. package/src/errors.ts +21 -0
  69. package/src/index.ts +4014 -7858
  70. package/src/instance-lifecycle.ts +351 -0
  71. package/src/join-warmup.ts +507 -0
  72. package/src/native-write-through-block-store.ts +1170 -0
  73. package/src/peer-session.ts +336 -0
  74. package/src/replication-announcement.ts +744 -0
  75. package/src/replicator-liveness.ts +439 -0
  76. package/src/sync/dispatch-lifecycle.ts +230 -0
  77. package/src/sync/factory.ts +154 -0
  78. package/src/sync/pending-sync-store.ts +1085 -0
  79. package/src/sync/rateless-iblt.ts +122 -132
  80. package/src/sync/simple.ts +334 -1144
  81. package/src/sync/sync-peer-state.ts +107 -0
@@ -39,7 +39,10 @@ import { Entry, Log } from "@peerbit/log";
39
39
  import { CONVERGENCE_MESSAGE_PRIORITY, SilentDelivery, } from "@peerbit/stream-interface";
40
40
  import { EntryWithRefs, createExchangeHeadsMessages, createRawExchangeHeadsMessages, } from "../exchange-heads.js";
41
41
  import { TransportMessage } from "../message.js";
42
+ import { DispatchLifecycleRegistry } from "./dispatch-lifecycle.js";
43
+ import { PendingSyncStore, QUEUED_SYNC_ALIAS_REFRESH_PENDING, } from "./pending-sync-store.js";
42
44
  import { emitSyncProfileDuration, syncProfileStart } from "./profile.js";
45
+ import { SyncPeerSlotRegistry, } from "./sync-peer-state.js";
43
46
  let RequestMaybeSync = (() => {
44
47
  let _classDecorators = [variant([0, 1])];
45
48
  let _classDescriptor;
@@ -429,14 +432,10 @@ export const MAX_PENDING_SIMPLE_SYNC_LOOKUPS_GLOBAL = 32;
429
432
  // Retry scanning can touch persistent indexes. Bound each pass so a full
430
433
  // adversarial queue cannot force 40,000 lookups in one event-loop turn.
431
434
  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;
435
+ // The pending-claim TTL, alias-refresh bound and pending-refresh sentinel
436
+ // moved into ./pending-sync-store.ts with the record store (stage-4 sync
437
+ // unification). Re-exported here for compatibility.
438
+ export { PENDING_SIMPLE_SYNC_KEY_TTL_MS } from "./pending-sync-store.js";
440
439
  // Bound retained request/response associations globally. Ten thousand hashes
441
440
  // covers several full default-size request batches while keeping adversarial or
442
441
  // abandoned requests to a small, predictable amount of heap.
@@ -455,38 +454,35 @@ const createDeferred = () => {
455
454
  return { promise, resolve, reject };
456
455
  };
457
456
  export class SimpleSyncronizer {
458
- // map of hash to public keys that we can ask for entries
459
- syncInFlightQueue;
460
- syncInFlightQueueInverted;
461
- syncInFlightQueueExpiresAt;
462
- syncInFlightQueueExpiryTimer;
463
- pendingSyncExpiryHeap;
464
- pendingSyncKeyExpiryNodes;
465
- pendingSyncAdmissionExpiryNodes;
457
+ // The pending-sync record store owns the claim/admission state. The
458
+ // legacy public map instances (syncInFlightQueue, syncInFlightQueueInverted,
459
+ // syncInFlight, ...) live inside it and are exposed through the accessors
460
+ // below under their historical names.
461
+ pendingSync;
466
462
  syncInFlightRetryIterator;
467
463
  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
464
  pendingCoordinateLookupCount;
484
465
  pendingCoordinateLookupCountByPeer;
485
466
  pendingCoordinateResponseCount;
486
467
  pendingCoordinateResponseCountByPeer;
468
+ // Per-peer slot rows for coordinate lookups, coordinate responses and
469
+ // active maybe-sync responses (shared registry; see sync-peer-state.ts
470
+ // for the lifetime policy). Release closures capture the row object:
471
+ // once a disconnect detaches the row, the peer's slots return to the
472
+ // aggregate quota immediately, and a late release only settles the
473
+ // detached row — it never touches a reconnected successor's quota.
474
+ peerSlotRows;
475
+ // map of hash to public keys that we can ask for entries
476
+ get syncInFlightQueue() {
477
+ return this.pendingSync.syncInFlightQueue;
478
+ }
479
+ get syncInFlightQueueInverted() {
480
+ return this.pendingSync.syncInFlightQueueInverted;
481
+ }
487
482
  // map of hash to public keys that we have asked for entries
488
- syncInFlight;
489
- syncInFlightTargetsByKey;
483
+ get syncInFlight() {
484
+ return this.pendingSync.syncInFlight;
485
+ }
490
486
  rpc;
491
487
  log;
492
488
  entryIndex;
@@ -515,39 +511,22 @@ export class SimpleSyncronizer {
515
511
  syncDispatchLifecycleController;
516
512
  syncDispatchTargetEpochCounter;
517
513
  syncDispatchTargetEpochs;
518
- syncDispatchTargets;
514
+ syncDispatchRegistry;
519
515
  repairSessionCounter;
520
516
  repairSessions;
521
517
  // Syncing and dedeplucation work
522
518
  syncMoreInterval;
523
519
  closed;
524
520
  constructor(properties) {
525
- this.syncInFlightQueue = new Map();
526
- this.syncInFlightQueueInverted = new Map();
527
- this.syncInFlightQueueExpiresAt = new Map();
528
- this.pendingSyncExpiryHeap = [];
529
- this.pendingSyncKeyExpiryNodes = new Map();
530
- this.pendingSyncAdmissionExpiryNodes = new Map();
531
- this.syncInFlightQueueClaimants = new Map();
532
- this.syncInFlightQueueClaimantIndexes = new Map();
533
- this.syncInFlightQueueRoundRobinCursor = new Map();
534
- this.syncInFlightQueuedCoordinates = new Set();
535
- this.syncInFlightQueuedHashByCoordinate = new Map();
536
- this.syncInFlightQueuedCoordinatesByHash = new Map();
537
- this.pendingSyncClaimCount = 0;
538
- this.pendingSyncAdmissionCount = 0;
539
- this.pendingSyncActiveAdmissionReservations = 0;
540
- this.pendingSyncAdmissionCountByPeer = new Map();
541
- this.pendingSyncAdmissionIdentitiesByPeer = new Map();
542
- this.pendingSyncAdmissionReservations = new Set();
543
- this.pendingSyncAdmissionReservationsByPeer = new Map();
544
- this.pendingSyncAdmissionReservationsByIdentity = new Map();
521
+ this.pendingSync = new PendingSyncStore({
522
+ coordinateToHash: properties.coordinateToHash,
523
+ isDispatchEpochCurrent: (peer, epoch) => this.syncDispatchTargetEpochs.get(peer) === epoch,
524
+ });
545
525
  this.pendingCoordinateLookupCount = 0;
546
526
  this.pendingCoordinateLookupCountByPeer = new Map();
547
527
  this.pendingCoordinateResponseCount = 0;
548
528
  this.pendingCoordinateResponseCountByPeer = new Map();
549
- this.syncInFlight = new Map();
550
- this.syncInFlightTargetsByKey = new Map();
529
+ this.peerSlotRows = new SyncPeerSlotRegistry();
551
530
  this.rpc = properties.rpc;
552
531
  this.log = properties.log;
553
532
  this.entryIndex = properties.entryIndex;
@@ -575,7 +554,19 @@ export class SimpleSyncronizer {
575
554
  this.syncDispatchLifecycleController = new AbortController();
576
555
  this.syncDispatchTargetEpochCounter = 0;
577
556
  this.syncDispatchTargetEpochs = new Map();
578
- this.syncDispatchTargets = new Map();
557
+ this.syncDispatchRegistry = new DispatchLifecycleRegistry({
558
+ isClosed: () => this.closed === true,
559
+ currentOwnershipLifecycleController: () => this.syncDispatchLifecycleController,
560
+ hasRetainedWork: (lifecycle) => lifecycle.retainedWork > 0,
561
+ // Target-activity strategy: dispatch-epoch identity.
562
+ isTargetCurrent: (targetLifecycle) => this.syncDispatchTargetEpochs.get(targetLifecycle.target) ===
563
+ targetLifecycle.epoch,
564
+ onTargetAborted: (targetLifecycle) => {
565
+ for (const batch of [...targetLifecycle.batches]) {
566
+ this.removePendingMaybeSyncResponseBatch(batch);
567
+ }
568
+ },
569
+ });
579
570
  this.repairSessionCounter = 0;
580
571
  this.repairSessions = new Map();
581
572
  }
@@ -708,7 +699,7 @@ export class SimpleSyncronizer {
708
699
  disposed: false,
709
700
  abortAllOnTargetDisconnect: options?.abortAllOnTargetDisconnect === true,
710
701
  };
711
- for (const target of [...new Set(targets)]) {
702
+ this.syncDispatchRegistry.register(lifecycle, targets, (forLifecycle, target) => {
712
703
  const expectedEpoch = options?.targetEpochs?.get(target);
713
704
  const currentEpoch = this.syncDispatchTargetEpochs.get(target);
714
705
  const epoch = expectedEpoch ??
@@ -717,10 +708,10 @@ export class SimpleSyncronizer {
717
708
  ? undefined
718
709
  : this.getOrCreateSyncDispatchTargetEpoch(target));
719
710
  if (!epoch) {
720
- continue;
711
+ return undefined;
721
712
  }
722
- const targetLifecycle = {
723
- lifecycle,
713
+ return {
714
+ lifecycle: forLifecycle,
724
715
  target,
725
716
  epoch,
726
717
  controller: new AbortController(),
@@ -728,94 +719,31 @@ export class SimpleSyncronizer {
728
719
  responseLeases: 0,
729
720
  activeWaiters: 0,
730
721
  };
731
- lifecycle.targets.set(target, targetLifecycle);
732
- let activeForTarget = this.syncDispatchTargets.get(target);
733
- if (!activeForTarget) {
734
- activeForTarget = new Set();
735
- this.syncDispatchTargets.set(target, activeForTarget);
736
- }
737
- activeForTarget.add(targetLifecycle);
738
- if (this.syncDispatchTargetEpochs.get(target) !== epoch) {
722
+ }, (targetLifecycle) => {
723
+ if (this.syncDispatchTargetEpochs.get(targetLifecycle.target) !==
724
+ targetLifecycle.epoch) {
739
725
  this.abortSyncDispatchTarget(targetLifecycle, new Error("sync target lifecycle is stale"));
740
726
  }
741
- }
742
- ownershipLifecycleController.signal.addEventListener("abort", lifecycle.onOwnerOrCallerAbort, { once: true });
743
- if (callerSignal && callerSignal !== ownershipLifecycleController.signal) {
744
- callerSignal.addEventListener("abort", lifecycle.onOwnerOrCallerAbort, {
745
- once: true,
746
- });
747
- }
748
- if (this.closed === true ||
749
- ownershipLifecycleController !== this.syncDispatchLifecycleController ||
750
- ownershipLifecycleController.signal.aborted ||
751
- callerSignal?.aborted) {
752
- lifecycle.onOwnerOrCallerAbort();
753
- }
727
+ });
754
728
  return lifecycle;
755
729
  }
756
730
  abortSyncDispatchTarget(targetLifecycle, reason) {
757
- if (!targetLifecycle.controller.signal.aborted) {
758
- targetLifecycle.controller.abort(reason);
759
- }
760
- for (const batch of [...targetLifecycle.batches]) {
761
- this.removePendingMaybeSyncResponseBatch(batch);
762
- }
763
- this.maybeDisposeSyncDispatchLifecycle(targetLifecycle.lifecycle);
731
+ this.syncDispatchRegistry.abortTarget(targetLifecycle, reason);
764
732
  }
765
733
  abortSyncDispatchLifecycle(lifecycle, reason) {
766
- if (!lifecycle.controller.signal.aborted) {
767
- lifecycle.controller.abort(reason);
768
- }
769
- for (const targetLifecycle of lifecycle.targets.values()) {
770
- this.abortSyncDispatchTarget(targetLifecycle, reason);
771
- }
772
- this.maybeDisposeSyncDispatchLifecycle(lifecycle);
734
+ this.syncDispatchRegistry.abortLifecycle(lifecycle, reason);
773
735
  }
774
736
  finishSyncDispatchLifecycle(lifecycle) {
775
- lifecycle.dispatchFinished = true;
776
- this.maybeDisposeSyncDispatchLifecycle(lifecycle);
737
+ this.syncDispatchRegistry.finish(lifecycle);
777
738
  }
778
739
  maybeDisposeSyncDispatchLifecycle(lifecycle) {
779
- if (lifecycle.disposed ||
780
- !lifecycle.dispatchFinished ||
781
- lifecycle.retainedWork > 0) {
782
- return;
783
- }
784
- lifecycle.disposed = true;
785
- lifecycle.ownershipLifecycleController.signal.removeEventListener("abort", lifecycle.onOwnerOrCallerAbort);
786
- if (lifecycle.callerSignal &&
787
- lifecycle.callerSignal !== lifecycle.ownershipLifecycleController.signal) {
788
- lifecycle.callerSignal.removeEventListener("abort", lifecycle.onOwnerOrCallerAbort);
789
- }
790
- for (const targetLifecycle of lifecycle.targets.values()) {
791
- const activeForTarget = this.syncDispatchTargets.get(targetLifecycle.target);
792
- activeForTarget?.delete(targetLifecycle);
793
- if (activeForTarget?.size === 0) {
794
- this.syncDispatchTargets.delete(targetLifecycle.target);
795
- }
796
- }
740
+ this.syncDispatchRegistry.maybeDispose(lifecycle);
797
741
  }
798
742
  isSyncDispatchLifecycleActive(lifecycle, target) {
799
- if (this.closed === true ||
800
- lifecycle.disposed ||
801
- lifecycle.ownershipLifecycleController !==
802
- this.syncDispatchLifecycleController ||
803
- lifecycle.ownershipLifecycleController.signal.aborted ||
804
- lifecycle.callerSignal?.aborted ||
805
- lifecycle.controller.signal.aborted) {
806
- return false;
807
- }
808
- if (target === undefined) {
809
- return true;
810
- }
811
- const targetLifecycle = lifecycle.targets.get(target);
812
- return (targetLifecycle !== undefined &&
813
- !targetLifecycle.controller.signal.aborted &&
814
- this.syncDispatchTargetEpochs.get(target) === targetLifecycle.epoch);
743
+ return this.syncDispatchRegistry.isLifecycleActive(lifecycle, target);
815
744
  }
816
745
  getSyncDispatchSignal(lifecycle, target) {
817
- return (lifecycle.targets.get(target)?.controller.signal ??
818
- lifecycle.controller.signal);
746
+ return this.syncDispatchRegistry.getSignal(lifecycle, target);
819
747
  }
820
748
  pendingMaybeSyncResponseWaiterBefore(left, right) {
821
749
  return left.order < right.order;
@@ -1336,6 +1264,14 @@ export class SimpleSyncronizer {
1336
1264
  beginDelivery: () => {
1337
1265
  for (const authorization of addedAuthorizations) {
1338
1266
  if (!authorization.settled) {
1267
+ if (authorization.deliveryInFlight !== true &&
1268
+ authorization.active === true &&
1269
+ authorization.slotRow?.attached === true) {
1270
+ // The response was accepted before the request send began:
1271
+ // custody of the hash-budget unit moves from the slot row to
1272
+ // finishDelivery for the duration of the send.
1273
+ authorization.slotRow.pendingResponseHashes -= 1;
1274
+ }
1339
1275
  authorization.deliveryInFlight = true;
1340
1276
  }
1341
1277
  }
@@ -1353,6 +1289,13 @@ export class SimpleSyncronizer {
1353
1289
  ?.get(authorization.hash) !== authorization) {
1354
1290
  releasedCount += 1;
1355
1291
  }
1292
+ else if (authorization.active === true &&
1293
+ authorization.slotRow?.attached === true) {
1294
+ // The response was accepted while the request send was in
1295
+ // flight; custody of the hash-budget unit returns to the
1296
+ // active lease's slot row.
1297
+ authorization.slotRow.pendingResponseHashes += 1;
1298
+ }
1356
1299
  }
1357
1300
  if (releasedCount > 0) {
1358
1301
  this.pendingMaybeSyncResponseCount -= releasedCount;
@@ -1609,35 +1552,65 @@ export class SimpleSyncronizer {
1609
1552
  if (acceptedByLifecycle.size === 0) {
1610
1553
  return [];
1611
1554
  }
1555
+ const slotRow = this.getOrCreateSyncResponseSlotRow(fromHash);
1556
+ slotRow.active += 1;
1612
1557
  this.activeMaybeSyncResponseCount += 1;
1613
1558
  this.activeMaybeSyncResponseCountByPeer.set(fromHash, (this.activeMaybeSyncResponseCountByPeer.get(fromHash) ?? 0) + 1);
1614
- let remainingLeases = acceptedByLifecycle.size;
1559
+ // Accepted authorizations leave their batch (above) but stay charged
1560
+ // against the global hash budget. Move that charge into row custody so a
1561
+ // disconnect can return it even when the response ship never settles;
1562
+ // units currently owned by finishDelivery (deliveryInFlight) stay out.
1563
+ for (const { authorizations } of acceptedByLifecycle.values()) {
1564
+ for (const authorization of authorizations) {
1565
+ authorization.slotRow = slotRow;
1566
+ if (authorization.deliveryInFlight !== true) {
1567
+ slotRow.pendingResponseHashes += 1;
1568
+ }
1569
+ }
1570
+ }
1571
+ let remainingPhysicalLeases = acceptedByLifecycle.size;
1615
1572
  return [...acceptedByLifecycle].map(([targetLifecycle, { hashes: acceptedHashes, authorizations }]) => {
1616
- let released = false;
1617
- return {
1618
- hashes: acceptedHashes,
1619
- signal: targetLifecycle.controller.signal,
1620
- release: (options) => {
1621
- if (released) {
1622
- return;
1623
- }
1624
- released = true;
1625
- const pendingForTarget = this.pendingMaybeSyncResponses.get(fromHash);
1626
- for (const authorization of authorizations) {
1627
- this.settlePendingMaybeSyncResponseAuthorization(authorization, options?.fulfilled === true);
1628
- if (pendingForTarget?.get(authorization.hash) === authorization) {
1629
- pendingForTarget.delete(authorization.hash);
1630
- }
1631
- }
1632
- if (pendingForTarget?.size === 0) {
1633
- this.pendingMaybeSyncResponses.delete(fromHash);
1573
+ let logicalReleased = false;
1574
+ let physicalReleased = false;
1575
+ const releaseLogical = (options) => {
1576
+ if (logicalReleased) {
1577
+ return;
1578
+ }
1579
+ logicalReleased = true;
1580
+ slotRow.activeReleases.delete(releaseLogical);
1581
+ const pendingForTarget = this.pendingMaybeSyncResponses.get(fromHash);
1582
+ for (const authorization of authorizations) {
1583
+ this.settlePendingMaybeSyncResponseAuthorization(authorization, options?.fulfilled === true);
1584
+ if (pendingForTarget?.get(authorization.hash) === authorization) {
1585
+ pendingForTarget.delete(authorization.hash);
1634
1586
  }
1635
- this.pendingMaybeSyncResponseCount -= authorizations.filter((authorization) => authorization.deliveryInFlight !== true).length;
1636
- targetLifecycle.responseLeases -= 1;
1637
- targetLifecycle.lifecycle.retainedWork -= 1;
1638
- remainingLeases -= 1;
1639
- if (remainingLeases === 0) {
1640
- this.activeMaybeSyncResponseCount -= 1;
1587
+ }
1588
+ if (pendingForTarget?.size === 0) {
1589
+ this.pendingMaybeSyncResponses.delete(fromHash);
1590
+ }
1591
+ const chargedToRow = authorizations.filter((authorization) => authorization.deliveryInFlight !== true).length;
1592
+ if (slotRow.attached) {
1593
+ this.pendingMaybeSyncResponseCount -= chargedToRow;
1594
+ slotRow.pendingResponseHashes -= chargedToRow;
1595
+ }
1596
+ // else: the hash budget already returned in aggregate when the row
1597
+ // detached; only the deliveryInFlight units (owned by
1598
+ // finishDelivery) remain charged.
1599
+ targetLifecycle.responseLeases -= 1;
1600
+ targetLifecycle.lifecycle.retainedWork -= 1;
1601
+ this.schedulePendingMaybeSyncResponseWaiter();
1602
+ this.maybeDisposeSyncDispatchLifecycle(targetLifecycle.lifecycle);
1603
+ };
1604
+ const releasePhysical = () => {
1605
+ if (physicalReleased) {
1606
+ return;
1607
+ }
1608
+ physicalReleased = true;
1609
+ remainingPhysicalLeases -= 1;
1610
+ if (remainingPhysicalLeases === 0) {
1611
+ slotRow.active -= 1;
1612
+ this.activeMaybeSyncResponseCount -= 1;
1613
+ if (slotRow.attached) {
1641
1614
  const activeForPeer = (this.activeMaybeSyncResponseCountByPeer.get(fromHash) ?? 1) -
1642
1615
  1;
1643
1616
  if (activeForPeer === 0) {
@@ -1646,9 +1619,21 @@ export class SimpleSyncronizer {
1646
1619
  else {
1647
1620
  this.activeMaybeSyncResponseCountByPeer.set(fromHash, activeForPeer);
1648
1621
  }
1622
+ this.maybeDropIdleSyncResponseSlotRow(slotRow);
1623
+ }
1624
+ }
1625
+ };
1626
+ slotRow.activeReleases.add(releaseLogical);
1627
+ return {
1628
+ hashes: acceptedHashes,
1629
+ signal: targetLifecycle.controller.signal,
1630
+ release: (options) => {
1631
+ try {
1632
+ releaseLogical(options);
1633
+ }
1634
+ finally {
1635
+ releasePhysical();
1649
1636
  }
1650
- this.schedulePendingMaybeSyncResponseWaiter();
1651
- this.maybeDisposeSyncDispatchLifecycle(targetLifecycle.lifecycle);
1652
1637
  },
1653
1638
  };
1654
1639
  });
@@ -2164,11 +2149,26 @@ export class SimpleSyncronizer {
2164
2149
  return true;
2165
2150
  }
2166
2151
  const target = from.hashcode();
2167
- const releaseLookup = this.tryAcquireCoordinateLookup(target);
2168
- if (!releaseLookup) {
2152
+ const lookupPermit = this.tryAcquireCoordinateLookup(target);
2153
+ if (!lookupPermit) {
2169
2154
  return true;
2170
2155
  }
2156
+ const { release: releaseLookup, row: slotRow } = lookupPermit;
2171
2157
  const lifecycle = this.captureSyncDispatchLifecycle([target]);
2158
+ let lifecycleFinished = false;
2159
+ const finishLifecycle = () => {
2160
+ if (lifecycleFinished) {
2161
+ return;
2162
+ }
2163
+ lifecycleFinished = true;
2164
+ slotRow.activeReleases.delete(finishLifecycle);
2165
+ this.finishSyncDispatchLifecycle(lifecycle);
2166
+ this.maybeDropIdleSyncResponseSlotRow(slotRow);
2167
+ };
2168
+ // Dispatch completion is logical ownership. A disconnected peer must
2169
+ // not retain this lifecycle/listeners while a non-abortable lookup or
2170
+ // shipment keeps only its global physical permit alive.
2171
+ slotRow.activeReleases.add(finishLifecycle);
2172
2172
  let lookupReleased = false;
2173
2173
  const finishLookup = () => {
2174
2174
  if (lookupReleased) {
@@ -2228,7 +2228,7 @@ export class SimpleSyncronizer {
2228
2228
  }
2229
2229
  finally {
2230
2230
  finishLookup();
2231
- this.finishSyncDispatchLifecycle(lifecycle);
2231
+ finishLifecycle();
2232
2232
  }
2233
2233
  }
2234
2234
  else {
@@ -2246,284 +2246,86 @@ export class SimpleSyncronizer {
2246
2246
  this.markRepairSessionResolvedHashes(properties.hashes);
2247
2247
  }
2248
2248
  getPendingSyncKeyIdentity(key) {
2249
- if (typeof key === "string") {
2250
- return key;
2251
- }
2252
- const hash = this.coordinateToHash.get(key);
2253
- return hash ?? key;
2254
- }
2255
- removeQueuedSyncCoordinateAlias(key) {
2256
- this.syncInFlightQueuedCoordinates.delete(key);
2257
- if (this.syncInFlightQueuedCoordinates.size === 0) {
2258
- this.syncInFlightQueuedCoordinateRefreshIterator = undefined;
2259
- }
2260
- const previousHash = this.syncInFlightQueuedHashByCoordinate.get(key);
2261
- this.syncInFlightQueuedHashByCoordinate.delete(key);
2262
- if (previousHash != null) {
2263
- const coordinates = this.syncInFlightQueuedCoordinatesByHash.get(previousHash);
2264
- coordinates?.delete(key);
2265
- if (coordinates?.size === 0) {
2266
- this.syncInFlightQueuedCoordinatesByHash.delete(previousHash);
2267
- }
2268
- }
2269
- }
2270
- refreshQueuedSyncCoordinateAlias(key) {
2271
- if (!this.syncInFlightQueue.has(key)) {
2272
- this.removeQueuedSyncCoordinateAlias(key);
2273
- return;
2274
- }
2275
- this.syncInFlightQueuedCoordinates.add(key);
2276
- const hash = this.coordinateToHash.get(key) ?? undefined;
2277
- const previousHash = this.syncInFlightQueuedHashByCoordinate.get(key);
2278
- if (previousHash !== hash) {
2279
- if (previousHash != null) {
2280
- const coordinates = this.syncInFlightQueuedCoordinatesByHash.get(previousHash);
2281
- coordinates?.delete(key);
2282
- if (coordinates?.size === 0) {
2283
- this.syncInFlightQueuedCoordinatesByHash.delete(previousHash);
2284
- }
2285
- }
2286
- if (hash == null) {
2287
- this.syncInFlightQueuedHashByCoordinate.delete(key);
2288
- }
2289
- else {
2290
- this.syncInFlightQueuedHashByCoordinate.set(key, hash);
2291
- }
2292
- }
2293
- if (hash != null) {
2294
- let coordinates = this.syncInFlightQueuedCoordinatesByHash.get(hash);
2295
- if (!coordinates) {
2296
- coordinates = new Set();
2297
- this.syncInFlightQueuedCoordinatesByHash.set(hash, coordinates);
2298
- }
2299
- coordinates.add(key);
2300
- this.reconcileQueuedSyncCoordinateAlias(key, hash);
2301
- }
2302
- }
2303
- reconcileQueuedSyncCoordinateAlias(coordinate, hash) {
2304
- const now = Date.now();
2305
- const coordinateExpiresAt = this.syncInFlightQueueExpiresAt.get(coordinate);
2306
- if (coordinateExpiresAt != null && coordinateExpiresAt <= now) {
2307
- this.clearSyncProcessKey(coordinate);
2308
- return;
2309
- }
2310
- const hashExpiresAt = this.syncInFlightQueueExpiresAt.get(hash);
2311
- if (hashExpiresAt != null && hashExpiresAt <= now) {
2312
- this.clearSyncProcessKey(hash);
2313
- return;
2314
- }
2315
- const hashClaimants = this.syncInFlightQueue.get(hash);
2316
- if (!hashClaimants || !this.syncInFlightQueue.has(coordinate)) {
2317
- return;
2318
- }
2319
- const expiresAt = Math.min(this.syncInFlightQueueExpiresAt.get(coordinate) ?? Infinity, this.syncInFlightQueueExpiresAt.get(hash) ?? Infinity);
2320
- for (const claimant of [...hashClaimants]) {
2321
- this.addPendingSyncClaim(coordinate, claimant, expiresAt);
2322
- }
2323
- if (Number.isFinite(expiresAt)) {
2324
- this.movePendingSyncKeyExpiryEarlier(coordinate, expiresAt);
2325
- }
2326
- for (const target of [...(this.syncInFlightTargetsByKey.get(hash) ?? [])]) {
2327
- const state = this.syncInFlight.get(target)?.get(hash);
2328
- if (state) {
2329
- this.setSyncInFlightTargetKey(target, coordinate, state.timestamp);
2330
- }
2331
- }
2332
- this.clearSyncProcessKey(hash);
2249
+ return this.pendingSync.getPendingSyncKeyIdentity(key);
2333
2250
  }
2334
2251
  refreshQueuedSyncCoordinateAliases() {
2335
- if (this.syncInFlightQueuedCoordinates.size === 0 &&
2336
- this.syncInFlightQueueClaimants.size < this.syncInFlightQueue.size) {
2337
- // Defensive compatibility for callers/tests that seed the public queue
2338
- // directly. Keep hydration bounded; internal writes register coordinates
2339
- // when the key is first admitted.
2340
- let inspected = 0;
2341
- for (const key of this.syncInFlightQueue.keys()) {
2342
- if (typeof key === "bigint") {
2343
- this.syncInFlightQueuedCoordinates.add(key);
2344
- }
2345
- inspected += 1;
2346
- if (inspected >= MAX_PENDING_SIMPLE_SYNC_ALIAS_REFRESH_PER_MESSAGE) {
2347
- break;
2348
- }
2349
- }
2350
- }
2351
- if (this.syncInFlightQueuedCoordinates.size === 0) {
2352
- this.syncInFlightQueuedCoordinateRefreshIterator = undefined;
2353
- return;
2354
- }
2355
- this.syncInFlightQueuedCoordinateRefreshIterator ??=
2356
- this.syncInFlightQueuedCoordinates.values();
2357
- for (let refreshed = 0; refreshed < MAX_PENDING_SIMPLE_SYNC_ALIAS_REFRESH_PER_MESSAGE; refreshed += 1) {
2358
- const next = this.syncInFlightQueuedCoordinateRefreshIterator.next();
2359
- if (next.done) {
2360
- this.syncInFlightQueuedCoordinateRefreshIterator = undefined;
2361
- break;
2362
- }
2363
- this.refreshQueuedSyncCoordinateAlias(next.value);
2364
- }
2252
+ this.pendingSync.refreshQueuedSyncCoordinateAliases();
2365
2253
  }
2366
2254
  getQueuedSyncKeyForAdmission(key) {
2367
- const getValidQueuedKey = (candidate) => {
2368
- if (!this.syncInFlightQueue.has(candidate)) {
2369
- return undefined;
2370
- }
2371
- const expiresAt = this.syncInFlightQueueExpiresAt.get(candidate);
2372
- if (expiresAt != null && expiresAt <= Date.now()) {
2373
- this.clearSyncProcessKey(candidate);
2374
- return undefined;
2375
- }
2376
- return candidate;
2377
- };
2378
- if (getValidQueuedKey(key) != null) {
2379
- if (typeof key === "bigint") {
2380
- this.refreshQueuedSyncCoordinateAlias(key);
2381
- return getValidQueuedKey(key);
2382
- }
2383
- return key;
2384
- }
2385
- if (typeof key === "string") {
2386
- const aliases = this.syncInFlightQueuedCoordinatesByHash.get(key);
2387
- if (aliases) {
2388
- let inspected = 0;
2389
- for (const alias of aliases) {
2390
- if (inspected >= MAX_PENDING_SIMPLE_SYNC_ALIAS_REFRESH_PER_MESSAGE) {
2391
- return QUEUED_SYNC_ALIAS_REFRESH_PENDING;
2392
- }
2393
- inspected += 1;
2394
- this.refreshQueuedSyncCoordinateAlias(alias);
2395
- if (this.syncInFlightQueuedCoordinatesByHash.get(key)?.has(alias) ===
2396
- true) {
2397
- const validAlias = getValidQueuedKey(alias);
2398
- if (validAlias != null) {
2399
- return validAlias;
2400
- }
2401
- }
2402
- }
2403
- if ((this.syncInFlightQueuedCoordinatesByHash.get(key)?.size ?? 0) > 0) {
2404
- return QUEUED_SYNC_ALIAS_REFRESH_PENDING;
2405
- }
2406
- }
2407
- return undefined;
2408
- }
2409
- const hash = this.coordinateToHash.get(key);
2410
- return hash != null ? getValidQueuedKey(hash) : undefined;
2255
+ return this.pendingSync.getQueuedSyncKeyForAdmission(key);
2411
2256
  }
2412
2257
  addPendingSyncClaim(key, from, expiresAt) {
2413
- const fromHash = from.hashcode();
2414
- let peers = this.syncInFlightQueue.get(key);
2415
- let claimants = this.syncInFlightQueueClaimants.get(key);
2416
- let claimantIndexes = this.syncInFlightQueueClaimantIndexes.get(key);
2417
- if (!peers) {
2418
- peers = [];
2419
- this.syncInFlightQueue.set(key, peers);
2420
- claimants = new Set();
2421
- this.syncInFlightQueueClaimants.set(key, claimants);
2422
- claimantIndexes = new Map();
2423
- this.syncInFlightQueueClaimantIndexes.set(key, claimantIndexes);
2424
- const deadline = expiresAt ?? Date.now() + PENDING_SIMPLE_SYNC_KEY_TTL_MS;
2425
- this.syncInFlightQueueExpiresAt.set(key, deadline);
2426
- const expiryNode = {
2427
- kind: "key",
2428
- key,
2429
- expiresAt: deadline,
2430
- heapIndex: -1,
2431
- };
2432
- this.pendingSyncKeyExpiryNodes.set(key, expiryNode);
2433
- this.pushPendingSyncExpiry(expiryNode);
2434
- if (typeof key === "bigint") {
2435
- this.refreshQueuedSyncCoordinateAlias(key);
2436
- }
2437
- }
2438
- else if (!claimants) {
2439
- // Defensive compatibility for callers/tests that seed the public queue
2440
- // maps directly. Internally every retained key gets this set at creation.
2441
- claimants = new Set(peers.map((peer) => peer.hashcode()));
2442
- this.syncInFlightQueueClaimants.set(key, claimants);
2443
- this.pendingSyncClaimCount += claimants.size;
2444
- }
2445
- if (!claimantIndexes) {
2446
- claimantIndexes = new Map();
2447
- for (let index = 0; index < peers.length; index += 1) {
2448
- claimantIndexes.set(peers[index].hashcode(), index);
2449
- }
2450
- this.syncInFlightQueueClaimantIndexes.set(key, claimantIndexes);
2451
- }
2452
- if (claimants.has(fromHash)) {
2453
- return false;
2454
- }
2455
- claimantIndexes.set(fromHash, peers.length);
2456
- peers.push(from);
2457
- claimants.add(fromHash);
2458
- let inverted = this.syncInFlightQueueInverted.get(fromHash);
2459
- if (!inverted) {
2460
- inverted = new Set();
2461
- this.syncInFlightQueueInverted.set(fromHash, inverted);
2462
- }
2463
- inverted.add(key);
2464
- this.pendingSyncClaimCount += 1;
2465
- this.schedulePendingSyncKeyExpiry();
2466
- return true;
2258
+ return this.pendingSync.addPendingSyncClaim(key, from, expiresAt);
2467
2259
  }
2468
2260
  hasPendingSyncClaim(key, peer) {
2469
- const claimants = this.syncInFlightQueueClaimants.get(key);
2470
- if (claimants) {
2471
- return claimants.has(peer);
2472
- }
2473
- const peers = this.syncInFlightQueue.get(key);
2474
- if (!peers) {
2475
- return false;
2476
- }
2477
- const hydrated = new Set(peers.map((candidate) => candidate.hashcode()));
2478
- this.syncInFlightQueueClaimants.set(key, hydrated);
2479
- const indexes = new Map();
2480
- for (let index = 0; index < peers.length; index += 1) {
2481
- indexes.set(peers[index].hashcode(), index);
2482
- }
2483
- this.syncInFlightQueueClaimantIndexes.set(key, indexes);
2484
- this.pendingSyncClaimCount += hydrated.size;
2485
- return hydrated.has(peer);
2261
+ return this.pendingSync.hasPendingSyncClaim(key, peer);
2486
2262
  }
2487
2263
  filterDispatchablePendingSyncClaims(keys, peer, epoch) {
2488
- if (this.syncDispatchTargetEpochs.get(peer) !== epoch) {
2489
- return [];
2490
- }
2491
- const now = Date.now();
2492
- const dispatchable = [];
2493
- for (const key of keys) {
2494
- const expiresAt = this.syncInFlightQueueExpiresAt.get(key);
2495
- if (expiresAt != null && expiresAt <= now) {
2496
- this.clearSyncProcessKey(key);
2497
- continue;
2498
- }
2499
- if (this.syncInFlightQueue.has(key) &&
2500
- this.hasPendingSyncClaim(key, peer)) {
2501
- dispatchable.push(key);
2502
- }
2503
- }
2504
- return dispatchable;
2264
+ return this.pendingSync.filterDispatchablePendingSyncClaims(keys, peer, epoch);
2505
2265
  }
2506
2266
  canStartPendingSyncLookup(peer) {
2507
- return (this.pendingSyncAdmissionReservations.size +
2267
+ return (this.pendingSync.pendingSyncAdmissionReservations.size +
2508
2268
  this.pendingCoordinateLookupCount <
2509
2269
  MAX_PENDING_SIMPLE_SYNC_LOOKUPS_GLOBAL &&
2510
- (this.pendingSyncAdmissionReservationsByPeer.get(peer)?.size ?? 0) +
2270
+ (this.pendingSync.pendingSyncAdmissionReservationsByPeer.get(peer)
2271
+ ?.size ?? 0) +
2511
2272
  (this.pendingCoordinateLookupCountByPeer.get(peer) ?? 0) <
2512
2273
  MAX_PENDING_SIMPLE_SYNC_LOOKUPS_PER_PEER);
2513
2274
  }
2275
+ getOrCreateSyncResponseSlotRow(peer) {
2276
+ return this.peerSlotRows.ensure(peer);
2277
+ }
2278
+ detachSyncResponseSlotRow(peer) {
2279
+ const row = this.peerSlotRows.detach(peer);
2280
+ if (!row) {
2281
+ return;
2282
+ }
2283
+ // Disconnect returns only the current peer generation's quota. The
2284
+ // global counters represent physically live, potentially non-abortable
2285
+ // work and remain charged until each captured release actually settles.
2286
+ this.pendingCoordinateLookupCountByPeer.delete(peer);
2287
+ this.pendingCoordinateResponseCountByPeer.delete(peer);
2288
+ this.activeMaybeSyncResponseCountByPeer.delete(peer);
2289
+ if (row.pendingResponseHashes > 0) {
2290
+ // Active authorizations' hash budget returns in aggregate with the
2291
+ // row. The settled releases below (and any late ship-side release)
2292
+ // see the detached row and skip the global budget.
2293
+ this.pendingMaybeSyncResponseCount -= row.pendingResponseHashes;
2294
+ row.pendingResponseHashes = 0;
2295
+ this.schedulePendingMaybeSyncResponseWaiter();
2296
+ }
2297
+ // Settle only logical active-response ownership so authorizations leave
2298
+ // pendingMaybeSyncResponses and responseLeases/retainedWork drain. The
2299
+ // eventual ship-side release still returns its global physical permit.
2300
+ // Closures capture THIS row (identity, not peer hash), so a reconnected
2301
+ // successor's row is never touched.
2302
+ for (const release of [...row.activeReleases]) {
2303
+ release();
2304
+ }
2305
+ }
2306
+ maybeDropIdleSyncResponseSlotRow(row) {
2307
+ this.peerSlotRows.maybeDropIdle(row);
2308
+ }
2514
2309
  tryAcquireCoordinateLookup(peer) {
2515
2310
  if (!this.canStartPendingSyncLookup(peer)) {
2516
2311
  return undefined;
2517
2312
  }
2313
+ const row = this.getOrCreateSyncResponseSlotRow(peer);
2314
+ row.lookups += 1;
2518
2315
  this.pendingCoordinateLookupCount += 1;
2519
2316
  this.pendingCoordinateLookupCountByPeer.set(peer, (this.pendingCoordinateLookupCountByPeer.get(peer) ?? 0) + 1);
2520
2317
  let released = false;
2521
- return () => {
2318
+ const release = () => {
2522
2319
  if (released) {
2523
2320
  return;
2524
2321
  }
2525
2322
  released = true;
2323
+ row.lookups -= 1;
2526
2324
  this.pendingCoordinateLookupCount -= 1;
2325
+ if (!row.attached) {
2326
+ // Per-peer quota returned when this generation detached.
2327
+ return;
2328
+ }
2527
2329
  const remaining = (this.pendingCoordinateLookupCountByPeer.get(peer) ?? 1) - 1;
2528
2330
  if (remaining === 0) {
2529
2331
  this.pendingCoordinateLookupCountByPeer.delete(peer);
@@ -2531,7 +2333,9 @@ export class SimpleSyncronizer {
2531
2333
  else {
2532
2334
  this.pendingCoordinateLookupCountByPeer.set(peer, remaining);
2533
2335
  }
2336
+ this.maybeDropIdleSyncResponseSlotRow(row);
2534
2337
  };
2338
+ return { row, release };
2535
2339
  }
2536
2340
  tryAcquireCoordinateResponse(peer) {
2537
2341
  if (this.pendingCoordinateResponseCount >=
@@ -2540,6 +2344,8 @@ export class SimpleSyncronizer {
2540
2344
  MAX_PENDING_SIMPLE_COORDINATE_RESPONSES_PER_PEER) {
2541
2345
  return undefined;
2542
2346
  }
2347
+ const row = this.getOrCreateSyncResponseSlotRow(peer);
2348
+ row.responses += 1;
2543
2349
  this.pendingCoordinateResponseCount += 1;
2544
2350
  this.pendingCoordinateResponseCountByPeer.set(peer, (this.pendingCoordinateResponseCountByPeer.get(peer) ?? 0) + 1);
2545
2351
  let released = false;
@@ -2548,7 +2354,12 @@ export class SimpleSyncronizer {
2548
2354
  return;
2549
2355
  }
2550
2356
  released = true;
2357
+ row.responses -= 1;
2551
2358
  this.pendingCoordinateResponseCount -= 1;
2359
+ if (!row.attached) {
2360
+ // Per-peer quota returned when this generation detached.
2361
+ return;
2362
+ }
2552
2363
  const remaining = (this.pendingCoordinateResponseCountByPeer.get(peer) ?? 1) - 1;
2553
2364
  if (remaining === 0) {
2554
2365
  this.pendingCoordinateResponseCountByPeer.delete(peer);
@@ -2556,347 +2367,35 @@ export class SimpleSyncronizer {
2556
2367
  else {
2557
2368
  this.pendingCoordinateResponseCountByPeer.set(peer, remaining);
2558
2369
  }
2370
+ this.maybeDropIdleSyncResponseSlotRow(row);
2559
2371
  };
2560
2372
  }
2561
2373
  reservePendingSyncAdmission(peer, identities) {
2562
- const count = identities.length;
2563
- if (count <= 0) {
2564
- return undefined;
2565
- }
2566
- const reservation = {
2567
- peer,
2568
- remaining: count,
2569
- active: true,
2570
- released: false,
2571
- expiresAt: Date.now() + PENDING_SIMPLE_SYNC_KEY_TTL_MS,
2572
- identities: new Set(identities),
2573
- retainedSettled: 0,
2574
- };
2575
- this.pendingSyncAdmissionReservations.add(reservation);
2576
- let peerReservations = this.pendingSyncAdmissionReservationsByPeer.get(peer);
2577
- if (!peerReservations) {
2578
- peerReservations = new Set();
2579
- this.pendingSyncAdmissionReservationsByPeer.set(peer, peerReservations);
2580
- }
2581
- peerReservations.add(reservation);
2582
- this.pendingSyncActiveAdmissionReservations += 1;
2583
- this.pendingSyncAdmissionCount += count;
2584
- this.pendingSyncAdmissionCountByPeer.set(peer, (this.pendingSyncAdmissionCountByPeer.get(peer) ?? 0) + count);
2585
- let reservedIdentities = this.pendingSyncAdmissionIdentitiesByPeer.get(peer);
2586
- if (!reservedIdentities) {
2587
- reservedIdentities = new Set();
2588
- this.pendingSyncAdmissionIdentitiesByPeer.set(peer, reservedIdentities);
2589
- }
2590
- for (const identity of identities) {
2591
- reservedIdentities.add(identity);
2592
- let reservationsForIdentity = this.pendingSyncAdmissionReservationsByIdentity.get(identity);
2593
- if (!reservationsForIdentity) {
2594
- reservationsForIdentity = new Set();
2595
- this.pendingSyncAdmissionReservationsByIdentity.set(identity, reservationsForIdentity);
2596
- }
2597
- reservationsForIdentity.add(reservation);
2598
- }
2599
- const expiryNode = {
2600
- kind: "admission",
2601
- reservation,
2602
- expiresAt: reservation.expiresAt,
2603
- heapIndex: -1,
2604
- };
2605
- this.pendingSyncAdmissionExpiryNodes.set(reservation, expiryNode);
2606
- this.pushPendingSyncExpiry(expiryNode);
2607
- this.schedulePendingSyncKeyExpiry();
2608
- return reservation;
2609
- }
2610
- removePendingSyncAdmissionIdentity(reservation, identity, options) {
2611
- if (!reservation.identities.delete(identity)) {
2612
- return false;
2613
- }
2614
- if (options?.retainQuota === true) {
2615
- reservation.retainedSettled += 1;
2616
- }
2617
- else {
2618
- reservation.remaining -= 1;
2619
- this.pendingSyncAdmissionCount -= 1;
2620
- const peerCount = (this.pendingSyncAdmissionCountByPeer.get(reservation.peer) ?? 0) - 1;
2621
- if (peerCount === 0) {
2622
- this.pendingSyncAdmissionCountByPeer.delete(reservation.peer);
2623
- }
2624
- else {
2625
- this.pendingSyncAdmissionCountByPeer.set(reservation.peer, peerCount);
2626
- }
2627
- }
2628
- const reservedIdentities = this.pendingSyncAdmissionIdentitiesByPeer.get(reservation.peer);
2629
- reservedIdentities?.delete(identity);
2630
- if (reservedIdentities?.size === 0) {
2631
- this.pendingSyncAdmissionIdentitiesByPeer.delete(reservation.peer);
2632
- }
2633
- const reservationsForIdentity = this.pendingSyncAdmissionReservationsByIdentity.get(identity);
2634
- reservationsForIdentity?.delete(reservation);
2635
- if (reservationsForIdentity?.size === 0) {
2636
- this.pendingSyncAdmissionReservationsByIdentity.delete(identity);
2637
- }
2638
- return true;
2374
+ return this.pendingSync.reservePendingSyncAdmission(peer, identities);
2639
2375
  }
2640
2376
  clearPendingSyncAdmissionIdentity(identity) {
2641
- const reservations = this.pendingSyncAdmissionReservationsByIdentity.get(identity);
2642
- if (!reservations) {
2643
- return;
2644
- }
2645
- for (const reservation of [...reservations]) {
2646
- this.removePendingSyncAdmissionIdentity(reservation, identity, {
2647
- retainQuota: true,
2648
- });
2649
- }
2377
+ this.pendingSync.clearPendingSyncAdmissionIdentity(identity);
2650
2378
  }
2651
2379
  consumePendingSyncAdmission(reservation, identity) {
2652
- if (!reservation.active || reservation.expiresAt <= Date.now()) {
2653
- if (reservation.expiresAt <= Date.now()) {
2654
- this.invalidatePendingSyncAdmission(reservation);
2655
- }
2656
- return "invalid";
2657
- }
2658
- if (!reservation.identities.has(identity)) {
2659
- return "settled";
2660
- }
2661
- this.removePendingSyncAdmissionIdentity(reservation, identity);
2662
- if (reservation.remaining === 0) {
2663
- this.removePendingSyncAdmissionExpiry(reservation);
2664
- reservation.active = false;
2665
- reservation.released = true;
2666
- this.pendingSyncActiveAdmissionReservations -= 1;
2667
- this.pendingSyncAdmissionReservations.delete(reservation);
2668
- const peerReservations = this.pendingSyncAdmissionReservationsByPeer.get(reservation.peer);
2669
- peerReservations?.delete(reservation);
2670
- if (peerReservations?.size === 0) {
2671
- this.pendingSyncAdmissionReservationsByPeer.delete(reservation.peer);
2672
- }
2673
- this.clearPendingSyncExpiryTimerIfIdle();
2674
- }
2675
- return "consumed";
2380
+ return this.pendingSync.consumePendingSyncAdmission(reservation, identity);
2676
2381
  }
2677
2382
  transferPendingSyncAdmissionIdentity(peer, identity) {
2678
- const reservations = this.pendingSyncAdmissionReservationsByIdentity.get(identity);
2679
- if (!reservations) {
2680
- return undefined;
2681
- }
2682
- for (const reservation of reservations) {
2683
- if (reservation.peer !== peer ||
2684
- reservation.released ||
2685
- reservation.expiresAt <= Date.now() ||
2686
- !this.removePendingSyncAdmissionIdentity(reservation, identity, {
2687
- retainQuota: true,
2688
- })) {
2689
- continue;
2690
- }
2691
- // The original resolver may be non-abortable and still retains its input
2692
- // arrays. Keep that reservation charged until its queueSync finally
2693
- // settles; the queued claim is counted separately and can disappear
2694
- // without returning the resolver's quota early.
2695
- return reservation.expiresAt;
2696
- }
2697
- return undefined;
2698
- }
2699
- invalidatePendingSyncAdmission(reservation) {
2700
- if (!reservation || reservation.released || !reservation.active) {
2701
- return;
2702
- }
2703
- // Expiry/disconnect invalidates late lookup results, but it must not return
2704
- // the quota slot while the underlying storage/index work is still alive.
2705
- // Those lookups are not generally abortable; only queueSync's finally block
2706
- // may release their active-work accounting.
2707
- this.removePendingSyncAdmissionExpiry(reservation);
2708
- reservation.active = false;
2709
- this.pendingSyncActiveAdmissionReservations -= 1;
2710
- this.clearPendingSyncExpiryTimerIfIdle();
2383
+ return this.pendingSync.transferPendingSyncAdmissionIdentity(peer, identity);
2711
2384
  }
2712
2385
  releasePendingSyncAdmission(reservation) {
2713
- if (!reservation || reservation.released) {
2714
- return;
2715
- }
2716
- this.removePendingSyncAdmissionExpiry(reservation);
2717
- for (const identity of [...reservation.identities]) {
2718
- this.removePendingSyncAdmissionIdentity(reservation, identity);
2719
- }
2720
- if (reservation.retainedSettled > 0) {
2721
- const retainedSettled = reservation.retainedSettled;
2722
- reservation.retainedSettled = 0;
2723
- reservation.remaining -= retainedSettled;
2724
- this.pendingSyncAdmissionCount -= retainedSettled;
2725
- const peerCount = (this.pendingSyncAdmissionCountByPeer.get(reservation.peer) ?? 0) -
2726
- retainedSettled;
2727
- if (peerCount === 0) {
2728
- this.pendingSyncAdmissionCountByPeer.delete(reservation.peer);
2729
- }
2730
- else {
2731
- this.pendingSyncAdmissionCountByPeer.set(reservation.peer, peerCount);
2732
- }
2733
- }
2734
- if (reservation.active) {
2735
- this.pendingSyncActiveAdmissionReservations -= 1;
2736
- }
2737
- reservation.active = false;
2738
- reservation.released = true;
2739
- this.pendingSyncAdmissionReservations.delete(reservation);
2740
- const peerReservations = this.pendingSyncAdmissionReservationsByPeer.get(reservation.peer);
2741
- peerReservations?.delete(reservation);
2742
- if (peerReservations?.size === 0) {
2743
- this.pendingSyncAdmissionReservationsByPeer.delete(reservation.peer);
2744
- }
2745
- this.clearPendingSyncExpiryTimerIfIdle();
2386
+ this.pendingSync.releasePendingSyncAdmission(reservation);
2746
2387
  }
2747
2388
  clearPendingSyncAdmissions(peer) {
2748
- const reservations = peer == null
2749
- ? this.pendingSyncAdmissionReservations
2750
- : this.pendingSyncAdmissionReservationsByPeer.get(peer);
2751
- if (!reservations) {
2752
- return;
2753
- }
2754
- for (const reservation of [...reservations]) {
2755
- this.invalidatePendingSyncAdmission(reservation);
2756
- }
2757
- }
2758
- swapPendingSyncExpiry(left, right) {
2759
- const leftNode = this.pendingSyncExpiryHeap[left];
2760
- const rightNode = this.pendingSyncExpiryHeap[right];
2761
- this.pendingSyncExpiryHeap[left] = rightNode;
2762
- this.pendingSyncExpiryHeap[right] = leftNode;
2763
- rightNode.heapIndex = left;
2764
- leftNode.heapIndex = right;
2765
- }
2766
- pushPendingSyncExpiry(node) {
2767
- node.heapIndex = this.pendingSyncExpiryHeap.length;
2768
- this.pendingSyncExpiryHeap.push(node);
2769
- let index = node.heapIndex;
2770
- while (index > 0) {
2771
- const parent = Math.floor((index - 1) / 2);
2772
- if (this.pendingSyncExpiryHeap[parent].expiresAt <=
2773
- this.pendingSyncExpiryHeap[index].expiresAt) {
2774
- break;
2775
- }
2776
- this.swapPendingSyncExpiry(parent, index);
2777
- index = parent;
2778
- }
2779
- }
2780
- removePendingSyncExpiry(node) {
2781
- const index = node.heapIndex;
2782
- if (index < 0 ||
2783
- index >= this.pendingSyncExpiryHeap.length ||
2784
- this.pendingSyncExpiryHeap[index] !== node) {
2785
- return;
2786
- }
2787
- const last = this.pendingSyncExpiryHeap.pop();
2788
- node.heapIndex = -1;
2789
- if (index >= this.pendingSyncExpiryHeap.length) {
2790
- return;
2791
- }
2792
- this.pendingSyncExpiryHeap[index] = last;
2793
- last.heapIndex = index;
2794
- let current = index;
2795
- while (current > 0) {
2796
- const parent = Math.floor((current - 1) / 2);
2797
- if (this.pendingSyncExpiryHeap[parent].expiresAt <=
2798
- this.pendingSyncExpiryHeap[current].expiresAt) {
2799
- break;
2800
- }
2801
- this.swapPendingSyncExpiry(parent, current);
2802
- current = parent;
2803
- }
2804
- for (;;) {
2805
- const left = current * 2 + 1;
2806
- const right = left + 1;
2807
- let smallest = current;
2808
- if (left < this.pendingSyncExpiryHeap.length &&
2809
- this.pendingSyncExpiryHeap[left].expiresAt <
2810
- this.pendingSyncExpiryHeap[smallest].expiresAt) {
2811
- smallest = left;
2812
- }
2813
- if (right < this.pendingSyncExpiryHeap.length &&
2814
- this.pendingSyncExpiryHeap[right].expiresAt <
2815
- this.pendingSyncExpiryHeap[smallest].expiresAt) {
2816
- smallest = right;
2817
- }
2818
- if (smallest === current) {
2819
- break;
2820
- }
2821
- this.swapPendingSyncExpiry(current, smallest);
2822
- current = smallest;
2823
- }
2824
- }
2825
- removePendingSyncKeyExpiry(key) {
2826
- const node = this.pendingSyncKeyExpiryNodes.get(key);
2827
- if (!node) {
2828
- return;
2829
- }
2830
- this.pendingSyncKeyExpiryNodes.delete(key);
2831
- this.removePendingSyncExpiry(node);
2389
+ this.pendingSync.clearPendingSyncAdmissions(peer);
2832
2390
  }
2833
2391
  movePendingSyncKeyExpiryEarlier(key, expiresAt) {
2834
- const current = this.syncInFlightQueueExpiresAt.get(key);
2835
- if (current == null || current <= expiresAt) {
2836
- return;
2837
- }
2838
- this.removePendingSyncKeyExpiry(key);
2839
- this.syncInFlightQueueExpiresAt.set(key, expiresAt);
2840
- const node = {
2841
- kind: "key",
2842
- key,
2843
- expiresAt,
2844
- heapIndex: -1,
2845
- };
2846
- this.pendingSyncKeyExpiryNodes.set(key, node);
2847
- this.pushPendingSyncExpiry(node);
2848
- this.schedulePendingSyncKeyExpiry();
2849
- }
2850
- removePendingSyncAdmissionExpiry(reservation) {
2851
- const node = this.pendingSyncAdmissionExpiryNodes.get(reservation);
2852
- if (!node) {
2853
- return;
2854
- }
2855
- this.pendingSyncAdmissionExpiryNodes.delete(reservation);
2856
- this.removePendingSyncExpiry(node);
2392
+ this.pendingSync.movePendingSyncKeyExpiryEarlier(key, expiresAt);
2857
2393
  }
2858
2394
  expirePendingSyncKeys(now = Date.now()) {
2859
- for (;;) {
2860
- const node = this.pendingSyncExpiryHeap[0];
2861
- if (!node || node.expiresAt > now) {
2862
- break;
2863
- }
2864
- this.removePendingSyncExpiry(node);
2865
- if (node.kind === "key") {
2866
- if (this.pendingSyncKeyExpiryNodes.get(node.key) !== node) {
2867
- continue;
2868
- }
2869
- this.pendingSyncKeyExpiryNodes.delete(node.key);
2870
- this.clearSyncProcessKey(node.key);
2871
- }
2872
- else {
2873
- if (this.pendingSyncAdmissionExpiryNodes.get(node.reservation) !== node) {
2874
- continue;
2875
- }
2876
- this.pendingSyncAdmissionExpiryNodes.delete(node.reservation);
2877
- this.invalidatePendingSyncAdmission(node.reservation);
2878
- }
2879
- }
2395
+ this.pendingSync.expirePendingSyncKeys(now);
2880
2396
  }
2881
2397
  clearPendingSyncExpiryTimerIfIdle() {
2882
- if (this.pendingSyncExpiryHeap.length === 0 &&
2883
- this.syncInFlightQueueExpiryTimer != null) {
2884
- clearTimeout(this.syncInFlightQueueExpiryTimer);
2885
- this.syncInFlightQueueExpiryTimer = undefined;
2886
- }
2887
- }
2888
- schedulePendingSyncKeyExpiry() {
2889
- if (this.syncInFlightQueueExpiryTimer != null ||
2890
- this.pendingSyncExpiryHeap.length === 0) {
2891
- return;
2892
- }
2893
- const expiresAt = this.pendingSyncExpiryHeap[0].expiresAt;
2894
- this.syncInFlightQueueExpiryTimer = setTimeout(() => {
2895
- this.syncInFlightQueueExpiryTimer = undefined;
2896
- this.expirePendingSyncKeys();
2897
- this.schedulePendingSyncKeyExpiry();
2898
- }, Math.max(0, expiresAt - Date.now()));
2899
- this.syncInFlightQueueExpiryTimer.unref?.();
2398
+ this.pendingSync.clearPendingSyncExpiryTimerIfIdle();
2900
2399
  }
2901
2400
  async queueSync(keys, from, options) {
2902
2401
  if (this.closed === true || keys.length === 0) {
@@ -2911,9 +2410,9 @@ export class SimpleSyncronizer {
2911
2410
  const peerClaimCount = this.syncInFlightQueueInverted.get(fromHash)?.size ?? 0;
2912
2411
  let availableClaims = Math.max(0, Math.min(MAX_PENDING_SIMPLE_SYNC_KEYS_PER_PEER -
2913
2412
  peerClaimCount -
2914
- (this.pendingSyncAdmissionCountByPeer.get(fromHash) ?? 0), MAX_PENDING_SIMPLE_SYNC_KEYS_GLOBAL -
2915
- this.pendingSyncClaimCount -
2916
- this.pendingSyncAdmissionCount));
2413
+ (this.pendingSync.pendingSyncAdmissionCountByPeer.get(fromHash) ?? 0), MAX_PENDING_SIMPLE_SYNC_KEYS_GLOBAL -
2414
+ this.pendingSync.pendingSyncClaimCount -
2415
+ this.pendingSync.pendingSyncAdmissionCount));
2917
2416
  const targetEpoch = this.getOrCreateSyncDispatchTargetEpoch(fromHash);
2918
2417
  const ownershipLifecycleController = this.syncDispatchLifecycleController;
2919
2418
  const isCapturedLifecycleActive = () => this.closed !== true &&
@@ -2934,7 +2433,7 @@ export class SimpleSyncronizer {
2934
2433
  const keysToCheck = [];
2935
2434
  const identitiesToCheck = [];
2936
2435
  const seen = new Set();
2937
- const pendingAdmissionIdentities = this.pendingSyncAdmissionIdentitiesByPeer.get(fromHash);
2436
+ const pendingAdmissionIdentities = this.pendingSync.pendingSyncAdmissionIdentitiesByPeer.get(fromHash);
2938
2437
  // Default senders use 1,024-key messages. Capping examined input at the
2939
2438
  // entire per-peer allowance prevents a duplicate-filled oversized vector
2940
2439
  // from turning admission itself into unbounded work.
@@ -3351,7 +2850,7 @@ export class SimpleSyncronizer {
3351
2850
  if (!isOpenLifecycleActive()) {
3352
2851
  return;
3353
2852
  }
3354
- const expiresAt = this.syncInFlightQueueExpiresAt.get(key);
2853
+ const expiresAt = this.pendingSync.syncInFlightQueueExpiresAt.get(key);
3355
2854
  if (expiresAt != null && expiresAt <= Date.now()) {
3356
2855
  this.clearSyncProcessKey(key);
3357
2856
  continue;
@@ -3364,7 +2863,7 @@ export class SimpleSyncronizer {
3364
2863
  if (!value) {
3365
2864
  continue;
3366
2865
  }
3367
- const currentExpiresAt = this.syncInFlightQueueExpiresAt.get(key);
2866
+ const currentExpiresAt = this.pendingSync.syncInFlightQueueExpiresAt.get(key);
3368
2867
  if (currentExpiresAt != null && currentExpiresAt <= Date.now()) {
3369
2868
  this.clearSyncProcessKey(key);
3370
2869
  continue;
@@ -3374,8 +2873,7 @@ export class SimpleSyncronizer {
3374
2873
  this.clearSyncProcessKey(key);
3375
2874
  continue;
3376
2875
  }
3377
- const cursor = (this.syncInFlightQueueRoundRobinCursor.get(key) ?? 0) %
3378
- value.length;
2876
+ const cursor = this.pendingSync.getRoundRobinCursor(key) % value.length;
3379
2877
  const candidate = value[cursor];
3380
2878
  const publicKeyHash = candidate.hashcode();
3381
2879
  const inflightTimestamp = this.syncInFlight
@@ -3394,7 +2892,7 @@ export class SimpleSyncronizer {
3394
2892
  }
3395
2893
  request.hashes.push(key);
3396
2894
  if (value.length > 1) {
3397
- this.syncInFlightQueueRoundRobinCursor.set(key, (cursor + 1) % value.length);
2895
+ this.pendingSync.setRoundRobinCursor(key, (cursor + 1) % value.length);
3398
2896
  }
3399
2897
  }
3400
2898
  }
@@ -3463,28 +2961,9 @@ export class SimpleSyncronizer {
3463
2961
  this.syncDispatchLifecycleController.abort();
3464
2962
  this.syncDispatchTargetEpochs.clear();
3465
2963
  this.clearPendingSyncAdmissions();
3466
- this.syncInFlightQueue.clear();
3467
- this.syncInFlightQueueInverted.clear();
3468
2964
  this.syncInFlightRetryIterator = undefined;
3469
2965
  this.syncInFlightRetryRemaining = 0;
3470
- this.syncInFlightQueueExpiresAt.clear();
3471
- this.pendingSyncExpiryHeap.length = 0;
3472
- this.pendingSyncKeyExpiryNodes.clear();
3473
- this.pendingSyncAdmissionExpiryNodes.clear();
3474
- this.syncInFlightQueueClaimants.clear();
3475
- this.syncInFlightQueueClaimantIndexes.clear();
3476
- this.syncInFlightQueueRoundRobinCursor.clear();
3477
- this.syncInFlightQueuedCoordinates.clear();
3478
- this.syncInFlightQueuedHashByCoordinate.clear();
3479
- this.syncInFlightQueuedCoordinatesByHash.clear();
3480
- this.syncInFlightQueuedCoordinateRefreshIterator = undefined;
3481
- this.pendingSyncClaimCount = 0;
3482
- if (this.syncInFlightQueueExpiryTimer != null) {
3483
- clearTimeout(this.syncInFlightQueueExpiryTimer);
3484
- this.syncInFlightQueueExpiryTimer = undefined;
3485
- }
3486
- this.syncInFlight.clear();
3487
- this.syncInFlightTargetsByKey.clear();
2966
+ this.pendingSync.clearForClose();
3488
2967
  this.recentlySentExchangeHeads.clear();
3489
2968
  this.clearPendingMaybeSyncResponses();
3490
2969
  for (const sessionId of [...this.repairSessions.keys()]) {
@@ -3529,202 +3008,31 @@ export class SimpleSyncronizer {
3529
3008
  return this.hasSyncProcessState() || this.repairSessions.size > 0;
3530
3009
  }
3531
3010
  hasSyncProcessState() {
3532
- return (this.syncInFlightQueue.size > 0 ||
3533
- this.syncInFlightQueueInverted.size > 0 ||
3534
- this.pendingSyncAdmissionCount > 0 ||
3535
- this.syncInFlight.size > 0);
3011
+ return this.pendingSync.hasSyncProcessState();
3536
3012
  }
3537
3013
  clearSyncProcessKey(key) {
3538
- const inflight = this.syncInFlightQueue.get(key);
3539
- if (inflight) {
3540
- for (const peer of inflight) {
3541
- const map = this.syncInFlightQueueInverted.get(peer.hashcode());
3542
- if (map) {
3543
- map.delete(key);
3544
- if (map.size === 0) {
3545
- this.syncInFlightQueueInverted.delete(peer.hashcode());
3546
- }
3547
- }
3548
- }
3549
- const trackedClaimants = this.syncInFlightQueueClaimants.get(key);
3550
- this.pendingSyncClaimCount = Math.max(0, this.pendingSyncClaimCount -
3551
- (trackedClaimants?.size ?? inflight.length));
3552
- this.syncInFlightQueue.delete(key);
3553
- }
3554
- this.syncInFlightQueueClaimants.delete(key);
3555
- this.syncInFlightQueueClaimantIndexes.delete(key);
3556
- this.syncInFlightQueueRoundRobinCursor.delete(key);
3557
- if (typeof key === "bigint") {
3558
- this.removeQueuedSyncCoordinateAlias(key);
3559
- }
3560
- this.removePendingSyncKeyExpiry(key);
3561
- this.syncInFlightQueueExpiresAt.delete(key);
3562
- this.clearPendingSyncExpiryTimerIfIdle();
3563
- this.clearSyncInFlightKey(key);
3014
+ this.pendingSync.clearSyncProcessKey(key);
3564
3015
  }
3565
3016
  removePendingSyncClaim(key, peer) {
3566
- const inflight = this.syncInFlightQueue.get(key);
3567
- if (!inflight) {
3568
- return;
3569
- }
3570
- let claimants = this.syncInFlightQueueClaimants.get(key);
3571
- let claimantIndexes = this.syncInFlightQueueClaimantIndexes.get(key);
3572
- if (!claimants || !claimantIndexes) {
3573
- claimants = new Set();
3574
- claimantIndexes = new Map();
3575
- for (let index = 0; index < inflight.length; index += 1) {
3576
- const claimant = inflight[index].hashcode();
3577
- claimants.add(claimant);
3578
- claimantIndexes.set(claimant, index);
3579
- }
3580
- if (!this.syncInFlightQueueClaimants.has(key)) {
3581
- this.pendingSyncClaimCount += claimants.size;
3582
- }
3583
- this.syncInFlightQueueClaimants.set(key, claimants);
3584
- this.syncInFlightQueueClaimantIndexes.set(key, claimantIndexes);
3585
- }
3586
- const index = claimantIndexes.get(peer);
3587
- if (index == null) {
3588
- return;
3589
- }
3590
- const lastIndex = inflight.length - 1;
3591
- if (index !== lastIndex) {
3592
- const lastClaimant = inflight[lastIndex];
3593
- const lastClaimantHash = lastClaimant.hashcode();
3594
- inflight[index] = lastClaimant;
3595
- claimantIndexes.set(lastClaimantHash, index);
3596
- }
3597
- inflight.pop();
3598
- claimantIndexes.delete(peer);
3599
- claimants.delete(peer);
3600
- this.pendingSyncClaimCount = Math.max(0, this.pendingSyncClaimCount - 1);
3601
- const inverted = this.syncInFlightQueueInverted.get(peer);
3602
- inverted?.delete(key);
3603
- if (inverted?.size === 0) {
3604
- this.syncInFlightQueueInverted.delete(peer);
3605
- }
3606
- if (inflight.length > 0) {
3607
- const cursor = this.syncInFlightQueueRoundRobinCursor.get(key) ?? 0;
3608
- this.syncInFlightQueueRoundRobinCursor.set(key, cursor === lastIndex
3609
- ? index % inflight.length
3610
- : cursor % inflight.length);
3611
- return;
3612
- }
3613
- this.syncInFlightQueue.delete(key);
3614
- this.syncInFlightQueueClaimants.delete(key);
3615
- this.syncInFlightQueueClaimantIndexes.delete(key);
3616
- this.syncInFlightQueueRoundRobinCursor.delete(key);
3617
- if (typeof key === "bigint") {
3618
- this.removeQueuedSyncCoordinateAlias(key);
3619
- }
3620
- this.removePendingSyncKeyExpiry(key);
3621
- this.syncInFlightQueueExpiresAt.delete(key);
3622
- this.clearSyncInFlightKey(key);
3623
- this.clearPendingSyncExpiryTimerIfIdle();
3017
+ this.pendingSync.removePendingSyncClaim(key, peer);
3624
3018
  }
3625
3019
  removeSyncInFlightTargetKey(peer, key) {
3626
- const map = this.syncInFlight.get(peer);
3627
- if (!map?.delete(key)) {
3628
- return;
3629
- }
3630
- if (map.size === 0) {
3631
- this.syncInFlight.delete(peer);
3632
- }
3633
- const targets = this.syncInFlightTargetsByKey.get(key);
3634
- targets?.delete(peer);
3635
- if (targets?.size === 0) {
3636
- this.syncInFlightTargetsByKey.delete(key);
3637
- }
3020
+ this.pendingSync.removeSyncInFlightTargetKey(peer, key);
3638
3021
  }
3639
3022
  setSyncInFlightTargetKey(peer, key, timestamp) {
3640
- let map = this.syncInFlight.get(peer);
3641
- if (!map) {
3642
- map = new Map();
3643
- this.syncInFlight.set(peer, map);
3644
- }
3645
- const existing = map.get(key);
3646
- if (!existing || existing.timestamp < timestamp) {
3647
- map.set(key, { timestamp });
3648
- }
3649
- let targets = this.syncInFlightTargetsByKey.get(key);
3650
- if (!targets) {
3651
- targets = new Set();
3652
- this.syncInFlightTargetsByKey.set(key, targets);
3653
- }
3654
- targets.add(peer);
3023
+ this.pendingSync.setSyncInFlightTargetKey(peer, key, timestamp);
3655
3024
  }
3656
3025
  clearSyncInFlightTarget(peer) {
3657
- const map = this.syncInFlight.get(peer);
3658
- if (!map) {
3659
- return;
3660
- }
3661
- for (const key of map.keys()) {
3662
- const targets = this.syncInFlightTargetsByKey.get(key);
3663
- targets?.delete(peer);
3664
- if (targets?.size === 0) {
3665
- this.syncInFlightTargetsByKey.delete(key);
3666
- }
3667
- }
3668
- this.syncInFlight.delete(peer);
3669
- }
3670
- clearSyncInFlightKey(key) {
3671
- const targets = this.syncInFlightTargetsByKey.get(key);
3672
- if (!targets) {
3673
- // Defensive compatibility for tests or integrations that seed the public
3674
- // syncInFlight map directly. Internal writes always populate the index.
3675
- for (const [peer, map] of this.syncInFlight) {
3676
- if (map.has(key)) {
3677
- this.removeSyncInFlightTargetKey(peer, key);
3678
- }
3679
- }
3680
- return;
3681
- }
3682
- for (const peer of [...targets]) {
3683
- this.removeSyncInFlightTargetKey(peer, key);
3684
- }
3685
- }
3686
- forEachKnownAlias(hash, callback) {
3687
- callback(hash);
3688
- for (const coordinate of [
3689
- ...(this.syncInFlightQueuedCoordinatesByHash.get(hash) ?? []),
3690
- ]) {
3691
- callback(coordinate);
3692
- }
3693
- }
3694
- clearSyncInFlightForPeer(publicKeyHash, hash) {
3695
- const map = this.syncInFlight.get(publicKeyHash);
3696
- if (!map) {
3697
- return;
3698
- }
3699
- this.refreshQueuedSyncCoordinateAliases();
3700
- this.forEachKnownAlias(hash, (key) => this.removeSyncInFlightTargetKey(publicKeyHash, key));
3026
+ this.pendingSync.clearSyncInFlightTarget(peer);
3701
3027
  }
3702
3028
  clearSyncInFlightForPeerHashes(publicKeyHash, hashes) {
3703
- const map = this.syncInFlight.get(publicKeyHash);
3704
- if (!map || hashes.length === 0) {
3705
- return;
3706
- }
3707
- this.refreshQueuedSyncCoordinateAliases();
3708
- for (const hash of hashes) {
3709
- this.forEachKnownAlias(hash, (key) => this.removeSyncInFlightTargetKey(publicKeyHash, key));
3710
- }
3029
+ this.pendingSync.clearSyncInFlightForPeerHashes(publicKeyHash, hashes);
3711
3030
  }
3712
3031
  clearSyncProcess(hash) {
3713
- this.refreshQueuedSyncCoordinateAliases();
3714
- this.forEachKnownAlias(hash, (key) => this.clearSyncProcessKey(key));
3032
+ this.pendingSync.clearSyncProcess(hash);
3715
3033
  }
3716
3034
  clearSyncProcesses(hashes) {
3717
- if (hashes.length === 0) {
3718
- return;
3719
- }
3720
- this.refreshQueuedSyncCoordinateAliases();
3721
- const keys = new Set();
3722
- for (const hash of hashes) {
3723
- this.forEachKnownAlias(hash, (key) => keys.add(key));
3724
- }
3725
- for (const key of keys) {
3726
- this.clearSyncProcessKey(key);
3727
- }
3035
+ this.pendingSync.clearSyncProcesses(hashes);
3728
3036
  }
3729
3037
  onPeerDisconnected(key) {
3730
3038
  const publicKeyHash = typeof key === "string" ? key : key.hashcode();
@@ -3734,7 +3042,7 @@ export class SimpleSyncronizer {
3734
3042
  this.syncDispatchTargetEpochs.delete(publicKeyHash);
3735
3043
  this.clearPendingSyncAdmissions(publicKeyHash);
3736
3044
  for (const targetLifecycle of [
3737
- ...(this.syncDispatchTargets.get(publicKeyHash) ?? []),
3045
+ ...(this.syncDispatchRegistry.activeTargets.get(publicKeyHash) ?? []),
3738
3046
  ]) {
3739
3047
  if (targetLifecycle.lifecycle.abortAllOnTargetDisconnect) {
3740
3048
  this.abortSyncDispatchLifecycle(targetLifecycle.lifecycle, new Error("sync target disconnected"));
@@ -3746,13 +3054,12 @@ export class SimpleSyncronizer {
3746
3054
  this.clearSyncInFlightTarget(publicKeyHash);
3747
3055
  this.recentlySentExchangeHeads.delete(publicKeyHash);
3748
3056
  this.clearPendingMaybeSyncResponses(publicKeyHash);
3749
- const map = this.syncInFlightQueueInverted.get(publicKeyHash);
3750
- if (map) {
3751
- for (const hash of [...map]) {
3752
- this.removePendingSyncClaim(hash, publicKeyHash);
3753
- }
3754
- this.syncInFlightQueueInverted.delete(publicKeyHash);
3755
- }
3057
+ this.pendingSync.removeClaimsForPeer(publicKeyHash);
3058
+ // Storage lookups and response ships for this peer may never settle
3059
+ // (the resolvers are not universally abortable). Detach the slot row so
3060
+ // the peer's quota returns immediately; the captured closures settle
3061
+ // against the detached row without touching a successor's quota.
3062
+ this.detachSyncResponseSlotRow(publicKeyHash);
3756
3063
  }
3757
3064
  get pending() {
3758
3065
  return this.syncInFlightQueue.size;