@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,351 @@
1
+ // One InstanceLifecycle per SharedLog.open(). Stage 2 of the
2
+ // session/lifecycle refactor introduced the object as a WRAPPER over the
3
+ // existing fences via late-bound readers; stage 3 migrated the documented
4
+ // multi-fence seams onto this API; stage 4 makes it the physical owner of
5
+ // the per-open state: the role sub-generation, the receive-ownership
6
+ // counters, and the ownership/membership AbortControllers themselves (the
7
+ // index.ts ratchet entries moved file-to-file into this file's baseline in
8
+ // scripts/ci/check-fence-ratchet.mjs). The remaining deps are late-bound
9
+ // readers for host state that outlives any single lifecycle (poison latch,
10
+ // close controller, coordinator/debouncer identities).
11
+ import { TerminalOperationNotStartedError } from "@peerbit/program";
12
+
13
+ export type InstanceLifecyclePhase =
14
+ | "opening" // installed by open(); markOpenComplete() not yet reached
15
+ | "active" // open() returned; current, unfenced, unpoisoned
16
+ | "poisoned" // poisonReplicationOwnership latched (survives until reopen)
17
+ | "terminating" // ownership controller aborted by close()/drop()/_close()
18
+ | "closed"; // host Program reports closed
19
+
20
+ export type InstanceLifecycleTerminalReason =
21
+ | "close"
22
+ | "drop"
23
+ | "internal-close";
24
+
25
+ export type InstanceLifecycleDeps = {
26
+ /** host._instanceLifecycle — identity anchor for isCurrent() */
27
+ getCurrentLifecycle: () => InstanceLifecycle | undefined;
28
+ /** host._closeController */
29
+ getCloseController: () => AbortController | undefined;
30
+ /** host._replicationRangeMutationFailure (poison latch, fence A2) */
31
+ getPoisonFailure: () => unknown;
32
+ /** host.closed */
33
+ isHostClosed: () => boolean;
34
+ /** host.isTerminating() — closed || parent-detach || closeController aborted */
35
+ isHostTerminating: () => boolean;
36
+ /** host._checkedPrune — identity only, never dereferenced (fence A3) */
37
+ getCheckedPruneCoordinator: () => object | undefined;
38
+ /** host._replicationRangeMutationsClosing (terminal lane fence, A4) */
39
+ areRangeMutationsClosing: () => boolean;
40
+ /** host._pruneRemovesClosing (terminal lane fence, A4) */
41
+ arePruneRemovesClosing: () => boolean;
42
+ /** host.pruneDebouncedFn — identity only */
43
+ getPruneDebouncer: () => object | undefined;
44
+ /** host.replicationChangeDebounceFn — identity only */
45
+ getReplicationChangeDebouncer: () => object | undefined;
46
+ /** host.rebalanceParticipationDebounced — identity only */
47
+ getRebalanceDebouncer: () => object | undefined;
48
+ };
49
+
50
+ export class InstanceLifecycle {
51
+ // The ONLY state that physically moved in stage 2 (absorbed the former
52
+ // SharedLog._localReplicationRoleGeneration; the accessor shim was deleted
53
+ // in stage 3). Bumped by replicate()'s
54
+ // fixed-role replacement and by full unreplication WITHOUT rotating the
55
+ // lifecycle: unreplicate() deliberately keeps the store open, so admitted
56
+ // adaptive planners are invalidated by this sub-generation, not by
57
+ // retiring the instance identity.
58
+ public roleGeneration = 0;
59
+
60
+ // Moved from SharedLog (fences C1/C2, same names — the sanctioned
61
+ // file-to-file ratchet move). Physically owned per-open counters read and
62
+ // written directly through the lifecycle owner; the fresh lifecycle object
63
+ // at open() IS the legacy reset-to-0 (same pattern as roleGeneration above).
64
+ //
65
+ // Receive-side ownership plans may span lower-log joins that invoke user
66
+ // code. Incremented synchronously with leader-cache invalidation so the
67
+ // handler can detect whether its pre-join plan needs one fresh
68
+ // post-persist audit.
69
+ public _receiveOwnershipRevision = 0;
70
+ // Count of ownership-changing range mutations from queue admission
71
+ // through settlement, including mutations already pending when a receive
72
+ // starts.
73
+ public _receiveOwnershipMutationAdmissions = 0;
74
+
75
+ // ---- stage 4: physically owned controllers (moved from SharedLog) ----
76
+ // Fences A1/A5; the index.ts ratchet entries _repairLifecycleController /
77
+ // _replicationLifecycleController moved file-to-file under these names.
78
+ // Rotate-with-the-lifecycle by construction: a fresh InstanceLifecycle IS
79
+ // the rotation of both controllers; within one lifecycle the ownership
80
+ // controller identity never changes (readonly) — poison and terminal
81
+ // paths abort it IN PLACE, exactly like the legacy un-reassigned host
82
+ // field. Background repair work can outlive the await that admitted it;
83
+ // the per-open replacement of this whole object keeps an older runner
84
+ // from dispatching into or mutating a freshly opened lifecycle.
85
+ //
86
+ // INVARIANT every rotation site must keep: whatever installs a new
87
+ // host._instanceLifecycle aborts the outgoing lifecycle's ownership
88
+ // controller in the same synchronous block (startRepairLifecycle does).
89
+ // The stale-capture predicates in isActiveFor lean on
90
+ // non-current ⇒ aborted now that the identity term compares against the
91
+ // owned field rather than the host-current controller.
92
+ public readonly ownershipLifecycleController = new AbortController();
93
+ // undefined until open() reaches resetSubscriptionChangeCallbackTracking
94
+ // (beginMembership) — preserves the legacy `?: AbortController` decl-site
95
+ // semantics for constructed-but-never-opened hosts.
96
+ public membershipLifecycleController?: AbortController;
97
+
98
+ // Stage-2 bookkeeping. WRITE-ONLY in production paths: no query method
99
+ // consults these (phase() is derived from the wrapped fences), so a
100
+ // missed or doubled transition cannot change behavior. Stage 3 flips
101
+ // phase() onto declaredPhase once the transitions have soaked in CI.
102
+ public declaredPhase: InstanceLifecyclePhase = "opening";
103
+ public terminalReason?: InstanceLifecycleTerminalReason;
104
+ public poisonCause?: unknown;
105
+
106
+ constructor(private readonly deps: InstanceLifecycleDeps) {}
107
+
108
+ // ---- rotation-point transitions (stage 2: diagnostics only) ----
109
+
110
+ markOpenComplete(): void {
111
+ if (this.declaredPhase === "opening") this.declaredPhase = "active";
112
+ }
113
+
114
+ beginTerminal(reason: InstanceLifecycleTerminalReason): void {
115
+ this.terminalReason ??= reason;
116
+ if (this.declaredPhase !== "poisoned") this.declaredPhase = "terminating";
117
+ }
118
+
119
+ markPoisoned(cause: unknown): void {
120
+ this.poisonCause ??= cause;
121
+ this.declaredPhase = "poisoned";
122
+ }
123
+
124
+ // ---- owned-controller transitions (stage 4) ----
125
+
126
+ /** ≡ legacy SharedLog.stopRepairLifecycle body (`controller?.abort()`),
127
+ * and the abort half of legacy startRepairLifecycle. Abort-in-place: the
128
+ * lifecycle identity is untouched (the poison contract — poison never
129
+ * rotates the lifecycle, so captures keep passing the identity term and
130
+ * fail on `signal.aborted`/poison exactly as before). */
131
+ abortOwnership(): void {
132
+ this.ownershipLifecycleController.abort();
133
+ }
134
+
135
+ /** ≡ the legacy guarded abort in stopSubscriptionChangeCallbackAdmission:
136
+ * `if (!c?.signal.aborted) c?.abort(reason)`. */
137
+ abortMembership(reason?: unknown): void {
138
+ if (!this.membershipLifecycleController?.signal.aborted) {
139
+ this.membershipLifecycleController?.abort(reason);
140
+ }
141
+ }
142
+
143
+ /** ≡ legacy `this._replicationLifecycleController = new AbortController()`
144
+ * in resetSubscriptionChangeCallbackTracking. Called exactly once per
145
+ * lifecycle, from open(); plain assignment kept (KEEP-OLD overwrite: the
146
+ * predecessor either is undefined or was aborted at close/drop). A
147
+ * direct out-of-open rotation (test-only) leaves the NEW lifecycle's
148
+ * membership undefined until the next open — a deliberate divergence
149
+ * from legacy, where the host field survived direct rotations; the two
150
+ * observables that path uses are pinned in the spec. */
151
+ beginMembership(): AbortController {
152
+ return (this.membershipLifecycleController = new AbortController());
153
+ }
154
+
155
+ // ---- role sub-generation (absorbed fence A6) ----
156
+
157
+ bumpRoleGeneration(): number {
158
+ return ++this.roleGeneration;
159
+ }
160
+
161
+ isRoleCurrent(capturedRoleGeneration: number): boolean {
162
+ return this.roleGeneration === capturedRoleGeneration;
163
+ }
164
+
165
+ // ---- identity ----
166
+
167
+ isCurrent(): boolean {
168
+ return this.deps.getCurrentLifecycle() === this;
169
+ }
170
+
171
+ // ---- derived phase (single source of truth = the wrapped fences) ----
172
+
173
+ phase(): InstanceLifecyclePhase {
174
+ if (this.deps.getPoisonFailure() !== undefined) return "poisoned";
175
+ if (this.deps.isHostClosed()) return "closed";
176
+ // The legacy `ownership == null` arm is gone: a lifecycle object owns
177
+ // its controller, so the absent-controller state is unrepresentable
178
+ // (clones have no lifecycle and cannot reach phase()).
179
+ if (!this.isCurrent() || this.ownershipLifecycleController.signal.aborted) {
180
+ return "terminating";
181
+ }
182
+ return this.declaredPhase === "opening" ? "opening" : "active";
183
+ }
184
+
185
+ // ---- ownership half: exact folds of the legacy predicates ----
186
+
187
+ /** ≡ SharedLog.isRepairLifecycleActive(controller). Stage 4 note: the
188
+ * identity term compares against the OWNED controller. For a stale
189
+ * captured lifecycle with its co-captured controller the term now
190
+ * passes where the legacy host-current compare failed; the predicate
191
+ * still returns false via `signal.aborted`, guaranteed by the rotation
192
+ * invariant (non-current ⇒ abortOwnership already ran — see the field
193
+ * comment above). */
194
+ isActiveFor(controller: AbortController | undefined): boolean {
195
+ return (
196
+ controller != null &&
197
+ controller === this.ownershipLifecycleController &&
198
+ !controller.signal.aborted &&
199
+ this.deps.getPoisonFailure() === undefined &&
200
+ !this.deps.isHostClosed()
201
+ );
202
+ }
203
+
204
+ /**
205
+ * Controller-free form for stage-3 seams. Because this object OWNS its
206
+ * ownership controller (stage 4), lifecycle and controller rotate
207
+ * together by construction, so `capturedLifecycle.isActive()` ⇔
208
+ * `isRepairLifecycleActive(capturedController)` for captures taken
209
+ * together — the equivalence the unit spec asserts.
210
+ */
211
+ isActive(): boolean {
212
+ return (
213
+ this.isCurrent() && this.isActiveFor(this.ownershipLifecycleController)
214
+ );
215
+ }
216
+
217
+ /** ≡ SharedLog.throwIfReplicationOwnershipPoisoned — message and `cause`
218
+ * must stay byte-identical for stage-3 drop-in. */
219
+ throwIfPoisoned(): void {
220
+ const failure = this.deps.getPoisonFailure();
221
+ if (failure !== undefined) {
222
+ throw new Error(
223
+ "Replication ownership recovery is required before further planning",
224
+ { cause: failure },
225
+ );
226
+ }
227
+ }
228
+
229
+ /** ≡ SharedLog.throwIfReplicationOwnershipLifecycleInactive. */
230
+ throwIfInactive(
231
+ controller: AbortController | undefined = this.ownershipLifecycleController,
232
+ ): void {
233
+ this.throwIfPoisoned();
234
+ if (!(this.isCurrent() && this.isActiveFor(controller))) {
235
+ throw new TerminalOperationNotStartedError(
236
+ "Replication ownership lifecycle is no longer active",
237
+ );
238
+ }
239
+ }
240
+
241
+ /** ≡ SharedLog.captureReplicationOwnershipLifecycle, but returns the
242
+ * lifecycle; stage-3 sites capture this instead of the raw controller
243
+ * (which stays reachable via ownershipSignal()). */
244
+ capture(): InstanceLifecycle {
245
+ this.throwIfInactive();
246
+ return this;
247
+ }
248
+
249
+ /** AbortControllers stay the signal carriers. */
250
+ ownershipSignal(): AbortSignal | undefined {
251
+ return this.ownershipLifecycleController.signal;
252
+ }
253
+
254
+ // ---- membership half (fence A5): exact fold ----
255
+
256
+ /** ≡ SharedLog.isReplicationLifecycleActive(controller).
257
+ * NOTE the asymmetry vs isActiveFor: this side uses isTerminating()
258
+ * (closeController + parent-detach), not `closed` — preserve exactly. */
259
+ isMembershipActiveFor(controller: AbortController | undefined): boolean {
260
+ return (
261
+ controller != null &&
262
+ controller === this.membershipLifecycleController &&
263
+ !controller.signal.aborted &&
264
+ !this.deps.isHostTerminating()
265
+ );
266
+ }
267
+
268
+ membershipSignal(): AbortSignal | undefined {
269
+ return this.membershipLifecycleController?.signal;
270
+ }
271
+
272
+ closeSignal(): AbortSignal | undefined {
273
+ return this.deps.getCloseController()?.signal;
274
+ }
275
+
276
+ // ---- checked-prune identity (seams 4 and 7) ----
277
+
278
+ /** ≡ prune()'s isCheckedPruneLifecycleCurrent triple for captures taken
279
+ * at the same admission point. Stage 3 extension: an omitted
280
+ * `closeController` SKIPS that term (the non-prune() checked-prune seats
281
+ * never compared it — adding the compare would strengthen their
282
+ * predicates), and `controller` lets a seat honor an explicitly threaded
283
+ * (possibly stale) ownership capture instead of the current one. */
284
+ isCheckedPruneCurrent(
285
+ coordinator: object | undefined,
286
+ closeController?: AbortController,
287
+ controller: AbortController | undefined = this.ownershipLifecycleController,
288
+ ): boolean {
289
+ return (
290
+ this.isCurrent() &&
291
+ this.isActiveFor(controller) &&
292
+ coordinator === this.deps.getCheckedPruneCoordinator() &&
293
+ (closeController === undefined ||
294
+ closeController === this.deps.getCloseController())
295
+ );
296
+ }
297
+
298
+ /** Exact fold of prune()'s throwIfCheckedPruneLifecycleInactive and of
299
+ * revalidateCheckedPruneOwnership's controller branch (there with the
300
+ * closeController term omitted). Error order MUST stay: poison Error →
301
+ * ownership TerminalOperationNotStartedError ("Replication ownership
302
+ * lifecycle is no longer active") → checked-prune
303
+ * TerminalOperationNotStartedError ("Checked prune lifecycle is no
304
+ * longer active"). */
305
+ throwIfCheckedPruneInactive(
306
+ coordinator: object | undefined,
307
+ closeController?: AbortController,
308
+ controller: AbortController | undefined = this.ownershipLifecycleController,
309
+ ): void {
310
+ this.throwIfInactive(controller);
311
+ if (
312
+ coordinator !== this.deps.getCheckedPruneCoordinator() ||
313
+ (closeController !== undefined &&
314
+ closeController !== this.deps.getCloseController())
315
+ ) {
316
+ throw new TerminalOperationNotStartedError(
317
+ "Checked prune lifecycle is no longer active",
318
+ );
319
+ }
320
+ }
321
+
322
+ // ---- terminal lane fences (A4): exposed, NOT folded into isActive ----
323
+ // Folding them would change which TerminalOperationNotStartedError message
324
+ // the admission sites throw ("Replication range mutations are closing",
325
+ // "Prune removals are closing") — a behavior change.
326
+
327
+ areRangeMutationsClosing(): boolean {
328
+ return this.deps.areRangeMutationsClosing();
329
+ }
330
+
331
+ arePruneRemovesClosing(): boolean {
332
+ return this.deps.arePruneRemovesClosing();
333
+ }
334
+
335
+ // ---- debouncer identities (seam 6 et al.) ----
336
+ // Bare === on purpose: at rebalanceParticipation the captured default
337
+ // parameter may be undefined and `undefined === undefined` legitimately
338
+ // passes today. A null-guard would break that.
339
+
340
+ isPruneDebouncerCurrent(fn: object | undefined): boolean {
341
+ return fn === this.deps.getPruneDebouncer();
342
+ }
343
+
344
+ isReplicationChangeDebouncerCurrent(fn: object | undefined): boolean {
345
+ return fn === this.deps.getReplicationChangeDebouncer();
346
+ }
347
+
348
+ isRebalanceDebouncerCurrent(fn: object | undefined): boolean {
349
+ return fn === this.deps.getRebalanceDebouncer();
350
+ }
351
+ }