@peerbit/shared-log 16.0.24 → 16.0.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.
@@ -25,6 +25,11 @@ const REQUIRED_SENDER_CAPABILITIES =
25
25
  const DEFAULT_REQUEST_RETRY_MS = 1_000;
26
26
  const DEFAULT_MAX_REQUEST_RETRY_MS = 30_000;
27
27
  const DEFAULT_REQUEST_MAX_ATTEMPTS = 7;
28
+ const DEFAULT_REMOTE_FULL_REARM_ATTEMPT_TIMEOUT_MS = 2_000;
29
+ const DEFAULT_REMOTE_FULL_REARM_COOLDOWN_MS = 5_000;
30
+ const MAX_REMOTE_FULL_REARM_OUTSTANDING_PER_SESSION = 2;
31
+ const MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL = 64;
32
+ const MAX_TIMER_MS = 2_147_483_647;
28
33
  const MAX_U64 = (1n << 64n) - 1n;
29
34
  const MAX_BACKOFF_EXPONENT = 20;
30
35
 
@@ -112,6 +117,16 @@ export type ReplicationInfoV2LocalCapabilityAdvertisement = {
112
117
  firstAttempt?: Promise<void>;
113
118
  };
114
119
 
120
+ type ReplicationInfoV2RemoteFullRearm = {
121
+ peerHash: string;
122
+ receiveEpoch: object | null;
123
+ receiverTransportSession: bigint;
124
+ nextAttemptAt: number;
125
+ outstandingAttempts: number;
126
+ inFlight?: Promise<void>;
127
+ controller?: AbortController;
128
+ };
129
+
115
130
  export type ReplicationInfoV2LocalCapabilityRefresh = {
116
131
  receiverTransportSession: bigint;
117
132
  requestNotBeforeMs: number;
@@ -183,6 +198,7 @@ export type ReplicationInfoV2ReceiveDeps = {
183
198
  peerSession: object;
184
199
  receiveEpoch: object | null;
185
200
  signal: AbortSignal;
201
+ requestRemoteFullRearm?: boolean;
186
202
  }) => Promise<ReplicationInfoV2LocalCapabilityRefresh | undefined>;
187
203
  onRequestError?: (error: unknown) => void;
188
204
  onLocalCapabilityError?: (error: unknown) => void;
@@ -190,6 +206,9 @@ export type ReplicationInfoV2ReceiveDeps = {
190
206
  requestRetryMs?: number;
191
207
  maxRequestRetryMs?: number;
192
208
  requestMaxAttempts?: number;
209
+ remoteFullRearmAttemptTimeoutMs?: number;
210
+ remoteFullRearmCooldownMs?: number;
211
+ maxRemoteFullRearmOutstandingGlobal?: number;
193
212
  };
194
213
 
195
214
  /**
@@ -217,12 +236,18 @@ export class ReplicationInfoV2ReceiveCoordinator {
217
236
  string,
218
237
  ReplicationInfoV2LocalCapabilityAdvertisement
219
238
  >;
239
+ _remoteFullRearmBySession!: WeakMap<object, ReplicationInfoV2RemoteFullRearm>;
240
+ _remoteFullRearmsInFlight!: Set<ReplicationInfoV2RemoteFullRearm>;
241
+ _remoteFullRearmOutstanding!: Set<object>;
220
242
  _reservedAdmissionsByPeer!: Map<string, ReplicationInfoV2ReceiveAdmission>;
221
243
 
222
244
  private readonly now: () => number;
223
245
  private readonly requestRetryMs: number;
224
246
  private readonly maxRequestRetryMs: number;
225
247
  private readonly requestMaxAttempts: number;
248
+ private readonly remoteFullRearmAttemptTimeoutMs: number;
249
+ private readonly remoteFullRearmCooldownMs: number;
250
+ private readonly maxRemoteFullRearmOutstandingGlobal: number;
226
251
 
227
252
  constructor(private readonly deps: ReplicationInfoV2ReceiveDeps) {
228
253
  this.now = deps.now ?? Date.now;
@@ -238,11 +263,46 @@ export class ReplicationInfoV2ReceiveCoordinator {
238
263
  1,
239
264
  Math.floor(deps.requestMaxAttempts ?? DEFAULT_REQUEST_MAX_ATTEMPTS),
240
265
  );
266
+ this.remoteFullRearmAttemptTimeoutMs = Math.max(
267
+ 1,
268
+ Math.min(
269
+ MAX_TIMER_MS,
270
+ Math.floor(
271
+ deps.remoteFullRearmAttemptTimeoutMs ??
272
+ DEFAULT_REMOTE_FULL_REARM_ATTEMPT_TIMEOUT_MS,
273
+ ),
274
+ ),
275
+ );
276
+ this.remoteFullRearmCooldownMs = Math.max(
277
+ this.requestRetryMs,
278
+ Math.min(
279
+ MAX_TIMER_MS,
280
+ Math.floor(
281
+ deps.remoteFullRearmCooldownMs ??
282
+ DEFAULT_REMOTE_FULL_REARM_COOLDOWN_MS,
283
+ ),
284
+ ),
285
+ );
286
+ // The injected value is a test seam that may only tighten this lifetime
287
+ // resource bound, never widen it.
288
+ this.maxRemoteFullRearmOutstandingGlobal = Math.min(
289
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL,
290
+ Math.max(
291
+ 1,
292
+ Math.floor(
293
+ deps.maxRemoteFullRearmOutstandingGlobal ??
294
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL,
295
+ ),
296
+ ),
297
+ );
241
298
  this._receiveStates = new Map();
242
299
  this._cutoverPeerSessions = new WeakSet();
243
300
  this._localCapabilityReadyBySession = new WeakMap();
244
301
  this._localCapabilityContextBySession = new WeakMap();
245
302
  this._localCapabilityAdvertisementsByPeer = new Map();
303
+ this._remoteFullRearmBySession = new WeakMap();
304
+ this._remoteFullRearmsInFlight = new Set();
305
+ this._remoteFullRearmOutstanding = new Set();
246
306
  this._reservedAdmissionsByPeer = new Map();
247
307
  }
248
308
 
@@ -253,10 +313,20 @@ export class ReplicationInfoV2ReceiveCoordinator {
253
313
  this._localCapabilityReadyBySession = new WeakMap();
254
314
  this._localCapabilityContextBySession = new WeakMap();
255
315
  this._localCapabilityAdvertisementsByPeer = new Map();
316
+ this._remoteFullRearmBySession = new WeakMap();
317
+ this._remoteFullRearmsInFlight = new Set();
256
318
  this._reservedAdmissionsByPeer = new Map();
257
319
  }
258
320
 
259
321
  clearForClose(): void {
322
+ for (const rearm of this._remoteFullRearmsInFlight ?? []) {
323
+ rearm.controller?.abort(
324
+ new Error(
325
+ "Replication-info V2 receiver closed during remote Full rearm",
326
+ ),
327
+ );
328
+ }
329
+ this._remoteFullRearmsInFlight?.clear();
260
330
  for (const advertisement of [
261
331
  ...(this._localCapabilityAdvertisementsByPeer?.values() ?? []),
262
332
  ]) {
@@ -270,6 +340,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
270
340
  this._cutoverPeerSessions = new WeakSet();
271
341
  this._localCapabilityReadyBySession = new WeakMap();
272
342
  this._localCapabilityContextBySession = new WeakMap();
343
+ this._remoteFullRearmBySession = new WeakMap();
273
344
  }
274
345
 
275
346
  clearPeer(peerHash: string, expectedSession?: object): void {
@@ -288,6 +359,25 @@ export class ReplicationInfoV2ReceiveCoordinator {
288
359
  this._localCapabilityContextBySession.delete(state.peerSession);
289
360
  this._cutoverPeerSessions.delete(state.peerSession);
290
361
  }
362
+ if (!expectedSession) {
363
+ for (const rearm of this._remoteFullRearmsInFlight) {
364
+ if (rearm.peerHash === peerHash) {
365
+ rearm.controller?.abort(
366
+ new Error("Replication-info V2 peer cleared during rearm"),
367
+ );
368
+ }
369
+ }
370
+ }
371
+ const rearmSession =
372
+ expectedSession ?? state?.peerSession ?? advertisement?.peerSession;
373
+ if (rearmSession) {
374
+ this._remoteFullRearmBySession
375
+ .get(rearmSession)
376
+ ?.controller?.abort(
377
+ new Error("Replication-info V2 peer session cleared during rearm"),
378
+ );
379
+ this._remoteFullRearmBySession.delete(rearmSession);
380
+ }
291
381
  if (expectedSession) {
292
382
  this._localCapabilityReadyBySession.delete(expectedSession);
293
383
  this._localCapabilityContextBySession.delete(expectedSession);
@@ -625,6 +715,207 @@ export class ReplicationInfoV2ReceiveCoordinator {
625
715
  };
626
716
  }
627
717
 
718
+ /**
719
+ * Send one transient, authenticated rearm hint for the exact current local
720
+ * receive grant. The hint asks a patched remote receiver to rotate its
721
+ * challenge and issue another Full request, rebuilding an outbound sender
722
+ * stream that was lost while both directions otherwise remained current.
723
+ *
724
+ * This deliberately does not mutate the steady advertisement worker. One
725
+ * attempt is coalesced per exact PeerSession/receive generation and bound to
726
+ * its own deadline as well as the caller's signal. A cooldown avoids rotating
727
+ * a healthy in-flight challenge on every recovery tick. The remote rotates
728
+ * only when its receive state is active; later hints during resync only nudge
729
+ * that same bounded request generation. At most two transports that disregard
730
+ * abort remain outstanding for one exact session (64 across this coordinator's
731
+ * lifetime); reaching either cap stops new hints while the persisted-readiness
732
+ * gate remains fail closed.
733
+ */
734
+ reAdvertiseLocalCapabilityForRemoteFull(properties: {
735
+ peerHash: string;
736
+ peerSession: object;
737
+ receiveEpoch: object | null;
738
+ signal: AbortSignal;
739
+ }): boolean {
740
+ const context = this._localCapabilityContextBySession.get(
741
+ properties.peerSession,
742
+ );
743
+ const ready = this._localCapabilityReadyBySession.get(
744
+ properties.peerSession,
745
+ );
746
+ if (
747
+ properties.signal.aborted ||
748
+ !context ||
749
+ context.peerHash !== properties.peerHash ||
750
+ context.lifecycleSignal.aborted ||
751
+ this.deps.isClosed() ||
752
+ !this.deps.isPeerStateCurrent(
753
+ properties.peerHash,
754
+ properties.peerSession,
755
+ properties.receiveEpoch,
756
+ ) ||
757
+ !ready ||
758
+ ready.peerHash !== properties.peerHash ||
759
+ ready.receiveEpoch !== properties.receiveEpoch ||
760
+ ready.receiverTransportSession !== this.deps.getReceiverTransportSession()
761
+ ) {
762
+ return false;
763
+ }
764
+
765
+ const now = this.now();
766
+ const receiverTransportSession = ready.receiverTransportSession;
767
+ let rearm = this._remoteFullRearmBySession.get(properties.peerSession);
768
+ if (
769
+ rearm &&
770
+ (rearm.peerHash !== properties.peerHash ||
771
+ rearm.receiveEpoch !== properties.receiveEpoch ||
772
+ rearm.receiverTransportSession !== receiverTransportSession)
773
+ ) {
774
+ rearm.controller?.abort(
775
+ new Error("Replication-info V2 remote Full rearm generation changed"),
776
+ );
777
+ rearm = undefined;
778
+ }
779
+ if (rearm?.inFlight) {
780
+ return true;
781
+ }
782
+ if (rearm !== undefined && now < rearm.nextAttemptAt) {
783
+ return true;
784
+ }
785
+ if (
786
+ (rearm?.outstandingAttempts ?? 0) >=
787
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_PER_SESSION ||
788
+ this._remoteFullRearmOutstanding.size >=
789
+ this.maxRemoteFullRearmOutstandingGlobal
790
+ ) {
791
+ return true;
792
+ }
793
+ if (!rearm) {
794
+ rearm = {
795
+ peerHash: properties.peerHash,
796
+ receiveEpoch: properties.receiveEpoch,
797
+ receiverTransportSession,
798
+ nextAttemptAt: now + this.remoteFullRearmCooldownMs,
799
+ outstandingAttempts: 0,
800
+ };
801
+ this._remoteFullRearmBySession.set(properties.peerSession, rearm);
802
+ } else {
803
+ rearm.nextAttemptAt = now + this.remoteFullRearmCooldownMs;
804
+ }
805
+ const attemptState = rearm;
806
+ const outstandingReservation = {};
807
+ attemptState.outstandingAttempts++;
808
+ this._remoteFullRearmOutstanding.add(outstandingReservation);
809
+
810
+ const attemptController = new AbortController();
811
+ const operationSignal = attemptController.signal;
812
+ attemptState.controller = attemptController;
813
+ this._remoteFullRearmsInFlight.add(attemptState);
814
+ const sourceSignals = Array.from(
815
+ new Set([context.lifecycleSignal, properties.signal]),
816
+ );
817
+ const onSourceAbort = (event: Event) => {
818
+ const source = event.currentTarget as AbortSignal;
819
+ attemptController.abort(source.reason);
820
+ };
821
+ for (const source of sourceSignals) {
822
+ source.addEventListener("abort", onSourceAbort, { once: true });
823
+ }
824
+ const attemptTimer = setTimeout(
825
+ () =>
826
+ attemptController.abort(
827
+ new Error("Replication-info V2 remote Full rearm attempt timed out"),
828
+ ),
829
+ this.remoteFullRearmAttemptTimeoutMs,
830
+ );
831
+ attemptTimer.unref?.();
832
+ let releasedOutstanding = false;
833
+ const releaseOutstanding = () => {
834
+ if (releasedOutstanding) return;
835
+ releasedOutstanding = true;
836
+ this._remoteFullRearmOutstanding.delete(outstandingReservation);
837
+ attemptState.outstandingAttempts--;
838
+ };
839
+ const refresh = Promise.resolve().then(() => {
840
+ if (operationSignal.aborted) {
841
+ throw operationSignal.reason;
842
+ }
843
+ return this.deps.refreshLocalCapability({
844
+ peerHash: properties.peerHash,
845
+ target: context.target,
846
+ peerSession: properties.peerSession,
847
+ receiveEpoch: properties.receiveEpoch,
848
+ signal: operationSignal,
849
+ requestRemoteFullRearm: true,
850
+ });
851
+ });
852
+ void refresh.then(releaseOutstanding, releaseOutstanding);
853
+
854
+ let operation: Promise<void>;
855
+ const detach = () => {
856
+ clearTimeout(attemptTimer);
857
+ operationSignal.removeEventListener("abort", detach);
858
+ for (const source of sourceSignals) {
859
+ source.removeEventListener("abort", onSourceAbort);
860
+ }
861
+ if (attemptState.inFlight === operation) {
862
+ this._remoteFullRearmsInFlight.delete(attemptState);
863
+ attemptState.inFlight = undefined;
864
+ attemptState.controller = undefined;
865
+ }
866
+ };
867
+ const raceWithAttemptSignal = <T>(promise: Promise<T>): Promise<T> =>
868
+ new Promise<T>((resolve, reject) => {
869
+ const onAbort = () => {
870
+ operationSignal.removeEventListener("abort", onAbort);
871
+ reject(operationSignal.reason);
872
+ };
873
+ operationSignal.addEventListener("abort", onAbort, { once: true });
874
+ if (operationSignal.aborted) {
875
+ onAbort();
876
+ return;
877
+ }
878
+ promise.then(
879
+ (value) => {
880
+ operationSignal.removeEventListener("abort", onAbort);
881
+ resolve(value);
882
+ },
883
+ (error) => {
884
+ operationSignal.removeEventListener("abort", onAbort);
885
+ reject(error);
886
+ },
887
+ );
888
+ });
889
+ operation = raceWithAttemptSignal(refresh)
890
+ .then(() => undefined)
891
+ .catch((error) => {
892
+ if (
893
+ !operationSignal.aborted &&
894
+ !this.deps.isClosed() &&
895
+ this.deps.isPeerStateCurrent(
896
+ properties.peerHash,
897
+ properties.peerSession,
898
+ properties.receiveEpoch,
899
+ )
900
+ ) {
901
+ this.deps.onLocalCapabilityError?.(error);
902
+ }
903
+ })
904
+ .finally(() => {
905
+ detach();
906
+ });
907
+ attemptState.inFlight = operation;
908
+ operationSignal.addEventListener("abort", detach, { once: true });
909
+ for (const source of sourceSignals) {
910
+ if (source.aborted) {
911
+ attemptController.abort(source.reason);
912
+ break;
913
+ }
914
+ }
915
+ void operation;
916
+ return true;
917
+ }
918
+
628
919
  private promoteLocalCapabilityAdvertisement(
629
920
  state: ReplicationInfoV2LocalCapabilityAdvertisement,
630
921
  ): boolean {
@@ -561,6 +561,22 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
561
561
  );
562
562
  }
563
563
 
564
+ /**
565
+ * Whether the exact current destination generation already has an outbound
566
+ * V2 stream. A missing/stale stream cannot be repaired by confirmation
567
+ * retries alone: the remote receiver must issue another Full request first.
568
+ */
569
+ hasCurrentStateForPeer(target: ApplicationConfirmationTarget): boolean {
570
+ const state = this._sendStates.get(target.peerHash);
571
+ return (
572
+ state !== undefined &&
573
+ state.peerSession === target.peerSession &&
574
+ state.receiverTransportSession === target.receiverTransportSession &&
575
+ this.isCurrent(state) &&
576
+ this.supportsApplicationConfirmation(state)
577
+ );
578
+ }
579
+
564
580
  private trackRetiringWorker(state: ReplicationInfoV2SendState): void {
565
581
  const worker = state.worker;
566
582
  if (!worker) {