@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.
- package/dist/src/checked-prune.d.ts.map +1 -1
- package/dist/src/checked-prune.js +12 -0
- package/dist/src/checked-prune.js.map +1 -1
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +15 -18
- package/dist/src/index.js.map +1 -1
- package/dist/src/instance-lifecycle.d.ts.map +1 -1
- package/dist/src/instance-lifecycle.js +9 -0
- package/dist/src/instance-lifecycle.js.map +1 -1
- package/dist/src/peer-session.d.ts.map +1 -1
- package/dist/src/peer-session.js +9 -0
- package/dist/src/peer-session.js.map +1 -1
- package/dist/src/pid.d.ts +0 -2
- package/dist/src/pid.d.ts.map +1 -1
- package/dist/src/pid.js +0 -5
- package/dist/src/pid.js.map +1 -1
- package/dist/src/replication-info-v2-receive.d.ts +9 -7
- package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
- package/dist/src/replication-info-v2-receive.js +14 -8
- package/dist/src/replication-info-v2-receive.js.map +1 -1
- package/dist/src/role.d.ts +0 -4
- package/dist/src/role.d.ts.map +1 -1
- package/dist/src/role.js +0 -9
- package/dist/src/role.js.map +1 -1
- package/package.json +6 -6
- package/src/checked-prune.ts +12 -0
- package/src/index.ts +17 -23
- package/src/instance-lifecycle.ts +9 -0
- package/src/peer-session.ts +9 -0
- package/src/pid.ts +0 -6
- package/src/replication-info-v2-receive.ts +14 -14
- package/src/role.ts +0 -13
package/src/checked-prune.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
(!
|
|
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
|
-
//
|
|
6879
|
-
//
|
|
6880
|
-
//
|
|
6881
|
-
//
|
|
6882
|
-
//
|
|
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
|
-
|
|
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({
|
|
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) ----
|
package/src/peer-session.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
302
|
-
|
|
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" })
|