@peerbit/shared-log 16.0.0 → 16.0.1

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.
@@ -76,6 +76,14 @@ export class CheckedPruneCoordinator<T, R extends "u32" | "u64"> {
76
76
  readonly retries = new Map<string, CheckedPruneRetryState<T, R>>();
77
77
  private readonly sessions = new Map<string, CheckedPruneSession<T, R>>();
78
78
  private readonly grantSends = new Map<string, Set<Promise<void>>>();
79
+ // Per-peer refcount of in-flight removal fences; while non-zero the peer is
80
+ // withheld from the exact-replicator set (see fencePeerRemoval /
81
+ // isPeerRemovalFenced).
82
+ //
83
+ // PERMANENT (fence census closed NO-GO 2026-08-12): a concurrency-DEPTH
84
+ // refcount held across removal lanes that may span a peer reconnect, so
85
+ // neither a session token (it would reset mid-drain) nor lifecycle identity
86
+ // (it cannot count open lanes) can replace it.
79
87
  private readonly peerRemovalFences = new Map<string, number>();
80
88
  private readonly restartReservations = new Map<string, object>();
81
89
  private readonly candidateTokens = new Map<string, object>();
@@ -88,6 +96,10 @@ export class CheckedPruneCoordinator<T, R extends "u32" | "u64"> {
88
96
  // a fresh coordinator); deliberately NOT touched by close() — the release
89
97
  // closures drain it, matching the legacy host counter which was reset
90
98
  // only at open.
99
+ //
100
+ // PERMANENT (fence census closed NO-GO 2026-08-12): a concurrency-DEPTH
101
+ // counter of re-entrant removals — identity tells you which coordinator a
102
+ // continuation started under, never how many removals are in flight now.
91
103
  private _checkedPruneRemoveBlocksLocalRangeMutationAdmission = 0;
92
104
 
93
105
  /** ≡ the legacy inc / `finally`-dec pair around the admitted lower-log
package/src/index.ts CHANGED
@@ -2017,7 +2017,6 @@ export class SharedLog<
2017
2017
 
2018
2018
  uniqueReplicators!: Set<string>;
2019
2019
  private _replicatorJoinEmitted!: Set<string>;
2020
- private _replicatorsReconciled!: boolean;
2021
2020
 
2022
2021
  /* private _totalParticipation!: number; */
2023
2022
 
@@ -3658,7 +3657,6 @@ export class SharedLog<
3658
3657
  this.recentlyRebalanced = new Cache<string>({ max: 1e4, ttl: 1e5 });
3659
3658
  this.uniqueReplicators = new Set();
3660
3659
  this._replicatorJoinEmitted = new Set();
3661
- this._replicatorsReconciled = false;
3662
3660
  this._liveness = this.createReplicatorLivenessMonitor();
3663
3661
  this._v2Receive = this.createReplicationInfoV2ReceiveCoordinator();
3664
3662
  this._v2Send = this.createReplicationInfoV2SendCoordinator();
@@ -6779,7 +6777,7 @@ export class SharedLog<
6779
6777
  rebalance?: boolean;
6780
6778
  checkDuplicates?: boolean;
6781
6779
  timestamp?: number;
6782
- allowLegacyOrderedReplacementPairs?: boolean;
6780
+ allowOrderedReplacementPairs?: boolean;
6783
6781
  onConfirmedDurableStateChanged?: () => void;
6784
6782
  onDurableApplyCommitted?: () => boolean | void;
6785
6783
  shouldApply?: () => boolean;
@@ -6821,7 +6819,7 @@ export class SharedLog<
6821
6819
  checkDuplicates,
6822
6820
  timestamp: ts,
6823
6821
  rebalance,
6824
- allowLegacyOrderedReplacementPairs,
6822
+ allowOrderedReplacementPairs,
6825
6823
  onConfirmedDurableStateChanged,
6826
6824
  onDurableApplyCommitted,
6827
6825
  shouldApply,
@@ -6830,7 +6828,7 @@ export class SharedLog<
6830
6828
  rebalance?: boolean;
6831
6829
  checkDuplicates?: boolean;
6832
6830
  timestamp?: number;
6833
- allowLegacyOrderedReplacementPairs?: boolean;
6831
+ allowOrderedReplacementPairs?: boolean;
6834
6832
  onConfirmedDurableStateChanged?: () => void;
6835
6833
  onDurableApplyCommitted?: () => boolean | void;
6836
6834
  shouldApply?: () => boolean;
@@ -6869,17 +6867,19 @@ export class SharedLog<
6869
6867
  incomingRangeCountsById.set(range.idString, count);
6870
6868
  if (
6871
6869
  count > 1 &&
6872
- (!allowLegacyOrderedReplacementPairs || reset === true || count > 2)
6870
+ (!allowOrderedReplacementPairs || reset === true || count > 2)
6873
6871
  ) {
6874
6872
  throw new Error(
6875
6873
  `Duplicate replication range id in announcement: ${range.idString}`,
6876
6874
  );
6877
6875
  }
6878
- // Released peers represented a non-reset replacement as the retired
6879
- // geometry followed by the current geometry under the same id. The
6880
- // sender is already authorized to replace that id with the final item,
6881
- // so collapsing an exact two-item incremental pair to its last item
6882
- // preserves rolling-upgrade compatibility without broadening authority.
6876
+ // Rolling-upgrade relaxation on the live V2 Added path: a peer may
6877
+ // still express a non-reset replacement as the retired geometry
6878
+ // followed by the current geometry under one id. The sender is already
6879
+ // authorized to replace that id with the final item, so collapsing an
6880
+ // EXACT two-item incremental pair to its last item accepts the older
6881
+ // wire shape without broadening authority. Deliberately narrow — reset
6882
+ // announcements and any run longer than two still fail as duplicates.
6883
6883
  incomingRangesById.set(range.idString, range);
6884
6884
  }
6885
6885
  const incomingRanges = [...incomingRangesById.values()];
@@ -14235,7 +14235,6 @@ export class SharedLog<
14235
14235
 
14236
14236
  this.uniqueReplicators = new Set();
14237
14237
  this._replicatorJoinEmitted = new Set();
14238
- this._replicatorsReconciled = false;
14239
14238
  // Deserialized instances never ran the constructor; create the monitor and
14240
14239
  // coordinator lazily on first open. Reopens keep the SAME instances so
14241
14240
  // stale async continuations observe resets via property lookup.
@@ -15513,16 +15512,8 @@ export class SharedLog<
15513
15512
  const existingSubscribersPromise = this.node.services.pubsub.getSubscribers(
15514
15513
  this.topic,
15515
15514
  );
15516
- const replicationLifecycleController =
15517
- this._instanceLifecycle?.membershipLifecycleController;
15518
-
15519
15515
  // We do this here, because these calls requires this.closed == false
15520
15516
  void this.pruneOfflineReplicators()
15521
- .then(() => {
15522
- if (this.isReplicationLifecycleActive(replicationLifecycleController)) {
15523
- this._replicatorsReconciled = true;
15524
- }
15525
- })
15526
15517
  .catch((error) => {
15527
15518
  if (isNotStartedError(error as Error)) {
15528
15519
  return;
@@ -19652,7 +19643,7 @@ export class SharedLog<
19652
19643
  reset: msg instanceof FullReplicationInfoV2Message,
19653
19644
  checkDuplicates: true,
19654
19645
  timestamp: Number(context.message.header.timestamp),
19655
- allowLegacyOrderedReplacementPairs:
19646
+ allowOrderedReplacementPairs:
19656
19647
  msg instanceof AddedReplicationInfoV2Message,
19657
19648
  shouldApply: () => {
19658
19649
  mutationGateChecked = true;
@@ -19761,7 +19752,6 @@ export class SharedLog<
19761
19752
  }
19762
19753
  }
19763
19754
 
19764
-
19765
19755
  async calculateTotalParticipation(options?: { sum?: boolean }) {
19766
19756
  if (options?.sum) {
19767
19757
  const ranges = await this.replicationIndex.iterate().all();
@@ -23063,7 +23053,11 @@ export class SharedLog<
23063
23053
  }
23064
23054
  const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
23065
23055
  if (
23066
- !this._v2Receive.isRequestParked({ peerHash, peerSession, receiveEpoch })
23056
+ !this._v2Receive.isRequestParked({
23057
+ peerHash,
23058
+ peerSession,
23059
+ receiveEpoch,
23060
+ })
23067
23061
  ) {
23068
23062
  // Active, or a bounded request cycle is still running its own
23069
23063
  // exponential retries. Keep polling for the next park.
@@ -66,10 +66,19 @@ export class InstanceLifecycle {
66
66
  // code. Incremented synchronously with leader-cache invalidation so the
67
67
  // handler can detect whether its pre-join plan needs one fresh
68
68
  // post-persist audit.
69
+ //
70
+ // PERMANENT (fence census closed NO-GO 2026-08-12): a sub-generation
71
+ // WITHIN one lifecycle — it advances while the lifecycle identity is
72
+ // deliberately unchanged, so an identity/session token cannot tell a
73
+ // pre-invalidation plan from a post-invalidation one.
69
74
  public _receiveOwnershipRevision = 0;
70
75
  // Count of ownership-changing range mutations from queue admission
71
76
  // through settlement, including mutations already pending when a receive
72
77
  // starts.
78
+ //
79
+ // PERMANENT (fence census closed NO-GO 2026-08-12): a concurrency-DEPTH
80
+ // refcount, not a staleness token — identity answers "which generation
81
+ // started this?", never "how many mutation lanes are open right now?".
73
82
  public _receiveOwnershipMutationAdmissions = 0;
74
83
 
75
84
  // ---- stage 4: physically owned controllers (moved from SharedLog) ----
@@ -131,6 +131,11 @@ export class PeerSessionRegistry {
131
131
  // peer stays subscribed), and it must also fence a peer that never had a
132
132
  // session. Unlike sessions this map IS cleared at _close (see
133
133
  // clearReceiveEpochsForClose) and replaced at open.
134
+ //
135
+ // PERMANENT (fence census closed NO-GO 2026-08-12): the per-PEER lifetime
136
+ // above is the whole mechanism — a session token would rotate at exactly
137
+ // the moments this map must survive, and could not fence a session-less
138
+ // peer at all.
134
139
  _replicationInfoReceiveEpochByPeer!: Map<string, object>;
135
140
  // Moved from SharedLog (fence B6, same name — the sanctioned file-to-file
136
141
  // ratchet move). Refcount of in-flight destructive peer cleanups: while
@@ -140,6 +145,10 @@ export class PeerSessionRegistry {
140
145
  // reconnect may rotate the session; a fresh session with a zero gate
141
146
  // would reopen receive admission mid-drain. The map instance is replaced
142
147
  // only at open (resetForOpen) and cleared in place at _close.
148
+ //
149
+ // PERMANENT (fence census closed NO-GO 2026-08-12): both structural
150
+ // reasons at once — a concurrency-DEPTH refcount whose per-PEER lifetime
151
+ // deliberately spans the session rotation a reconnect causes mid-drain.
143
152
  _receiveCleanupGateByPeer!: Map<string, number>;
144
153
  // Moved from SharedLog (fence B5, same name). Peers whose replication-info
145
154
  // is fenced: added when a departure/unsubscribe rotation or a reconnect
package/src/pid.ts CHANGED
@@ -3,8 +3,6 @@ const MIN_MEMORY_HEADROOM_BALANCE_SCALER = 0.25;
3
3
  export class PIDReplicationController {
4
4
  integral!: number;
5
5
  prevError!: number;
6
- prevMemoryUsage!: number;
7
- prevTotalFactor!: number;
8
6
  kp: number;
9
7
  ki: number;
10
8
  kd: number;
@@ -42,9 +40,6 @@ export class PIDReplicationController {
42
40
  let { memoryUsage, totalFactor, peerCount, cpuUsage, currentFactor } =
43
41
  properties;
44
42
 
45
- this.prevTotalFactor = totalFactor;
46
- this.prevMemoryUsage = memoryUsage;
47
-
48
43
  const estimatedTotalSize =
49
44
  currentFactor > 0 ? memoryUsage / currentFactor : 1e5;
50
45
 
@@ -238,6 +233,5 @@ export class PIDReplicationController {
238
233
  reset() {
239
234
  this.prevError = 0;
240
235
  this.integral = 0;
241
- this.prevMemoryUsage = 0;
242
236
  }
243
237
  }
@@ -84,12 +84,6 @@ type LocalCapabilityReadyProperties = {
84
84
 
85
85
  export type ReplicationInfoV2LocalCapabilityAdvertisementHandle = {
86
86
  firstAttempt: Promise<void>;
87
- /**
88
- * B12: the two-phase legacy barrier is retired — an ACKed advert promotes
89
- * readiness immediately. Retained as a no-op so the handle shape (and the
90
- * tests that drive it) stay stable.
91
- */
92
- releaseLegacyBarrier(): void;
93
87
  };
94
88
 
95
89
  export type ReplicationInfoV2LocalCapabilityContext = {
@@ -203,6 +197,14 @@ export type ReplicationInfoV2ReceiveDeps = {
203
197
  */
204
198
  export class ReplicationInfoV2ReceiveCoordinator {
205
199
  _receiveStates!: Map<string, ReplicationInfoV2ReceiveState>;
200
+ /**
201
+ * Post-B12 V2 resync memory: sessions that have already taken one full
202
+ * replication-info snapshot. Two load-bearing reads, both in
203
+ * observeCapability: phase selection (a remembered session opens in
204
+ * "resync" instead of "awaiting-full"), and capability refresh (the
205
+ * preserveCutover decision, which keeps the memory only when a re-advert
206
+ * arrives under the same session with a ready sender).
207
+ */
206
208
  _cutoverPeerSessions!: WeakSet<object>;
207
209
  _localCapabilityReadyBySession!: WeakMap<object, LocalCapabilityReady>;
208
210
  _localCapabilityContextBySession!: WeakMap<
@@ -292,15 +294,17 @@ export class ReplicationInfoV2ReceiveCoordinator {
292
294
  }
293
295
 
294
296
  /** Revoke an unauthenticated or downgraded capability generation. */
295
- revokePeerCapability(peerHash: string, reopenLegacy = true): void {
297
+ revokePeerCapability(peerHash: string): void {
296
298
  const state = this._receiveStates.get(peerHash);
297
299
  if (!state) {
298
300
  return;
299
301
  }
300
302
  this.clearState(state);
301
- if (reopenLegacy) {
302
- this._cutoverPeerSessions.delete(state.peerSession);
303
- }
303
+ // Unconditional: the revoked session must lose its resync memory so a
304
+ // re-advert starts from the first phase again. (This used to sit behind
305
+ // an opt-out parameter that defaulted to true, had no else-arm, and that
306
+ // no caller ever overrode.)
307
+ this._cutoverPeerSessions.delete(state.peerSession);
304
308
  }
305
309
 
306
310
  private clearState(state: ReplicationInfoV2ReceiveState): void {
@@ -531,7 +535,6 @@ export class ReplicationInfoV2ReceiveCoordinator {
531
535
  ) {
532
536
  return {
533
537
  firstAttempt: Promise.resolve(),
534
- releaseLegacyBarrier: () => {},
535
538
  };
536
539
  }
537
540
  let context = this._localCapabilityContextBySession.get(
@@ -545,7 +548,6 @@ export class ReplicationInfoV2ReceiveCoordinator {
545
548
  ) {
546
549
  return {
547
550
  firstAttempt: Promise.resolve(),
548
- releaseLegacyBarrier: () => {},
549
551
  };
550
552
  }
551
553
  if (!context) {
@@ -584,7 +586,6 @@ export class ReplicationInfoV2ReceiveCoordinator {
584
586
  ) {
585
587
  return {
586
588
  firstAttempt: Promise.resolve(),
587
- releaseLegacyBarrier: () => {},
588
589
  };
589
590
  }
590
591
  if (!state) {
@@ -619,7 +620,6 @@ export class ReplicationInfoV2ReceiveCoordinator {
619
620
  (state.firstAttempt = this.runLocalCapabilityAdvertisement(state));
620
621
  return {
621
622
  firstAttempt,
622
- releaseLegacyBarrier: () => {},
623
623
  };
624
624
  }
625
625
 
package/src/role.ts CHANGED
@@ -6,19 +6,10 @@
6
6
  import { field, variant, vec } from "@dao-xyz/borsh";
7
7
  import { MAX_U32, denormalizer } from "./integers.js";
8
8
 
9
- export const overlaps = (x1: number, x2: number, y1: number, y2: number) => {
10
- if (x1 <= y2 && y1 <= x2) {
11
- return true;
12
- }
13
- return false;
14
- };
15
-
16
9
  export abstract class Role {
17
10
  abstract equals(other: Role): boolean;
18
11
  }
19
12
 
20
- export const NO_TYPE_VARIANT = new Uint8Array([0]);
21
-
22
13
  @variant(0)
23
14
  export class NoType extends Role {
24
15
  equals(other: Role) {
@@ -26,8 +17,6 @@ export class NoType extends Role {
26
17
  }
27
18
  }
28
19
 
29
- export const OBSERVER_TYPE_VARIANT = new Uint8Array([1]);
30
-
31
20
  @variant(1)
32
21
  export class Observer extends Role {
33
22
  equals(other: Role) {
@@ -35,8 +24,6 @@ export class Observer extends Role {
35
24
  }
36
25
  }
37
26
 
38
- export const REPLICATOR_TYPE_VARIANT = new Uint8Array([2]);
39
-
40
27
  const denormalizeru32 = denormalizer("u32");
41
28
  export class RoleReplicationSegment {
42
29
  @field({ type: "u64" })