@serfab/cadre-core 0.13.0 → 1.1.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.
- package/README.md +20 -6
- package/dist/bootstrap-peer-store-file.js +2 -2
- package/dist/bootstrap-peer-store.d.ts +32 -12
- package/dist/bootstrap-peer-store.js +41 -15
- package/dist/bootstrap-peer-store.js.map +1 -1
- package/dist/cadre-node.d.ts +558 -80
- package/dist/cadre-node.js +1060 -170
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +2 -2
- package/dist/control-authorization.js +1 -0
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +13 -26
- package/dist/control-cohort.js +0 -26
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +299 -44
- package/dist/control-database.js +469 -69
- package/dist/control-database.js.map +1 -1
- package/dist/control-read-retry.d.ts +40 -11
- package/dist/control-read-retry.js +48 -15
- package/dist/control-read-retry.js.map +1 -1
- package/dist/control-retry.d.ts +58 -3
- package/dist/control-retry.js +58 -3
- package/dist/control-retry.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +103 -19
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +80 -11
- package/dist/control-write-retry.js +176 -29
- package/dist/control-write-retry.js.map +1 -1
- package/dist/enrolled-machine-store.js +2 -2
- package/dist/index.d.ts +14 -7
- package/dist/index.js +33 -6
- package/dist/index.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +15 -7
- package/dist/node-local-snapshot.js +23 -8
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/peer-dial.d.ts +143 -0
- package/dist/peer-dial.js +176 -0
- package/dist/peer-dial.js.map +1 -0
- package/dist/relay-addrs.d.ts +90 -52
- package/dist/relay-addrs.js +91 -59
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +71 -3
- package/dist/relay-reservation.js +116 -8
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +30 -1
- package/dist/seed-bootstrap.js +62 -17
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/storage-scope.d.ts +62 -0
- package/dist/storage-scope.js +75 -0
- package/dist/storage-scope.js.map +1 -0
- package/dist/strand-database.d.ts +34 -16
- package/dist/strand-database.js +38 -24
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +152 -0
- package/dist/strand-first-sync-gate.js +208 -0
- package/dist/strand-first-sync-gate.js.map +1 -0
- package/dist/strand-formation-manager.d.ts +87 -5
- package/dist/strand-formation-manager.js +109 -5
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +21 -1
- package/dist/strand-formation-protocol.js +30 -0
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +243 -6
- package/dist/strand-instance-manager.js +470 -37
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-key.d.ts +20 -12
- package/dist/strand-member-key.js +20 -12
- package/dist/strand-member-key.js.map +1 -1
- package/dist/strand-member-registry.js +3 -0
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +361 -0
- package/dist/strand-membership-reconciler.js +548 -0
- package/dist/strand-membership-reconciler.js.map +1 -0
- package/dist/strand-membership-writer.d.ts +131 -11
- package/dist/strand-membership-writer.js +396 -134
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-network-config.d.ts +46 -14
- package/dist/strand-network-config.js +79 -23
- package/dist/strand-network-config.js.map +1 -1
- package/dist/strand-solicitation.d.ts +10 -1
- package/dist/strand-solicitation.js +5 -0
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +11 -7
- package/dist/strand-wake-protocol.js +20 -52
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/strand-watcher.js +6 -0
- package/dist/strand-watcher.js.map +1 -1
- package/dist/timed-step.d.ts +12 -0
- package/dist/timed-step.js +28 -0
- package/dist/timed-step.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +19 -0
- package/dist/timeout-scheduler.js +22 -0
- package/dist/timeout-scheduler.js.map +1 -0
- package/dist/types.d.ts +262 -30
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/bootstrap-peer-store-file.ts +2 -2
- package/src/bootstrap-peer-store.ts +69 -27
- package/src/cadre-node.ts +7301 -6353
- package/src/control-authorization.ts +2 -1
- package/src/control-cohort.ts +122 -130
- package/src/control-database.ts +537 -69
- package/src/control-read-retry.ts +202 -167
- package/src/control-retry.ts +297 -190
- package/src/control-schema.ts +103 -19
- package/src/control-write-retry.ts +533 -383
- package/src/enrolled-machine-store.ts +2 -2
- package/src/index.ts +83 -6
- package/src/node-local-snapshot.ts +25 -8
- package/src/peer-dial.ts +215 -0
- package/src/relay-addrs.ts +458 -436
- package/src/relay-reservation.ts +951 -828
- package/src/seed-bootstrap.ts +76 -18
- package/src/storage-scope.ts +78 -0
- package/src/strand-database.ts +50 -27
- package/src/strand-first-sync-gate.ts +258 -0
- package/src/strand-formation-manager.ts +140 -5
- package/src/strand-formation-protocol.ts +43 -1
- package/src/strand-instance-manager.ts +577 -37
- package/src/strand-member-key.ts +20 -12
- package/src/strand-member-registry.ts +3 -0
- package/src/strand-membership-reconciler.ts +680 -0
- package/src/strand-membership-writer.ts +527 -189
- package/src/strand-network-config.ts +102 -24
- package/src/strand-solicitation.ts +16 -1
- package/src/strand-wake-protocol.ts +21 -66
- package/src/strand-watcher.ts +6 -0
- package/src/timed-step.ts +28 -0
- package/src/timeout-scheduler.ts +28 -0
- package/src/types.ts +1789 -1554
|
@@ -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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"}
|