@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
@@ -20,6 +20,7 @@ import {
20
20
  } from "../exchange-heads.js";
21
21
  import { TransportMessage } from "../message.js";
22
22
  import type { EntryReplicated } from "../ranges.js";
23
+ import { DispatchLifecycleRegistry } from "./dispatch-lifecycle.js";
23
24
  import type {
24
25
  HashSymbolHashListResolver,
25
26
  HashSymbolResolver,
@@ -32,7 +33,16 @@ import type {
32
33
  SyncableKey,
33
34
  Syncronizer,
34
35
  } from "./index.js";
36
+ import {
37
+ type PendingSyncAdmissionReservation,
38
+ PendingSyncStore,
39
+ QUEUED_SYNC_ALIAS_REFRESH_PENDING,
40
+ } from "./pending-sync-store.js";
35
41
  import { emitSyncProfileDuration, syncProfileStart } from "./profile.js";
42
+ import {
43
+ SyncPeerSlotRegistry,
44
+ type SyncPeerSlotRow,
45
+ } from "./sync-peer-state.js";
36
46
 
37
47
  @variant([0, 1])
38
48
  export class RequestMaybeSync extends TransportMessage {
@@ -332,16 +342,10 @@ export const MAX_PENDING_SIMPLE_SYNC_LOOKUPS_GLOBAL = 32;
332
342
  // Retry scanning can touch persistent indexes. Bound each pass so a full
333
343
  // adversarial queue cannot force 40,000 lookups in one event-loop turn.
334
344
  export const MAX_SIMPLE_SYNC_RETRY_KEYS_PER_TICK = 4_096;
335
- // Late coordinate-to-hash cache fills are discovered incrementally. Keep this
336
- // independent of retained queue size so an empty or repeated request cannot
337
- // force an O(global pending keys) reverse-alias rebuild.
338
- const MAX_PENDING_SIMPLE_SYNC_ALIAS_REFRESH_PER_MESSAGE = 128;
339
- const QUEUED_SYNC_ALIAS_REFRESH_PENDING = Symbol(
340
- "queued-sync-alias-refresh-pending",
341
- );
342
- // This is an absolute first-seen lifetime. Repeated claims and additional peers
343
- // deliberately do not slide the deadline.
344
- export const PENDING_SIMPLE_SYNC_KEY_TTL_MS = 60_000;
345
+ // The pending-claim TTL, alias-refresh bound and pending-refresh sentinel
346
+ // moved into ./pending-sync-store.ts with the record store (stage-4 sync
347
+ // unification). Re-exported here for compatibility.
348
+ export { PENDING_SIMPLE_SYNC_KEY_TTL_MS } from "./pending-sync-store.js";
345
349
  // Bound retained request/response associations globally. Ten thousand hashes
346
350
  // covers several full default-size request batches while keeping adversarial or
347
351
  // abandoned requests to a small, predictable amount of heap.
@@ -351,30 +355,6 @@ export const MAX_ACTIVE_SIMPLE_SYNC_RESPONSES_GLOBAL = 32;
351
355
  const MAX_PENDING_MAYBE_SYNC_RESPONSE_WAITER_BYPASSES = 32;
352
356
  const MAX_PENDING_MAYBE_SYNC_RESPONSE_WAITERS = 10_000;
353
357
 
354
- type PendingSyncAdmissionReservation = {
355
- peer: string;
356
- remaining: number;
357
- active: boolean;
358
- released: boolean;
359
- expiresAt: number;
360
- identities: Set<SyncableKey>;
361
- retainedSettled: number;
362
- };
363
-
364
- type PendingSyncExpiryNode =
365
- | {
366
- kind: "key";
367
- key: SyncableKey;
368
- expiresAt: number;
369
- heapIndex: number;
370
- }
371
- | {
372
- kind: "admission";
373
- reservation: PendingSyncAdmissionReservation;
374
- expiresAt: number;
375
- heapIndex: number;
376
- };
377
-
378
358
  type PendingMaybeSyncResponse = {
379
359
  hashes: Set<string>;
380
360
  target: string;
@@ -396,6 +376,12 @@ type PendingMaybeSyncResponseAuthorization = {
396
376
  deliveryInFlight?: boolean;
397
377
  settled?: "fulfilled" | "released";
398
378
  active?: boolean;
379
+ // Set when consume accepts the authorization: the peer slot row that holds
380
+ // this authorization's pending-response hash-budget unit whenever
381
+ // deliveryInFlight !== true (deliveryInFlight custody belongs to
382
+ // finishDelivery). Row identity, not peer hash: a late settle never
383
+ // touches a reconnected successor's row.
384
+ slotRow?: SyncPeerSlotRow;
399
385
  };
400
386
 
401
387
  type PendingMaybeSyncResponseReservation = {
@@ -498,53 +484,40 @@ type RepairSessionState = {
498
484
  export class SimpleSyncronizer<R extends "u32" | "u64">
499
485
  implements Syncronizer<R>
500
486
  {
501
- // map of hash to public keys that we can ask for entries
502
- syncInFlightQueue: Map<SyncableKey, PublicSignKey[]>;
503
- syncInFlightQueueInverted: Map<string, Set<SyncableKey>>;
504
- private syncInFlightQueueExpiresAt: Map<SyncableKey, number>;
505
- private syncInFlightQueueExpiryTimer?: ReturnType<typeof setTimeout>;
506
- private pendingSyncExpiryHeap: PendingSyncExpiryNode[];
507
- private pendingSyncKeyExpiryNodes: Map<SyncableKey, PendingSyncExpiryNode>;
508
- private pendingSyncAdmissionExpiryNodes: Map<
509
- PendingSyncAdmissionReservation,
510
- PendingSyncExpiryNode
511
- >;
487
+ // The pending-sync record store owns the claim/admission state. The
488
+ // legacy public map instances (syncInFlightQueue, syncInFlightQueueInverted,
489
+ // syncInFlight, ...) live inside it and are exposed through the accessors
490
+ // below under their historical names.
491
+ private readonly pendingSync: PendingSyncStore;
512
492
  private syncInFlightRetryIterator?: IterableIterator<
513
493
  [SyncableKey, PublicSignKey[]]
514
494
  >;
515
495
  private syncInFlightRetryRemaining = 0;
516
- private syncInFlightQueueClaimants: Map<SyncableKey, Set<string>>;
517
- private syncInFlightQueueClaimantIndexes: Map<
518
- SyncableKey,
519
- Map<string, number>
520
- >;
521
- private syncInFlightQueueRoundRobinCursor: Map<SyncableKey, number>;
522
- private syncInFlightQueuedCoordinates: Set<bigint>;
523
- private syncInFlightQueuedHashByCoordinate: Map<bigint, string>;
524
- private syncInFlightQueuedCoordinatesByHash: Map<string, Set<bigint>>;
525
- private syncInFlightQueuedCoordinateRefreshIterator?: IterableIterator<bigint>;
526
- private pendingSyncClaimCount: number;
527
- private pendingSyncAdmissionCount: number;
528
- private pendingSyncActiveAdmissionReservations: number;
529
- private pendingSyncAdmissionCountByPeer: Map<string, number>;
530
- private pendingSyncAdmissionIdentitiesByPeer: Map<string, Set<SyncableKey>>;
531
- private pendingSyncAdmissionReservations: Set<PendingSyncAdmissionReservation>;
532
- private pendingSyncAdmissionReservationsByPeer: Map<
533
- string,
534
- Set<PendingSyncAdmissionReservation>
535
- >;
536
- private pendingSyncAdmissionReservationsByIdentity: Map<
537
- SyncableKey,
538
- Set<PendingSyncAdmissionReservation>
539
- >;
540
496
  private pendingCoordinateLookupCount: number;
541
497
  private pendingCoordinateLookupCountByPeer: Map<string, number>;
542
498
  private pendingCoordinateResponseCount: number;
543
499
  private pendingCoordinateResponseCountByPeer: Map<string, number>;
500
+ // Per-peer slot rows for coordinate lookups, coordinate responses and
501
+ // active maybe-sync responses (shared registry; see sync-peer-state.ts
502
+ // for the lifetime policy). Release closures capture the row object:
503
+ // once a disconnect detaches the row, the peer's slots return to the
504
+ // aggregate quota immediately, and a late release only settles the
505
+ // detached row — it never touches a reconnected successor's quota.
506
+ private readonly peerSlotRows: SyncPeerSlotRegistry;
507
+
508
+ // map of hash to public keys that we can ask for entries
509
+ get syncInFlightQueue(): Map<SyncableKey, PublicSignKey[]> {
510
+ return this.pendingSync.syncInFlightQueue;
511
+ }
512
+
513
+ get syncInFlightQueueInverted(): Map<string, Set<SyncableKey>> {
514
+ return this.pendingSync.syncInFlightQueueInverted;
515
+ }
544
516
 
545
517
  // map of hash to public keys that we have asked for entries
546
- syncInFlight!: Map<string, Map<SyncableKey, { timestamp: number }>>;
547
- private syncInFlightTargetsByKey: Map<SyncableKey, Set<string>>;
518
+ get syncInFlight(): Map<string, Map<SyncableKey, { timestamp: number }>> {
519
+ return this.pendingSync.syncInFlight;
520
+ }
548
521
 
549
522
  rpc: RPC<TransportMessage, TransportMessage>;
550
523
  log: Log<any>;
@@ -581,7 +554,11 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
581
554
  private syncDispatchLifecycleController: AbortController;
582
555
  private syncDispatchTargetEpochCounter: number;
583
556
  private syncDispatchTargetEpochs: Map<string, SyncDispatchTargetEpoch>;
584
- private syncDispatchTargets: Map<string, Set<SyncDispatchTargetLifecycle>>;
557
+ private readonly syncDispatchRegistry: DispatchLifecycleRegistry<
558
+ SyncDispatchLifecycle,
559
+ SyncDispatchTargetLifecycle
560
+ >;
561
+
585
562
  private repairSessionCounter: number;
586
563
  private repairSessions: Map<string, RepairSessionState>;
587
564
 
@@ -606,32 +583,16 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
606
583
  peerSupportsRawExchangeHeads?: (peer: string) => boolean;
607
584
  sendRawExchangeHeads?: RawExchangeHeadsSender;
608
585
  }) {
609
- this.syncInFlightQueue = new Map();
610
- this.syncInFlightQueueInverted = new Map();
611
- this.syncInFlightQueueExpiresAt = new Map();
612
- this.pendingSyncExpiryHeap = [];
613
- this.pendingSyncKeyExpiryNodes = new Map();
614
- this.pendingSyncAdmissionExpiryNodes = new Map();
615
- this.syncInFlightQueueClaimants = new Map();
616
- this.syncInFlightQueueClaimantIndexes = new Map();
617
- this.syncInFlightQueueRoundRobinCursor = new Map();
618
- this.syncInFlightQueuedCoordinates = new Set();
619
- this.syncInFlightQueuedHashByCoordinate = new Map();
620
- this.syncInFlightQueuedCoordinatesByHash = new Map();
621
- this.pendingSyncClaimCount = 0;
622
- this.pendingSyncAdmissionCount = 0;
623
- this.pendingSyncActiveAdmissionReservations = 0;
624
- this.pendingSyncAdmissionCountByPeer = new Map();
625
- this.pendingSyncAdmissionIdentitiesByPeer = new Map();
626
- this.pendingSyncAdmissionReservations = new Set();
627
- this.pendingSyncAdmissionReservationsByPeer = new Map();
628
- this.pendingSyncAdmissionReservationsByIdentity = new Map();
586
+ this.pendingSync = new PendingSyncStore({
587
+ coordinateToHash: properties.coordinateToHash,
588
+ isDispatchEpochCurrent: (peer, epoch) =>
589
+ this.syncDispatchTargetEpochs.get(peer) === epoch,
590
+ });
629
591
  this.pendingCoordinateLookupCount = 0;
630
592
  this.pendingCoordinateLookupCountByPeer = new Map();
631
593
  this.pendingCoordinateResponseCount = 0;
632
594
  this.pendingCoordinateResponseCountByPeer = new Map();
633
- this.syncInFlight = new Map();
634
- this.syncInFlightTargetsByKey = new Map();
595
+ this.peerSlotRows = new SyncPeerSlotRegistry();
635
596
  this.rpc = properties.rpc;
636
597
  this.log = properties.log;
637
598
  this.entryIndex = properties.entryIndex;
@@ -659,7 +620,24 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
659
620
  this.syncDispatchLifecycleController = new AbortController();
660
621
  this.syncDispatchTargetEpochCounter = 0;
661
622
  this.syncDispatchTargetEpochs = new Map();
662
- this.syncDispatchTargets = new Map();
623
+ this.syncDispatchRegistry = new DispatchLifecycleRegistry<
624
+ SyncDispatchLifecycle,
625
+ SyncDispatchTargetLifecycle
626
+ >({
627
+ isClosed: () => this.closed === true,
628
+ currentOwnershipLifecycleController: () =>
629
+ this.syncDispatchLifecycleController,
630
+ hasRetainedWork: (lifecycle) => lifecycle.retainedWork > 0,
631
+ // Target-activity strategy: dispatch-epoch identity.
632
+ isTargetCurrent: (targetLifecycle) =>
633
+ this.syncDispatchTargetEpochs.get(targetLifecycle.target) ===
634
+ targetLifecycle.epoch,
635
+ onTargetAborted: (targetLifecycle) => {
636
+ for (const batch of [...targetLifecycle.batches]) {
637
+ this.removePendingMaybeSyncResponseBatch(batch);
638
+ }
639
+ },
640
+ });
663
641
  this.repairSessionCounter = 0;
664
642
  this.repairSessions = new Map();
665
643
  }
@@ -839,60 +817,43 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
839
817
  abortAllOnTargetDisconnect: options?.abortAllOnTargetDisconnect === true,
840
818
  } satisfies SyncDispatchLifecycle;
841
819
 
842
- for (const target of [...new Set(targets)]) {
843
- const expectedEpoch = options?.targetEpochs?.get(target);
844
- const currentEpoch = this.syncDispatchTargetEpochs.get(target);
845
- const epoch =
846
- expectedEpoch ??
847
- currentEpoch ??
848
- (options?.createTargetEpochs === false
849
- ? undefined
850
- : this.getOrCreateSyncDispatchTargetEpoch(target));
851
- if (!epoch) {
852
- continue;
853
- }
854
- const targetLifecycle: SyncDispatchTargetLifecycle = {
855
- lifecycle,
856
- target,
857
- epoch,
858
- controller: new AbortController(),
859
- batches: new Set(),
860
- responseLeases: 0,
861
- activeWaiters: 0,
862
- };
863
- lifecycle.targets.set(target, targetLifecycle);
864
- let activeForTarget = this.syncDispatchTargets.get(target);
865
- if (!activeForTarget) {
866
- activeForTarget = new Set();
867
- this.syncDispatchTargets.set(target, activeForTarget);
868
- }
869
- activeForTarget.add(targetLifecycle);
870
- if (this.syncDispatchTargetEpochs.get(target) !== epoch) {
871
- this.abortSyncDispatchTarget(
872
- targetLifecycle,
873
- new Error("sync target lifecycle is stale"),
874
- );
875
- }
876
- }
877
-
878
- ownershipLifecycleController.signal.addEventListener(
879
- "abort",
880
- lifecycle.onOwnerOrCallerAbort,
881
- { once: true },
820
+ this.syncDispatchRegistry.register(
821
+ lifecycle,
822
+ targets,
823
+ (forLifecycle, target) => {
824
+ const expectedEpoch = options?.targetEpochs?.get(target);
825
+ const currentEpoch = this.syncDispatchTargetEpochs.get(target);
826
+ const epoch =
827
+ expectedEpoch ??
828
+ currentEpoch ??
829
+ (options?.createTargetEpochs === false
830
+ ? undefined
831
+ : this.getOrCreateSyncDispatchTargetEpoch(target));
832
+ if (!epoch) {
833
+ return undefined;
834
+ }
835
+ return {
836
+ lifecycle: forLifecycle,
837
+ target,
838
+ epoch,
839
+ controller: new AbortController(),
840
+ batches: new Set(),
841
+ responseLeases: 0,
842
+ activeWaiters: 0,
843
+ };
844
+ },
845
+ (targetLifecycle) => {
846
+ if (
847
+ this.syncDispatchTargetEpochs.get(targetLifecycle.target) !==
848
+ targetLifecycle.epoch
849
+ ) {
850
+ this.abortSyncDispatchTarget(
851
+ targetLifecycle,
852
+ new Error("sync target lifecycle is stale"),
853
+ );
854
+ }
855
+ },
882
856
  );
883
- if (callerSignal && callerSignal !== ownershipLifecycleController.signal) {
884
- callerSignal.addEventListener("abort", lifecycle.onOwnerOrCallerAbort, {
885
- once: true,
886
- });
887
- }
888
- if (
889
- this.closed === true ||
890
- ownershipLifecycleController !== this.syncDispatchLifecycleController ||
891
- ownershipLifecycleController.signal.aborted ||
892
- callerSignal?.aborted
893
- ) {
894
- lifecycle.onOwnerOrCallerAbort();
895
- }
896
857
  return lifecycle;
897
858
  }
898
859
 
@@ -900,102 +861,38 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
900
861
  targetLifecycle: SyncDispatchTargetLifecycle,
901
862
  reason?: unknown,
902
863
  ): void {
903
- if (!targetLifecycle.controller.signal.aborted) {
904
- targetLifecycle.controller.abort(reason);
905
- }
906
- for (const batch of [...targetLifecycle.batches]) {
907
- this.removePendingMaybeSyncResponseBatch(batch);
908
- }
909
- this.maybeDisposeSyncDispatchLifecycle(targetLifecycle.lifecycle);
864
+ this.syncDispatchRegistry.abortTarget(targetLifecycle, reason);
910
865
  }
911
866
 
912
867
  private abortSyncDispatchLifecycle(
913
868
  lifecycle: SyncDispatchLifecycle,
914
869
  reason?: unknown,
915
870
  ): void {
916
- if (!lifecycle.controller.signal.aborted) {
917
- lifecycle.controller.abort(reason);
918
- }
919
- for (const targetLifecycle of lifecycle.targets.values()) {
920
- this.abortSyncDispatchTarget(targetLifecycle, reason);
921
- }
922
- this.maybeDisposeSyncDispatchLifecycle(lifecycle);
871
+ this.syncDispatchRegistry.abortLifecycle(lifecycle, reason);
923
872
  }
924
873
 
925
874
  private finishSyncDispatchLifecycle(lifecycle: SyncDispatchLifecycle): void {
926
- lifecycle.dispatchFinished = true;
927
- this.maybeDisposeSyncDispatchLifecycle(lifecycle);
875
+ this.syncDispatchRegistry.finish(lifecycle);
928
876
  }
929
877
 
930
878
  private maybeDisposeSyncDispatchLifecycle(
931
879
  lifecycle: SyncDispatchLifecycle,
932
880
  ): void {
933
- if (
934
- lifecycle.disposed ||
935
- !lifecycle.dispatchFinished ||
936
- lifecycle.retainedWork > 0
937
- ) {
938
- return;
939
- }
940
- lifecycle.disposed = true;
941
- lifecycle.ownershipLifecycleController.signal.removeEventListener(
942
- "abort",
943
- lifecycle.onOwnerOrCallerAbort,
944
- );
945
- if (
946
- lifecycle.callerSignal &&
947
- lifecycle.callerSignal !== lifecycle.ownershipLifecycleController.signal
948
- ) {
949
- lifecycle.callerSignal.removeEventListener(
950
- "abort",
951
- lifecycle.onOwnerOrCallerAbort,
952
- );
953
- }
954
- for (const targetLifecycle of lifecycle.targets.values()) {
955
- const activeForTarget = this.syncDispatchTargets.get(
956
- targetLifecycle.target,
957
- );
958
- activeForTarget?.delete(targetLifecycle);
959
- if (activeForTarget?.size === 0) {
960
- this.syncDispatchTargets.delete(targetLifecycle.target);
961
- }
962
- }
881
+ this.syncDispatchRegistry.maybeDispose(lifecycle);
963
882
  }
964
883
 
965
884
  private isSyncDispatchLifecycleActive(
966
885
  lifecycle: SyncDispatchLifecycle,
967
886
  target?: string,
968
887
  ): boolean {
969
- if (
970
- this.closed === true ||
971
- lifecycle.disposed ||
972
- lifecycle.ownershipLifecycleController !==
973
- this.syncDispatchLifecycleController ||
974
- lifecycle.ownershipLifecycleController.signal.aborted ||
975
- lifecycle.callerSignal?.aborted ||
976
- lifecycle.controller.signal.aborted
977
- ) {
978
- return false;
979
- }
980
- if (target === undefined) {
981
- return true;
982
- }
983
- const targetLifecycle = lifecycle.targets.get(target);
984
- return (
985
- targetLifecycle !== undefined &&
986
- !targetLifecycle.controller.signal.aborted &&
987
- this.syncDispatchTargetEpochs.get(target) === targetLifecycle.epoch
988
- );
888
+ return this.syncDispatchRegistry.isLifecycleActive(lifecycle, target);
989
889
  }
990
890
 
991
891
  private getSyncDispatchSignal(
992
892
  lifecycle: SyncDispatchLifecycle,
993
893
  target: string,
994
894
  ): AbortSignal {
995
- return (
996
- lifecycle.targets.get(target)?.controller.signal ??
997
- lifecycle.controller.signal
998
- );
895
+ return this.syncDispatchRegistry.getSignal(lifecycle, target);
999
896
  }
1000
897
 
1001
898
  private pendingMaybeSyncResponseWaiterBefore(
@@ -1697,6 +1594,16 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
1697
1594
  beginDelivery: () => {
1698
1595
  for (const authorization of addedAuthorizations) {
1699
1596
  if (!authorization.settled) {
1597
+ if (
1598
+ authorization.deliveryInFlight !== true &&
1599
+ authorization.active === true &&
1600
+ authorization.slotRow?.attached === true
1601
+ ) {
1602
+ // The response was accepted before the request send began:
1603
+ // custody of the hash-budget unit moves from the slot row to
1604
+ // finishDelivery for the duration of the send.
1605
+ authorization.slotRow.pendingResponseHashes -= 1;
1606
+ }
1700
1607
  authorization.deliveryInFlight = true;
1701
1608
  }
1702
1609
  }
@@ -1715,6 +1622,14 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
1715
1622
  ?.get(authorization.hash) !== authorization
1716
1623
  ) {
1717
1624
  releasedCount += 1;
1625
+ } else if (
1626
+ authorization.active === true &&
1627
+ authorization.slotRow?.attached === true
1628
+ ) {
1629
+ // The response was accepted while the request send was in
1630
+ // flight; custody of the hash-budget unit returns to the
1631
+ // active lease's slot row.
1632
+ authorization.slotRow.pendingResponseHashes += 1;
1718
1633
  }
1719
1634
  }
1720
1635
  if (releasedCount > 0) {
@@ -2048,45 +1963,74 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2048
1963
  if (acceptedByLifecycle.size === 0) {
2049
1964
  return [];
2050
1965
  }
1966
+ const slotRow = this.getOrCreateSyncResponseSlotRow(fromHash);
1967
+ slotRow.active += 1;
2051
1968
  this.activeMaybeSyncResponseCount += 1;
2052
1969
  this.activeMaybeSyncResponseCountByPeer.set(
2053
1970
  fromHash,
2054
1971
  (this.activeMaybeSyncResponseCountByPeer.get(fromHash) ?? 0) + 1,
2055
1972
  );
2056
- let remainingLeases = acceptedByLifecycle.size;
1973
+ // Accepted authorizations leave their batch (above) but stay charged
1974
+ // against the global hash budget. Move that charge into row custody so a
1975
+ // disconnect can return it even when the response ship never settles;
1976
+ // units currently owned by finishDelivery (deliveryInFlight) stay out.
1977
+ for (const { authorizations } of acceptedByLifecycle.values()) {
1978
+ for (const authorization of authorizations) {
1979
+ authorization.slotRow = slotRow;
1980
+ if (authorization.deliveryInFlight !== true) {
1981
+ slotRow.pendingResponseHashes += 1;
1982
+ }
1983
+ }
1984
+ }
1985
+ let remainingPhysicalLeases = acceptedByLifecycle.size;
2057
1986
  return [...acceptedByLifecycle].map(
2058
1987
  ([targetLifecycle, { hashes: acceptedHashes, authorizations }]) => {
2059
- let released = false;
2060
- return {
2061
- hashes: acceptedHashes,
2062
- signal: targetLifecycle.controller.signal,
2063
- release: (options?: { fulfilled?: boolean }) => {
2064
- if (released) {
2065
- return;
2066
- }
2067
- released = true;
2068
- const pendingForTarget =
2069
- this.pendingMaybeSyncResponses.get(fromHash);
2070
- for (const authorization of authorizations) {
2071
- this.settlePendingMaybeSyncResponseAuthorization(
2072
- authorization,
2073
- options?.fulfilled === true,
2074
- );
2075
- if (pendingForTarget?.get(authorization.hash) === authorization) {
2076
- pendingForTarget.delete(authorization.hash);
2077
- }
2078
- }
2079
- if (pendingForTarget?.size === 0) {
2080
- this.pendingMaybeSyncResponses.delete(fromHash);
1988
+ let logicalReleased = false;
1989
+ let physicalReleased = false;
1990
+ const releaseLogical = (options?: { fulfilled?: boolean }) => {
1991
+ if (logicalReleased) {
1992
+ return;
1993
+ }
1994
+ logicalReleased = true;
1995
+ slotRow.activeReleases.delete(releaseLogical);
1996
+ const pendingForTarget = this.pendingMaybeSyncResponses.get(fromHash);
1997
+ for (const authorization of authorizations) {
1998
+ this.settlePendingMaybeSyncResponseAuthorization(
1999
+ authorization,
2000
+ options?.fulfilled === true,
2001
+ );
2002
+ if (pendingForTarget?.get(authorization.hash) === authorization) {
2003
+ pendingForTarget.delete(authorization.hash);
2081
2004
  }
2082
- this.pendingMaybeSyncResponseCount -= authorizations.filter(
2083
- (authorization) => authorization.deliveryInFlight !== true,
2084
- ).length;
2085
- targetLifecycle.responseLeases -= 1;
2086
- targetLifecycle.lifecycle.retainedWork -= 1;
2087
- remainingLeases -= 1;
2088
- if (remainingLeases === 0) {
2089
- this.activeMaybeSyncResponseCount -= 1;
2005
+ }
2006
+ if (pendingForTarget?.size === 0) {
2007
+ this.pendingMaybeSyncResponses.delete(fromHash);
2008
+ }
2009
+ const chargedToRow = authorizations.filter(
2010
+ (authorization) => authorization.deliveryInFlight !== true,
2011
+ ).length;
2012
+ if (slotRow.attached) {
2013
+ this.pendingMaybeSyncResponseCount -= chargedToRow;
2014
+ slotRow.pendingResponseHashes -= chargedToRow;
2015
+ }
2016
+ // else: the hash budget already returned in aggregate when the row
2017
+ // detached; only the deliveryInFlight units (owned by
2018
+ // finishDelivery) remain charged.
2019
+ targetLifecycle.responseLeases -= 1;
2020
+ targetLifecycle.lifecycle.retainedWork -= 1;
2021
+ this.schedulePendingMaybeSyncResponseWaiter();
2022
+ this.maybeDisposeSyncDispatchLifecycle(targetLifecycle.lifecycle);
2023
+ };
2024
+ const releasePhysical = () => {
2025
+ if (physicalReleased) {
2026
+ return;
2027
+ }
2028
+ physicalReleased = true;
2029
+ remainingPhysicalLeases -= 1;
2030
+ if (remainingPhysicalLeases === 0) {
2031
+ slotRow.active -= 1;
2032
+ this.activeMaybeSyncResponseCount -= 1;
2033
+ if (slotRow.attached) {
2090
2034
  const activeForPeer =
2091
2035
  (this.activeMaybeSyncResponseCountByPeer.get(fromHash) ?? 1) -
2092
2036
  1;
@@ -2098,9 +2042,20 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2098
2042
  activeForPeer,
2099
2043
  );
2100
2044
  }
2045
+ this.maybeDropIdleSyncResponseSlotRow(slotRow);
2046
+ }
2047
+ }
2048
+ };
2049
+ slotRow.activeReleases.add(releaseLogical);
2050
+ return {
2051
+ hashes: acceptedHashes,
2052
+ signal: targetLifecycle.controller.signal,
2053
+ release: (options?: { fulfilled?: boolean }) => {
2054
+ try {
2055
+ releaseLogical(options);
2056
+ } finally {
2057
+ releasePhysical();
2101
2058
  }
2102
- this.schedulePendingMaybeSyncResponseWaiter();
2103
- this.maybeDisposeSyncDispatchLifecycle(targetLifecycle.lifecycle);
2104
2059
  },
2105
2060
  };
2106
2061
  },
@@ -2739,11 +2694,26 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2739
2694
  return true;
2740
2695
  }
2741
2696
  const target = from.hashcode();
2742
- const releaseLookup = this.tryAcquireCoordinateLookup(target);
2743
- if (!releaseLookup) {
2697
+ const lookupPermit = this.tryAcquireCoordinateLookup(target);
2698
+ if (!lookupPermit) {
2744
2699
  return true;
2745
2700
  }
2701
+ const { release: releaseLookup, row: slotRow } = lookupPermit;
2746
2702
  const lifecycle = this.captureSyncDispatchLifecycle([target]);
2703
+ let lifecycleFinished = false;
2704
+ const finishLifecycle = () => {
2705
+ if (lifecycleFinished) {
2706
+ return;
2707
+ }
2708
+ lifecycleFinished = true;
2709
+ slotRow.activeReleases.delete(finishLifecycle);
2710
+ this.finishSyncDispatchLifecycle(lifecycle);
2711
+ this.maybeDropIdleSyncResponseSlotRow(slotRow);
2712
+ };
2713
+ // Dispatch completion is logical ownership. A disconnected peer must
2714
+ // not retain this lifecycle/listeners while a non-abortable lookup or
2715
+ // shipment keeps only its global physical permit alive.
2716
+ slotRow.activeReleases.add(finishLifecycle);
2747
2717
  let lookupReleased = false;
2748
2718
  const finishLookup = () => {
2749
2719
  if (lookupReleased) {
@@ -2815,7 +2785,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2815
2785
  return true;
2816
2786
  } finally {
2817
2787
  finishLookup();
2818
- this.finishSyncDispatchLifecycle(lifecycle);
2788
+ finishLifecycle();
2819
2789
  }
2820
2790
  } else {
2821
2791
  return false; // no message was consumed
@@ -2844,194 +2814,17 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
2844
2814
  }
2845
2815
 
2846
2816
  private getPendingSyncKeyIdentity(key: SyncableKey): SyncableKey {
2847
- if (typeof key === "string") {
2848
- return key;
2849
- }
2850
- const hash = this.coordinateToHash.get(key);
2851
- return hash ?? key;
2852
- }
2853
-
2854
- private removeQueuedSyncCoordinateAlias(key: bigint): void {
2855
- this.syncInFlightQueuedCoordinates.delete(key);
2856
- if (this.syncInFlightQueuedCoordinates.size === 0) {
2857
- this.syncInFlightQueuedCoordinateRefreshIterator = undefined;
2858
- }
2859
- const previousHash = this.syncInFlightQueuedHashByCoordinate.get(key);
2860
- this.syncInFlightQueuedHashByCoordinate.delete(key);
2861
- if (previousHash != null) {
2862
- const coordinates =
2863
- this.syncInFlightQueuedCoordinatesByHash.get(previousHash);
2864
- coordinates?.delete(key);
2865
- if (coordinates?.size === 0) {
2866
- this.syncInFlightQueuedCoordinatesByHash.delete(previousHash);
2867
- }
2868
- }
2869
- }
2870
-
2871
- private refreshQueuedSyncCoordinateAlias(key: bigint): void {
2872
- if (!this.syncInFlightQueue.has(key)) {
2873
- this.removeQueuedSyncCoordinateAlias(key);
2874
- return;
2875
- }
2876
- this.syncInFlightQueuedCoordinates.add(key);
2877
- const hash = this.coordinateToHash.get(key) ?? undefined;
2878
- const previousHash = this.syncInFlightQueuedHashByCoordinate.get(key);
2879
- if (previousHash !== hash) {
2880
- if (previousHash != null) {
2881
- const coordinates =
2882
- this.syncInFlightQueuedCoordinatesByHash.get(previousHash);
2883
- coordinates?.delete(key);
2884
- if (coordinates?.size === 0) {
2885
- this.syncInFlightQueuedCoordinatesByHash.delete(previousHash);
2886
- }
2887
- }
2888
- if (hash == null) {
2889
- this.syncInFlightQueuedHashByCoordinate.delete(key);
2890
- } else {
2891
- this.syncInFlightQueuedHashByCoordinate.set(key, hash);
2892
- }
2893
- }
2894
- if (hash != null) {
2895
- let coordinates = this.syncInFlightQueuedCoordinatesByHash.get(hash);
2896
- if (!coordinates) {
2897
- coordinates = new Set();
2898
- this.syncInFlightQueuedCoordinatesByHash.set(hash, coordinates);
2899
- }
2900
- coordinates.add(key);
2901
- this.reconcileQueuedSyncCoordinateAlias(key, hash);
2902
- }
2903
- }
2904
-
2905
- private reconcileQueuedSyncCoordinateAlias(
2906
- coordinate: bigint,
2907
- hash: string,
2908
- ): void {
2909
- const now = Date.now();
2910
- const coordinateExpiresAt = this.syncInFlightQueueExpiresAt.get(coordinate);
2911
- if (coordinateExpiresAt != null && coordinateExpiresAt <= now) {
2912
- this.clearSyncProcessKey(coordinate);
2913
- return;
2914
- }
2915
- const hashExpiresAt = this.syncInFlightQueueExpiresAt.get(hash);
2916
- if (hashExpiresAt != null && hashExpiresAt <= now) {
2917
- this.clearSyncProcessKey(hash);
2918
- return;
2919
- }
2920
- const hashClaimants = this.syncInFlightQueue.get(hash);
2921
- if (!hashClaimants || !this.syncInFlightQueue.has(coordinate)) {
2922
- return;
2923
- }
2924
- const expiresAt = Math.min(
2925
- this.syncInFlightQueueExpiresAt.get(coordinate) ?? Infinity,
2926
- this.syncInFlightQueueExpiresAt.get(hash) ?? Infinity,
2927
- );
2928
- for (const claimant of [...hashClaimants]) {
2929
- this.addPendingSyncClaim(coordinate, claimant, expiresAt);
2930
- }
2931
- if (Number.isFinite(expiresAt)) {
2932
- this.movePendingSyncKeyExpiryEarlier(coordinate, expiresAt);
2933
- }
2934
- for (const target of [...(this.syncInFlightTargetsByKey.get(hash) ?? [])]) {
2935
- const state = this.syncInFlight.get(target)?.get(hash);
2936
- if (state) {
2937
- this.setSyncInFlightTargetKey(target, coordinate, state.timestamp);
2938
- }
2939
- }
2940
- this.clearSyncProcessKey(hash);
2817
+ return this.pendingSync.getPendingSyncKeyIdentity(key);
2941
2818
  }
2942
2819
 
2943
2820
  private refreshQueuedSyncCoordinateAliases(): void {
2944
- if (
2945
- this.syncInFlightQueuedCoordinates.size === 0 &&
2946
- this.syncInFlightQueueClaimants.size < this.syncInFlightQueue.size
2947
- ) {
2948
- // Defensive compatibility for callers/tests that seed the public queue
2949
- // directly. Keep hydration bounded; internal writes register coordinates
2950
- // when the key is first admitted.
2951
- let inspected = 0;
2952
- for (const key of this.syncInFlightQueue.keys()) {
2953
- if (typeof key === "bigint") {
2954
- this.syncInFlightQueuedCoordinates.add(key);
2955
- }
2956
- inspected += 1;
2957
- if (inspected >= MAX_PENDING_SIMPLE_SYNC_ALIAS_REFRESH_PER_MESSAGE) {
2958
- break;
2959
- }
2960
- }
2961
- }
2962
- if (this.syncInFlightQueuedCoordinates.size === 0) {
2963
- this.syncInFlightQueuedCoordinateRefreshIterator = undefined;
2964
- return;
2965
- }
2966
- this.syncInFlightQueuedCoordinateRefreshIterator ??=
2967
- this.syncInFlightQueuedCoordinates.values();
2968
- for (
2969
- let refreshed = 0;
2970
- refreshed < MAX_PENDING_SIMPLE_SYNC_ALIAS_REFRESH_PER_MESSAGE;
2971
- refreshed += 1
2972
- ) {
2973
- const next = this.syncInFlightQueuedCoordinateRefreshIterator.next();
2974
- if (next.done) {
2975
- this.syncInFlightQueuedCoordinateRefreshIterator = undefined;
2976
- break;
2977
- }
2978
- this.refreshQueuedSyncCoordinateAlias(next.value);
2979
- }
2821
+ this.pendingSync.refreshQueuedSyncCoordinateAliases();
2980
2822
  }
2981
2823
 
2982
2824
  private getQueuedSyncKeyForAdmission(
2983
2825
  key: SyncableKey,
2984
2826
  ): SyncableKey | typeof QUEUED_SYNC_ALIAS_REFRESH_PENDING | undefined {
2985
- const getValidQueuedKey = (
2986
- candidate: SyncableKey,
2987
- ): SyncableKey | undefined => {
2988
- if (!this.syncInFlightQueue.has(candidate)) {
2989
- return undefined;
2990
- }
2991
- const expiresAt = this.syncInFlightQueueExpiresAt.get(candidate);
2992
- if (expiresAt != null && expiresAt <= Date.now()) {
2993
- this.clearSyncProcessKey(candidate);
2994
- return undefined;
2995
- }
2996
- return candidate;
2997
- };
2998
- if (getValidQueuedKey(key) != null) {
2999
- if (typeof key === "bigint") {
3000
- this.refreshQueuedSyncCoordinateAlias(key);
3001
- return getValidQueuedKey(key);
3002
- }
3003
- return key;
3004
- }
3005
- if (typeof key === "string") {
3006
- const aliases = this.syncInFlightQueuedCoordinatesByHash.get(key);
3007
- if (aliases) {
3008
- let inspected = 0;
3009
- for (const alias of aliases) {
3010
- if (inspected >= MAX_PENDING_SIMPLE_SYNC_ALIAS_REFRESH_PER_MESSAGE) {
3011
- return QUEUED_SYNC_ALIAS_REFRESH_PENDING;
3012
- }
3013
- inspected += 1;
3014
- this.refreshQueuedSyncCoordinateAlias(alias);
3015
- if (
3016
- this.syncInFlightQueuedCoordinatesByHash.get(key)?.has(alias) ===
3017
- true
3018
- ) {
3019
- const validAlias = getValidQueuedKey(alias);
3020
- if (validAlias != null) {
3021
- return validAlias;
3022
- }
3023
- }
3024
- }
3025
- if (
3026
- (this.syncInFlightQueuedCoordinatesByHash.get(key)?.size ?? 0) > 0
3027
- ) {
3028
- return QUEUED_SYNC_ALIAS_REFRESH_PENDING;
3029
- }
3030
- }
3031
- return undefined;
3032
- }
3033
- const hash = this.coordinateToHash.get(key);
3034
- return hash != null ? getValidQueuedKey(hash) : undefined;
2827
+ return this.pendingSync.getQueuedSyncKeyForAdmission(key);
3035
2828
  }
3036
2829
 
3037
2830
  private addPendingSyncClaim(
@@ -3039,80 +2832,11 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3039
2832
  from: PublicSignKey,
3040
2833
  expiresAt?: number,
3041
2834
  ): boolean {
3042
- const fromHash = from.hashcode();
3043
- let peers = this.syncInFlightQueue.get(key);
3044
- let claimants = this.syncInFlightQueueClaimants.get(key);
3045
- let claimantIndexes = this.syncInFlightQueueClaimantIndexes.get(key);
3046
- if (!peers) {
3047
- peers = [];
3048
- this.syncInFlightQueue.set(key, peers);
3049
- claimants = new Set();
3050
- this.syncInFlightQueueClaimants.set(key, claimants);
3051
- claimantIndexes = new Map();
3052
- this.syncInFlightQueueClaimantIndexes.set(key, claimantIndexes);
3053
- const deadline = expiresAt ?? Date.now() + PENDING_SIMPLE_SYNC_KEY_TTL_MS;
3054
- this.syncInFlightQueueExpiresAt.set(key, deadline);
3055
- const expiryNode: PendingSyncExpiryNode = {
3056
- kind: "key",
3057
- key,
3058
- expiresAt: deadline,
3059
- heapIndex: -1,
3060
- };
3061
- this.pendingSyncKeyExpiryNodes.set(key, expiryNode);
3062
- this.pushPendingSyncExpiry(expiryNode);
3063
- if (typeof key === "bigint") {
3064
- this.refreshQueuedSyncCoordinateAlias(key);
3065
- }
3066
- } else if (!claimants) {
3067
- // Defensive compatibility for callers/tests that seed the public queue
3068
- // maps directly. Internally every retained key gets this set at creation.
3069
- claimants = new Set(peers.map((peer) => peer.hashcode()));
3070
- this.syncInFlightQueueClaimants.set(key, claimants);
3071
- this.pendingSyncClaimCount += claimants.size;
3072
- }
3073
- if (!claimantIndexes) {
3074
- claimantIndexes = new Map();
3075
- for (let index = 0; index < peers.length; index += 1) {
3076
- claimantIndexes.set(peers[index]!.hashcode(), index);
3077
- }
3078
- this.syncInFlightQueueClaimantIndexes.set(key, claimantIndexes);
3079
- }
3080
- if (claimants.has(fromHash)) {
3081
- return false;
3082
- }
3083
-
3084
- claimantIndexes.set(fromHash, peers.length);
3085
- peers.push(from);
3086
- claimants.add(fromHash);
3087
- let inverted = this.syncInFlightQueueInverted.get(fromHash);
3088
- if (!inverted) {
3089
- inverted = new Set();
3090
- this.syncInFlightQueueInverted.set(fromHash, inverted);
3091
- }
3092
- inverted.add(key);
3093
- this.pendingSyncClaimCount += 1;
3094
- this.schedulePendingSyncKeyExpiry();
3095
- return true;
2835
+ return this.pendingSync.addPendingSyncClaim(key, from, expiresAt);
3096
2836
  }
3097
2837
 
3098
2838
  private hasPendingSyncClaim(key: SyncableKey, peer: string): boolean {
3099
- const claimants = this.syncInFlightQueueClaimants.get(key);
3100
- if (claimants) {
3101
- return claimants.has(peer);
3102
- }
3103
- const peers = this.syncInFlightQueue.get(key);
3104
- if (!peers) {
3105
- return false;
3106
- }
3107
- const hydrated = new Set(peers.map((candidate) => candidate.hashcode()));
3108
- this.syncInFlightQueueClaimants.set(key, hydrated);
3109
- const indexes = new Map<string, number>();
3110
- for (let index = 0; index < peers.length; index += 1) {
3111
- indexes.set(peers[index]!.hashcode(), index);
3112
- }
3113
- this.syncInFlightQueueClaimantIndexes.set(key, indexes);
3114
- this.pendingSyncClaimCount += hydrated.size;
3115
- return hydrated.has(peer);
2839
+ return this.pendingSync.hasPendingSyncClaim(key, peer);
3116
2840
  }
3117
2841
 
3118
2842
  private filterDispatchablePendingSyncClaims(
@@ -3120,54 +2844,87 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3120
2844
  peer: string,
3121
2845
  epoch: SyncDispatchTargetEpoch,
3122
2846
  ): SyncableKey[] {
3123
- if (this.syncDispatchTargetEpochs.get(peer) !== epoch) {
3124
- return [];
3125
- }
3126
- const now = Date.now();
3127
- const dispatchable: SyncableKey[] = [];
3128
- for (const key of keys) {
3129
- const expiresAt = this.syncInFlightQueueExpiresAt.get(key);
3130
- if (expiresAt != null && expiresAt <= now) {
3131
- this.clearSyncProcessKey(key);
3132
- continue;
3133
- }
3134
- if (
3135
- this.syncInFlightQueue.has(key) &&
3136
- this.hasPendingSyncClaim(key, peer)
3137
- ) {
3138
- dispatchable.push(key);
3139
- }
3140
- }
3141
- return dispatchable;
2847
+ return this.pendingSync.filterDispatchablePendingSyncClaims(
2848
+ keys,
2849
+ peer,
2850
+ epoch,
2851
+ );
3142
2852
  }
3143
2853
 
3144
2854
  private canStartPendingSyncLookup(peer: string): boolean {
3145
2855
  return (
3146
- this.pendingSyncAdmissionReservations.size +
2856
+ this.pendingSync.pendingSyncAdmissionReservations.size +
3147
2857
  this.pendingCoordinateLookupCount <
3148
2858
  MAX_PENDING_SIMPLE_SYNC_LOOKUPS_GLOBAL &&
3149
- (this.pendingSyncAdmissionReservationsByPeer.get(peer)?.size ?? 0) +
2859
+ (this.pendingSync.pendingSyncAdmissionReservationsByPeer.get(peer)
2860
+ ?.size ?? 0) +
3150
2861
  (this.pendingCoordinateLookupCountByPeer.get(peer) ?? 0) <
3151
2862
  MAX_PENDING_SIMPLE_SYNC_LOOKUPS_PER_PEER
3152
2863
  );
3153
2864
  }
3154
2865
 
3155
- private tryAcquireCoordinateLookup(peer: string): (() => void) | undefined {
2866
+ private getOrCreateSyncResponseSlotRow(peer: string): SyncPeerSlotRow {
2867
+ return this.peerSlotRows.ensure(peer);
2868
+ }
2869
+
2870
+ private detachSyncResponseSlotRow(peer: string): void {
2871
+ const row = this.peerSlotRows.detach(peer);
2872
+ if (!row) {
2873
+ return;
2874
+ }
2875
+ // Disconnect returns only the current peer generation's quota. The
2876
+ // global counters represent physically live, potentially non-abortable
2877
+ // work and remain charged until each captured release actually settles.
2878
+ this.pendingCoordinateLookupCountByPeer.delete(peer);
2879
+ this.pendingCoordinateResponseCountByPeer.delete(peer);
2880
+ this.activeMaybeSyncResponseCountByPeer.delete(peer);
2881
+ if (row.pendingResponseHashes > 0) {
2882
+ // Active authorizations' hash budget returns in aggregate with the
2883
+ // row. The settled releases below (and any late ship-side release)
2884
+ // see the detached row and skip the global budget.
2885
+ this.pendingMaybeSyncResponseCount -= row.pendingResponseHashes;
2886
+ row.pendingResponseHashes = 0;
2887
+ this.schedulePendingMaybeSyncResponseWaiter();
2888
+ }
2889
+ // Settle only logical active-response ownership so authorizations leave
2890
+ // pendingMaybeSyncResponses and responseLeases/retainedWork drain. The
2891
+ // eventual ship-side release still returns its global physical permit.
2892
+ // Closures capture THIS row (identity, not peer hash), so a reconnected
2893
+ // successor's row is never touched.
2894
+ for (const release of [...row.activeReleases]) {
2895
+ release();
2896
+ }
2897
+ }
2898
+
2899
+ private maybeDropIdleSyncResponseSlotRow(row: SyncPeerSlotRow): void {
2900
+ this.peerSlotRows.maybeDropIdle(row);
2901
+ }
2902
+
2903
+ private tryAcquireCoordinateLookup(
2904
+ peer: string,
2905
+ ): { row: SyncPeerSlotRow; release: () => void } | undefined {
3156
2906
  if (!this.canStartPendingSyncLookup(peer)) {
3157
2907
  return undefined;
3158
2908
  }
2909
+ const row = this.getOrCreateSyncResponseSlotRow(peer);
2910
+ row.lookups += 1;
3159
2911
  this.pendingCoordinateLookupCount += 1;
3160
2912
  this.pendingCoordinateLookupCountByPeer.set(
3161
2913
  peer,
3162
2914
  (this.pendingCoordinateLookupCountByPeer.get(peer) ?? 0) + 1,
3163
2915
  );
3164
2916
  let released = false;
3165
- return () => {
2917
+ const release = () => {
3166
2918
  if (released) {
3167
2919
  return;
3168
2920
  }
3169
2921
  released = true;
2922
+ row.lookups -= 1;
3170
2923
  this.pendingCoordinateLookupCount -= 1;
2924
+ if (!row.attached) {
2925
+ // Per-peer quota returned when this generation detached.
2926
+ return;
2927
+ }
3171
2928
  const remaining =
3172
2929
  (this.pendingCoordinateLookupCountByPeer.get(peer) ?? 1) - 1;
3173
2930
  if (remaining === 0) {
@@ -3175,7 +2932,9 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3175
2932
  } else {
3176
2933
  this.pendingCoordinateLookupCountByPeer.set(peer, remaining);
3177
2934
  }
2935
+ this.maybeDropIdleSyncResponseSlotRow(row);
3178
2936
  };
2937
+ return { row, release };
3179
2938
  }
3180
2939
 
3181
2940
  private tryAcquireCoordinateResponse(peer: string): (() => void) | undefined {
@@ -3187,6 +2946,8 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3187
2946
  ) {
3188
2947
  return undefined;
3189
2948
  }
2949
+ const row = this.getOrCreateSyncResponseSlotRow(peer);
2950
+ row.responses += 1;
3190
2951
  this.pendingCoordinateResponseCount += 1;
3191
2952
  this.pendingCoordinateResponseCountByPeer.set(
3192
2953
  peer,
@@ -3198,7 +2959,12 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3198
2959
  return;
3199
2960
  }
3200
2961
  released = true;
2962
+ row.responses -= 1;
3201
2963
  this.pendingCoordinateResponseCount -= 1;
2964
+ if (!row.attached) {
2965
+ // Per-peer quota returned when this generation detached.
2966
+ return;
2967
+ }
3202
2968
  const remaining =
3203
2969
  (this.pendingCoordinateResponseCountByPeer.get(peer) ?? 1) - 1;
3204
2970
  if (remaining === 0) {
@@ -3206,6 +2972,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3206
2972
  } else {
3207
2973
  this.pendingCoordinateResponseCountByPeer.set(peer, remaining);
3208
2974
  }
2975
+ this.maybeDropIdleSyncResponseSlotRow(row);
3209
2976
  };
3210
2977
  }
3211
2978
 
@@ -3213,418 +2980,53 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3213
2980
  peer: string,
3214
2981
  identities: SyncableKey[],
3215
2982
  ): PendingSyncAdmissionReservation | undefined {
3216
- const count = identities.length;
3217
- if (count <= 0) {
3218
- return undefined;
3219
- }
3220
- const reservation: PendingSyncAdmissionReservation = {
3221
- peer,
3222
- remaining: count,
3223
- active: true,
3224
- released: false,
3225
- expiresAt: Date.now() + PENDING_SIMPLE_SYNC_KEY_TTL_MS,
3226
- identities: new Set(identities),
3227
- retainedSettled: 0,
3228
- };
3229
- this.pendingSyncAdmissionReservations.add(reservation);
3230
- let peerReservations =
3231
- this.pendingSyncAdmissionReservationsByPeer.get(peer);
3232
- if (!peerReservations) {
3233
- peerReservations = new Set();
3234
- this.pendingSyncAdmissionReservationsByPeer.set(peer, peerReservations);
3235
- }
3236
- peerReservations.add(reservation);
3237
- this.pendingSyncActiveAdmissionReservations += 1;
3238
- this.pendingSyncAdmissionCount += count;
3239
- this.pendingSyncAdmissionCountByPeer.set(
3240
- peer,
3241
- (this.pendingSyncAdmissionCountByPeer.get(peer) ?? 0) + count,
3242
- );
3243
- let reservedIdentities =
3244
- this.pendingSyncAdmissionIdentitiesByPeer.get(peer);
3245
- if (!reservedIdentities) {
3246
- reservedIdentities = new Set();
3247
- this.pendingSyncAdmissionIdentitiesByPeer.set(peer, reservedIdentities);
3248
- }
3249
- for (const identity of identities) {
3250
- reservedIdentities.add(identity);
3251
- let reservationsForIdentity =
3252
- this.pendingSyncAdmissionReservationsByIdentity.get(identity);
3253
- if (!reservationsForIdentity) {
3254
- reservationsForIdentity = new Set();
3255
- this.pendingSyncAdmissionReservationsByIdentity.set(
3256
- identity,
3257
- reservationsForIdentity,
3258
- );
3259
- }
3260
- reservationsForIdentity.add(reservation);
3261
- }
3262
- const expiryNode: PendingSyncExpiryNode = {
3263
- kind: "admission",
3264
- reservation,
3265
- expiresAt: reservation.expiresAt,
3266
- heapIndex: -1,
3267
- };
3268
- this.pendingSyncAdmissionExpiryNodes.set(reservation, expiryNode);
3269
- this.pushPendingSyncExpiry(expiryNode);
3270
- this.schedulePendingSyncKeyExpiry();
3271
- return reservation;
3272
- }
3273
-
3274
- private removePendingSyncAdmissionIdentity(
3275
- reservation: PendingSyncAdmissionReservation,
3276
- identity: SyncableKey,
3277
- options?: { retainQuota?: boolean },
3278
- ): boolean {
3279
- if (!reservation.identities.delete(identity)) {
3280
- return false;
3281
- }
3282
- if (options?.retainQuota === true) {
3283
- reservation.retainedSettled += 1;
3284
- } else {
3285
- reservation.remaining -= 1;
3286
- this.pendingSyncAdmissionCount -= 1;
3287
- const peerCount =
3288
- (this.pendingSyncAdmissionCountByPeer.get(reservation.peer) ?? 0) - 1;
3289
- if (peerCount === 0) {
3290
- this.pendingSyncAdmissionCountByPeer.delete(reservation.peer);
3291
- } else {
3292
- this.pendingSyncAdmissionCountByPeer.set(reservation.peer, peerCount);
3293
- }
3294
- }
3295
- const reservedIdentities = this.pendingSyncAdmissionIdentitiesByPeer.get(
3296
- reservation.peer,
3297
- );
3298
- reservedIdentities?.delete(identity);
3299
- if (reservedIdentities?.size === 0) {
3300
- this.pendingSyncAdmissionIdentitiesByPeer.delete(reservation.peer);
3301
- }
3302
- const reservationsForIdentity =
3303
- this.pendingSyncAdmissionReservationsByIdentity.get(identity);
3304
- reservationsForIdentity?.delete(reservation);
3305
- if (reservationsForIdentity?.size === 0) {
3306
- this.pendingSyncAdmissionReservationsByIdentity.delete(identity);
3307
- }
3308
- return true;
2983
+ return this.pendingSync.reservePendingSyncAdmission(peer, identities);
3309
2984
  }
3310
2985
 
3311
2986
  private clearPendingSyncAdmissionIdentity(identity: SyncableKey): void {
3312
- const reservations =
3313
- this.pendingSyncAdmissionReservationsByIdentity.get(identity);
3314
- if (!reservations) {
3315
- return;
3316
- }
3317
- for (const reservation of [...reservations]) {
3318
- this.removePendingSyncAdmissionIdentity(reservation, identity, {
3319
- retainQuota: true,
3320
- });
3321
- }
2987
+ this.pendingSync.clearPendingSyncAdmissionIdentity(identity);
3322
2988
  }
3323
2989
 
3324
2990
  private consumePendingSyncAdmission(
3325
2991
  reservation: PendingSyncAdmissionReservation,
3326
2992
  identity: SyncableKey,
3327
2993
  ): "consumed" | "settled" | "invalid" {
3328
- if (!reservation.active || reservation.expiresAt <= Date.now()) {
3329
- if (reservation.expiresAt <= Date.now()) {
3330
- this.invalidatePendingSyncAdmission(reservation);
3331
- }
3332
- return "invalid";
3333
- }
3334
- if (!reservation.identities.has(identity)) {
3335
- return "settled";
3336
- }
3337
- this.removePendingSyncAdmissionIdentity(reservation, identity);
3338
- if (reservation.remaining === 0) {
3339
- this.removePendingSyncAdmissionExpiry(reservation);
3340
- reservation.active = false;
3341
- reservation.released = true;
3342
- this.pendingSyncActiveAdmissionReservations -= 1;
3343
- this.pendingSyncAdmissionReservations.delete(reservation);
3344
- const peerReservations = this.pendingSyncAdmissionReservationsByPeer.get(
3345
- reservation.peer,
3346
- );
3347
- peerReservations?.delete(reservation);
3348
- if (peerReservations?.size === 0) {
3349
- this.pendingSyncAdmissionReservationsByPeer.delete(reservation.peer);
3350
- }
3351
- this.clearPendingSyncExpiryTimerIfIdle();
3352
- }
3353
- return "consumed";
2994
+ return this.pendingSync.consumePendingSyncAdmission(reservation, identity);
3354
2995
  }
3355
2996
 
3356
2997
  private transferPendingSyncAdmissionIdentity(
3357
2998
  peer: string,
3358
2999
  identity: SyncableKey,
3359
3000
  ): number | undefined {
3360
- const reservations =
3361
- this.pendingSyncAdmissionReservationsByIdentity.get(identity);
3362
- if (!reservations) {
3363
- return undefined;
3364
- }
3365
- for (const reservation of reservations) {
3366
- if (
3367
- reservation.peer !== peer ||
3368
- reservation.released ||
3369
- reservation.expiresAt <= Date.now() ||
3370
- !this.removePendingSyncAdmissionIdentity(reservation, identity, {
3371
- retainQuota: true,
3372
- })
3373
- ) {
3374
- continue;
3375
- }
3376
- // The original resolver may be non-abortable and still retains its input
3377
- // arrays. Keep that reservation charged until its queueSync finally
3378
- // settles; the queued claim is counted separately and can disappear
3379
- // without returning the resolver's quota early.
3380
- return reservation.expiresAt;
3381
- }
3382
- return undefined;
3383
- }
3384
-
3385
- private invalidatePendingSyncAdmission(
3386
- reservation?: PendingSyncAdmissionReservation,
3387
- ): void {
3388
- if (!reservation || reservation.released || !reservation.active) {
3389
- return;
3390
- }
3391
- // Expiry/disconnect invalidates late lookup results, but it must not return
3392
- // the quota slot while the underlying storage/index work is still alive.
3393
- // Those lookups are not generally abortable; only queueSync's finally block
3394
- // may release their active-work accounting.
3395
- this.removePendingSyncAdmissionExpiry(reservation);
3396
- reservation.active = false;
3397
- this.pendingSyncActiveAdmissionReservations -= 1;
3398
- this.clearPendingSyncExpiryTimerIfIdle();
3001
+ return this.pendingSync.transferPendingSyncAdmissionIdentity(
3002
+ peer,
3003
+ identity,
3004
+ );
3399
3005
  }
3400
3006
 
3401
3007
  private releasePendingSyncAdmission(
3402
3008
  reservation?: PendingSyncAdmissionReservation,
3403
3009
  ): void {
3404
- if (!reservation || reservation.released) {
3405
- return;
3406
- }
3407
- this.removePendingSyncAdmissionExpiry(reservation);
3408
- for (const identity of [...reservation.identities]) {
3409
- this.removePendingSyncAdmissionIdentity(reservation, identity);
3410
- }
3411
- if (reservation.retainedSettled > 0) {
3412
- const retainedSettled = reservation.retainedSettled;
3413
- reservation.retainedSettled = 0;
3414
- reservation.remaining -= retainedSettled;
3415
- this.pendingSyncAdmissionCount -= retainedSettled;
3416
- const peerCount =
3417
- (this.pendingSyncAdmissionCountByPeer.get(reservation.peer) ?? 0) -
3418
- retainedSettled;
3419
- if (peerCount === 0) {
3420
- this.pendingSyncAdmissionCountByPeer.delete(reservation.peer);
3421
- } else {
3422
- this.pendingSyncAdmissionCountByPeer.set(reservation.peer, peerCount);
3423
- }
3424
- }
3425
- if (reservation.active) {
3426
- this.pendingSyncActiveAdmissionReservations -= 1;
3427
- }
3428
- reservation.active = false;
3429
- reservation.released = true;
3430
- this.pendingSyncAdmissionReservations.delete(reservation);
3431
- const peerReservations = this.pendingSyncAdmissionReservationsByPeer.get(
3432
- reservation.peer,
3433
- );
3434
- peerReservations?.delete(reservation);
3435
- if (peerReservations?.size === 0) {
3436
- this.pendingSyncAdmissionReservationsByPeer.delete(reservation.peer);
3437
- }
3438
- this.clearPendingSyncExpiryTimerIfIdle();
3010
+ this.pendingSync.releasePendingSyncAdmission(reservation);
3439
3011
  }
3440
3012
 
3441
3013
  private clearPendingSyncAdmissions(peer?: string): void {
3442
- const reservations =
3443
- peer == null
3444
- ? this.pendingSyncAdmissionReservations
3445
- : this.pendingSyncAdmissionReservationsByPeer.get(peer);
3446
- if (!reservations) {
3447
- return;
3448
- }
3449
- for (const reservation of [...reservations]) {
3450
- this.invalidatePendingSyncAdmission(reservation);
3451
- }
3452
- }
3453
-
3454
- private swapPendingSyncExpiry(left: number, right: number): void {
3455
- const leftNode = this.pendingSyncExpiryHeap[left]!;
3456
- const rightNode = this.pendingSyncExpiryHeap[right]!;
3457
- this.pendingSyncExpiryHeap[left] = rightNode;
3458
- this.pendingSyncExpiryHeap[right] = leftNode;
3459
- rightNode.heapIndex = left;
3460
- leftNode.heapIndex = right;
3461
- }
3462
-
3463
- private pushPendingSyncExpiry(node: PendingSyncExpiryNode): void {
3464
- node.heapIndex = this.pendingSyncExpiryHeap.length;
3465
- this.pendingSyncExpiryHeap.push(node);
3466
- let index = node.heapIndex;
3467
- while (index > 0) {
3468
- const parent = Math.floor((index - 1) / 2);
3469
- if (
3470
- this.pendingSyncExpiryHeap[parent]!.expiresAt <=
3471
- this.pendingSyncExpiryHeap[index]!.expiresAt
3472
- ) {
3473
- break;
3474
- }
3475
- this.swapPendingSyncExpiry(parent, index);
3476
- index = parent;
3477
- }
3478
- }
3479
-
3480
- private removePendingSyncExpiry(node: PendingSyncExpiryNode): void {
3481
- const index = node.heapIndex;
3482
- if (
3483
- index < 0 ||
3484
- index >= this.pendingSyncExpiryHeap.length ||
3485
- this.pendingSyncExpiryHeap[index] !== node
3486
- ) {
3487
- return;
3488
- }
3489
- const last = this.pendingSyncExpiryHeap.pop()!;
3490
- node.heapIndex = -1;
3491
- if (index >= this.pendingSyncExpiryHeap.length) {
3492
- return;
3493
- }
3494
- this.pendingSyncExpiryHeap[index] = last;
3495
- last.heapIndex = index;
3496
-
3497
- let current = index;
3498
- while (current > 0) {
3499
- const parent = Math.floor((current - 1) / 2);
3500
- if (
3501
- this.pendingSyncExpiryHeap[parent]!.expiresAt <=
3502
- this.pendingSyncExpiryHeap[current]!.expiresAt
3503
- ) {
3504
- break;
3505
- }
3506
- this.swapPendingSyncExpiry(parent, current);
3507
- current = parent;
3508
- }
3509
- for (;;) {
3510
- const left = current * 2 + 1;
3511
- const right = left + 1;
3512
- let smallest = current;
3513
- if (
3514
- left < this.pendingSyncExpiryHeap.length &&
3515
- this.pendingSyncExpiryHeap[left]!.expiresAt <
3516
- this.pendingSyncExpiryHeap[smallest]!.expiresAt
3517
- ) {
3518
- smallest = left;
3519
- }
3520
- if (
3521
- right < this.pendingSyncExpiryHeap.length &&
3522
- this.pendingSyncExpiryHeap[right]!.expiresAt <
3523
- this.pendingSyncExpiryHeap[smallest]!.expiresAt
3524
- ) {
3525
- smallest = right;
3526
- }
3527
- if (smallest === current) {
3528
- break;
3529
- }
3530
- this.swapPendingSyncExpiry(current, smallest);
3531
- current = smallest;
3532
- }
3533
- }
3534
-
3535
- private removePendingSyncKeyExpiry(key: SyncableKey): void {
3536
- const node = this.pendingSyncKeyExpiryNodes.get(key);
3537
- if (!node) {
3538
- return;
3539
- }
3540
- this.pendingSyncKeyExpiryNodes.delete(key);
3541
- this.removePendingSyncExpiry(node);
3014
+ this.pendingSync.clearPendingSyncAdmissions(peer);
3542
3015
  }
3543
3016
 
3544
3017
  private movePendingSyncKeyExpiryEarlier(
3545
3018
  key: SyncableKey,
3546
3019
  expiresAt: number,
3547
3020
  ): void {
3548
- const current = this.syncInFlightQueueExpiresAt.get(key);
3549
- if (current == null || current <= expiresAt) {
3550
- return;
3551
- }
3552
- this.removePendingSyncKeyExpiry(key);
3553
- this.syncInFlightQueueExpiresAt.set(key, expiresAt);
3554
- const node: PendingSyncExpiryNode = {
3555
- kind: "key",
3556
- key,
3557
- expiresAt,
3558
- heapIndex: -1,
3559
- };
3560
- this.pendingSyncKeyExpiryNodes.set(key, node);
3561
- this.pushPendingSyncExpiry(node);
3562
- this.schedulePendingSyncKeyExpiry();
3563
- }
3564
-
3565
- private removePendingSyncAdmissionExpiry(
3566
- reservation: PendingSyncAdmissionReservation,
3567
- ): void {
3568
- const node = this.pendingSyncAdmissionExpiryNodes.get(reservation);
3569
- if (!node) {
3570
- return;
3571
- }
3572
- this.pendingSyncAdmissionExpiryNodes.delete(reservation);
3573
- this.removePendingSyncExpiry(node);
3021
+ this.pendingSync.movePendingSyncKeyExpiryEarlier(key, expiresAt);
3574
3022
  }
3575
3023
 
3576
3024
  private expirePendingSyncKeys(now = Date.now()): void {
3577
- for (;;) {
3578
- const node = this.pendingSyncExpiryHeap[0];
3579
- if (!node || node.expiresAt > now) {
3580
- break;
3581
- }
3582
- this.removePendingSyncExpiry(node);
3583
- if (node.kind === "key") {
3584
- if (this.pendingSyncKeyExpiryNodes.get(node.key) !== node) {
3585
- continue;
3586
- }
3587
- this.pendingSyncKeyExpiryNodes.delete(node.key);
3588
- this.clearSyncProcessKey(node.key);
3589
- } else {
3590
- if (
3591
- this.pendingSyncAdmissionExpiryNodes.get(node.reservation) !== node
3592
- ) {
3593
- continue;
3594
- }
3595
- this.pendingSyncAdmissionExpiryNodes.delete(node.reservation);
3596
- this.invalidatePendingSyncAdmission(node.reservation);
3597
- }
3598
- }
3025
+ this.pendingSync.expirePendingSyncKeys(now);
3599
3026
  }
3600
3027
 
3601
3028
  private clearPendingSyncExpiryTimerIfIdle(): void {
3602
- if (
3603
- this.pendingSyncExpiryHeap.length === 0 &&
3604
- this.syncInFlightQueueExpiryTimer != null
3605
- ) {
3606
- clearTimeout(this.syncInFlightQueueExpiryTimer);
3607
- this.syncInFlightQueueExpiryTimer = undefined;
3608
- }
3609
- }
3610
-
3611
- private schedulePendingSyncKeyExpiry(): void {
3612
- if (
3613
- this.syncInFlightQueueExpiryTimer != null ||
3614
- this.pendingSyncExpiryHeap.length === 0
3615
- ) {
3616
- return;
3617
- }
3618
- const expiresAt = this.pendingSyncExpiryHeap[0]!.expiresAt;
3619
- this.syncInFlightQueueExpiryTimer = setTimeout(
3620
- () => {
3621
- this.syncInFlightQueueExpiryTimer = undefined;
3622
- this.expirePendingSyncKeys();
3623
- this.schedulePendingSyncKeyExpiry();
3624
- },
3625
- Math.max(0, expiresAt - Date.now()),
3626
- );
3627
- this.syncInFlightQueueExpiryTimer.unref?.();
3029
+ this.pendingSync.clearPendingSyncExpiryTimerIfIdle();
3628
3030
  }
3629
3031
 
3630
3032
  async queueSync(
@@ -3649,10 +3051,10 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3649
3051
  Math.min(
3650
3052
  MAX_PENDING_SIMPLE_SYNC_KEYS_PER_PEER -
3651
3053
  peerClaimCount -
3652
- (this.pendingSyncAdmissionCountByPeer.get(fromHash) ?? 0),
3054
+ (this.pendingSync.pendingSyncAdmissionCountByPeer.get(fromHash) ?? 0),
3653
3055
  MAX_PENDING_SIMPLE_SYNC_KEYS_GLOBAL -
3654
- this.pendingSyncClaimCount -
3655
- this.pendingSyncAdmissionCount,
3056
+ this.pendingSync.pendingSyncClaimCount -
3057
+ this.pendingSync.pendingSyncAdmissionCount,
3656
3058
  ),
3657
3059
  );
3658
3060
  const targetEpoch = this.getOrCreateSyncDispatchTargetEpoch(fromHash);
@@ -3677,7 +3079,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
3677
3079
  const identitiesToCheck: SyncableKey[] = [];
3678
3080
  const seen = new Set<SyncableKey>();
3679
3081
  const pendingAdmissionIdentities =
3680
- this.pendingSyncAdmissionIdentitiesByPeer.get(fromHash);
3082
+ this.pendingSync.pendingSyncAdmissionIdentitiesByPeer.get(fromHash);
3681
3083
  // Default senders use 1,024-key messages. Capping examined input at the
3682
3084
  // entire per-peer allowance prevents a duplicate-filled oversized vector
3683
3085
  // from turning admission itself into unbounded work.
@@ -4179,7 +3581,8 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
4179
3581
  if (!isOpenLifecycleActive()) {
4180
3582
  return;
4181
3583
  }
4182
- const expiresAt = this.syncInFlightQueueExpiresAt.get(key);
3584
+ const expiresAt =
3585
+ this.pendingSync.syncInFlightQueueExpiresAt.get(key);
4183
3586
  if (expiresAt != null && expiresAt <= Date.now()) {
4184
3587
  this.clearSyncProcessKey(key);
4185
3588
  continue;
@@ -4193,7 +3596,8 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
4193
3596
  if (!value) {
4194
3597
  continue;
4195
3598
  }
4196
- const currentExpiresAt = this.syncInFlightQueueExpiresAt.get(key);
3599
+ const currentExpiresAt =
3600
+ this.pendingSync.syncInFlightQueueExpiresAt.get(key);
4197
3601
  if (currentExpiresAt != null && currentExpiresAt <= Date.now()) {
4198
3602
  this.clearSyncProcessKey(key);
4199
3603
  continue;
@@ -4206,8 +3610,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
4206
3610
  }
4207
3611
 
4208
3612
  const cursor =
4209
- (this.syncInFlightQueueRoundRobinCursor.get(key) ?? 0) %
4210
- value.length;
3613
+ this.pendingSync.getRoundRobinCursor(key) % value.length;
4211
3614
  const candidate = value[cursor]!;
4212
3615
  const publicKeyHash = candidate.hashcode();
4213
3616
  const inflightTimestamp = this.syncInFlight
@@ -4228,7 +3631,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
4228
3631
  }
4229
3632
  request.hashes.push(key);
4230
3633
  if (value.length > 1) {
4231
- this.syncInFlightQueueRoundRobinCursor.set(
3634
+ this.pendingSync.setRoundRobinCursor(
4232
3635
  key,
4233
3636
  (cursor + 1) % value.length,
4234
3637
  );
@@ -4304,28 +3707,9 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
4304
3707
  this.syncDispatchLifecycleController.abort();
4305
3708
  this.syncDispatchTargetEpochs.clear();
4306
3709
  this.clearPendingSyncAdmissions();
4307
- this.syncInFlightQueue.clear();
4308
- this.syncInFlightQueueInverted.clear();
4309
3710
  this.syncInFlightRetryIterator = undefined;
4310
3711
  this.syncInFlightRetryRemaining = 0;
4311
- this.syncInFlightQueueExpiresAt.clear();
4312
- this.pendingSyncExpiryHeap.length = 0;
4313
- this.pendingSyncKeyExpiryNodes.clear();
4314
- this.pendingSyncAdmissionExpiryNodes.clear();
4315
- this.syncInFlightQueueClaimants.clear();
4316
- this.syncInFlightQueueClaimantIndexes.clear();
4317
- this.syncInFlightQueueRoundRobinCursor.clear();
4318
- this.syncInFlightQueuedCoordinates.clear();
4319
- this.syncInFlightQueuedHashByCoordinate.clear();
4320
- this.syncInFlightQueuedCoordinatesByHash.clear();
4321
- this.syncInFlightQueuedCoordinateRefreshIterator = undefined;
4322
- this.pendingSyncClaimCount = 0;
4323
- if (this.syncInFlightQueueExpiryTimer != null) {
4324
- clearTimeout(this.syncInFlightQueueExpiryTimer);
4325
- this.syncInFlightQueueExpiryTimer = undefined;
4326
- }
4327
- this.syncInFlight.clear();
4328
- this.syncInFlightTargetsByKey.clear();
3712
+ this.pendingSync.clearForClose();
4329
3713
  this.recentlySentExchangeHeads.clear();
4330
3714
  this.clearPendingMaybeSyncResponses();
4331
3715
  for (const sessionId of [...this.repairSessions.keys()]) {
@@ -4376,127 +3760,19 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
4376
3760
  }
4377
3761
 
4378
3762
  private hasSyncProcessState(): boolean {
4379
- return (
4380
- this.syncInFlightQueue.size > 0 ||
4381
- this.syncInFlightQueueInverted.size > 0 ||
4382
- this.pendingSyncAdmissionCount > 0 ||
4383
- this.syncInFlight.size > 0
4384
- );
3763
+ return this.pendingSync.hasSyncProcessState();
4385
3764
  }
4386
3765
 
4387
3766
  private clearSyncProcessKey(key: SyncableKey) {
4388
- const inflight = this.syncInFlightQueue.get(key);
4389
- if (inflight) {
4390
- for (const peer of inflight) {
4391
- const map = this.syncInFlightQueueInverted.get(peer.hashcode());
4392
- if (map) {
4393
- map.delete(key);
4394
- if (map.size === 0) {
4395
- this.syncInFlightQueueInverted.delete(peer.hashcode());
4396
- }
4397
- }
4398
- }
4399
-
4400
- const trackedClaimants = this.syncInFlightQueueClaimants.get(key);
4401
- this.pendingSyncClaimCount = Math.max(
4402
- 0,
4403
- this.pendingSyncClaimCount -
4404
- (trackedClaimants?.size ?? inflight.length),
4405
- );
4406
- this.syncInFlightQueue.delete(key);
4407
- }
4408
- this.syncInFlightQueueClaimants.delete(key);
4409
- this.syncInFlightQueueClaimantIndexes.delete(key);
4410
- this.syncInFlightQueueRoundRobinCursor.delete(key);
4411
- if (typeof key === "bigint") {
4412
- this.removeQueuedSyncCoordinateAlias(key);
4413
- }
4414
- this.removePendingSyncKeyExpiry(key);
4415
- this.syncInFlightQueueExpiresAt.delete(key);
4416
- this.clearPendingSyncExpiryTimerIfIdle();
4417
-
4418
- this.clearSyncInFlightKey(key);
3767
+ this.pendingSync.clearSyncProcessKey(key);
4419
3768
  }
4420
3769
 
4421
3770
  private removePendingSyncClaim(key: SyncableKey, peer: string): void {
4422
- const inflight = this.syncInFlightQueue.get(key);
4423
- if (!inflight) {
4424
- return;
4425
- }
4426
- let claimants = this.syncInFlightQueueClaimants.get(key);
4427
- let claimantIndexes = this.syncInFlightQueueClaimantIndexes.get(key);
4428
- if (!claimants || !claimantIndexes) {
4429
- claimants = new Set();
4430
- claimantIndexes = new Map();
4431
- for (let index = 0; index < inflight.length; index += 1) {
4432
- const claimant = inflight[index]!.hashcode();
4433
- claimants.add(claimant);
4434
- claimantIndexes.set(claimant, index);
4435
- }
4436
- if (!this.syncInFlightQueueClaimants.has(key)) {
4437
- this.pendingSyncClaimCount += claimants.size;
4438
- }
4439
- this.syncInFlightQueueClaimants.set(key, claimants);
4440
- this.syncInFlightQueueClaimantIndexes.set(key, claimantIndexes);
4441
- }
4442
- const index = claimantIndexes.get(peer);
4443
- if (index == null) {
4444
- return;
4445
- }
4446
-
4447
- const lastIndex = inflight.length - 1;
4448
- if (index !== lastIndex) {
4449
- const lastClaimant = inflight[lastIndex]!;
4450
- const lastClaimantHash = lastClaimant.hashcode();
4451
- inflight[index] = lastClaimant;
4452
- claimantIndexes.set(lastClaimantHash, index);
4453
- }
4454
- inflight.pop();
4455
- claimantIndexes.delete(peer);
4456
- claimants.delete(peer);
4457
- this.pendingSyncClaimCount = Math.max(0, this.pendingSyncClaimCount - 1);
4458
- const inverted = this.syncInFlightQueueInverted.get(peer);
4459
- inverted?.delete(key);
4460
- if (inverted?.size === 0) {
4461
- this.syncInFlightQueueInverted.delete(peer);
4462
- }
4463
- if (inflight.length > 0) {
4464
- const cursor = this.syncInFlightQueueRoundRobinCursor.get(key) ?? 0;
4465
- this.syncInFlightQueueRoundRobinCursor.set(
4466
- key,
4467
- cursor === lastIndex
4468
- ? index % inflight.length
4469
- : cursor % inflight.length,
4470
- );
4471
- return;
4472
- }
4473
-
4474
- this.syncInFlightQueue.delete(key);
4475
- this.syncInFlightQueueClaimants.delete(key);
4476
- this.syncInFlightQueueClaimantIndexes.delete(key);
4477
- this.syncInFlightQueueRoundRobinCursor.delete(key);
4478
- if (typeof key === "bigint") {
4479
- this.removeQueuedSyncCoordinateAlias(key);
4480
- }
4481
- this.removePendingSyncKeyExpiry(key);
4482
- this.syncInFlightQueueExpiresAt.delete(key);
4483
- this.clearSyncInFlightKey(key);
4484
- this.clearPendingSyncExpiryTimerIfIdle();
3771
+ this.pendingSync.removePendingSyncClaim(key, peer);
4485
3772
  }
4486
3773
 
4487
3774
  private removeSyncInFlightTargetKey(peer: string, key: SyncableKey): void {
4488
- const map = this.syncInFlight.get(peer);
4489
- if (!map?.delete(key)) {
4490
- return;
4491
- }
4492
- if (map.size === 0) {
4493
- this.syncInFlight.delete(peer);
4494
- }
4495
- const targets = this.syncInFlightTargetsByKey.get(key);
4496
- targets?.delete(peer);
4497
- if (targets?.size === 0) {
4498
- this.syncInFlightTargetsByKey.delete(key);
4499
- }
3775
+ this.pendingSync.removeSyncInFlightTargetKey(peer, key);
4500
3776
  }
4501
3777
 
4502
3778
  private setSyncInFlightTargetKey(
@@ -4504,111 +3780,26 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
4504
3780
  key: SyncableKey,
4505
3781
  timestamp: number,
4506
3782
  ): void {
4507
- let map = this.syncInFlight.get(peer);
4508
- if (!map) {
4509
- map = new Map();
4510
- this.syncInFlight.set(peer, map);
4511
- }
4512
- const existing = map.get(key);
4513
- if (!existing || existing.timestamp < timestamp) {
4514
- map.set(key, { timestamp });
4515
- }
4516
- let targets = this.syncInFlightTargetsByKey.get(key);
4517
- if (!targets) {
4518
- targets = new Set();
4519
- this.syncInFlightTargetsByKey.set(key, targets);
4520
- }
4521
- targets.add(peer);
3783
+ this.pendingSync.setSyncInFlightTargetKey(peer, key, timestamp);
4522
3784
  }
4523
3785
 
4524
3786
  private clearSyncInFlightTarget(peer: string): void {
4525
- const map = this.syncInFlight.get(peer);
4526
- if (!map) {
4527
- return;
4528
- }
4529
- for (const key of map.keys()) {
4530
- const targets = this.syncInFlightTargetsByKey.get(key);
4531
- targets?.delete(peer);
4532
- if (targets?.size === 0) {
4533
- this.syncInFlightTargetsByKey.delete(key);
4534
- }
4535
- }
4536
- this.syncInFlight.delete(peer);
4537
- }
4538
-
4539
- private clearSyncInFlightKey(key: SyncableKey) {
4540
- const targets = this.syncInFlightTargetsByKey.get(key);
4541
- if (!targets) {
4542
- // Defensive compatibility for tests or integrations that seed the public
4543
- // syncInFlight map directly. Internal writes always populate the index.
4544
- for (const [peer, map] of this.syncInFlight) {
4545
- if (map.has(key)) {
4546
- this.removeSyncInFlightTargetKey(peer, key);
4547
- }
4548
- }
4549
- return;
4550
- }
4551
- for (const peer of [...targets]) {
4552
- this.removeSyncInFlightTargetKey(peer, key);
4553
- }
4554
- }
4555
-
4556
- private forEachKnownAlias(
4557
- hash: string,
4558
- callback: (key: SyncableKey) => void,
4559
- ): void {
4560
- callback(hash);
4561
- for (const coordinate of [
4562
- ...(this.syncInFlightQueuedCoordinatesByHash.get(hash) ?? []),
4563
- ]) {
4564
- callback(coordinate);
4565
- }
4566
- }
4567
-
4568
- private clearSyncInFlightForPeer(publicKeyHash: string, hash: string) {
4569
- const map = this.syncInFlight.get(publicKeyHash);
4570
- if (!map) {
4571
- return;
4572
- }
4573
- this.refreshQueuedSyncCoordinateAliases();
4574
- this.forEachKnownAlias(hash, (key) =>
4575
- this.removeSyncInFlightTargetKey(publicKeyHash, key),
4576
- );
3787
+ this.pendingSync.clearSyncInFlightTarget(peer);
4577
3788
  }
4578
3789
 
4579
3790
  private clearSyncInFlightForPeerHashes(
4580
3791
  publicKeyHash: string,
4581
3792
  hashes: string[],
4582
3793
  ) {
4583
- const map = this.syncInFlight.get(publicKeyHash);
4584
- if (!map || hashes.length === 0) {
4585
- return;
4586
- }
4587
- this.refreshQueuedSyncCoordinateAliases();
4588
- for (const hash of hashes) {
4589
- this.forEachKnownAlias(hash, (key) =>
4590
- this.removeSyncInFlightTargetKey(publicKeyHash, key),
4591
- );
4592
- }
3794
+ this.pendingSync.clearSyncInFlightForPeerHashes(publicKeyHash, hashes);
4593
3795
  }
4594
3796
 
4595
3797
  private clearSyncProcess(hash: string) {
4596
- this.refreshQueuedSyncCoordinateAliases();
4597
- this.forEachKnownAlias(hash, (key) => this.clearSyncProcessKey(key));
3798
+ this.pendingSync.clearSyncProcess(hash);
4598
3799
  }
4599
3800
 
4600
3801
  private clearSyncProcesses(hashes: string[]) {
4601
- if (hashes.length === 0) {
4602
- return;
4603
- }
4604
- this.refreshQueuedSyncCoordinateAliases();
4605
- const keys = new Set<SyncableKey>();
4606
- for (const hash of hashes) {
4607
- this.forEachKnownAlias(hash, (key) => keys.add(key));
4608
- }
4609
- for (const key of keys) {
4610
- this.clearSyncProcessKey(key);
4611
- }
3802
+ this.pendingSync.clearSyncProcesses(hashes);
4612
3803
  }
4613
3804
 
4614
3805
  onPeerDisconnected(key: PublicSignKey | string): Promise<void> | void {
@@ -4619,7 +3810,7 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
4619
3810
  this.syncDispatchTargetEpochs.delete(publicKeyHash);
4620
3811
  this.clearPendingSyncAdmissions(publicKeyHash);
4621
3812
  for (const targetLifecycle of [
4622
- ...(this.syncDispatchTargets.get(publicKeyHash) ?? []),
3813
+ ...(this.syncDispatchRegistry.activeTargets.get(publicKeyHash) ?? []),
4623
3814
  ]) {
4624
3815
  if (targetLifecycle.lifecycle.abortAllOnTargetDisconnect) {
4625
3816
  this.abortSyncDispatchLifecycle(
@@ -4636,13 +3827,12 @@ export class SimpleSyncronizer<R extends "u32" | "u64">
4636
3827
  this.clearSyncInFlightTarget(publicKeyHash);
4637
3828
  this.recentlySentExchangeHeads.delete(publicKeyHash);
4638
3829
  this.clearPendingMaybeSyncResponses(publicKeyHash);
4639
- const map = this.syncInFlightQueueInverted.get(publicKeyHash);
4640
- if (map) {
4641
- for (const hash of [...map]) {
4642
- this.removePendingSyncClaim(hash, publicKeyHash);
4643
- }
4644
- this.syncInFlightQueueInverted.delete(publicKeyHash);
4645
- }
3830
+ this.pendingSync.removeClaimsForPeer(publicKeyHash);
3831
+ // Storage lookups and response ships for this peer may never settle
3832
+ // (the resolvers are not universally abortable). Detach the slot row so
3833
+ // the peer's quota returns immediately; the captured closures settle
3834
+ // against the detached row without touching a successor's quota.
3835
+ this.detachSyncResponseSlotRow(publicKeyHash);
4646
3836
  }
4647
3837
 
4648
3838
  get pending() {