@serfab/cadre-core 0.12.0 → 1.0.0

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 (155) hide show
  1. package/README.md +23 -8
  2. package/dist/bootstrap-peer-store-file.js +2 -2
  3. package/dist/bootstrap-peer-store.d.ts +32 -12
  4. package/dist/bootstrap-peer-store.js +41 -15
  5. package/dist/bootstrap-peer-store.js.map +1 -1
  6. package/dist/cadre-node.d.ts +883 -102
  7. package/dist/cadre-node.js +1800 -209
  8. package/dist/cadre-node.js.map +1 -1
  9. package/dist/control-authorization.d.ts +2 -2
  10. package/dist/control-authorization.js +1 -0
  11. package/dist/control-authorization.js.map +1 -1
  12. package/dist/control-cohort.d.ts +13 -26
  13. package/dist/control-cohort.js +0 -26
  14. package/dist/control-cohort.js.map +1 -1
  15. package/dist/control-database.d.ts +318 -44
  16. package/dist/control-database.js +516 -74
  17. package/dist/control-database.js.map +1 -1
  18. package/dist/control-read-retry.d.ts +40 -11
  19. package/dist/control-read-retry.js +48 -15
  20. package/dist/control-read-retry.js.map +1 -1
  21. package/dist/control-retry.d.ts +58 -3
  22. package/dist/control-retry.js +58 -3
  23. package/dist/control-retry.js.map +1 -1
  24. package/dist/control-schema.d.ts +1 -1
  25. package/dist/control-schema.js +133 -21
  26. package/dist/control-schema.js.map +1 -1
  27. package/dist/control-write-retry.d.ts +80 -11
  28. package/dist/control-write-retry.js +168 -29
  29. package/dist/control-write-retry.js.map +1 -1
  30. package/dist/enrolled-machine-store-file.d.ts +13 -0
  31. package/dist/enrolled-machine-store-file.js +41 -0
  32. package/dist/enrolled-machine-store-file.js.map +1 -0
  33. package/dist/enrolled-machine-store.d.ts +81 -0
  34. package/dist/enrolled-machine-store.js +225 -0
  35. package/dist/enrolled-machine-store.js.map +1 -0
  36. package/dist/file-durable-slot.js +5 -3
  37. package/dist/file-durable-slot.js.map +1 -1
  38. package/dist/index.d.ts +17 -8
  39. package/dist/index.js +54 -9
  40. package/dist/index.js.map +1 -1
  41. package/dist/membership-connection-gater.d.ts +15 -1
  42. package/dist/membership-connection-gater.js +9 -3
  43. package/dist/membership-connection-gater.js.map +1 -1
  44. package/dist/node-local-snapshot.d.ts +21 -7
  45. package/dist/node-local-snapshot.js +30 -9
  46. package/dist/node-local-snapshot.js.map +1 -1
  47. package/dist/peer-dial.d.ts +143 -0
  48. package/dist/peer-dial.js +176 -0
  49. package/dist/peer-dial.js.map +1 -0
  50. package/dist/{strand-backfill.d.ts → peer-join-backfill.d.ts} +49 -20
  51. package/dist/{strand-backfill.js → peer-join-backfill.js} +133 -52
  52. package/dist/peer-join-backfill.js.map +1 -0
  53. package/dist/relay-addrs.d.ts +160 -49
  54. package/dist/relay-addrs.js +287 -60
  55. package/dist/relay-addrs.js.map +1 -1
  56. package/dist/relay-reservation.d.ts +71 -3
  57. package/dist/relay-reservation.js +116 -8
  58. package/dist/relay-reservation.js.map +1 -1
  59. package/dist/seed-bootstrap.d.ts +30 -1
  60. package/dist/seed-bootstrap.js +62 -17
  61. package/dist/seed-bootstrap.js.map +1 -1
  62. package/dist/storage-scope.d.ts +62 -0
  63. package/dist/storage-scope.js +75 -0
  64. package/dist/storage-scope.js.map +1 -0
  65. package/dist/strand-database.d.ts +41 -11
  66. package/dist/strand-database.js +48 -18
  67. package/dist/strand-database.js.map +1 -1
  68. package/dist/strand-first-sync-gate.d.ts +152 -0
  69. package/dist/strand-first-sync-gate.js +208 -0
  70. package/dist/strand-first-sync-gate.js.map +1 -0
  71. package/dist/strand-formation-manager.d.ts +99 -5
  72. package/dist/strand-formation-manager.js +119 -8
  73. package/dist/strand-formation-manager.js.map +1 -1
  74. package/dist/strand-formation-protocol.d.ts +97 -3
  75. package/dist/strand-formation-protocol.js +108 -2
  76. package/dist/strand-formation-protocol.js.map +1 -1
  77. package/dist/strand-instance-manager.d.ts +389 -19
  78. package/dist/strand-instance-manager.js +678 -65
  79. package/dist/strand-instance-manager.js.map +1 -1
  80. package/dist/strand-member-key.d.ts +20 -12
  81. package/dist/strand-member-key.js +20 -12
  82. package/dist/strand-member-key.js.map +1 -1
  83. package/dist/strand-member-registry.js +3 -0
  84. package/dist/strand-member-registry.js.map +1 -1
  85. package/dist/strand-membership-reconciler.d.ts +361 -0
  86. package/dist/strand-membership-reconciler.js +548 -0
  87. package/dist/strand-membership-reconciler.js.map +1 -0
  88. package/dist/strand-membership-writer.d.ts +143 -11
  89. package/dist/strand-membership-writer.js +419 -135
  90. package/dist/strand-membership-writer.js.map +1 -1
  91. package/dist/strand-network-config.d.ts +116 -0
  92. package/dist/strand-network-config.js +192 -0
  93. package/dist/strand-network-config.js.map +1 -0
  94. package/dist/strand-revocation-enforcer.d.ts +392 -0
  95. package/dist/strand-revocation-enforcer.js +533 -0
  96. package/dist/strand-revocation-enforcer.js.map +1 -0
  97. package/dist/strand-solicitation.d.ts +17 -1
  98. package/dist/strand-solicitation.js +13 -2
  99. package/dist/strand-solicitation.js.map +1 -1
  100. package/dist/strand-wake-protocol.d.ts +11 -7
  101. package/dist/strand-wake-protocol.js +20 -52
  102. package/dist/strand-wake-protocol.js.map +1 -1
  103. package/dist/strand-watcher.js +12 -1
  104. package/dist/strand-watcher.js.map +1 -1
  105. package/dist/timed-step.d.ts +12 -0
  106. package/dist/timed-step.js +28 -0
  107. package/dist/timed-step.js.map +1 -0
  108. package/dist/timeout-scheduler.d.ts +19 -0
  109. package/dist/timeout-scheduler.js +22 -0
  110. package/dist/timeout-scheduler.js.map +1 -0
  111. package/dist/types.d.ts +472 -46
  112. package/dist/types.js +7 -1
  113. package/dist/types.js.map +1 -1
  114. package/package.json +16 -8
  115. package/src/bootstrap-peer-store-file.ts +2 -2
  116. package/src/bootstrap-peer-store.ts +69 -27
  117. package/src/cadre-node.ts +1926 -228
  118. package/src/control-authorization.ts +2 -1
  119. package/src/control-cohort.ts +122 -130
  120. package/src/control-database.ts +586 -74
  121. package/src/control-read-retry.ts +202 -167
  122. package/src/control-retry.ts +297 -190
  123. package/src/control-schema.ts +133 -21
  124. package/src/control-write-retry.ts +525 -383
  125. package/src/enrolled-machine-store-file.ts +43 -0
  126. package/src/enrolled-machine-store.ts +280 -0
  127. package/src/file-durable-slot.ts +5 -3
  128. package/src/index.ts +134 -15
  129. package/src/membership-connection-gater.ts +9 -3
  130. package/src/node-local-snapshot.ts +38 -9
  131. package/src/peer-addr-book.ts +251 -250
  132. package/src/peer-dial.ts +215 -0
  133. package/src/{strand-backfill.ts → peer-join-backfill.ts} +478 -389
  134. package/src/relay-addrs.ts +458 -218
  135. package/src/relay-reservation.ts +951 -828
  136. package/src/seed-bootstrap.ts +76 -18
  137. package/src/storage-scope.ts +78 -0
  138. package/src/strand-database.ts +61 -21
  139. package/src/strand-first-sync-gate.ts +258 -0
  140. package/src/strand-formation-manager.ts +162 -8
  141. package/src/strand-formation-protocol.ts +850 -692
  142. package/src/strand-instance-manager.ts +877 -74
  143. package/src/strand-member-key.ts +20 -12
  144. package/src/strand-member-registry.ts +3 -0
  145. package/src/strand-membership-reconciler.ts +680 -0
  146. package/src/strand-membership-writer.ts +550 -190
  147. package/src/strand-network-config.ts +250 -0
  148. package/src/strand-revocation-enforcer.ts +653 -0
  149. package/src/strand-solicitation.ts +499 -471
  150. package/src/strand-wake-protocol.ts +21 -66
  151. package/src/strand-watcher.ts +325 -314
  152. package/src/timed-step.ts +28 -0
  153. package/src/timeout-scheduler.ts +28 -0
  154. package/src/types.ts +487 -52
  155. package/dist/strand-backfill.js.map +0 -1
@@ -0,0 +1,548 @@
1
+ /**
2
+ * Strand membership reconciliation: the per-instance background loop that finishes a
3
+ * party's join at strand bring-up. Every machine of a party that runs a closed strand
4
+ * ends up with (1) the party's `Strand.Member` row seated — redeeming the
5
+ * formation-staged invitation if one is pending — and (2) its own durable
6
+ * machine→party binding (`Strand.MemberPeer`) written, which is what revocation
7
+ * enforcement and future admission control key on.
8
+ *
9
+ * ## Contract
10
+ *
11
+ * - **Never blocks or fails bring-up.** Armed by `StrandInstanceManager.buildStrandRuntime`
12
+ * right after the strand database initializes (launch AND hibernation resume — the one
13
+ * seam where a transport peer id and a live `Database` both exist), and every pass is
14
+ * fully contained: a joiner at that instant has typically not synced the founder's rows
15
+ * and may lack write quorum, so failure means "retry next tick", never a throw.
16
+ * - **Kicked the moment its database appears.** A joining machine's first pass runs while
17
+ * the first-sync write gate still withholds `instance.database`, so it finds no database
18
+ * and returns at once. `StrandInstanceManager.publishDatabase` calls `reconcile()` as it
19
+ * hands the database to the app, which is what makes the join finish about a second after
20
+ * the strand becomes writable rather than on the next timer.
21
+ * - **Retries fast while the join is unfinished, slowly while it is only waiting.** A pass
22
+ * that leaves the join unfinished re-arms on a ladder starting at
23
+ * {@link INITIAL_JOIN_RETRY_INTERVAL_MS} and doubling, capped at the configured poll
24
+ * interval (default {@link DEFAULT_REVOCATION_POLL_INTERVAL_MS}; an embedder's
25
+ * `revocationEnforcement.pollIntervalMs` is mirrored here by the instance manager). A pass
26
+ * that finds no member row AND no staged invitation is not an unfinished join but a wait
27
+ * for someone to admit this party — nothing this machine can do faster — so it re-arms on
28
+ * the flat poll interval and resets the ladder. The loop stops on the done state.
29
+ * - **Done state**: the party's member row is visible locally AND this machine's own
30
+ * binding is in place — the loop stops for good. A LATER revocation is the enforcer's
31
+ * business, not this loop's; a resume rebuilds the reconciler and re-verifies from
32
+ * scratch (every write is idempotent, so re-running is free).
33
+ *
34
+ * ## One pass
35
+ *
36
+ * 1. **Self-revocation check.** If the enforcer flags this node's own party as revoked,
37
+ * stop rather than fight — re-admission arrives (if ever) via a fresh formation.
38
+ * 2. **Ensure membership.** Member row visible → done with this step; if a staged
39
+ * invitation is still unspent, BURN it ({@link burnInvite} — the `ConsumedInvite` row
40
+ * alone) so the bearer credential cannot be spent by anyone else, ignoring burn
41
+ * failures beyond a log. Member row absent + staged invitation → {@link consumeInvite}
42
+ * under the party's own key, retried across passes until the `Invite` row has
43
+ * replicated here and the write commits. Absent with no invitation (founder rows not
44
+ * yet synced, or a machine whose party never joined) → keep waiting, escalating to one
45
+ * visible warning after {@link IDLE_PASSES_BEFORE_ESCALATION} passes, never throwing.
46
+ * 3. **Ensure the binding.** {@link registerMemberPeer} (insert-if-absent, restart-safe)
47
+ * with this node's own strand transport peer id — only after step 2 sees the member
48
+ * row locally (its deferred `MemberExists` reads the live local table).
49
+ *
50
+ * ## Terminal states (stop without done)
51
+ *
52
+ * - **Sealed strand**: `consumeInvite` rejected by `ConsumedInvite.NotSealed` — nobody
53
+ * can ever be admitted, so retrying forever is noise. Terminal-with-log.
54
+ * - **Self-revoked** (step 1 above).
55
+ * - **Undecodable party key**: nothing can be signed; loud log, stop.
56
+ *
57
+ * A DEAD invitation that is not terminal for the loop — expired, cancelled, or already
58
+ * consumed by someone else (`ConsumedInvite`'s primary key) — is dropped with a log and
59
+ * the loop keeps idling: a fresh formation stages a new invitation, and a manager-side
60
+ * admission (`addMemberByManager`) seats the member row without one.
61
+ *
62
+ * ## Sharing the database with the app
63
+ *
64
+ * The strand `Database` this loop writes to is the one the app holds. Every write here
65
+ * passes `joinOpenTransaction: false`, so it runs as a transaction of its own and never
66
+ * joins one the app has open: the writer refuses with `StrandTransactionBusyError`, having
67
+ * written nothing, and the pass retries on the ladder. A busy refusal is never read as a
68
+ * dead invitation, and a busy burn leaves the invitation staged for the next pass.
69
+ *
70
+ * ## Half-committed join
71
+ *
72
+ * On a networked strand optimystic commits `Member` and `ConsumedInvite` as separate
73
+ * collections and can report that only some of them were saved (`CoordinatorPartialCommitError`,
74
+ * or the plugin's legacy `PartialCommitError`). When `ConsumedInvite` is saved and `Member` is
75
+ * not, the invitation is spent and the schema seats a `Member` through an invitation only in the
76
+ * same transaction as a fresh consumption, so this loop can never seat the party from it. Such a
77
+ * failure is recognised by TYPE before any message text is read (its message names the
78
+ * `ConsumedInvite` collection, which a text match once mistook for a dead invitation), reported
79
+ * with ONE `console.warn`, and the invitation is dropped. The loop keeps running like it does
80
+ * after a dead invitation: a manager admission, or a `Member` row that did land, is picked up by
81
+ * the next pass, which then writes the binding. Whether sereus should repair such a join itself
82
+ * is `blocked/strand-half-committed-join-recovery`.
83
+ */
84
+ import debug from 'debug';
85
+ import { CoordinatorPartialCommitError } from '@optimystic/db-core';
86
+ import { PartialCommitError } from '@optimystic/quereus-plugin-optimystic';
87
+ import { causeChain } from './control-retry.js';
88
+ import { strandMemberKeyPair } from './strand-member-key.js';
89
+ import { StrandTransactionBusyError, burnInvite, consumeInvite, isStrandMember, registerMemberPeer, } from './strand-membership-writer.js';
90
+ import { DEFAULT_REVOCATION_POLL_INTERVAL_MS } from './strand-revocation-enforcer.js';
91
+ import { defaultTimeoutScheduler } from './timeout-scheduler.js';
92
+ const log = debug('sereus:cadre:strand-membership-reconciler');
93
+ /**
94
+ * Idle passes (no member row, no staged invitation) before ONE `console.warn`
95
+ * escalation — at the default 30 s cadence, about five minutes of waiting. The loop
96
+ * keeps retrying quietly after it; the warning exists so a joiner stuck waiting on
97
+ * founder-row replication is visible without the debug namespace enabled.
98
+ */
99
+ export const IDLE_PASSES_BEFORE_ESCALATION = 10;
100
+ /**
101
+ * First retry delay, ms, for a pass that left the join UNFINISHED — a `consumeInvite`
102
+ * whose `Strand.Invite` row has not replicated to this machine yet, a cohort briefly
103
+ * unwritable, a database not yet published. Doubles per such pass up to the configured
104
+ * poll interval. Sized for what it retries: invite-row replication resolves in about a
105
+ * second over a direct connection and a few over a relay. The poll interval is NOT sized
106
+ * for that — it is the revocation enforcer's deny-set refresh cadence, mirrored here for
107
+ * the idle case (waiting to be admitted at all), which is why this ladder exists instead
108
+ * of a shorter interval.
109
+ */
110
+ export const INITIAL_JOIN_RETRY_INTERVAL_MS = 1000;
111
+ /** Every write this loop makes runs in a transaction of its own — see "Sharing the database with the app". */
112
+ const OWN_TRANSACTION = { joinOpenTransaction: false };
113
+ /**
114
+ * The `consumeInvite` rejection that means the strand is SEALED — `ConsumedInvite.NotSealed`
115
+ * fired, which Quereus renders `CHECK constraint failed: NotSealed (<its expression>)`.
116
+ * Terminal for a non-member: nobody can ever be admitted again.
117
+ */
118
+ const SEALED_REJECTION = /CHECK constraint failed: NotSealed\b/;
119
+ /**
120
+ * `consumeInvite` rejections that mean the INVITATION is dead but the loop should keep
121
+ * waiting for membership to arrive another way: expired (`CHECK constraint failed: NotExpired`),
122
+ * cancelled (`CHECK constraint failed: NotCancelled`), or already consumed by someone else
123
+ * (`UNIQUE constraint failed: ConsumedInvite.InviteKey`, the primary key).
124
+ *
125
+ * Anchored to the engine's full constraint-failure texts, not the bare names: other failures
126
+ * embed those names — a half-committed join's message names the `default/strand/ConsumedInvite`
127
+ * collection, which the bare `ConsumedInvite` this used to match read as a dead invitation. The
128
+ * texts are pinned against the real engine in `strand-membership-reconciler.spec.ts`, so a
129
+ * rewording in Quereus fails a spec instead of silently turning a dead invitation into an
130
+ * endless retry.
131
+ */
132
+ const DEAD_INVITE_REJECTION = /CHECK constraint failed: (?:NotExpired|NotCancelled)\b|UNIQUE constraint failed: ConsumedInvite\.InviteKey\b/;
133
+ /**
134
+ * Route a `consumeInvite` rejection. Typed checks run before any text check: a busy refusal
135
+ * (another transaction was open, so nothing ran) and a half-committed join are recognised by
136
+ * their error's type anywhere in the `cause` chain. The busy refusal carries Quereus's own
137
+ * refusal as its cause, and a half-committed join's message embeds collection names and the
138
+ * underlying failure's text, either of which a text matcher can misread. The sealed and
139
+ * dead-invitation checks then read the top-level message.
140
+ *
141
+ * A second loaded copy of `@optimystic/db-core` (or of the plugin) would fail `instanceof`; the
142
+ * half-commit then falls through the anchored texts to `retry`, and once the saved `ConsumedInvite`
143
+ * row is visible here a later attempt fails on its primary key and drops the invitation quietly —
144
+ * the old silent outcome, a few passes later, never a wrong write.
145
+ */
146
+ export function classifyConsumeFailure(error) {
147
+ if (isTransactionBusy(error))
148
+ return { kind: 'busy' };
149
+ const halfCommitted = halfCommittedJoin(error);
150
+ if (halfCommitted)
151
+ return halfCommitted;
152
+ const message = errorMessage(error);
153
+ if (SEALED_REJECTION.test(message))
154
+ return { kind: 'sealed' };
155
+ if (DEAD_INVITE_REJECTION.test(message))
156
+ return { kind: 'dead-invite' };
157
+ return { kind: 'retry' };
158
+ }
159
+ /**
160
+ * The first partial-commit error in the `cause` chain, as the lists it reports.
161
+ *
162
+ * NOTE: `PartialCommitError` is imported from the plugin's root entry while strands register the
163
+ * plugin through its `/plugin` entry (`quereus-plugin-sereus`'s `compose-strand.ts`); the two are
164
+ * one class only because the plugin's build emits both entries over one shared chunk — the same
165
+ * dependency, and the same remedy if upstream ever bundles them apart, as the NOTE on
166
+ * `reportsPossiblyStoredWrite` in `control-write-retry.ts`.
167
+ */
168
+ function halfCommittedJoin(error) {
169
+ for (const link of causeChain(error)) {
170
+ if (link instanceof CoordinatorPartialCommitError) {
171
+ return { kind: 'half-committed', saved: link.committedCollections, unsaved: link.failedCollections };
172
+ }
173
+ if (link instanceof PartialCommitError) {
174
+ return { kind: 'half-committed', saved: link.persisted, unsaved: link.unpersisted };
175
+ }
176
+ }
177
+ return undefined;
178
+ }
179
+ /** Whether a writer refused because another transaction was open (see "Sharing the database with the app"). */
180
+ function isTransactionBusy(error) {
181
+ return causeChain(error).some((link) => link instanceof StrandTransactionBusyError);
182
+ }
183
+ function errorMessage(error) {
184
+ return error instanceof Error ? error.message : String(error);
185
+ }
186
+ /**
187
+ * The per-strand membership reconciliation loop. Lifecycle mirrors
188
+ * `StrandRevocationEnforcer`: created in `StrandInstanceManager.buildStrandRuntime`
189
+ * (closed strands with a party key only), started right after the strand database
190
+ * initializes, stopped and dropped in `releaseRuntime` — so quiesce → resume rebuilds
191
+ * it and re-runs the (idempotent) ladder from scratch.
192
+ */
193
+ export class StrandMembershipReconciler {
194
+ constructor(deps, config) {
195
+ this.deps = deps;
196
+ /** Tail of the pass chain — serializes passes (no two in flight). */
197
+ this.tail = Promise.resolve();
198
+ this.started = false;
199
+ this.stoppedFlag = false;
200
+ this.doneFlag = false;
201
+ this.idlePasses = 0;
202
+ this.idleEscalated = false;
203
+ /**
204
+ * Whether the pass that just ran found nothing to act on — no member row and no staged
205
+ * invitation. That is a wait to be admitted, not an unfinished join, so it re-arms on the
206
+ * flat poll interval; every other unfinished outcome climbs the ladder.
207
+ *
208
+ * NOTE: the ladder is therefore the DEFAULT for any outcome nobody classified — a new
209
+ * early return added to {@link doPass} inherits the fast retry unless it calls
210
+ * {@link noteIdlePass}. Right for every outcome that exists today (each is an unfinished
211
+ * join this machine can make progress on). If an early return is ever added for a
212
+ * condition retrying cannot resolve, classify it idle.
213
+ */
214
+ this.lastPassIdle = false;
215
+ this.scheduler = deps.scheduler ?? defaultTimeoutScheduler;
216
+ this.pollIntervalMs = config?.pollIntervalMs ?? DEFAULT_REVOCATION_POLL_INTERVAL_MS;
217
+ }
218
+ /** True once member row + own binding were both confirmed and the loop stopped. */
219
+ get done() {
220
+ return this.doneFlag;
221
+ }
222
+ /** True once the loop has stopped — done, terminal, or externally stopped. */
223
+ get stopped() {
224
+ return this.stoppedFlag;
225
+ }
226
+ /**
227
+ * Kick an immediate pass (not awaited — bring-up never blocks on it). Every later pass
228
+ * is armed only once the previous one settles — see the retry ladder in the module doc —
229
+ * so there is no repeating interval to arm here.
230
+ */
231
+ start() {
232
+ if (this.started || this.stoppedFlag)
233
+ return;
234
+ this.started = true;
235
+ void this.reconcile();
236
+ log('[%s] membership reconciler started (retry from %dms, idle poll %dms)', this.deps.label, Math.min(INITIAL_JOIN_RETRY_INTERVAL_MS, this.pollIntervalMs), this.pollIntervalMs);
237
+ }
238
+ /**
239
+ * Resolve once no pass is in flight. {@link stop} only disarms the retry TIMER — a pass
240
+ * already past its stopped check runs to completion, so a caller that must know the
241
+ * loop can no longer write (`StrandInstanceManager.clearOwnMemberPeerBinding`, which
242
+ * would otherwise have its removal undone by a racing `registerMemberPeer`) awaits
243
+ * this after stopping. Never rejects: the pass chain contains every failure.
244
+ */
245
+ async settle() {
246
+ await this.tail;
247
+ }
248
+ /** Disarm the retry timer; a pass already in flight completes but writes idempotently. */
249
+ stop() {
250
+ if (this.stoppedFlag)
251
+ return;
252
+ this.stoppedFlag = true;
253
+ this.clearTimer();
254
+ log('[%s] membership reconciler stopped%s', this.deps.label, this.doneFlag ? ' (done)' : '');
255
+ }
256
+ /**
257
+ * Run one pass now. Never rejects. Serialized: concurrent calls chain, so no two
258
+ * passes overlap and each explicit call gets a pass that STARTS after the call. On a
259
+ * started loop the pass re-arms the retry timer where it settles, so an explicit call —
260
+ * `StrandInstanceManager.publishDatabase`'s kick when a joiner's database is finally
261
+ * published — REPLACES the pending timer rather than running alongside it.
262
+ */
263
+ reconcile() {
264
+ // The catch is what keeps the chain alive. `doPass` contains its own failures, but its
265
+ // `finally` calls into the injected scheduler, and a throw from there would leave `tail`
266
+ // REJECTED — after which every later `reconcile()` short-circuits on it and the join
267
+ // stalls for good, silently. Swallowing it here makes the "never rejects" contract this
268
+ // method, {@link settle} and the two `void`-ed call sites all rely on structural rather
269
+ // than incidental.
270
+ const run = this.tail.then(() => this.doPass()).catch((error) => {
271
+ log('[%s] reconcile pass threw outside its own handler — the chain continues: %o', this.deps.label, error);
272
+ });
273
+ this.tail = run;
274
+ return run;
275
+ }
276
+ /** One serialized pass; contains every failure (contract: never rejects). */
277
+ async doPass() {
278
+ if (this.stoppedFlag || this.doneFlag)
279
+ return;
280
+ this.lastPassIdle = false;
281
+ try {
282
+ if (this.deps.isSelfRevoked?.() === true) {
283
+ // NOTE: "arrives via a fresh formation" is aspirational, not current behaviour.
284
+ // A fresh formation stages a new invitation (`adoptFormationMembershipInvite`)
285
+ // but does NOT re-arm this loop — `finish` latches `stoppedFlag` and `start()`
286
+ // early-returns on it, so only a strand relaunch (quiesce → resume, or a process
287
+ // restart) builds a reconciler that would redeem it. The same latch applies on the
288
+ // `done` path below, which is the case a REMOVED party actually hits: it finished
289
+ // its first join long before it was removed. Measured end to end in
290
+ // `strand-party-removal-via-formation-e2e.integration.ts` (test 2), tracked as
291
+ // `backlog/bug-removed-party-cannot-redeem-its-way-back`.
292
+ this.finish('this party is revoked from the strand — re-admission arrives (if ever) via a fresh formation');
293
+ return;
294
+ }
295
+ const keyPair = this.resolveKeyPair();
296
+ if (!keyPair)
297
+ return; // undecodable key already stopped the loop
298
+ const db = this.deps.getDatabase();
299
+ if (!db)
300
+ return; // no live database this instant (quiesce race) — next tick decides
301
+ if (!(await this.ensureMembership(db, keyPair)))
302
+ return;
303
+ // NOTE: the redemption (`Member` + `ConsumedInvite`) and the binding below are two
304
+ // SEPARATE commits — measured at 27 and 18 `/cluster` streams on 2026-09-17.
305
+ // One write batch could carry both (`MemberPeer.MemberExists`
306
+ // reads the LIVE `Member` table, and `MemberPeer.Authorized`'s add branch only verifies a
307
+ // self-signature over the new row), worth perhaps a third of that plus one commit
308
+ // round-trip. Left as two deliberately: the saving is unmeasured, and merging them merges
309
+ // their failure modes — today a redemption that lands but reports torn simply heals on the
310
+ // next pass, which sees the member row and proceeds to the binding. If a joiner's write
311
+ // cost ever shows up in a measurement, try the single transaction and measure against the
312
+ // 27 + 18 baseline.
313
+ await this.ensureBinding(db, keyPair);
314
+ }
315
+ catch (error) {
316
+ log('[%s] reconcile pass failed — retrying next tick: %o', this.deps.label, error);
317
+ }
318
+ finally {
319
+ this.scheduleNext();
320
+ }
321
+ }
322
+ /**
323
+ * Arm the next pass, now that this one has settled — so a slow pass never stacks ticks
324
+ * behind it. Only a STARTED loop schedules: a caller driving {@link reconcile} by hand
325
+ * (the unit tests, and the publish kick on a launch whose `start()` has not run yet) gets
326
+ * exactly the passes it asks for and no background timer.
327
+ */
328
+ scheduleNext() {
329
+ if (!this.started || this.stoppedFlag || this.doneFlag)
330
+ return;
331
+ this.clearTimer();
332
+ const delayMs = this.nextDelayMs();
333
+ this.timer = this.scheduler.setTimeout(() => {
334
+ this.timer = undefined;
335
+ void this.reconcile();
336
+ }, delayMs);
337
+ }
338
+ /**
339
+ * The delay before the next pass: the flat poll interval while the loop is merely idling
340
+ * (no member row, no staged invitation — {@link noteIdlePass}), otherwise the next rung
341
+ * of the doubling ladder from {@link INITIAL_JOIN_RETRY_INTERVAL_MS}, capped at the poll
342
+ * interval. A pass that lands idle resets the ladder, so a later unfinished join starts
343
+ * over at the bottom rung.
344
+ */
345
+ nextDelayMs() {
346
+ if (this.lastPassIdle) {
347
+ this.retryDelayMs = undefined;
348
+ return this.pollIntervalMs;
349
+ }
350
+ this.retryDelayMs = Math.min(this.retryDelayMs === undefined ? INITIAL_JOIN_RETRY_INTERVAL_MS : this.retryDelayMs * 2, this.pollIntervalMs);
351
+ return this.retryDelayMs;
352
+ }
353
+ clearTimer() {
354
+ if (this.timer !== undefined) {
355
+ this.scheduler.clearTimeout(this.timer);
356
+ this.timer = undefined;
357
+ }
358
+ }
359
+ /**
360
+ * Step 2 of the pass: `true` iff the party's member row is visible locally by the
361
+ * time this returns (already present, or seated by redeeming the staged invitation).
362
+ */
363
+ async ensureMembership(db, keyPair) {
364
+ if (await isStrandMember(db, keyPair.publicKeyB64)) {
365
+ await this.burnLeftoverInvite(db, keyPair);
366
+ return true;
367
+ }
368
+ const invite = this.deps.pendingInvite?.get();
369
+ if (!invite) {
370
+ this.noteIdlePass();
371
+ return false;
372
+ }
373
+ try {
374
+ await consumeInvite(db, {
375
+ inviteKey: invite.inviteKey,
376
+ invitePrivateKey: invite.invitePrivateKey,
377
+ memberKey: keyPair.publicKeyB64,
378
+ }, OWN_TRANSACTION);
379
+ }
380
+ catch (error) {
381
+ this.handleConsumeFailure(error);
382
+ return false;
383
+ }
384
+ this.deps.pendingInvite?.clear();
385
+ log('[%s] redeemed the staged membership invitation — Member row seated under this party\'s key', this.deps.label);
386
+ return true;
387
+ }
388
+ /**
389
+ * The already-member arm: spend a still-staged invitation so nobody else can. Burn
390
+ * failures (already spent, cancelled, expired — or a racing seal) are logged and
391
+ * otherwise ignored, and the stage is cleared either way: with the member row
392
+ * present the invitation has no further local use, and keeping a possibly-dead
393
+ * credential staged would leave `getPendingMembershipInvite` lying. The one exception
394
+ * is a busy refusal (the app had a transaction open): nothing was tried, so the
395
+ * invitation stays staged and the next pass burns it.
396
+ *
397
+ * NOTE: accepted tradeoff — a burn that failed for a TRANSIENT reason is never
398
+ * retried, so that bearer credential stays spendable until it expires. Weighed and
399
+ * kept because it lands in the same state the strand already documents as normal
400
+ * ("removal does not cancel an unspent invitation", docs/strands.md) and retrying
401
+ * would mean keeping a dead-or-alive credential staged indefinitely. Revisit if
402
+ * unspent invitations ever become a real admission risk — which is when
403
+ * `feat-strand-member-allowlist-admission` lands.
404
+ */
405
+ async burnLeftoverInvite(db, keyPair) {
406
+ const pending = this.deps.pendingInvite;
407
+ const invite = pending?.get();
408
+ if (!pending || !invite)
409
+ return;
410
+ try {
411
+ await burnInvite(db, {
412
+ inviteKey: invite.inviteKey,
413
+ invitePrivateKey: invite.invitePrivateKey,
414
+ memberKey: keyPair.publicKeyB64,
415
+ }, OWN_TRANSACTION);
416
+ log('[%s] burned the leftover invitation (member row already present)', this.deps.label);
417
+ }
418
+ catch (error) {
419
+ if (isTransactionBusy(error)) {
420
+ log('[%s] burning the leftover invitation deferred — the app has a transaction open; it stays staged '
421
+ + 'for the next pass', this.deps.label);
422
+ return;
423
+ }
424
+ log('[%s] burning the leftover invitation failed (already spent, cancelled, or expired) — dropping it: %o', this.deps.label, error);
425
+ }
426
+ pending.clear();
427
+ }
428
+ /**
429
+ * Step 3 of the pass: write this machine's own `MemberPeer` binding (insert-if-absent)
430
+ * and, on success, latch the done state and stop the loop. A missing transport peer id
431
+ * (a quiesce racing the pass) defers to the next tick; a write failure — a busy refusal
432
+ * included — is contained by the pass's outer catch and retried.
433
+ */
434
+ async ensureBinding(db, keyPair) {
435
+ const peerId = this.deps.getOwnPeerId();
436
+ if (!peerId) {
437
+ log('[%s] no live transport peer id this pass — binding deferred', this.deps.label);
438
+ return;
439
+ }
440
+ await registerMemberPeer(db, { memberKeyPair: keyPair, peerId }, OWN_TRANSACTION);
441
+ this.doneFlag = true;
442
+ this.finish('member row and own MemberPeer binding are both in place');
443
+ }
444
+ /**
445
+ * Act on a `consumeInvite` rejection as {@link classifyConsumeFailure} routes it: busy → keep
446
+ * the invitation and retry; half-committed → warn and drop; sealed → terminal; dead
447
+ * invitation → drop; anything else → retry.
448
+ */
449
+ handleConsumeFailure(error) {
450
+ const failure = classifyConsumeFailure(error);
451
+ switch (failure.kind) {
452
+ case 'busy':
453
+ log('[%s] redeeming the staged invitation deferred — the app has a transaction open on the strand '
454
+ + 'database; retrying next tick', this.deps.label);
455
+ return;
456
+ case 'half-committed':
457
+ this.reportHalfCommittedJoin(failure, error);
458
+ return;
459
+ case 'sealed':
460
+ // The staged credential is dead with the seal; drop it so the cache stays honest.
461
+ this.deps.pendingInvite?.clear();
462
+ this.finish('the strand is sealed — nobody can ever be admitted, so the staged invitation is dead');
463
+ return;
464
+ case 'dead-invite':
465
+ // NOTE: accepted tradeoff — dropping a dead invitation does not mark the pass idle,
466
+ // so the next attempt is one ladder rung away rather than a full poll interval. Kept:
467
+ // the pass AFTER it finds no member row and no invitation, marks itself idle, and
468
+ // re-arms flat, so the cost is one extra read per dead credential. Revisit if a
469
+ // source of dead invitations ever repeats per pass.
470
+ log('[%s] the staged invitation is dead (expired, cancelled, or consumed elsewhere) — dropping it; '
471
+ + 'a fresh formation stages a new one: %s', this.deps.label, errorMessage(error));
472
+ this.deps.pendingInvite?.clear();
473
+ return;
474
+ case 'retry':
475
+ log('[%s] consumeInvite failed — retrying next tick (Invite row not yet replicated here, or a transient '
476
+ + 'write failure): %s', this.deps.label, errorMessage(error));
477
+ return;
478
+ }
479
+ }
480
+ /**
481
+ * A redemption only part of which was saved (see "Half-committed join" in the module doc):
482
+ * ONE visible warning naming both halves, and the staged invitation dropped. The pass is not
483
+ * marked idle, so the next one runs a ladder rung later and either finds a `Member` row (a
484
+ * saved one, or a manager's admission) and writes the binding, or idles flat.
485
+ *
486
+ * NOTE: the invitation is dropped whichever half was saved. In the observed shape
487
+ * (`ConsumedInvite` saved, `Member` not) it is spent and a retry could only fail on
488
+ * `ConsumedInvite`'s primary key. In the opposite shape (`Member` saved, `ConsumedInvite` not)
489
+ * keeping it would let the already-member arm burn it; dropping it leaves that bearer
490
+ * credential spendable until it expires — the state the burn arm's accepted tradeoff already
491
+ * lands in. Not branched on, because telling the shapes apart means parsing the lists, which
492
+ * are collection ids on the coordinator path but free-form tree labels on the plugin's legacy
493
+ * path. Revisit if a `Member`-saved half-commit is ever observed, or when
494
+ * `blocked/strand-half-committed-join-recovery` is decided.
495
+ */
496
+ reportHalfCommittedJoin(failure, error) {
497
+ console.warn(`[sereus] strand ${this.deps.label}: redeeming the staged membership invitation was only partly saved. `
498
+ + `Saved: [${failure.saved.join(', ')}]. Not saved: [${failure.unsaved.join(', ')}]. `
499
+ + 'The invitation is dropped: a saved ConsumedInvite row spends it, so it cannot be redeemed again. '
500
+ + 'If this party\'s Member row is not among the saved, the party is not a member of the strand and '
501
+ + 'cannot become one from this invitation — a manager must admit it directly (addMemberByManager). '
502
+ + 'The membership loop keeps checking and completes the join once a Member row for this party appears.');
503
+ log('[%s] half-committed redemption, full error: %o', this.deps.label, error);
504
+ this.deps.pendingInvite?.clear();
505
+ // The idle passes that follow are the wait this warning already explained; the escalation's
506
+ // "waiting on the founder rows to replicate" would be a second, misleading warning.
507
+ this.idleEscalated = true;
508
+ }
509
+ /** Decode the party key once; an undecodable key is terminal (nothing can be signed). */
510
+ resolveKeyPair() {
511
+ if (this.keyPair)
512
+ return this.keyPair;
513
+ try {
514
+ this.keyPair = strandMemberKeyPair(this.deps.partyMemberPrivateKey);
515
+ }
516
+ catch (error) {
517
+ this.finish(`the party membership key does not decode — nothing can be signed (${errorMessage(error)})`);
518
+ return undefined;
519
+ }
520
+ return this.keyPair;
521
+ }
522
+ /**
523
+ * Count a no-member/no-invitation pass; escalate to ONE visible warning at the bound.
524
+ *
525
+ * NOTE: an idling loop never gives up — a machine whose party is never admitted keeps
526
+ * polling for the life of the process, one `Strand.Member` scan per strand per
527
+ * interval (30 s by default). Negligible at the handful of strands a device runs and
528
+ * the handful of members a strand has; if a node ever runs strands by the hundred, or
529
+ * a strand's member set grows large, bound the idle phase (give up after N passes and
530
+ * surface it) rather than shortening the interval.
531
+ */
532
+ noteIdlePass() {
533
+ this.lastPassIdle = true;
534
+ this.idlePasses += 1;
535
+ if (this.idlePasses >= IDLE_PASSES_BEFORE_ESCALATION && !this.idleEscalated) {
536
+ this.idleEscalated = true;
537
+ console.warn(`[sereus] strand ${this.deps.label}: ${this.idlePasses} membership reconcile passes and still no `
538
+ + 'Member row for this party and no staged invitation — a joiner is waiting on the founder rows to '
539
+ + 'replicate (or on a manager admission). The loop keeps retrying quietly.');
540
+ }
541
+ }
542
+ /** Stop with a reason — the terminal and done paths' shared exit. */
543
+ finish(reason) {
544
+ log('[%s] membership reconciliation stopping: %s', this.deps.label, reason);
545
+ this.stop();
546
+ }
547
+ }
548
+ //# sourceMappingURL=strand-membership-reconciler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strand-membership-reconciler.js","sourceRoot":"","sources":["../src/strand-membership-reconciler.ts"],"names":[],"mappings":"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"}