@serfab/cadre-core 1.3.0 → 1.5.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 +5 -3
- package/dist/cadre-node.d.ts +70 -34
- package/dist/cadre-node.js +197 -60
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-database.d.ts +24 -10
- package/dist/control-database.js +48 -16
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +4 -4
- package/dist/control-formation-recorder.js +6 -6
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-read-retry.d.ts +12 -0
- package/dist/control-read-retry.js +12 -0
- package/dist/control-read-retry.js.map +1 -1
- package/dist/control-write-retry.d.ts +5 -4
- package/dist/control-write-retry.js +24 -7
- package/dist/control-write-retry.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/dist/link-budget.d.ts +182 -0
- package/dist/link-budget.js +195 -0
- package/dist/link-budget.js.map +1 -0
- package/dist/membership-connection-gater.d.ts +18 -5
- package/dist/membership-connection-gater.js +50 -10
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/peer-dial.d.ts +36 -17
- package/dist/peer-dial.js +37 -17
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +58 -4
- package/dist/peer-join-backfill.js +165 -20
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/relay-reservation.d.ts +59 -11
- package/dist/relay-reservation.js +169 -43
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.js +2 -1
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/storage-scope.d.ts +50 -12
- package/dist/storage-scope.js +85 -12
- package/dist/storage-scope.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +43 -5
- package/dist/strand-first-sync-gate.js +55 -7
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-manager.js +2 -1
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-id.d.ts +35 -0
- package/dist/strand-id.js +46 -0
- package/dist/strand-id.js.map +1 -0
- package/dist/strand-instance-manager.d.ts +27 -2
- package/dist/strand-instance-manager.js +75 -15
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +200 -45
- package/dist/strand-membership-reconciler.js +302 -86
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +3 -2
- package/dist/strand-membership-writer.js +67 -90
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-network-config.d.ts +3 -3
- package/dist/strand-network-config.js +3 -3
- package/dist/strand-solicitation.js +2 -1
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +45 -0
- package/dist/strand-watcher.js +74 -13
- package/dist/strand-watcher.js.map +1 -1
- package/dist/types.d.ts +190 -24
- package/dist/types.js +73 -4
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/cadre-node.ts +7445 -7302
- package/src/control-database.ts +48 -16
- package/src/control-formation-recorder.ts +6 -6
- package/src/control-read-retry.ts +214 -202
- package/src/control-write-retry.ts +24 -7
- package/src/index.ts +29 -2
- package/src/link-budget.ts +217 -0
- package/src/membership-connection-gater.ts +56 -11
- package/src/peer-dial.ts +39 -17
- package/src/peer-join-backfill.ts +192 -23
- package/src/relay-reservation.ts +1119 -951
- package/src/seed-bootstrap.ts +2 -1
- package/src/storage-scope.ts +163 -78
- package/src/strand-first-sync-gate.ts +55 -7
- package/src/strand-formation-manager.ts +647 -646
- package/src/strand-id.ts +48 -0
- package/src/strand-instance-manager.ts +1516 -1440
- package/src/strand-membership-reconciler.ts +334 -92
- package/src/strand-membership-writer.ts +1962 -1987
- package/src/strand-network-config.ts +250 -250
- package/src/strand-solicitation.ts +500 -499
- package/src/strand-watcher.ts +388 -325
- package/src/types.ts +192 -23
|
@@ -26,39 +26,93 @@
|
|
|
26
26
|
* that finds no member row AND no staged invitation is not an unfinished join but a wait
|
|
27
27
|
* for someone to admit this party — nothing this machine can do faster — so it re-arms on
|
|
28
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
|
|
30
|
-
*
|
|
31
|
-
* business, not this loop's; a resume rebuilds the reconciler and
|
|
32
|
-
* scratch (every write is idempotent, so re-running is free)
|
|
29
|
+
* - **Done state**: the party's member row is visible locally, this machine's own binding
|
|
30
|
+
* is in place, AND no invitation is still staged — the loop stops. A LATER revocation is
|
|
31
|
+
* the enforcer's business, not this loop's; a resume rebuilds the reconciler and
|
|
32
|
+
* re-verifies from scratch (every write is idempotent, so re-running is free), and a
|
|
33
|
+
* fresh invitation staged on a done loop re-arms it in place ({@link rearm}).
|
|
33
34
|
*
|
|
34
35
|
* ## One pass
|
|
35
36
|
*
|
|
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
|
|
37
|
+
* 1. **Self-revocation check.** If the enforcer flags this node's own party as revoked and
|
|
38
|
+
* no invitation is staged, stop rather than fight — re-admission arrives (if ever) via
|
|
39
|
+
* a fresh formation, which re-arms the loop. With an invitation staged the loop reports
|
|
40
|
+
* the dead end (below) and carries on: the redemption is attempted like any other, and a
|
|
41
|
+
* manager's re-admission lets a later pass finish it.
|
|
38
42
|
* 2. **Ensure membership.** Member row visible → done with this step; if a staged
|
|
39
43
|
* invitation is still unspent, BURN it ({@link burnInvite} — the `ConsumedInvite` row
|
|
40
|
-
* alone) so the bearer credential cannot be spent by anyone else
|
|
41
|
-
*
|
|
44
|
+
* alone) so the bearer credential cannot be spent by anyone else. A burn refused by the
|
|
45
|
+
* cohort keeps the invitation staged for the next pass (it is the same write gate a
|
|
46
|
+
* redemption faces, and the local member row may be stale — see "Re-arming"); a dead
|
|
47
|
+
* invitation is dropped. Member row absent + staged invitation → {@link consumeInvite}
|
|
42
48
|
* under the party's own key, retried across passes until the `Invite` row has
|
|
43
49
|
* replicated here and the write commits. Absent with no invitation (founder rows not
|
|
44
50
|
* yet synced, or a machine whose party never joined) → keep waiting, escalating to one
|
|
45
51
|
* visible warning after {@link IDLE_PASSES_BEFORE_ESCALATION} passes, never throwing.
|
|
46
52
|
* 3. **Ensure the binding.** {@link registerMemberPeer} (insert-if-absent, restart-safe)
|
|
47
53
|
* 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).
|
|
54
|
+
* row locally (its deferred `MemberExists` reads the live local table). Done only once
|
|
55
|
+
* the binding is in place AND nothing is staged; a binding written while a burn keeps
|
|
56
|
+
* being refused leaves the loop live to settle the invitation later.
|
|
49
57
|
*
|
|
50
|
-
* ##
|
|
58
|
+
* ## Stops, and re-arming
|
|
59
|
+
*
|
|
60
|
+
* Two kinds of stop. A RE-ARMABLE stop is one a fresh invitation should reopen:
|
|
61
|
+
*
|
|
62
|
+
* - **Done** (above).
|
|
63
|
+
* - **Self-revoked with nothing staged** (step 1 above).
|
|
64
|
+
*
|
|
65
|
+
* A PERMANENT stop is one a fresh invitation changes nothing about — {@link rearm} is a
|
|
66
|
+
* no-op after it:
|
|
51
67
|
*
|
|
52
68
|
* - **Sealed strand**: `consumeInvite` rejected by `ConsumedInvite.NotSealed` — nobody
|
|
53
69
|
* can ever be admitted, so retrying forever is noise. Terminal-with-log.
|
|
54
|
-
* - **Self-revoked** (step 1 above).
|
|
55
70
|
* - **Undecodable party key**: nothing can be signed; loud log, stop.
|
|
71
|
+
* - **The public {@link stop}**: `StrandInstanceManager.releaseRuntime` and
|
|
72
|
+
* `clearOwnMemberPeerBinding` both call it and mean it — the latter stops the loop and
|
|
73
|
+
* awaits {@link settle} precisely so no pass can re-register the binding it is about to
|
|
74
|
+
* delete, and a re-arm that restarted the loop would undo that.
|
|
75
|
+
*
|
|
76
|
+
* `CadreNode.adoptFormationMembershipInvite` stages every fresh invitation and notifies
|
|
77
|
+
* the instance manager, which calls {@link rearm}: a done loop reopens and runs one pass
|
|
78
|
+
* at once; a running loop is merely kicked. This is what lets a party that was REMOVED
|
|
79
|
+
* from the strand, and then handed a new invitation, actually attempt it — the loop
|
|
80
|
+
* finished during its first join, long before the removal.
|
|
81
|
+
*
|
|
82
|
+
* On a removed party the local replica is usually stale: the cohort cut this machine off
|
|
83
|
+
* at about the moment the removal was written, so `isStrandMember` still answers true and
|
|
84
|
+
* a re-armed pass lands in the already-member arm. That is why the burn arm keeps a
|
|
85
|
+
* refused invitation staged and why the done state requires nothing staged — otherwise
|
|
86
|
+
* the fresh credential would be dropped on the first refused burn and the loop would latch
|
|
87
|
+
* done again, with the only trace a debug line.
|
|
56
88
|
*
|
|
57
89
|
* A DEAD invitation that is not terminal for the loop — expired, cancelled, or already
|
|
58
90
|
* consumed by someone else (`ConsumedInvite`'s primary key) — is dropped with a log and
|
|
59
91
|
* the loop keeps idling: a fresh formation stages a new invitation, and a manager-side
|
|
60
92
|
* admission (`addMemberByManager`) seats the member row without one.
|
|
61
93
|
*
|
|
94
|
+
* ## Reporting a blocked re-join
|
|
95
|
+
*
|
|
96
|
+
* Redeeming (or burning) writes into the strand, and the machines that would carry that
|
|
97
|
+
* write are exactly the ones the remaining members refuse once this party is removed. So
|
|
98
|
+
* the attempt is expected to fail, and the failure is REPORTED rather than left to the
|
|
99
|
+
* ladder: one `console.warn` plus the {@link StrandMembershipReconcilerDeps.onRejoinBlocked}
|
|
100
|
+
* callback (`CadreNode`'s `strand:rejoin-blocked` event), at most once per re-arm cycle.
|
|
101
|
+
* Two triggers:
|
|
102
|
+
*
|
|
103
|
+
* - **Confirmed** — a pass finds this party self-revoked AND an invitation staged. The node
|
|
104
|
+
* knows it was removed and is holding a credential it cannot spend.
|
|
105
|
+
* - **Probable** — {@link UNFINISHED_PASSES_BEFORE_ESCALATION} consecutive attempted passes
|
|
106
|
+
* left the invitation staged. The case that matters most in the field: a removed party
|
|
107
|
+
* that never learned it was removed (the removal did not replicate here before the cut).
|
|
108
|
+
* It is a suspicion, not a verdict — the invitation's `Invite` row may simply not have
|
|
109
|
+
* replicated here yet — and the warning names both causes.
|
|
110
|
+
*
|
|
111
|
+
* The remedy in either case is a remaining manager admitting this party's member key
|
|
112
|
+
* directly (`addMemberByManager`): that lifts the refusal, replication resumes, and the
|
|
113
|
+
* still-running loop sees the member row, burns the leftover credential and finishes by
|
|
114
|
+
* itself.
|
|
115
|
+
*
|
|
62
116
|
* ## Sharing the database with the app
|
|
63
117
|
*
|
|
64
118
|
* The strand `Database` this loop writes to is the one the app holds. Every write here
|
|
@@ -91,6 +145,16 @@ import { type TimeoutScheduler } from './timeout-scheduler.js';
|
|
|
91
145
|
* founder-row replication is visible without the debug namespace enabled.
|
|
92
146
|
*/
|
|
93
147
|
export declare const IDLE_PASSES_BEFORE_ESCALATION = 10;
|
|
148
|
+
/**
|
|
149
|
+
* Consecutive passes that ran against a live database and ended with the staged
|
|
150
|
+
* invitation still unsettled — whatever refused it — before the loop reports a probable
|
|
151
|
+
* blocked re-join (see "Reporting a blocked re-join" in the module doc). The same order
|
|
152
|
+
* as {@link IDLE_PASSES_BEFORE_ESCALATION}: about three minutes at the default cadence
|
|
153
|
+
* (the ladder's five doubling rungs, then five passes at the 30 s cap). Counted by outcome
|
|
154
|
+
* rather than by classified failure because a cut-off machine can fail before any write is
|
|
155
|
+
* classified: its membership READ may already throw.
|
|
156
|
+
*/
|
|
157
|
+
export declare const UNFINISHED_PASSES_BEFORE_ESCALATION = 10;
|
|
94
158
|
/**
|
|
95
159
|
* First retry delay, ms, for a pass that left the join UNFINISHED — a `consumeInvite`
|
|
96
160
|
* whose `Strand.Invite` row has not replicated to this machine yet, a cohort briefly
|
|
@@ -111,13 +175,13 @@ export interface HalfCommittedJoin {
|
|
|
111
175
|
saved: readonly string[];
|
|
112
176
|
unsaved: readonly string[];
|
|
113
177
|
}
|
|
114
|
-
/** Where {@link classifyConsumeFailure} routes a failed `consumeInvite`. */
|
|
178
|
+
/** Where {@link classifyConsumeFailure} routes a failed `consumeInvite` or `burnInvite`. */
|
|
115
179
|
export type ConsumeFailure = HalfCommittedJoin
|
|
116
180
|
/** The app had a transaction open, so nothing was tried — keep the invitation, retry. */
|
|
117
181
|
| {
|
|
118
182
|
kind: 'busy';
|
|
119
183
|
}
|
|
120
|
-
/** The strand is sealed — terminal. */
|
|
184
|
+
/** The strand is sealed — terminal for a non-member; a member's leftover invitation is merely dead. */
|
|
121
185
|
| {
|
|
122
186
|
kind: 'sealed';
|
|
123
187
|
}
|
|
@@ -125,17 +189,22 @@ export type ConsumeFailure = HalfCommittedJoin
|
|
|
125
189
|
| {
|
|
126
190
|
kind: 'dead-invite';
|
|
127
191
|
}
|
|
128
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* Anything else — the `Invite` row has not replicated here yet, or the cohort refused the
|
|
194
|
+
* write (a removed party's machines are denied) — keep the invitation, retry.
|
|
195
|
+
*/
|
|
129
196
|
| {
|
|
130
197
|
kind: 'retry';
|
|
131
198
|
};
|
|
132
199
|
/**
|
|
133
|
-
* Route a `consumeInvite` rejection.
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
200
|
+
* Route a `consumeInvite` or `burnInvite` rejection. Both write the `Strand.ConsumedInvite`
|
|
201
|
+
* row under the same `NotExpired` / `NotCancelled` / `NotSealed` / primary-key constraints
|
|
202
|
+
* (a burn writes that row alone), so one classifier serves both. Typed checks run before any
|
|
203
|
+
* text check: a busy refusal (another transaction was open, so nothing ran) and a
|
|
204
|
+
* half-committed join are recognised by their error's type anywhere in the `cause` chain.
|
|
205
|
+
* The busy refusal carries Quereus's own refusal as its cause, and a half-committed join's
|
|
206
|
+
* message embeds collection names and the underlying failure's text, either of which a text
|
|
207
|
+
* matcher can misread. The sealed and dead-invitation checks then read the top-level message.
|
|
139
208
|
*
|
|
140
209
|
* A second loaded copy of `@optimystic/db-core` (or of the plugin) would fail `instanceof`; the
|
|
141
210
|
* half-commit then falls through the anchored texts to `retry`, and once the saved `ConsumedInvite`
|
|
@@ -153,8 +222,12 @@ export declare function classifyConsumeFailure(error: unknown): ConsumeFailure;
|
|
|
153
222
|
export interface PendingMembershipInviteSource {
|
|
154
223
|
/** The staged invitation, or `undefined` when none is pending for this strand. */
|
|
155
224
|
get(): StrandMembershipInvite | undefined;
|
|
156
|
-
/**
|
|
157
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Drop `settled` (spent, burned, or dead) — only if it is still the staged one. A
|
|
227
|
+
* re-formation can replace the entry between a pass's {@link get} and this call, and
|
|
228
|
+
* the fresh invitation it staged must survive the older one being settled.
|
|
229
|
+
*/
|
|
230
|
+
clear(settled: StrandMembershipInvite): void;
|
|
158
231
|
}
|
|
159
232
|
export interface StrandMembershipReconcilerDeps {
|
|
160
233
|
/** Log tag naming which strand this reconciler serves (the strand id). */
|
|
@@ -180,10 +253,17 @@ export interface StrandMembershipReconcilerDeps {
|
|
|
180
253
|
pendingInvite?: PendingMembershipInviteSource;
|
|
181
254
|
/**
|
|
182
255
|
* Whether the revocation enforcer currently flags THIS node's own party as revoked.
|
|
183
|
-
* When it does, the loop stops rather than fight the enforcer
|
|
184
|
-
*
|
|
256
|
+
* When it does and nothing is staged, the loop stops rather than fight the enforcer;
|
|
257
|
+
* with an invitation staged it reports the blocked re-join and keeps trying — see the
|
|
258
|
+
* module doc. Absent (enforcement disarmed) means "not known revoked".
|
|
185
259
|
*/
|
|
186
260
|
isSelfRevoked?: () => boolean;
|
|
261
|
+
/**
|
|
262
|
+
* Called when the loop reports a blocked re-join (see "Reporting a blocked re-join" in
|
|
263
|
+
* the module doc) — at most once per re-arm cycle, after the `console.warn`. `CadreNode`
|
|
264
|
+
* wires it to its `strand:rejoin-blocked` event.
|
|
265
|
+
*/
|
|
266
|
+
onRejoinBlocked?: () => void;
|
|
187
267
|
/**
|
|
188
268
|
* Timer seam for the retry ladder; omit for real (unref'd) timeouts. Timeouts rather
|
|
189
269
|
* than the enforcer's repeating interval because the delay changes per pass and the next
|
|
@@ -224,11 +304,21 @@ export declare class StrandMembershipReconciler {
|
|
|
224
304
|
private timer;
|
|
225
305
|
private started;
|
|
226
306
|
private stoppedFlag;
|
|
307
|
+
/**
|
|
308
|
+
* Whether the last stop was one {@link rearm} may not undo — see "Stops, and re-arming"
|
|
309
|
+
* in the module doc. Latched by the public {@link stop} and the permanent terminal
|
|
310
|
+
* states, never cleared.
|
|
311
|
+
*/
|
|
312
|
+
private permanentlyStopped;
|
|
227
313
|
private doneFlag;
|
|
228
314
|
/** Lazily decoded party keypair — see {@link StrandMembershipReconcilerDeps.partyMemberPrivateKey}. */
|
|
229
315
|
private keyPair;
|
|
230
316
|
private idlePasses;
|
|
231
317
|
private idleEscalated;
|
|
318
|
+
/** Consecutive attempted passes that left the invitation staged — see {@link UNFINISHED_PASSES_BEFORE_ESCALATION}. */
|
|
319
|
+
private unfinishedPasses;
|
|
320
|
+
/** Whether the blocked re-join was already reported this re-arm cycle (one report, either trigger). */
|
|
321
|
+
private rejoinBlockedReported;
|
|
232
322
|
/**
|
|
233
323
|
* Whether the pass that just ran found nothing to act on — no member row and no staged
|
|
234
324
|
* invitation. That is a wait to be admitted, not an unfinished join, so it re-arms on the
|
|
@@ -244,9 +334,12 @@ export declare class StrandMembershipReconciler {
|
|
|
244
334
|
/** Current rung of the unfinished-join retry ladder, ms; unset before the first retry. */
|
|
245
335
|
private retryDelayMs;
|
|
246
336
|
constructor(deps: StrandMembershipReconcilerDeps, config?: StrandMembershipReconciliationConfig);
|
|
247
|
-
/**
|
|
337
|
+
/**
|
|
338
|
+
* True once member row + own binding were both confirmed with nothing staged and the
|
|
339
|
+
* loop stopped. Cleared again by {@link rearm}.
|
|
340
|
+
*/
|
|
248
341
|
get done(): boolean;
|
|
249
|
-
/** True
|
|
342
|
+
/** True while the loop is stopped — done, terminal, or externally stopped. */
|
|
250
343
|
get stopped(): boolean;
|
|
251
344
|
/**
|
|
252
345
|
* Kick an immediate pass (not awaited — bring-up never blocks on it). Every later pass
|
|
@@ -262,8 +355,20 @@ export declare class StrandMembershipReconciler {
|
|
|
262
355
|
* this after stopping. Never rejects: the pass chain contains every failure.
|
|
263
356
|
*/
|
|
264
357
|
settle(): Promise<void>;
|
|
265
|
-
/**
|
|
358
|
+
/**
|
|
359
|
+
* Disarm the retry timer, for good: a later {@link rearm} is refused. A pass already in
|
|
360
|
+
* flight completes but writes idempotently — {@link settle} is what waits it out.
|
|
361
|
+
*/
|
|
266
362
|
stop(): void;
|
|
363
|
+
/**
|
|
364
|
+
* A fresh invitation was staged: reopen a loop that stopped on a re-armable state (done,
|
|
365
|
+
* or self-revoked with nothing staged) and run one pass at once; merely kick a loop that
|
|
366
|
+
* is still running; do nothing after a permanent stop. Never rejects, and serialized
|
|
367
|
+
* like {@link reconcile}: the reopen happens when the queued pass STARTS, after any pass
|
|
368
|
+
* in flight has settled, so a `done` that pass latches cannot swallow the re-arm — and a
|
|
369
|
+
* public {@link stop} that lands first still wins, because the queued pass re-checks it.
|
|
370
|
+
*/
|
|
371
|
+
rearm(): Promise<void>;
|
|
267
372
|
/**
|
|
268
373
|
* Run one pass now. Never rejects. Serialized: concurrent calls chain, so no two
|
|
269
374
|
* passes overlap and each explicit call gets a pass that STARTS after the call. On a
|
|
@@ -272,8 +377,22 @@ export declare class StrandMembershipReconciler {
|
|
|
272
377
|
* published — REPLACES the pending timer rather than running alongside it.
|
|
273
378
|
*/
|
|
274
379
|
reconcile(): Promise<void>;
|
|
380
|
+
/** Chain `pass` after every pass already queued; the tail never rejects. */
|
|
381
|
+
private enqueue;
|
|
382
|
+
/**
|
|
383
|
+
* The re-arm itself, run at the head of the queued pass: `false` refuses the pass (a
|
|
384
|
+
* permanent stop), a running loop is left as it is, and a re-armable stop is reopened
|
|
385
|
+
* with the idle and unfinished counters, both escalation latches and the retry ladder
|
|
386
|
+
* reset — a fresh invitation is a fresh cycle.
|
|
387
|
+
*/
|
|
388
|
+
private reopen;
|
|
275
389
|
/** One serialized pass; contains every failure (contract: never rejects). */
|
|
276
390
|
private doPass;
|
|
391
|
+
/**
|
|
392
|
+
* Steps 2 and 3 against a live database, with the outcome counted toward the probable
|
|
393
|
+
* blocked re-join report whether the attempt returned or threw.
|
|
394
|
+
*/
|
|
395
|
+
private attempt;
|
|
277
396
|
/**
|
|
278
397
|
* Arm the next pass, now that this one has settled — so a slow pass never stacks ticks
|
|
279
398
|
* behind it. Only a STARTED loop schedules: a caller driving {@link reconcile} by hand
|
|
@@ -296,28 +415,39 @@ export declare class StrandMembershipReconciler {
|
|
|
296
415
|
*/
|
|
297
416
|
private ensureMembership;
|
|
298
417
|
/**
|
|
299
|
-
* The already-member arm: spend a still-staged invitation so nobody else can.
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* present
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
418
|
+
* The already-member arm: spend a still-staged invitation so nobody else can. The stage
|
|
419
|
+
* is cleared once the burn lands or the invitation is known dead (already spent,
|
|
420
|
+
* cancelled, expired, or the strand sealed around a party that is still a member) — with
|
|
421
|
+
* the member row present a dead credential has no further local use, and keeping it
|
|
422
|
+
* staged would leave `getPendingMembershipInvite` lying. A refused burn keeps it staged:
|
|
423
|
+
* a busy refusal tried nothing, and a write the cohort refused is the removed-party shape
|
|
424
|
+
* ("Stops, and re-arming" in the module doc), where the local member row is stale and the
|
|
425
|
+
* credential is the one thing a later manager admission lets this loop settle.
|
|
306
426
|
*
|
|
307
|
-
* NOTE:
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* `feat-strand-member-allowlist-admission` lands.
|
|
427
|
+
* NOTE: this REPLACES an earlier accepted tradeoff that never retried a burn which failed
|
|
428
|
+
* for a transient reason and dropped the invitation instead. That decision was made when
|
|
429
|
+
* this arm could only ever see an invitation staged before the first join finished, so a
|
|
430
|
+
* dropped credential cost nothing but a spendable bearer token. {@link rearm} makes the
|
|
431
|
+
* arm reachable with a freshly issued invitation the app wants redeemed, and dropping
|
|
432
|
+
* that one on the first refused write is the silent failure this loop exists to avoid.
|
|
314
433
|
*/
|
|
315
434
|
private burnLeftoverInvite;
|
|
435
|
+
/**
|
|
436
|
+
* Act on a `burnInvite` rejection as {@link classifyConsumeFailure} routes it. Unlike a
|
|
437
|
+
* redemption, a sealed strand is not terminal here — the member row is present, so this
|
|
438
|
+
* party is a member of the sealed strand and only its leftover credential is dead — and a
|
|
439
|
+
* half-committed burn (one collection, so a report of it means the commit's durability
|
|
440
|
+
* was in doubt) is retried: the next pass either lands it or fails on the row's primary
|
|
441
|
+
* key, which drops it as dead.
|
|
442
|
+
*/
|
|
443
|
+
private handleBurnFailure;
|
|
316
444
|
/**
|
|
317
445
|
* Step 3 of the pass: write this machine's own `MemberPeer` binding (insert-if-absent)
|
|
318
|
-
* and,
|
|
319
|
-
* (a quiesce racing the pass) defers to the next tick; a
|
|
320
|
-
* included — is contained by the pass's outer catch and
|
|
446
|
+
* and, once it is in place with nothing staged, latch the done state and stop the loop.
|
|
447
|
+
* A missing transport peer id (a quiesce racing the pass) defers to the next tick; a
|
|
448
|
+
* write failure — a busy refusal included — is contained by the pass's outer catch and
|
|
449
|
+
* retried. A binding written while an invitation is still staged (its burn keeps being
|
|
450
|
+
* refused) is not done: the loop stays live so a later pass can settle the credential.
|
|
321
451
|
*/
|
|
322
452
|
private ensureBinding;
|
|
323
453
|
/**
|
|
@@ -326,6 +456,26 @@ export declare class StrandMembershipReconciler {
|
|
|
326
456
|
* invitation → drop; anything else → retry.
|
|
327
457
|
*/
|
|
328
458
|
private handleConsumeFailure;
|
|
459
|
+
/**
|
|
460
|
+
* Count an attempted pass that left the invitation staged (whatever stopped it — a
|
|
461
|
+
* refused or busy write, or a read that threw on a cut-off machine); one that settled
|
|
462
|
+
* it, or finished, ends the streak. At {@link UNFINISHED_PASSES_BEFORE_ESCALATION}
|
|
463
|
+
* report the PROBABLE blocked re-join.
|
|
464
|
+
*/
|
|
465
|
+
private noteAttemptOutcome;
|
|
466
|
+
/**
|
|
467
|
+
* Step 1's CONFIRMED trigger: self-revoked with an invitation staged. Reports (once per
|
|
468
|
+
* re-arm cycle) and answers `true` so the pass carries on — the redemption is attempted
|
|
469
|
+
* and refused like any other write, and a manager's re-admission lets a later pass finish.
|
|
470
|
+
* `false` (nothing staged) means step 1 stops the loop as before.
|
|
471
|
+
*/
|
|
472
|
+
private stagedInviteBlockedBySelfRevocation;
|
|
473
|
+
/**
|
|
474
|
+
* ONE `console.warn` and one callback per re-arm cycle, whichever trigger fires first —
|
|
475
|
+
* see "Reporting a blocked re-join" in the module doc. `cause` is the trigger's own
|
|
476
|
+
* sentence; the remedy and the loop's posture are the same for both.
|
|
477
|
+
*/
|
|
478
|
+
private reportRejoinBlocked;
|
|
329
479
|
/**
|
|
330
480
|
* A redemption only part of which was saved (see "Half-committed join" in the module doc):
|
|
331
481
|
* ONE visible warning naming both halves, and the staged invitation dropped. The pass is not
|
|
@@ -356,6 +506,11 @@ export declare class StrandMembershipReconciler {
|
|
|
356
506
|
* surface it) rather than shortening the interval.
|
|
357
507
|
*/
|
|
358
508
|
private noteIdlePass;
|
|
359
|
-
/**
|
|
509
|
+
/**
|
|
510
|
+
* Stop with a reason — the terminal and done paths' shared exit. `permanent` says whether
|
|
511
|
+
* a fresh invitation may reopen the loop ("Stops, and re-arming" in the module doc).
|
|
512
|
+
*/
|
|
360
513
|
private finish;
|
|
514
|
+
/** Latch the stop and disarm the timer; a permanent latch sticks even on an already-stopped loop. */
|
|
515
|
+
private halt;
|
|
361
516
|
}
|