@peerbit/shared-log 13.2.23 → 13.2.25

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 (175) 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 +139 -133
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/index.js +1491 -4265
  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/leader-plan-cache.d.ts +33 -0
  26. package/dist/src/leader-plan-cache.d.ts.map +1 -0
  27. package/dist/src/leader-plan-cache.js +49 -0
  28. package/dist/src/leader-plan-cache.js.map +1 -0
  29. package/dist/src/native-write-through-block-store.d.ts +123 -0
  30. package/dist/src/native-write-through-block-store.d.ts.map +1 -0
  31. package/dist/src/native-write-through-block-store.js +989 -0
  32. package/dist/src/native-write-through-block-store.js.map +1 -0
  33. package/dist/src/peer-session.d.ts +117 -0
  34. package/dist/src/peer-session.d.ts.map +1 -0
  35. package/dist/src/peer-session.js +259 -0
  36. package/dist/src/peer-session.js.map +1 -0
  37. package/dist/src/ranges.d.ts +2 -2
  38. package/dist/src/ranges.d.ts.map +1 -1
  39. package/dist/src/ranges.js +10 -8
  40. package/dist/src/ranges.js.map +1 -1
  41. package/dist/src/replication-announcement.d.ts +116 -0
  42. package/dist/src/replication-announcement.d.ts.map +1 -0
  43. package/dist/src/replication-announcement.js +515 -0
  44. package/dist/src/replication-announcement.js.map +1 -0
  45. package/dist/src/replicator-liveness.d.ts +59 -0
  46. package/dist/src/replicator-liveness.d.ts.map +1 -0
  47. package/dist/src/replicator-liveness.js +304 -0
  48. package/dist/src/replicator-liveness.js.map +1 -0
  49. package/dist/src/sync/dispatch-lifecycle.d.ts +48 -0
  50. package/dist/src/sync/dispatch-lifecycle.d.ts.map +1 -0
  51. package/dist/src/sync/dispatch-lifecycle.js +141 -0
  52. package/dist/src/sync/dispatch-lifecycle.js.map +1 -0
  53. package/dist/src/sync/factory.d.ts +29 -0
  54. package/dist/src/sync/factory.d.ts.map +1 -0
  55. package/dist/src/sync/factory.js +87 -0
  56. package/dist/src/sync/factory.js.map +1 -0
  57. package/dist/src/sync/pending-sync-store.d.ts +105 -0
  58. package/dist/src/sync/pending-sync-store.d.ts.map +1 -0
  59. package/dist/src/sync/pending-sync-store.js +877 -0
  60. package/dist/src/sync/pending-sync-store.js.map +1 -0
  61. package/dist/src/sync/rateless-iblt.d.ts +4 -1
  62. package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
  63. package/dist/src/sync/rateless-iblt.js +102 -106
  64. package/dist/src/sync/rateless-iblt.js.map +1 -1
  65. package/dist/src/sync/simple.d.ts +10 -41
  66. package/dist/src/sync/simple.d.ts.map +1 -1
  67. package/dist/src/sync/simple.js +259 -952
  68. package/dist/src/sync/simple.js.map +1 -1
  69. package/dist/src/sync/sync-peer-state.d.ts +16 -0
  70. package/dist/src/sync/sync-peer-state.d.ts.map +1 -0
  71. package/dist/src/sync/sync-peer-state.js +82 -0
  72. package/dist/src/sync/sync-peer-state.js.map +1 -0
  73. package/package.json +22 -21
  74. package/src/checked-prune.ts +35 -0
  75. package/src/coordinate-persistence.ts +1796 -0
  76. package/src/errors.ts +21 -0
  77. package/src/index.ts +6367 -10052
  78. package/src/instance-lifecycle.ts +351 -0
  79. package/src/join-warmup.ts +507 -0
  80. package/src/leader-plan-cache.ts +61 -0
  81. package/src/native-write-through-block-store.ts +1170 -0
  82. package/src/peer-session.ts +336 -0
  83. package/src/ranges.ts +22 -16
  84. package/src/replication-announcement.ts +744 -0
  85. package/src/replicator-liveness.ts +439 -0
  86. package/src/sync/dispatch-lifecycle.ts +230 -0
  87. package/src/sync/factory.ts +154 -0
  88. package/src/sync/pending-sync-store.ts +1085 -0
  89. package/src/sync/rateless-iblt.ts +122 -132
  90. package/src/sync/simple.ts +334 -1144
  91. package/src/sync/sync-peer-state.ts +107 -0
  92. package/dist/benchmark/adaptive-ingest.d.ts +0 -2
  93. package/dist/benchmark/adaptive-ingest.d.ts.map +0 -1
  94. package/dist/benchmark/adaptive-ingest.js +0 -275
  95. package/dist/benchmark/adaptive-ingest.js.map +0 -1
  96. package/dist/benchmark/get-samples.d.ts +0 -2
  97. package/dist/benchmark/get-samples.d.ts.map +0 -1
  98. package/dist/benchmark/get-samples.js +0 -194
  99. package/dist/benchmark/get-samples.js.map +0 -1
  100. package/dist/benchmark/index.d.ts +0 -2
  101. package/dist/benchmark/index.d.ts.map +0 -1
  102. package/dist/benchmark/index.js +0 -224
  103. package/dist/benchmark/index.js.map +0 -1
  104. package/dist/benchmark/join-backfill-repair.d.ts +0 -2
  105. package/dist/benchmark/join-backfill-repair.d.ts.map +0 -1
  106. package/dist/benchmark/join-backfill-repair.js +0 -288
  107. package/dist/benchmark/join-backfill-repair.js.map +0 -1
  108. package/dist/benchmark/memory/child.d.ts +0 -2
  109. package/dist/benchmark/memory/child.d.ts.map +0 -1
  110. package/dist/benchmark/memory/child.js +0 -204
  111. package/dist/benchmark/memory/child.js.map +0 -1
  112. package/dist/benchmark/memory/index.d.ts +0 -2
  113. package/dist/benchmark/memory/index.d.ts.map +0 -1
  114. package/dist/benchmark/memory/index.js +0 -81
  115. package/dist/benchmark/memory/index.js.map +0 -1
  116. package/dist/benchmark/memory/utils.d.ts +0 -13
  117. package/dist/benchmark/memory/utils.d.ts.map +0 -1
  118. package/dist/benchmark/memory/utils.js +0 -2
  119. package/dist/benchmark/memory/utils.js.map +0 -1
  120. package/dist/benchmark/native-graph.d.ts +0 -2
  121. package/dist/benchmark/native-graph.d.ts.map +0 -1
  122. package/dist/benchmark/native-graph.js +0 -359
  123. package/dist/benchmark/native-graph.js.map +0 -1
  124. package/dist/benchmark/network-preset-e2e.d.ts +0 -2
  125. package/dist/benchmark/network-preset-e2e.d.ts.map +0 -1
  126. package/dist/benchmark/network-preset-e2e.js +0 -497
  127. package/dist/benchmark/network-preset-e2e.js.map +0 -1
  128. package/dist/benchmark/partial-sync.d.ts +0 -3
  129. package/dist/benchmark/partial-sync.d.ts.map +0 -1
  130. package/dist/benchmark/partial-sync.js +0 -121
  131. package/dist/benchmark/partial-sync.js.map +0 -1
  132. package/dist/benchmark/pid-convergence.d.ts +0 -2
  133. package/dist/benchmark/pid-convergence.d.ts.map +0 -1
  134. package/dist/benchmark/pid-convergence.js +0 -138
  135. package/dist/benchmark/pid-convergence.js.map +0 -1
  136. package/dist/benchmark/rateless-iblt-sender-startsync.d.ts +0 -2
  137. package/dist/benchmark/rateless-iblt-sender-startsync.d.ts.map +0 -1
  138. package/dist/benchmark/rateless-iblt-sender-startsync.js +0 -104
  139. package/dist/benchmark/rateless-iblt-sender-startsync.js.map +0 -1
  140. package/dist/benchmark/rateless-iblt-startsync-cache.d.ts +0 -2
  141. package/dist/benchmark/rateless-iblt-startsync-cache.d.ts.map +0 -1
  142. package/dist/benchmark/rateless-iblt-startsync-cache.js +0 -112
  143. package/dist/benchmark/rateless-iblt-startsync-cache.js.map +0 -1
  144. package/dist/benchmark/receive-prune.d.ts +0 -2
  145. package/dist/benchmark/receive-prune.d.ts.map +0 -1
  146. package/dist/benchmark/receive-prune.js +0 -818
  147. package/dist/benchmark/receive-prune.js.map +0 -1
  148. package/dist/benchmark/replication-prune.d.ts +0 -2
  149. package/dist/benchmark/replication-prune.d.ts.map +0 -1
  150. package/dist/benchmark/replication-prune.js +0 -105
  151. package/dist/benchmark/replication-prune.js.map +0 -1
  152. package/dist/benchmark/replication.d.ts +0 -2
  153. package/dist/benchmark/replication.d.ts.map +0 -1
  154. package/dist/benchmark/replication.js +0 -88
  155. package/dist/benchmark/replication.js.map +0 -1
  156. package/dist/benchmark/sync-batch-sweep.d.ts +0 -2
  157. package/dist/benchmark/sync-batch-sweep.d.ts.map +0 -1
  158. package/dist/benchmark/sync-batch-sweep.js +0 -353
  159. package/dist/benchmark/sync-batch-sweep.js.map +0 -1
  160. package/dist/benchmark/sync-catchup.d.ts +0 -2
  161. package/dist/benchmark/sync-catchup.d.ts.map +0 -1
  162. package/dist/benchmark/sync-catchup.js +0 -277
  163. package/dist/benchmark/sync-catchup.js.map +0 -1
  164. package/dist/benchmark/sync-phase-profile.d.ts +0 -2
  165. package/dist/benchmark/sync-phase-profile.d.ts.map +0 -1
  166. package/dist/benchmark/sync-phase-profile.js +0 -303
  167. package/dist/benchmark/sync-phase-profile.js.map +0 -1
  168. package/dist/benchmark/to-rebalance.d.ts +0 -2
  169. package/dist/benchmark/to-rebalance.d.ts.map +0 -1
  170. package/dist/benchmark/to-rebalance.js +0 -117
  171. package/dist/benchmark/to-rebalance.js.map +0 -1
  172. package/dist/benchmark/utils.d.ts +0 -24
  173. package/dist/benchmark/utils.d.ts.map +0 -1
  174. package/dist/benchmark/utils.js +0 -47
  175. package/dist/benchmark/utils.js.map +0 -1
@@ -0,0 +1,336 @@
1
+ // One PeerSession per peer connection-epoch. The session instance IS the
2
+ // opaque subscription-epoch token formerly stored by SharedLog: every `===`
3
+ // epoch comparison keeps working because tokens were always compared by
4
+ // identity and never inspected. Compound predicates now live on
5
+ // isCurrent()/isActive()/isReceiveAdmissionOpen().
6
+
7
+ export type PeerSessionKind = "opening" | "departing";
8
+
9
+ export type PeerSessionPhase =
10
+ | "opening" // subscribe transition; reconnect barrier not yet completed
11
+ | "open" // reconnect barrier completed (replication-info unblocked)
12
+ | "departing" // created by an unsubscribe epoch-advance, fencing the old connection
13
+ | "superseded"; // a newer session replaced this one (terminal)
14
+
15
+ export type PeerSessionDeps = {
16
+ // Late-bound readers into state that stays physically on SharedLog in
17
+ // stage 2. Delegating closures — NOT bound method refs — so sinon spies
18
+ // installed on the SharedLog instance keep intercepting (same constraint
19
+ // as the stage-1 coordinators).
20
+ isReplicationLifecycleActive: (
21
+ controller: AbortController | undefined,
22
+ ) => boolean;
23
+ getReplicationLifecycleController: () => AbortController | undefined;
24
+ };
25
+
26
+ export type PeerReceiveAdmissionOptions = {
27
+ allowReplicationInfoBlocked?: boolean;
28
+ allowCleanupGate?: boolean;
29
+ };
30
+
31
+ export class PeerSession {
32
+ readonly peerHash: string;
33
+ readonly kind: PeerSessionKind;
34
+ // The lifecycle controller live at rotation. All current seams pair the
35
+ // epoch check with a lifecycle check against a controller captured in the
36
+ // same synchronous window as the epoch advance; capturing it here
37
+ // reproduces that pairing exactly.
38
+ readonly replicationLifecycleController: AbortController | undefined;
39
+ phase: PeerSessionPhase;
40
+ // Diagnostics only in stage 2: the destructive removal funnel observed a
41
+ // committed replicator removal under this connection-epoch.
42
+ replicatorRemoved = false;
43
+ // Reconnect-barrier WINDOW sub-state (stage-3 home of the legacy
44
+ // _subscriptionOpeningEpochByPeer map entry, whose value was always this
45
+ // session). True exactly while the barrier window is open: set when
46
+ // handleSubscriptionChange starts the opening barrier (same synchronous
47
+ // window as the rotation that made this session current), cleared in the
48
+ // barrier's `finally` — including the barrier-throw path — where the map
49
+ // entry used to be deleted. Deliberately NOT derived from
50
+ // `phase === "opening"`: phase is "opening" from rotate() — BEFORE the
51
+ // barrier starts — and stays "opening" forever if the barrier throws
52
+ // (markOpen never runs), which would wrongly keep granting the opening
53
+ // lease bypasses that today's window drops.
54
+ openingBarrierActive = false;
55
+ readonly createdAt = Date.now(); // diagnostics only
56
+
57
+ constructor(
58
+ private readonly registry: PeerSessionRegistry,
59
+ peerHash: string,
60
+ kind: PeerSessionKind,
61
+ replicationLifecycleController: AbortController | undefined,
62
+ ) {
63
+ this.peerHash = peerHash;
64
+ this.kind = kind;
65
+ this.replicationLifecycleController = replicationLifecycleController;
66
+ this.phase = kind;
67
+ }
68
+
69
+ /** ≡ former SharedLog.isCurrentSubscriptionEpoch(this.peerHash, this). */
70
+ isCurrent(): boolean {
71
+ return this.registry.current(this.peerHash) === this;
72
+ }
73
+
74
+ /** Barrier window opens — ≡ the legacy
75
+ * `_subscriptionOpeningEpochByPeer.set(peerHash, thisSession)`. */
76
+ beginOpeningBarrier(): void {
77
+ this.openingBarrierActive = true;
78
+ }
79
+
80
+ /** Barrier window closes — ≡ the legacy identity-guarded
81
+ * `_subscriptionOpeningEpochByPeer.delete(peerHash)`. Unconditional:
82
+ * the map guard only avoided clobbering a NEWER peer-keyed entry, and
83
+ * per-session flags cannot collide (a newer barrier flags its own,
84
+ * already-rotated session). */
85
+ finishOpeningBarrier(): void {
86
+ this.openingBarrierActive = false;
87
+ }
88
+
89
+ /** ≡ ownsReplicationLifecycle() && ownsSubscriptionEpoch() with both
90
+ * captured at the epoch-advance. */
91
+ isActive(): boolean {
92
+ return (
93
+ this.isCurrent() &&
94
+ this.registry.deps.isReplicationLifecycleActive(
95
+ this.replicationLifecycleController,
96
+ )
97
+ );
98
+ }
99
+
100
+ /** ≡ former SharedLog.isPeerReceiveAdmissionOpen(peerHash,
101
+ * this.replicationLifecycleController, this, options). */
102
+ isReceiveAdmissionOpen(options?: PeerReceiveAdmissionOptions): boolean {
103
+ return this.registry.isReceiveAdmissionOpen(
104
+ this.peerHash,
105
+ this,
106
+ this.replicationLifecycleController,
107
+ options,
108
+ );
109
+ }
110
+ }
111
+
112
+ export class PeerSessionRegistry {
113
+ // The per-peer session map (formerly _subscriptionEpochByPeer; renamed
114
+ // once the last raw-token comparison in index.ts was gone — the values
115
+ // ARE the legacy epoch tokens, compared by identity and never inspected).
116
+ // The rename retires the map's fence-ratchet baseline entry: session
117
+ // identity is the mechanism the ratchet drains fences INTO, not a fence.
118
+ sessions!: Map<string, PeerSession>;
119
+ // Moved from SharedLog (fence B2, same name — the sanctioned file-to-file
120
+ // ratchet move). Local receive generations fence replication-info handlers
121
+ // that were admitted before a liveness eviction but reach the per-peer
122
+ // apply lane after it. Per-PEER, not per-session, on purpose: the token
123
+ // advances at removeReplicator, where the session does NOT rotate (the
124
+ // peer stays subscribed), and it must also fence a peer that never had a
125
+ // session. Unlike sessions this map IS cleared at _close (see
126
+ // clearReceiveEpochsForClose) and replaced at open.
127
+ _replicationInfoReceiveEpochByPeer!: Map<string, object>;
128
+ // Moved from SharedLog (fence B6, same name — the sanctioned file-to-file
129
+ // ratchet move). Refcount of in-flight destructive peer cleanups: while
130
+ // non-zero, receive admission for the peer is closed and prune final
131
+ // confirmations ignore the peer. Per-PEER, not per-session, on purpose:
132
+ // the gate is held across removeReplicator's awaited lanes while a
133
+ // reconnect may rotate the session; a fresh session with a zero gate
134
+ // would reopen receive admission mid-drain. The map instance is replaced
135
+ // only at open (resetForOpen) and cleared in place at _close.
136
+ _receiveCleanupGateByPeer!: Map<string, number>;
137
+ // Moved from SharedLog (fence B5, same name). Peers whose replication-info
138
+ // is fenced: added when a departure/unsubscribe rotation or a reconnect
139
+ // barrier starts, removed only when an opening barrier commits. Per-PEER,
140
+ // not per-session, on purpose (same reasoning as B2/B6 above): the block
141
+ // is set under one session (the departing rotation) and cleared under a
142
+ // LATER one (the opening barrier), so its lifetime deliberately spans
143
+ // session identities; readers (prune final-confirmation filter,
144
+ // announcement repair targeting, pruneOfflineReplicators) ask about peers
145
+ // that may have no session at all; and the `??`-fallback rotation in
146
+ // handleSubscriptionChange rotates WITHOUT touching blocked state — a
147
+ // per-session flag would silently reset it there. Replaced only at open
148
+ // (resetForOpen); deliberately NOT cleared at _close, matching the legacy
149
+ // host field site-for-site. Kept as a public field so existing tests can
150
+ // keep instrumenting the raw Set instance (events.spec.ts spies on its
151
+ // `delete`).
152
+ _replicationInfoBlockedPeers!: Set<string>;
153
+
154
+ constructor(readonly deps: PeerSessionDeps) {
155
+ this.resetForOpen();
156
+ }
157
+
158
+ /** open()-time re-init: REPLACE the map instances, matching today's
159
+ * `= new Map()` re-inits (index.ts ctor / open).
160
+ * The session map is intentionally NOT cleared at _close — tokens must
161
+ * stay current across close so a late continuation's epoch check resolves
162
+ * exactly as it does today (close-safety comes from the paired
163
+ * lifecycle-controller check, not the epoch). There is NO clearForClose()
164
+ * for sessions; the receive-epoch map, by contrast, IS cleared at close. */
165
+ resetForOpen(): void {
166
+ this.sessions = new Map();
167
+ this._replicationInfoReceiveEpochByPeer = new Map();
168
+ this._receiveCleanupGateByPeer = new Map();
169
+ this._replicationInfoBlockedPeers = new Set();
170
+ }
171
+
172
+ /** ≡ the legacy `this._replicationInfoBlockedPeers.add(peerHash)` (host
173
+ * Set, fence B5). */
174
+ blockReplicationInfo(peerHash: string): void {
175
+ this._replicationInfoBlockedPeers.add(peerHash);
176
+ }
177
+
178
+ /** ≡ the legacy `this._replicationInfoBlockedPeers.delete(peerHash)`. */
179
+ unblockReplicationInfo(peerHash: string): void {
180
+ this._replicationInfoBlockedPeers.delete(peerHash);
181
+ }
182
+
183
+ /** ≡ the legacy `this._replicationInfoBlockedPeers.has(peerHash)`. */
184
+ isReplicationInfoBlocked(peerHash: string): boolean {
185
+ return this._replicationInfoBlockedPeers.has(peerHash);
186
+ }
187
+
188
+ /** _close counterpart of the legacy
189
+ * `this._replicationInfoReceiveEpochByPeer?.clear()`: a receive-epoch
190
+ * capture held across close must compare against null, never against a
191
+ * surviving token. Sessions deliberately survive close (see
192
+ * resetForOpen); the paired membership-lifecycle term at every
193
+ * receive-epoch check site makes the ordering unobservable either way. */
194
+ clearReceiveEpochsForClose(): void {
195
+ this._replicationInfoReceiveEpochByPeer.clear();
196
+ }
197
+
198
+ /** _close counterpart of the legacy
199
+ * `this._receiveCleanupGateByPeer?.clear()`: an in-place clear (NOT a
200
+ * map replacement) so an in-flight removal's captured release still
201
+ * drains the instance it incremented — see acquireReceiveCleanupGate. */
202
+ clearCleanupGatesForClose(): void {
203
+ this._receiveCleanupGateByPeer.clear();
204
+ }
205
+
206
+ /** ≡ removeReplicator's inline `blockPeerReceiveAdmission` +
207
+ * release-in-`finally` pair (fence B6). Acquire captures the CURRENT
208
+ * gate-map instance and increments once; the returned release is
209
+ * idempotent and decrements the captured map, deleting the entry at 0.
210
+ * The map-instance capture is load-bearing: close/reopen replaces the
211
+ * map (resetForOpen), and a late release must drain the exact map it
212
+ * incremented — a release against a fresh open's map would corrupt that
213
+ * open's refcounts. The `?? 1` mirrors the legacy release: an entry
214
+ * cleared at _close decrements to 0 and stays deleted. */
215
+ acquireReceiveCleanupGate(peerHash: string): () => void {
216
+ const gates = this._receiveCleanupGateByPeer;
217
+ gates.set(peerHash, (gates.get(peerHash) ?? 0) + 1);
218
+ let released = false;
219
+ return () => {
220
+ if (released) {
221
+ return;
222
+ }
223
+ released = true;
224
+ const remaining = (gates.get(peerHash) ?? 1) - 1;
225
+ if (remaining > 0) {
226
+ gates.set(peerHash, remaining);
227
+ } else {
228
+ gates.delete(peerHash);
229
+ }
230
+ };
231
+ }
232
+
233
+ /** ≡ the legacy `(this._receiveCleanupGateByPeer.get(peer) ?? 0) === 0`
234
+ * read (receive-admission term and prune final-confirmation filter). */
235
+ isReceiveCleanupGateOpen(peerHash: string): boolean {
236
+ return (this._receiveCleanupGateByPeer.get(peerHash) ?? 0) === 0;
237
+ }
238
+
239
+ /** ≡ legacy SharedLog.advanceReplicationInfoReceiveEpoch. Advances
240
+ * WITHOUT rotating the session — the recovery fence must survive
241
+ * removeReplicator, where the peer stays subscribed. */
242
+ advanceReceiveEpoch(peerHash: string): object {
243
+ const next = {};
244
+ this._replicationInfoReceiveEpochByPeer.set(peerHash, next);
245
+ return next;
246
+ }
247
+
248
+ /** ≡ legacy SharedLog.getReplicationInfoReceiveEpoch (`?? null`). */
249
+ receiveEpoch(peerHash: string): object | null {
250
+ return this._replicationInfoReceiveEpochByPeer.get(peerHash) ?? null;
251
+ }
252
+
253
+ /** ≡ legacy SharedLog.isCurrentReplicationInfoReceiveEpoch. */
254
+ isReceiveEpochCurrent(peerHash: string, epoch: object | null): boolean {
255
+ return this.receiveEpoch(peerHash) === epoch;
256
+ }
257
+
258
+ current(peerHash: string): PeerSession | null {
259
+ return this.sessions.get(peerHash) ?? null;
260
+ }
261
+
262
+ /** null is a VALID current value: a peer that never subscribed has no
263
+ * session, and the former host predicate admitted
264
+ * isCurrent(peer, null) === true. Preserved exactly. */
265
+ isCurrent(peerHash: string, session: object | null): boolean {
266
+ return this.current(peerHash) === session;
267
+ }
268
+
269
+ /** The only creation point. Supersedes (never deletes) the previous
270
+ * session — per-peer entries are never removed, matching today's map. */
271
+ rotate(peerHash: string, kind: PeerSessionKind): PeerSession {
272
+ const previous = this.sessions.get(peerHash);
273
+ if (previous) {
274
+ previous.phase = "superseded";
275
+ }
276
+ const next = new PeerSession(
277
+ this,
278
+ peerHash,
279
+ kind,
280
+ this.deps.getReplicationLifecycleController(),
281
+ );
282
+ this.sessions.set(peerHash, next);
283
+ return next;
284
+ }
285
+
286
+ /** Reconnect barrier completed. Identity-guarded phase mark; no-op when
287
+ * the expected token was superseded meanwhile. */
288
+ markOpen(peerHash: string, expectedSession: object): void {
289
+ const current = this.sessions.get(peerHash);
290
+ if (
291
+ current !== undefined &&
292
+ current === expectedSession &&
293
+ current.kind === "opening"
294
+ ) {
295
+ current.phase = "open";
296
+ }
297
+ }
298
+
299
+ /** Destructive removal committed for this peer. Mirrors the removal
300
+ * funnel's epoch scoping: an epoch-scoped removal only stamps the
301
+ * session it was scoped to — a reconnect during the removal's awaited
302
+ * lanes must not inherit the stamp. Undefined = unscoped removal,
303
+ * which stamps the current session as before; null scopes to "peer had
304
+ * no session at capture", so a session created meanwhile is never
305
+ * stamped. */
306
+ noteReplicatorRemoved(
307
+ peerHash: string,
308
+ expectedSession?: object | null,
309
+ ): void {
310
+ const current = this.sessions.get(peerHash);
311
+ if (
312
+ current &&
313
+ (expectedSession === undefined || current === expectedSession)
314
+ ) {
315
+ current.replicatorRemoved = true;
316
+ }
317
+ }
318
+
319
+ /** Preserves the former SharedLog.isPeerReceiveAdmissionOpen predicate term
320
+ * for term. `session` may be null (pre-session peer). */
321
+ isReceiveAdmissionOpen(
322
+ peerHash: string,
323
+ session: object | null,
324
+ replicationLifecycleController: AbortController | undefined,
325
+ options?: PeerReceiveAdmissionOptions,
326
+ ): boolean {
327
+ return (
328
+ this.deps.isReplicationLifecycleActive(replicationLifecycleController) &&
329
+ this.isCurrent(peerHash, session) &&
330
+ (options?.allowReplicationInfoBlocked === true ||
331
+ !this.isReplicationInfoBlocked(peerHash)) &&
332
+ (options?.allowCleanupGate === true ||
333
+ this.isReceiveCleanupGateOpen(peerHash))
334
+ );
335
+ }
336
+ }
package/src/ranges.ts CHANGED
@@ -126,7 +126,7 @@ export class EntryReplicatedU32 implements EntryReplicated<"u32"> {
126
126
  @field({ type: Uint8Array })
127
127
  private _meta: Uint8Array;
128
128
 
129
- private _metaResolved: ShallowMeta;
129
+ private _metaResolved?: ShallowMeta;
130
130
 
131
131
  constructor(properties: {
132
132
  coordinates: number[];
@@ -154,13 +154,16 @@ export class EntryReplicatedU32 implements EntryReplicated<"u32"> {
154
154
  properties.meta?.clock.timestamp.wallTime ?? properties.wallTime!;
155
155
  this.hashNumber = properties.hashNumber;
156
156
  this._meta =
157
- properties.metaBytes ??
158
- serialize(
159
- properties.meta instanceof Meta
160
- ? new ShallowMeta(properties.meta)
161
- : properties.meta!,
162
- );
163
- this._metaResolved = properties.meta as ShallowMeta;
157
+ properties.metaBytes != null
158
+ ? Uint8Array.from(properties.metaBytes)
159
+ : Uint8Array.from(
160
+ serialize(
161
+ properties.meta instanceof Meta
162
+ ? new ShallowMeta(properties.meta)
163
+ : properties.meta!,
164
+ ),
165
+ );
166
+ this._metaResolved = undefined;
164
167
  this.assignedToRangeBoundary = properties.assignedToRangeBoundary;
165
168
  }
166
169
 
@@ -199,7 +202,7 @@ export class EntryReplicatedU64 implements EntryReplicated<"u64"> {
199
202
  @field({ type: Uint8Array })
200
203
  private _meta: Uint8Array;
201
204
 
202
- private _metaResolved: ShallowMeta;
205
+ private _metaResolved?: ShallowMeta;
203
206
 
204
207
  constructor(properties: {
205
208
  coordinates: bigint[];
@@ -227,13 +230,16 @@ export class EntryReplicatedU64 implements EntryReplicated<"u64"> {
227
230
  this.wallTime =
228
231
  properties.meta?.clock.timestamp.wallTime ?? properties.wallTime!;
229
232
  this._meta =
230
- properties.metaBytes ??
231
- serialize(
232
- properties.meta instanceof Meta
233
- ? new ShallowMeta(properties.meta)
234
- : properties.meta!,
235
- );
236
- this._metaResolved = properties.meta as ShallowMeta;
233
+ properties.metaBytes != null
234
+ ? Uint8Array.from(properties.metaBytes)
235
+ : Uint8Array.from(
236
+ serialize(
237
+ properties.meta instanceof Meta
238
+ ? new ShallowMeta(properties.meta)
239
+ : properties.meta!,
240
+ ),
241
+ );
242
+ this._metaResolved = undefined;
237
243
  this.assignedToRangeBoundary = properties.assignedToRangeBoundary;
238
244
  }
239
245