@peerbit/shared-log 16.0.26 → 16.0.27

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.
@@ -122,11 +122,46 @@ type ReplicationInfoV2RemoteFullRearm = {
122
122
  receiveEpoch: object | null;
123
123
  receiverTransportSession: bigint;
124
124
  nextAttemptAt: number;
125
+ attemptsStarted: number;
125
126
  outstandingAttempts: number;
126
127
  inFlight?: Promise<void>;
127
128
  controller?: AbortController;
128
129
  };
129
130
 
131
+ export type ReplicationInfoV2ReceiveDiagnostic = Readonly<{
132
+ state: "absent" | "current" | "stale";
133
+ /** Current-state classification; no historical packet/drop log is retained. */
134
+ reason:
135
+ | "state-absent"
136
+ | "peer-session-mismatch"
137
+ | "receive-epoch-mismatch"
138
+ | "transport-session-mismatch"
139
+ | "state-not-current"
140
+ | "admission-pending"
141
+ | "active"
142
+ | "request-parked"
143
+ | "request-in-flight"
144
+ | "request-scheduled"
145
+ | "awaiting-full"
146
+ | "resync";
147
+ phase?: ReplicationInfoV2ReceivePhase;
148
+ requestState?:
149
+ | "idle"
150
+ | "scheduled"
151
+ | "in-flight"
152
+ | "parked"
153
+ | "admission-pending";
154
+ lastAppliedSequence?: string;
155
+ requestAttempts?: number;
156
+ capabilityAdvertisementAttempts: number;
157
+ capabilityAdvertisement: "absent" | "advertising" | "ready" | "stale";
158
+ remoteFullRearm: "idle" | "in-flight" | "cooldown" | "limited";
159
+ remoteFullRearmAttempts: number;
160
+ remoteFullRearmOutstanding: number;
161
+ remoteFullRearmGlobalOutstanding: number;
162
+ remoteFullRearmCooldownRemainingMs?: number;
163
+ }>;
164
+
130
165
  export type ReplicationInfoV2LocalCapabilityRefresh = {
131
166
  receiverTransportSession: bigint;
132
167
  requestNotBeforeMs: number;
@@ -796,6 +831,7 @@ export class ReplicationInfoV2ReceiveCoordinator {
796
831
  receiveEpoch: properties.receiveEpoch,
797
832
  receiverTransportSession,
798
833
  nextAttemptAt: now + this.remoteFullRearmCooldownMs,
834
+ attemptsStarted: 0,
799
835
  outstandingAttempts: 0,
800
836
  };
801
837
  this._remoteFullRearmBySession.set(properties.peerSession, rearm);
@@ -804,6 +840,10 @@ export class ReplicationInfoV2ReceiveCoordinator {
804
840
  }
805
841
  const attemptState = rearm;
806
842
  const outstandingReservation = {};
843
+ attemptState.attemptsStarted = Math.min(
844
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL,
845
+ attemptState.attemptsStarted + 1,
846
+ );
807
847
  attemptState.outstandingAttempts++;
808
848
  this._remoteFullRearmOutstanding.add(outstandingReservation);
809
849
 
@@ -916,6 +956,175 @@ export class ReplicationInfoV2ReceiveCoordinator {
916
956
  return true;
917
957
  }
918
958
 
959
+ /**
960
+ * Bounded, read-only state for persisted-readiness troubleshooting. This
961
+ * intentionally omits transport sessions, challenges, payloads, and maps.
962
+ */
963
+ diagnosePeer(properties: {
964
+ peerHash: string;
965
+ peerSession?: object;
966
+ receiveEpoch?: object | null;
967
+ senderTransportSession?: bigint;
968
+ }): ReplicationInfoV2ReceiveDiagnostic {
969
+ const state = this._receiveStates.get(properties.peerHash);
970
+ const advertisement = this._localCapabilityAdvertisementsByPeer.get(
971
+ properties.peerHash,
972
+ );
973
+ const session =
974
+ properties.peerSession ??
975
+ state?.peerSession ??
976
+ advertisement?.peerSession;
977
+ const ready = session
978
+ ? this._localCapabilityReadyBySession.get(session)
979
+ : undefined;
980
+ let capabilityAdvertisement: ReplicationInfoV2ReceiveDiagnostic["capabilityAdvertisement"] =
981
+ "absent";
982
+ if (ready && ready.peerHash === properties.peerHash) {
983
+ capabilityAdvertisement =
984
+ session !== undefined &&
985
+ this.deps.isPeerSessionCurrent(properties.peerHash, session) &&
986
+ this.deps.isReceiveEpochCurrent(
987
+ properties.peerHash,
988
+ ready.receiveEpoch,
989
+ ) &&
990
+ ready.receiverTransportSession ===
991
+ this.deps.getReceiverTransportSession() &&
992
+ (properties.receiveEpoch === undefined ||
993
+ ready.receiveEpoch === properties.receiveEpoch)
994
+ ? "ready"
995
+ : "stale";
996
+ } else if (advertisement) {
997
+ capabilityAdvertisement =
998
+ advertisement.peerHash === properties.peerHash &&
999
+ advertisement.peerSession === session &&
1000
+ (properties.receiveEpoch === undefined ||
1001
+ advertisement.receiveEpoch === properties.receiveEpoch) &&
1002
+ this.isLocalCapabilityAdvertisementGenerationCurrent(advertisement)
1003
+ ? "advertising"
1004
+ : "stale";
1005
+ }
1006
+
1007
+ const rearm = session
1008
+ ? this._remoteFullRearmBySession.get(session)
1009
+ : undefined;
1010
+ const now = this.now();
1011
+ const rearmLimited =
1012
+ (rearm?.outstandingAttempts ?? 0) >=
1013
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_PER_SESSION ||
1014
+ this._remoteFullRearmOutstanding.size >=
1015
+ this.maxRemoteFullRearmOutstandingGlobal;
1016
+ const rearmStatus: ReplicationInfoV2ReceiveDiagnostic["remoteFullRearm"] =
1017
+ rearm?.inFlight
1018
+ ? "in-flight"
1019
+ : rearmLimited
1020
+ ? "limited"
1021
+ : rearm && now < rearm.nextAttemptAt
1022
+ ? "cooldown"
1023
+ : "idle";
1024
+ const common = {
1025
+ capabilityAdvertisementAttempts: Math.min(
1026
+ MAX_BACKOFF_EXPONENT + 1,
1027
+ Math.max(0, advertisement?.attempts ?? 0),
1028
+ ),
1029
+ capabilityAdvertisement,
1030
+ remoteFullRearm: rearmStatus,
1031
+ remoteFullRearmAttempts: Math.min(
1032
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_GLOBAL,
1033
+ Math.max(0, rearm?.attemptsStarted ?? 0),
1034
+ ),
1035
+ remoteFullRearmOutstanding: Math.min(
1036
+ MAX_REMOTE_FULL_REARM_OUTSTANDING_PER_SESSION,
1037
+ Math.max(0, rearm?.outstandingAttempts ?? 0),
1038
+ ),
1039
+ remoteFullRearmGlobalOutstanding: Math.min(
1040
+ this.maxRemoteFullRearmOutstandingGlobal,
1041
+ this._remoteFullRearmOutstanding.size,
1042
+ ),
1043
+ ...(rearm && now < rearm.nextAttemptAt
1044
+ ? {
1045
+ remoteFullRearmCooldownRemainingMs: Math.min(
1046
+ MAX_TIMER_MS,
1047
+ Math.max(0, rearm.nextAttemptAt - now),
1048
+ ),
1049
+ }
1050
+ : {}),
1051
+ };
1052
+ if (!state) {
1053
+ return Object.freeze({
1054
+ state: "absent" as const,
1055
+ reason: "state-absent" as const,
1056
+ ...common,
1057
+ });
1058
+ }
1059
+
1060
+ let status: "current" | "stale" = "current";
1061
+ let reason: ReplicationInfoV2ReceiveDiagnostic["reason"];
1062
+ if (
1063
+ properties.peerSession !== undefined &&
1064
+ state.peerSession !== properties.peerSession
1065
+ ) {
1066
+ status = "stale";
1067
+ reason = "peer-session-mismatch";
1068
+ } else if (
1069
+ properties.receiveEpoch !== undefined &&
1070
+ state.receiveEpoch !== properties.receiveEpoch
1071
+ ) {
1072
+ status = "stale";
1073
+ reason = "receive-epoch-mismatch";
1074
+ } else if (
1075
+ properties.senderTransportSession !== undefined &&
1076
+ state.senderTransportSession !== properties.senderTransportSession
1077
+ ) {
1078
+ status = "stale";
1079
+ reason = "transport-session-mismatch";
1080
+ } else if (!this.isStateCurrent(state)) {
1081
+ status = "stale";
1082
+ reason = "state-not-current";
1083
+ } else if (
1084
+ state.reservedAdmission !== undefined ||
1085
+ this._reservedAdmissionsByPeer.has(properties.peerHash)
1086
+ ) {
1087
+ reason = "admission-pending";
1088
+ } else if (state.phase === "active") {
1089
+ reason = "active";
1090
+ } else if (state.requestParked) {
1091
+ reason = "request-parked";
1092
+ } else if (state.requestInFlight !== undefined) {
1093
+ reason = "request-in-flight";
1094
+ } else if (state.requestTimer !== undefined) {
1095
+ reason = "request-scheduled";
1096
+ } else {
1097
+ reason = state.phase;
1098
+ }
1099
+ const requestState: NonNullable<
1100
+ ReplicationInfoV2ReceiveDiagnostic["requestState"]
1101
+ > =
1102
+ state.reservedAdmission !== undefined ||
1103
+ this._reservedAdmissionsByPeer.has(properties.peerHash)
1104
+ ? "admission-pending"
1105
+ : state.requestParked
1106
+ ? "parked"
1107
+ : state.requestInFlight !== undefined
1108
+ ? "in-flight"
1109
+ : state.requestTimer !== undefined
1110
+ ? "scheduled"
1111
+ : "idle";
1112
+ return Object.freeze({
1113
+ state: status,
1114
+ reason,
1115
+ phase: state.phase,
1116
+ requestState,
1117
+ ...(state.lastSequence === undefined
1118
+ ? {}
1119
+ : { lastAppliedSequence: state.lastSequence.toString() }),
1120
+ requestAttempts: Math.min(
1121
+ this.requestMaxAttempts,
1122
+ Math.max(0, state.requestAttempts),
1123
+ ),
1124
+ ...common,
1125
+ });
1126
+ }
1127
+
919
1128
  private promoteLocalCapabilityAdvertisement(
920
1129
  state: ReplicationInfoV2LocalCapabilityAdvertisement,
921
1130
  ): boolean {
@@ -55,6 +55,7 @@ type ApplicationConfirmationTarget = {
55
55
  type ApplicationConfirmationWaiter = {
56
56
  revision: bigint;
57
57
  minPeers: number;
58
+ startedAt: number;
58
59
  target?: ApplicationConfirmationTarget;
59
60
  resolve: () => void;
60
61
  reject: (error: unknown) => void;
@@ -63,6 +64,37 @@ type ApplicationConfirmationWaiter = {
63
64
  onAbort?: () => void;
64
65
  };
65
66
 
67
+ export type ReplicationInfoV2SendDiagnostic = Readonly<{
68
+ state: "absent" | "current" | "stale";
69
+ /** Current-state classification; no historical packet/drop log is retained. */
70
+ reason:
71
+ | "state-absent"
72
+ | "state-retiring"
73
+ | "peer-session-mismatch"
74
+ | "transport-session-mismatch"
75
+ | "destination-not-current"
76
+ | "destination-not-open"
77
+ | "ownership-inactive"
78
+ | "retry-backoff"
79
+ | "send-in-flight"
80
+ | "send-pending"
81
+ | "confirmation-in-flight"
82
+ | "latest-applied"
83
+ | "latest-unconfirmed";
84
+ established?: boolean;
85
+ suspended?: boolean;
86
+ currentRevision: string;
87
+ pendingRevision?: string;
88
+ inFlightRevision?: string;
89
+ inFlightSequence?: string;
90
+ appliedRevision?: string;
91
+ confirmationRevision?: string;
92
+ confirmationSequence?: string;
93
+ retryAttempts?: number;
94
+ confirmationWaiters: number;
95
+ oldestConfirmationAgeMs?: number;
96
+ }>;
97
+
66
98
  export type ReplicationInfoV2ConfirmationOptions = {
67
99
  /** Distinct current peers that must report durable application. Defaults to 1. */
68
100
  minPeers?: number;
@@ -460,6 +492,7 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
460
492
  const waiter: ApplicationConfirmationWaiter = {
461
493
  revision: this._revision,
462
494
  minPeers: properties.minPeers,
495
+ startedAt: Date.now(),
463
496
  target: properties.target ? { ...properties.target } : undefined,
464
497
  resolve,
465
498
  reject,
@@ -488,6 +521,137 @@ export class ReplicationInfoV2SendCoordinator<R extends "u32" | "u64"> {
488
521
  });
489
522
  }
490
523
 
524
+ /**
525
+ * Bounded, read-only state for persisted-readiness troubleshooting. Opaque
526
+ * session/challenge values and peer maps are intentionally excluded.
527
+ */
528
+ diagnosePeer(target: {
529
+ peerHash: string;
530
+ peerSession?: object;
531
+ receiverTransportSession?: bigint;
532
+ }): ReplicationInfoV2SendDiagnostic {
533
+ const state = this._sendStates.get(target.peerHash);
534
+ let matchingConfirmationCount = 0;
535
+ let oldestStartedAt: number | undefined;
536
+ for (const waiter of this._confirmations) {
537
+ if (
538
+ waiter.target?.peerHash !== target.peerHash ||
539
+ (target.peerSession !== undefined &&
540
+ waiter.target.peerSession !== target.peerSession) ||
541
+ (target.receiverTransportSession !== undefined &&
542
+ waiter.target.receiverTransportSession !==
543
+ target.receiverTransportSession)
544
+ ) {
545
+ continue;
546
+ }
547
+ matchingConfirmationCount++;
548
+ oldestStartedAt =
549
+ oldestStartedAt === undefined
550
+ ? waiter.startedAt
551
+ : Math.min(oldestStartedAt, waiter.startedAt);
552
+ }
553
+ const common = {
554
+ currentRevision: this._revision.toString(),
555
+ confirmationWaiters: Math.min(
556
+ this.maxConfirmationWaiters,
557
+ matchingConfirmationCount,
558
+ ),
559
+ ...(oldestStartedAt === undefined
560
+ ? {}
561
+ : {
562
+ oldestConfirmationAgeMs: Math.min(
563
+ MAX_TIMER_MS,
564
+ Math.max(0, Date.now() - oldestStartedAt),
565
+ ),
566
+ }),
567
+ };
568
+ if (!state) {
569
+ return Object.freeze({
570
+ state: this._retiringWorkersByPeer.has(target.peerHash)
571
+ ? ("stale" as const)
572
+ : ("absent" as const),
573
+ reason: this._retiringWorkersByPeer.has(target.peerHash)
574
+ ? ("state-retiring" as const)
575
+ : ("state-absent" as const),
576
+ ...common,
577
+ });
578
+ }
579
+
580
+ let status: "current" | "stale" = "current";
581
+ let reason: ReplicationInfoV2SendDiagnostic["reason"];
582
+ if (
583
+ target.peerSession !== undefined &&
584
+ state.peerSession !== target.peerSession
585
+ ) {
586
+ status = "stale";
587
+ reason = "peer-session-mismatch";
588
+ } else if (
589
+ target.receiverTransportSession !== undefined &&
590
+ state.receiverTransportSession !== target.receiverTransportSession
591
+ ) {
592
+ status = "stale";
593
+ reason = "transport-session-mismatch";
594
+ } else if (!this.isDestinationCurrent(state)) {
595
+ status = "stale";
596
+ reason = "destination-not-current";
597
+ } else if (!this.isDestinationReady(state)) {
598
+ reason = "destination-not-open";
599
+ } else if (
600
+ !this.deps.isReplicationOwnershipLifecycleActive(
601
+ state.ownershipLifecycleController,
602
+ )
603
+ ) {
604
+ reason = "ownership-inactive";
605
+ } else if (state.suspended || state.retryTimer !== undefined) {
606
+ reason = "retry-backoff";
607
+ } else if (state.applicationConfirmationRequest !== undefined) {
608
+ reason = "confirmation-in-flight";
609
+ } else if (state.inFlightRevision !== undefined) {
610
+ reason = "send-in-flight";
611
+ } else if (state.pending !== undefined) {
612
+ reason = "send-pending";
613
+ } else if (
614
+ state.appliedRevision !== undefined &&
615
+ state.appliedRevision >= this._revision
616
+ ) {
617
+ reason = "latest-applied";
618
+ } else {
619
+ reason = "latest-unconfirmed";
620
+ }
621
+
622
+ return Object.freeze({
623
+ state: status,
624
+ reason,
625
+ established: state.established,
626
+ suspended: state.suspended,
627
+ ...common,
628
+ ...(state.pending === undefined
629
+ ? {}
630
+ : { pendingRevision: state.pending.revision.toString() }),
631
+ ...(state.inFlightRevision === undefined
632
+ ? {}
633
+ : { inFlightRevision: state.inFlightRevision.toString() }),
634
+ ...(state.inFlightSequence === undefined
635
+ ? {}
636
+ : { inFlightSequence: state.inFlightSequence.toString() }),
637
+ ...(state.appliedRevision === undefined
638
+ ? {}
639
+ : { appliedRevision: state.appliedRevision.toString() }),
640
+ ...(state.applicationConfirmationRequest === undefined
641
+ ? {}
642
+ : {
643
+ confirmationRevision:
644
+ state.applicationConfirmationRequest.revision.toString(),
645
+ confirmationSequence:
646
+ state.applicationConfirmationRequest.sequence.toString(),
647
+ }),
648
+ retryAttempts: Math.min(
649
+ MAX_BACKOFF_EXPONENT + 1,
650
+ Math.max(0, state.retryAttempts),
651
+ ),
652
+ });
653
+ }
654
+
491
655
  /**
492
656
  * Wait until the latest committed local replication revision is reported as
493
657
  * durably applied by the configured number of current peers. Revisions newer