@serfab/cadre-core 1.5.0 → 1.7.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/dist/cadre-node.d.ts +206 -67
- package/dist/cadre-node.js +399 -98
- package/dist/cadre-node.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +112 -0
- package/dist/joined-strand-store.js +263 -0
- package/dist/joined-strand-store.js.map +1 -0
- package/dist/key-store.d.ts +6 -0
- package/dist/key-store.js.map +1 -1
- package/dist/link-budget.d.ts +95 -53
- package/dist/link-budget.js +97 -53
- package/dist/link-budget.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +5 -4
- package/dist/node-local-snapshot.js +6 -3
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/peer-dial.d.ts +3 -3
- package/dist/peer-dial.js +3 -3
- package/dist/peer-join-backfill.d.ts +35 -16
- package/dist/peer-join-backfill.js +117 -57
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/relay-reservation.d.ts +2 -2
- package/dist/relay-reservation.js +2 -2
- package/dist/seed-bootstrap.js +1 -1
- package/dist/strand-first-sync-gate.d.ts +88 -43
- package/dist/strand-first-sync-gate.js +92 -46
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +39 -0
- package/dist/strand-instance-manager.js +80 -3
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-peer-book-file.d.ts +12 -0
- package/dist/strand-peer-book-file.js +37 -0
- package/dist/strand-peer-book-file.js.map +1 -0
- package/dist/strand-peer-book-protocol.d.ts +159 -0
- package/dist/strand-peer-book-protocol.js +360 -0
- package/dist/strand-peer-book-protocol.js.map +1 -0
- package/dist/strand-peer-book-swap.d.ts +104 -0
- package/dist/strand-peer-book-swap.js +284 -0
- package/dist/strand-peer-book-swap.js.map +1 -0
- package/dist/strand-peer-book.d.ts +159 -0
- package/dist/strand-peer-book.js +354 -0
- package/dist/strand-peer-book.js.map +1 -0
- package/dist/strand-peer-observer.d.ts +88 -0
- package/dist/strand-peer-observer.js +180 -0
- package/dist/strand-peer-observer.js.map +1 -0
- package/dist/types.d.ts +84 -17
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/package.json +11 -7
- package/src/cadre-node.ts +7776 -7445
- package/src/index.ts +68 -0
- package/src/joined-strand-store.ts +308 -0
- package/src/key-store.ts +6 -0
- package/src/link-budget.ts +100 -53
- package/src/node-local-snapshot.ts +293 -289
- package/src/peer-dial.ts +3 -3
- package/src/peer-join-backfill.ts +709 -647
- package/src/relay-reservation.ts +2 -2
- package/src/seed-bootstrap.ts +1 -1
- package/src/strand-first-sync-gate.ts +352 -306
- package/src/strand-instance-manager.ts +106 -3
- package/src/strand-peer-book-file.ts +39 -0
- package/src/strand-peer-book-protocol.ts +463 -0
- package/src/strand-peer-book-swap.ts +338 -0
- package/src/strand-peer-book.ts +468 -0
- package/src/strand-peer-observer.ts +235 -0
- package/src/types.ts +86 -17
|
@@ -17,9 +17,17 @@
|
|
|
17
17
|
* freshly initialized `StrandDatabase` back from the app — `StrandInstance.database` stays
|
|
18
18
|
* unset and the instance reports `'syncing'` — and probes on a cadence until the Header is
|
|
19
19
|
* readable AND a read of each app table settles, at which point the database is published
|
|
20
|
-
* and the instance goes `'active'`. A machine
|
|
21
|
-
*
|
|
22
|
-
* offline-first writes on a machine that has synced
|
|
20
|
+
* and the instance goes `'active'`. A machine whose store already holds the Header and every
|
|
21
|
+
* app table (a founder, and normally a restart or a hibernation resume) passes the probe on
|
|
22
|
+
* the first try and is never gated, so offline-first writes on a machine that has synced
|
|
23
|
+
* before keep working.
|
|
24
|
+
*
|
|
25
|
+
* "Has synced before" is decided by what the local store actually holds, not by whether this
|
|
26
|
+
* machine once read the strand: a machine that left soon after its first sync can hold less
|
|
27
|
+
* than it read, and is then gated like a joiner. The re-attach measurement on
|
|
28
|
+
* {@link DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS} saw exactly that — a machine back over the store
|
|
29
|
+
* it kept, without the `Strand.Header` collection it had read before it left. Gating it is
|
|
30
|
+
* right: writing into a collection this machine does not hold would fork it.
|
|
23
31
|
*
|
|
24
32
|
* Why the app tables too: the Header is ONE collection. It reaches a joiner ahead of the
|
|
25
33
|
* founder's app-table collections (peer-join backfill and pull-on-read deliver those
|
|
@@ -36,10 +44,12 @@
|
|
|
36
44
|
* every machine; the first write to it still creates its collection — see `docs/strands.md`
|
|
37
45
|
* ("Joining") for that residual.
|
|
38
46
|
*
|
|
39
|
-
* Owned by `StrandInstanceManager`: created in `buildStrandRuntime` for
|
|
40
|
-
* whose first probe
|
|
41
|
-
* resume rebuild re-probes over the same store), and
|
|
42
|
-
*
|
|
47
|
+
* Owned by `StrandInstanceManager`: created in `buildStrandRuntime` for every launch, armed
|
|
48
|
+
* only for a non-founder launch whose first probe fails, stopped and dropped in
|
|
49
|
+
* `releaseRuntime` (so a quiesce → resume rebuild re-probes over the same store), and opened
|
|
50
|
+
* by `publishDatabase` whenever the database is published without its loop — a launch that
|
|
51
|
+
* needed no gate, or a founder request that ran the bootstrap against the still-gated
|
|
52
|
+
* database (`foundExistingStrand`).
|
|
43
53
|
*
|
|
44
54
|
* NOTE: the gate covers app and membership tables, not the schema catalog. Every launch —
|
|
45
55
|
* joiner or founder — writes the catalog collection (`optimystic/schema` plus two
|
|
@@ -57,48 +67,80 @@ import { type TimeoutScheduler } from './timeout-scheduler.js';
|
|
|
57
67
|
/** How often a gated launch re-probes (`Strand.Header`, then every `App` table) while waiting for its first sync. */
|
|
58
68
|
export declare const DEFAULT_STRAND_FIRST_SYNC_POLL_MS = 500;
|
|
59
69
|
/**
|
|
60
|
-
* How long `CadreNode.addStrand` waits for a
|
|
61
|
-
*
|
|
70
|
+
* How long `CadreNode.addStrand` waits for a machine's first sync before rejecting with
|
|
71
|
+
* {@link StrandAwaitingFirstSyncError}. It has to cover the slowest attach sereus carries,
|
|
72
|
+
* which is not a first join: a machine RE-attaching through a relay on a slow link can take
|
|
73
|
+
* longer than one joining for the first time.
|
|
62
74
|
*
|
|
63
75
|
* Measured over a DIRECT connection (2026-09-16): the joiner reads the founder's rows about
|
|
64
|
-
* 1.3 s after `addStrand` would previously have resolved. That is
|
|
65
|
-
*
|
|
76
|
+
* 1.3 s after `addStrand` would previously have resolved. That is the fast case, and it is not
|
|
77
|
+
* what this budget has to cover.
|
|
66
78
|
*
|
|
67
|
-
*
|
|
79
|
+
* Every harness figure below was taken on one shape: one Windows developer machine, two
|
|
68
80
|
* relay-only `CadreNode`s (`listenAddrs: []`) on a shared loopback dedicated relay, with a
|
|
69
81
|
* 900 ms one-way per-frame outbound delay applied to every websocket by
|
|
70
82
|
* `integration-tests/src/harness/ws-latency.ts` in `pipelined` mode — frames stay overlapped
|
|
71
83
|
* in flight, so the figure is latency and bandwidth stays unlimited; the harness's other mode
|
|
72
84
|
* (`serial`) is a per-socket frame-rate cap and its delays are NOT comparable
|
|
73
|
-
* (`docs/testing.md` → "Link latency"). A round trip therefore costs about
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
+
* (`docs/testing.md` → "Link latency"). A round trip therefore costs about 1.8 s. Times run
|
|
86
|
+
* from the `addStrand` call to writable.
|
|
87
|
+
*
|
|
88
|
+
* FRESH JOIN (2026-09-26), a machine holding nothing of the strand yet: 23, 27, 31 and 41 s
|
|
89
|
+
* over four runs at optimystic's 1000 ms cohort read deadline, and 35, 42 and 46 s over three
|
|
90
|
+
* runs at a 5000 ms one; the re-attach scenario below later took 38.7, 38.7 and 56.9 s for
|
|
91
|
+
* the same fresh join. **The 5000 ms band, 35-57 s, is the one in force**: sereus declares
|
|
92
|
+
* 5000 ms (`COHORT_READ_DEADLINE_MS`, `quereus-plugin-sereus/src/cluster-size.ts`), because
|
|
93
|
+
* 1000 ms is shorter than one round trip on that link and leaves every cohort read with no
|
|
94
|
+
* answer to corroborate against. Widening that deadline makes a consult against a peer that
|
|
95
|
+
* cannot answer cost longer, and this phase runs several of those — which is why both bands
|
|
96
|
+
* are recorded here, and why a change to either number has to be weighed against the other.
|
|
97
|
+
*
|
|
98
|
+
* RE-ATTACH (2026-09-26, the opt-in
|
|
99
|
+
* `integration-tests/src/scenarios/strand-reattach-first-sync-measure.integration.ts` — re-run
|
|
100
|
+
* it before changing this number): B attached once on an undelayed link, left with
|
|
101
|
+
* `stopStrand`, A wrote while it was away, the delay was raised, and B called `addStrand`
|
|
102
|
+
* again.
|
|
103
|
+
* - Over an EMPTY store (one minted per launch, cadre's default with no storage provider):
|
|
104
|
+
* gated in all four runs and writable at 38.7-56.8 s, the same as a fresh join.
|
|
105
|
+
* - Over the store it KEPT (a phone with durable storage), bimodal. In 3 of 8 runs the store
|
|
106
|
+
* held everything and the launch came up writable (5.6 s, the launch itself). In the other
|
|
107
|
+
* 5 it lacked the `Strand.Header` collection B had read on its first attach, so B was gated
|
|
108
|
+
* like a joiner and became writable at 64.1-78.7 s — the slowest attach measured, about
|
|
109
|
+
* twice a fresh join. Why syncing over a partial store is slower sits inside optimystic
|
|
110
|
+
* (`tickets/blocked/report-reattach-over-partial-replica-to-optimystic`).
|
|
111
|
+
* - Neither a redial nor catching up on missed writes explains the time: B's strand node
|
|
112
|
+
* connected to A's at 11 s in every arm, fresh join included, and a run with 20 missed
|
|
113
|
+
* writes fell inside the run-to-run spread of those with 1.
|
|
114
|
+
* This budget covers WRITABLE, not caught up: in the gated kept-store runs the row A wrote
|
|
115
|
+
* while B was away became readable only at 140-162 s.
|
|
116
|
+
*
|
|
117
|
+
* Two samples from outside this harness, neither reproduced here and neither discarded: a
|
|
118
|
+
* reporter's re-attach of the same shape (optimystic #22; their harness, storage and delay
|
|
119
|
+
* injector are unknown to us) opened the gate at about 150 s, after the previous 120 s budget
|
|
120
|
+
* had already rejected; and a real Galaxy S7 joining fresh through relay.sereus.org took 178 s.
|
|
121
|
+
*
|
|
122
|
+
* 300 s clears the worst harness sample (79 s) by about 3.8x, the reporter's re-attach by 2x
|
|
123
|
+
* and the device join by about 1.7x. 240 s was rejected: it clears the device join by only
|
|
124
|
+
* 1.35x, and that sample was not a re-attach. History: the original 30 s sat INSIDE the 1000 ms
|
|
125
|
+
* fresh band and refused about half of those joins while their sync was progressing normally;
|
|
126
|
+
* 120 s was sized from fresh joins only and refused the reporter's re-attach.
|
|
85
127
|
*
|
|
86
|
-
* NOTE: accepted tradeoff — what
|
|
87
|
-
* before it is told "not yet", so a strand none of whose members is reachable at all takes
|
|
88
|
-
* minutes to report instead of
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
128
|
+
* NOTE: accepted tradeoff — what 300 s costs. This wait is what an app's `addStrand` sits in
|
|
129
|
+
* before it is told "not yet", so a strand none of whose members is reachable at all takes five
|
|
130
|
+
* minutes to report instead of two. The slow report was weighed against refusing attaches that
|
|
131
|
+
* were working, and the refusal is the worse failure. The cost is bounded — the rejection is
|
|
132
|
+
* retryable, the strand stays launched and keeps probing, and `strand:writable` fires the
|
|
133
|
+
* moment the sync lands, so an app that listens for the event rather than awaiting the call is
|
|
134
|
+
* unaffected either way. Revisit if the gate ever learns whether ANY other member is connected:
|
|
135
|
+
* "no peer at all" could then be reported at once and this budget would only ever be spent on a
|
|
136
|
+
* sync that is actually in progress — worth more the longer this budget gets.
|
|
95
137
|
*/
|
|
96
|
-
export declare const DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS =
|
|
138
|
+
export declare const DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS = 300000;
|
|
97
139
|
/** Embedder-facing tuning for the gate, threaded from `CadreNodeConfig.strandFirstSync`. */
|
|
98
140
|
export interface StrandFirstSyncConfig {
|
|
99
141
|
/**
|
|
100
|
-
* Default wait for `CadreNode.addStrand` (and `whenStrandWritable`) before a
|
|
101
|
-
*
|
|
142
|
+
* Default wait for `CadreNode.addStrand` (and `whenStrandWritable`) before a machine whose
|
|
143
|
+
* first sync has not completed is reported as not yet writable. Default
|
|
102
144
|
* {@link DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS}.
|
|
103
145
|
*/
|
|
104
146
|
timeoutMs?: number;
|
|
@@ -106,11 +148,12 @@ export interface StrandFirstSyncConfig {
|
|
|
106
148
|
pollIntervalMs?: number;
|
|
107
149
|
}
|
|
108
150
|
/**
|
|
109
|
-
* Thrown by `CadreNode.addStrand` / `whenStrandWritable` when a
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* `
|
|
151
|
+
* Thrown by `CadreNode.addStrand` / `whenStrandWritable` when a machine has not received the
|
|
152
|
+
* strand's data from another member within the wait budget — because no other member is
|
|
153
|
+
* reachable, or because one is and the sync is still arriving over a slow link; the gate
|
|
154
|
+
* cannot tell the two apart. RETRYABLE: the strand stays launched and keeps probing, so a
|
|
155
|
+
* later `addStrand` (or `whenStrandWritable`) for the same strand completes the attach once
|
|
156
|
+
* the data has arrived; the `strand:writable` event fires at that moment too.
|
|
114
157
|
*/
|
|
115
158
|
export declare class StrandAwaitingFirstSyncError extends Error {
|
|
116
159
|
readonly strandId: string;
|
|
@@ -171,8 +214,10 @@ export declare class StrandFirstSyncGate {
|
|
|
171
214
|
/** Arm the probe loop. The first probe runs after one interval — the caller already probed once. */
|
|
172
215
|
start(): void;
|
|
173
216
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
217
|
+
* Open without the probe loop: the caller is publishing the database itself — a launch
|
|
218
|
+
* whose first probe passed (or a founder's), which never armed the loop, or a founder
|
|
219
|
+
* bootstrap just run against the gated database. Stops the loop; `onHeaderHeld` is NOT
|
|
220
|
+
* invoked.
|
|
176
221
|
*/
|
|
177
222
|
open(): void;
|
|
178
223
|
/** Disarm the loop; a probe already in flight completes and is then ignored. */
|
|
@@ -17,9 +17,17 @@
|
|
|
17
17
|
* freshly initialized `StrandDatabase` back from the app — `StrandInstance.database` stays
|
|
18
18
|
* unset and the instance reports `'syncing'` — and probes on a cadence until the Header is
|
|
19
19
|
* readable AND a read of each app table settles, at which point the database is published
|
|
20
|
-
* and the instance goes `'active'`. A machine
|
|
21
|
-
*
|
|
22
|
-
* offline-first writes on a machine that has synced
|
|
20
|
+
* and the instance goes `'active'`. A machine whose store already holds the Header and every
|
|
21
|
+
* app table (a founder, and normally a restart or a hibernation resume) passes the probe on
|
|
22
|
+
* the first try and is never gated, so offline-first writes on a machine that has synced
|
|
23
|
+
* before keep working.
|
|
24
|
+
*
|
|
25
|
+
* "Has synced before" is decided by what the local store actually holds, not by whether this
|
|
26
|
+
* machine once read the strand: a machine that left soon after its first sync can hold less
|
|
27
|
+
* than it read, and is then gated like a joiner. The re-attach measurement on
|
|
28
|
+
* {@link DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS} saw exactly that — a machine back over the store
|
|
29
|
+
* it kept, without the `Strand.Header` collection it had read before it left. Gating it is
|
|
30
|
+
* right: writing into a collection this machine does not hold would fork it.
|
|
23
31
|
*
|
|
24
32
|
* Why the app tables too: the Header is ONE collection. It reaches a joiner ahead of the
|
|
25
33
|
* founder's app-table collections (peer-join backfill and pull-on-read deliver those
|
|
@@ -36,10 +44,12 @@
|
|
|
36
44
|
* every machine; the first write to it still creates its collection — see `docs/strands.md`
|
|
37
45
|
* ("Joining") for that residual.
|
|
38
46
|
*
|
|
39
|
-
* Owned by `StrandInstanceManager`: created in `buildStrandRuntime` for
|
|
40
|
-
* whose first probe
|
|
41
|
-
* resume rebuild re-probes over the same store), and
|
|
42
|
-
*
|
|
47
|
+
* Owned by `StrandInstanceManager`: created in `buildStrandRuntime` for every launch, armed
|
|
48
|
+
* only for a non-founder launch whose first probe fails, stopped and dropped in
|
|
49
|
+
* `releaseRuntime` (so a quiesce → resume rebuild re-probes over the same store), and opened
|
|
50
|
+
* by `publishDatabase` whenever the database is published without its loop — a launch that
|
|
51
|
+
* needed no gate, or a founder request that ran the bootstrap against the still-gated
|
|
52
|
+
* database (`foundExistingStrand`).
|
|
43
53
|
*
|
|
44
54
|
* NOTE: the gate covers app and membership tables, not the schema catalog. Every launch —
|
|
45
55
|
* joiner or founder — writes the catalog collection (`optimystic/schema` plus two
|
|
@@ -57,56 +67,90 @@ const log = debug('sereus:cadre:strand-first-sync');
|
|
|
57
67
|
/** How often a gated launch re-probes (`Strand.Header`, then every `App` table) while waiting for its first sync. */
|
|
58
68
|
export const DEFAULT_STRAND_FIRST_SYNC_POLL_MS = 500;
|
|
59
69
|
/**
|
|
60
|
-
* How long `CadreNode.addStrand` waits for a
|
|
61
|
-
*
|
|
70
|
+
* How long `CadreNode.addStrand` waits for a machine's first sync before rejecting with
|
|
71
|
+
* {@link StrandAwaitingFirstSyncError}. It has to cover the slowest attach sereus carries,
|
|
72
|
+
* which is not a first join: a machine RE-attaching through a relay on a slow link can take
|
|
73
|
+
* longer than one joining for the first time.
|
|
62
74
|
*
|
|
63
75
|
* Measured over a DIRECT connection (2026-09-16): the joiner reads the founder's rows about
|
|
64
|
-
* 1.3 s after `addStrand` would previously have resolved. That is
|
|
65
|
-
*
|
|
76
|
+
* 1.3 s after `addStrand` would previously have resolved. That is the fast case, and it is not
|
|
77
|
+
* what this budget has to cover.
|
|
66
78
|
*
|
|
67
|
-
*
|
|
79
|
+
* Every harness figure below was taken on one shape: one Windows developer machine, two
|
|
68
80
|
* relay-only `CadreNode`s (`listenAddrs: []`) on a shared loopback dedicated relay, with a
|
|
69
81
|
* 900 ms one-way per-frame outbound delay applied to every websocket by
|
|
70
82
|
* `integration-tests/src/harness/ws-latency.ts` in `pipelined` mode — frames stay overlapped
|
|
71
83
|
* in flight, so the figure is latency and bandwidth stays unlimited; the harness's other mode
|
|
72
84
|
* (`serial`) is a per-socket frame-rate cap and its delays are NOT comparable
|
|
73
|
-
* (`docs/testing.md` → "Link latency"). A round trip therefore costs about
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
+
* (`docs/testing.md` → "Link latency"). A round trip therefore costs about 1.8 s. Times run
|
|
86
|
+
* from the `addStrand` call to writable.
|
|
87
|
+
*
|
|
88
|
+
* FRESH JOIN (2026-09-26), a machine holding nothing of the strand yet: 23, 27, 31 and 41 s
|
|
89
|
+
* over four runs at optimystic's 1000 ms cohort read deadline, and 35, 42 and 46 s over three
|
|
90
|
+
* runs at a 5000 ms one; the re-attach scenario below later took 38.7, 38.7 and 56.9 s for
|
|
91
|
+
* the same fresh join. **The 5000 ms band, 35-57 s, is the one in force**: sereus declares
|
|
92
|
+
* 5000 ms (`COHORT_READ_DEADLINE_MS`, `quereus-plugin-sereus/src/cluster-size.ts`), because
|
|
93
|
+
* 1000 ms is shorter than one round trip on that link and leaves every cohort read with no
|
|
94
|
+
* answer to corroborate against. Widening that deadline makes a consult against a peer that
|
|
95
|
+
* cannot answer cost longer, and this phase runs several of those — which is why both bands
|
|
96
|
+
* are recorded here, and why a change to either number has to be weighed against the other.
|
|
97
|
+
*
|
|
98
|
+
* RE-ATTACH (2026-09-26, the opt-in
|
|
99
|
+
* `integration-tests/src/scenarios/strand-reattach-first-sync-measure.integration.ts` — re-run
|
|
100
|
+
* it before changing this number): B attached once on an undelayed link, left with
|
|
101
|
+
* `stopStrand`, A wrote while it was away, the delay was raised, and B called `addStrand`
|
|
102
|
+
* again.
|
|
103
|
+
* - Over an EMPTY store (one minted per launch, cadre's default with no storage provider):
|
|
104
|
+
* gated in all four runs and writable at 38.7-56.8 s, the same as a fresh join.
|
|
105
|
+
* - Over the store it KEPT (a phone with durable storage), bimodal. In 3 of 8 runs the store
|
|
106
|
+
* held everything and the launch came up writable (5.6 s, the launch itself). In the other
|
|
107
|
+
* 5 it lacked the `Strand.Header` collection B had read on its first attach, so B was gated
|
|
108
|
+
* like a joiner and became writable at 64.1-78.7 s — the slowest attach measured, about
|
|
109
|
+
* twice a fresh join. Why syncing over a partial store is slower sits inside optimystic
|
|
110
|
+
* (`tickets/blocked/report-reattach-over-partial-replica-to-optimystic`).
|
|
111
|
+
* - Neither a redial nor catching up on missed writes explains the time: B's strand node
|
|
112
|
+
* connected to A's at 11 s in every arm, fresh join included, and a run with 20 missed
|
|
113
|
+
* writes fell inside the run-to-run spread of those with 1.
|
|
114
|
+
* This budget covers WRITABLE, not caught up: in the gated kept-store runs the row A wrote
|
|
115
|
+
* while B was away became readable only at 140-162 s.
|
|
116
|
+
*
|
|
117
|
+
* Two samples from outside this harness, neither reproduced here and neither discarded: a
|
|
118
|
+
* reporter's re-attach of the same shape (optimystic #22; their harness, storage and delay
|
|
119
|
+
* injector are unknown to us) opened the gate at about 150 s, after the previous 120 s budget
|
|
120
|
+
* had already rejected; and a real Galaxy S7 joining fresh through relay.sereus.org took 178 s.
|
|
121
|
+
*
|
|
122
|
+
* 300 s clears the worst harness sample (79 s) by about 3.8x, the reporter's re-attach by 2x
|
|
123
|
+
* and the device join by about 1.7x. 240 s was rejected: it clears the device join by only
|
|
124
|
+
* 1.35x, and that sample was not a re-attach. History: the original 30 s sat INSIDE the 1000 ms
|
|
125
|
+
* fresh band and refused about half of those joins while their sync was progressing normally;
|
|
126
|
+
* 120 s was sized from fresh joins only and refused the reporter's re-attach.
|
|
85
127
|
*
|
|
86
|
-
* NOTE: accepted tradeoff — what
|
|
87
|
-
* before it is told "not yet", so a strand none of whose members is reachable at all takes
|
|
88
|
-
* minutes to report instead of
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
128
|
+
* NOTE: accepted tradeoff — what 300 s costs. This wait is what an app's `addStrand` sits in
|
|
129
|
+
* before it is told "not yet", so a strand none of whose members is reachable at all takes five
|
|
130
|
+
* minutes to report instead of two. The slow report was weighed against refusing attaches that
|
|
131
|
+
* were working, and the refusal is the worse failure. The cost is bounded — the rejection is
|
|
132
|
+
* retryable, the strand stays launched and keeps probing, and `strand:writable` fires the
|
|
133
|
+
* moment the sync lands, so an app that listens for the event rather than awaiting the call is
|
|
134
|
+
* unaffected either way. Revisit if the gate ever learns whether ANY other member is connected:
|
|
135
|
+
* "no peer at all" could then be reported at once and this budget would only ever be spent on a
|
|
136
|
+
* sync that is actually in progress — worth more the longer this budget gets.
|
|
95
137
|
*/
|
|
96
|
-
export const DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS =
|
|
138
|
+
export const DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS = 300000;
|
|
97
139
|
/**
|
|
98
|
-
* Thrown by `CadreNode.addStrand` / `whenStrandWritable` when a
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* `
|
|
140
|
+
* Thrown by `CadreNode.addStrand` / `whenStrandWritable` when a machine has not received the
|
|
141
|
+
* strand's data from another member within the wait budget — because no other member is
|
|
142
|
+
* reachable, or because one is and the sync is still arriving over a slow link; the gate
|
|
143
|
+
* cannot tell the two apart. RETRYABLE: the strand stays launched and keeps probing, so a
|
|
144
|
+
* later `addStrand` (or `whenStrandWritable`) for the same strand completes the attach once
|
|
145
|
+
* the data has arrived; the `strand:writable` event fires at that moment too.
|
|
103
146
|
*/
|
|
104
147
|
export class StrandAwaitingFirstSyncError extends Error {
|
|
105
148
|
constructor(strandId, waitedMs) {
|
|
106
|
-
super(`Strand ${strandId} is not writable yet:
|
|
107
|
-
`
|
|
108
|
-
'
|
|
109
|
-
|
|
149
|
+
super(`Strand ${strandId} is not writable yet: this machine has not yet received the strand's data ` +
|
|
150
|
+
`from another member (waited ${waitedMs} ms). Either no other member is reachable, or the ` +
|
|
151
|
+
'link to one is slow and the sync is still in progress. The strand stays launched and keeps ' +
|
|
152
|
+
'trying — call addStrand or whenStrandWritable(strandId) again to keep waiting, or listen ' +
|
|
153
|
+
"for the 'strand:writable' event.");
|
|
110
154
|
this.name = 'StrandAwaitingFirstSyncError';
|
|
111
155
|
this.strandId = strandId;
|
|
112
156
|
this.waitedMs = waitedMs;
|
|
@@ -192,15 +236,17 @@ export class StrandFirstSyncGate {
|
|
|
192
236
|
log('[%s] first-sync gate armed: no Strand.Header held yet; probing every %dms', this.deps.label, this.pollIntervalMs);
|
|
193
237
|
}
|
|
194
238
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
239
|
+
* Open without the probe loop: the caller is publishing the database itself — a launch
|
|
240
|
+
* whose first probe passed (or a founder's), which never armed the loop, or a founder
|
|
241
|
+
* bootstrap just run against the gated database. Stops the loop; `onHeaderHeld` is NOT
|
|
242
|
+
* invoked.
|
|
197
243
|
*/
|
|
198
244
|
open() {
|
|
199
245
|
if (this.opened)
|
|
200
246
|
return;
|
|
201
247
|
this.opened = true;
|
|
202
248
|
this.stop();
|
|
203
|
-
log('[%s]
|
|
249
|
+
log('[%s] database published by the caller; first-sync gate not needed or force-opened', this.deps.label);
|
|
204
250
|
}
|
|
205
251
|
/** Disarm the loop; a probe already in flight completes and is then ignored. */
|
|
206
252
|
stop() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"strand-first-sync-gate.js","sourceRoot":"","sources":["../src/strand-first-sync-gate.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"strand-first-sync-gate.js","sourceRoot":"","sources":["../src/strand-first-sync-gate.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { PrivateKey } from '@libp2p/interface';
|
|
2
2
|
import { type PeerJoinBackfillConfig } from './peer-join-backfill.js';
|
|
3
|
+
import { type StrandPeerObservation } from './strand-peer-observer.js';
|
|
4
|
+
import type { StrandPeerBookStore } from './strand-peer-book.js';
|
|
3
5
|
import { type StrandRevocationEnforcementConfig } from './strand-revocation-enforcer.js';
|
|
4
6
|
import { type PendingMembershipInviteSource, type StrandMembershipReconciliationConfig } from './strand-membership-reconciler.js';
|
|
5
7
|
import { type StrandFirstSyncConfig } from './strand-first-sync-gate.js';
|
|
@@ -200,6 +202,26 @@ export interface StartStrandConfig {
|
|
|
200
202
|
* hibernation wake's rebuilt supervisors carry it too.
|
|
201
203
|
*/
|
|
202
204
|
announceDelegateToRelay?: (strandId: string, relayAddr: string, delegatePeerId: string) => Promise<void>;
|
|
205
|
+
/**
|
|
206
|
+
* Called when the strand's libp2p node identifies a STRAND peer — one whose
|
|
207
|
+
* identify result names this strand's block-transfer protocol, so never the
|
|
208
|
+
* circuit relay or a bootstrap node — with the addresses it is dialable at
|
|
209
|
+
* (`strand-peer-observer.ts`). Throttled per peer; must not throw. `CadreNode`
|
|
210
|
+
* wires it to the node-local strand peer book, which is what lets a restarted
|
|
211
|
+
* machine dial the peers it was talking to. Absent ⇒ nothing is observed.
|
|
212
|
+
* Retained with the launch config, so a hibernation wake re-arms it on the
|
|
213
|
+
* rebuilt node.
|
|
214
|
+
*/
|
|
215
|
+
onStrandPeerIdentified?: (strandId: string, observation: StrandPeerObservation) => void;
|
|
216
|
+
/**
|
|
217
|
+
* The node-local strand peer book the book SWAP reads and writes
|
|
218
|
+
* (`strand-peer-book-swap.ts`): the strand node signs its own addresses with
|
|
219
|
+
* {@link privateKey}, exchanges signed entries with every strand peer it identifies,
|
|
220
|
+
* and files what verifies. Absent ⇒ no swap runs — a peer that dials this node's
|
|
221
|
+
* swap handler gets no answer. `CadreNode` passes its book store. Retained with the
|
|
222
|
+
* launch config, so a hibernation wake re-arms the swap on the rebuilt node.
|
|
223
|
+
*/
|
|
224
|
+
strandPeerBook?: StrandPeerBookStore;
|
|
203
225
|
}
|
|
204
226
|
/**
|
|
205
227
|
* Volatile inputs re-resolved when resuming a quiesced strand — the ones that can
|
|
@@ -259,6 +281,23 @@ export declare class StrandInstanceManager {
|
|
|
259
281
|
* missed writes).
|
|
260
282
|
*/
|
|
261
283
|
private backfills;
|
|
284
|
+
/**
|
|
285
|
+
* The per-strand peer-book observation writer (`strand-peer-observer.ts`), keyed
|
|
286
|
+
* by strand id. Same lifecycle rationale as {@link backfills}: started in
|
|
287
|
+
* `buildStrandRuntime` right after the libp2p node exists, stopped and dropped in
|
|
288
|
+
* `releaseRuntime`, so quiesce → resume re-arms it on the rebuilt node. Only
|
|
289
|
+
* present when the launch config supplied `onStrandPeerIdentified`.
|
|
290
|
+
*/
|
|
291
|
+
private peerObservers;
|
|
292
|
+
/**
|
|
293
|
+
* Per-strand strand peer book SWAP driver ({@link StrandPeerBookSwap}) — the signed
|
|
294
|
+
* exchange of book entries between strand peers. Same lifetime as the peer-book
|
|
295
|
+
* observer above: armed in `buildStrandRuntime` right after the libp2p node exists
|
|
296
|
+
* (its protocol handler must be registered before an early peer dials it), stopped
|
|
297
|
+
* and dropped in `releaseRuntime`. Only present when the launch config supplied
|
|
298
|
+
* `strandPeerBook`.
|
|
299
|
+
*/
|
|
300
|
+
private peerBookSwaps;
|
|
262
301
|
/**
|
|
263
302
|
* The per-strand revoked-peer enforcer (closed strands only), keyed by strand
|
|
264
303
|
* id. Same lifecycle rationale as {@link backfills}: created in
|
|
@@ -3,6 +3,8 @@ import { createLibp2pNode } from '@optimystic/db-p2p';
|
|
|
3
3
|
import { wrapStorageWithCache, disposeStorageCache } from '@serfab/quereus-plugin-sereus';
|
|
4
4
|
import { StrandDatabase } from './strand-database.js';
|
|
5
5
|
import { PeerJoinBackfill } from './peer-join-backfill.js';
|
|
6
|
+
import { StrandPeerObserver } from './strand-peer-observer.js';
|
|
7
|
+
import { StrandPeerBookSwap } from './strand-peer-book-swap.js';
|
|
6
8
|
import { StrandRevocationEnforcer, createRevocationConnectionGater, readStrandRevocationRows } from './strand-revocation-enforcer.js';
|
|
7
9
|
import { StrandMembershipReconciler } from './strand-membership-reconciler.js';
|
|
8
10
|
import { removeMemberPeer } from './strand-membership-writer.js';
|
|
@@ -13,7 +15,7 @@ import { StrandFirstSyncGate, StrandAwaitingFirstSyncError, strandFirstSyncCompl
|
|
|
13
15
|
import { DEFAULT_CONNECTION_MONITOR, resolveStrandClusterSize, strandClusterPolicy } from './types.js';
|
|
14
16
|
import { strandNodeAddrs } from './strand-network-config.js';
|
|
15
17
|
import { superviseRelayReservation } from './relay-reservation.js';
|
|
16
|
-
import { peerJoinPushBudget, relayReservationBudgetMs, resolveLinkRoundTripMs } from './link-budget.js';
|
|
18
|
+
import { connectionManagerTimeouts, peerJoinPushBudget, relayReservationBudgetMs, resolveLinkRoundTripMs } from './link-budget.js';
|
|
17
19
|
const log = debug('sereus:cadre:strand-manager');
|
|
18
20
|
const timing = debug('sereus:cadre:timing');
|
|
19
21
|
/**
|
|
@@ -119,6 +121,23 @@ export class StrandInstanceManager {
|
|
|
119
121
|
* missed writes).
|
|
120
122
|
*/
|
|
121
123
|
this.backfills = new Map();
|
|
124
|
+
/**
|
|
125
|
+
* The per-strand peer-book observation writer (`strand-peer-observer.ts`), keyed
|
|
126
|
+
* by strand id. Same lifecycle rationale as {@link backfills}: started in
|
|
127
|
+
* `buildStrandRuntime` right after the libp2p node exists, stopped and dropped in
|
|
128
|
+
* `releaseRuntime`, so quiesce → resume re-arms it on the rebuilt node. Only
|
|
129
|
+
* present when the launch config supplied `onStrandPeerIdentified`.
|
|
130
|
+
*/
|
|
131
|
+
this.peerObservers = new Map();
|
|
132
|
+
/**
|
|
133
|
+
* Per-strand strand peer book SWAP driver ({@link StrandPeerBookSwap}) — the signed
|
|
134
|
+
* exchange of book entries between strand peers. Same lifetime as the peer-book
|
|
135
|
+
* observer above: armed in `buildStrandRuntime` right after the libp2p node exists
|
|
136
|
+
* (its protocol handler must be registered before an early peer dials it), stopped
|
|
137
|
+
* and dropped in `releaseRuntime`. Only present when the launch config supplied
|
|
138
|
+
* `strandPeerBook`.
|
|
139
|
+
*/
|
|
140
|
+
this.peerBookSwaps = new Map();
|
|
122
141
|
/**
|
|
123
142
|
* The per-strand revoked-peer enforcer (closed strands only), keyed by strand
|
|
124
143
|
* id. Same lifecycle rationale as {@link backfills}: created in
|
|
@@ -433,6 +452,10 @@ export class StrandInstanceManager {
|
|
|
433
452
|
// widened ping deadline has to reach the strand nodes too (see
|
|
434
453
|
// DEFAULT_CONNECTION_MONITOR).
|
|
435
454
|
connectionMonitor: config.network?.connectionMonitor ?? DEFAULT_CONNECTION_MONITOR,
|
|
455
|
+
// The same declared-link limits the control node takes, and for the same reason: a
|
|
456
|
+
// strand node is the listener for every other member's strand node (`link-budget.ts`,
|
|
457
|
+
// `connectionManagerTimeouts`).
|
|
458
|
+
connectionManager: connectionManagerTimeouts(config.network?.linkRoundTripMs),
|
|
436
459
|
// Listen entries plus the WebSocket transport switch they imply — a strand node
|
|
437
460
|
// announces nothing the operator configured (`strand-network-config.ts`), and
|
|
438
461
|
// spreads AFTER `transports` above because the switch is a no-op whenever the
|
|
@@ -450,6 +473,46 @@ export class StrandInstanceManager {
|
|
|
450
473
|
});
|
|
451
474
|
timing('[buildStrandRuntime:%s] createLibp2pNode: %dms', strandId, Math.round(performance.now() - t0));
|
|
452
475
|
instance.libp2pNode = node;
|
|
476
|
+
// The peer-book observation writer, armed BEFORE the database bring-up below
|
|
477
|
+
// (unlike the backfill) because the bootstrap dials that follow `libp2p.start()`
|
|
478
|
+
// land during that bring-up, and they are exactly the peers worth remembering:
|
|
479
|
+
// identify for an early peer may already have fired by the time anything after
|
|
480
|
+
// the database initializes runs. Registered in the map immediately so the
|
|
481
|
+
// failure rollback (releaseRuntime) stops it like every other runtime component.
|
|
482
|
+
const onStrandPeerIdentified = config.onStrandPeerIdentified;
|
|
483
|
+
if (onStrandPeerIdentified) {
|
|
484
|
+
const observer = new StrandPeerObserver({
|
|
485
|
+
label: strandId,
|
|
486
|
+
libp2p: node,
|
|
487
|
+
protocolPrefix,
|
|
488
|
+
onObserved: (observation) => onStrandPeerIdentified(strandId, observation)
|
|
489
|
+
});
|
|
490
|
+
this.peerObservers.set(strandId, observer);
|
|
491
|
+
observer.start();
|
|
492
|
+
}
|
|
493
|
+
// The strand peer book SWAP, armed at the same moment and for the same reason as
|
|
494
|
+
// the observer: a peer that dials this node's swap handler before it is registered
|
|
495
|
+
// fails its exchange and sits out a ten-minute throttle. The own entry is signed
|
|
496
|
+
// now and re-signed on every address change, which covers the relay reservation
|
|
497
|
+
// landing below (`self:peer:update` fires when the circuit addr is added), so no
|
|
498
|
+
// separate post-relay step is needed. Registered in the map immediately so the
|
|
499
|
+
// failure rollback (releaseRuntime) stops it like every other runtime component.
|
|
500
|
+
//
|
|
501
|
+
// NOTE: this is a third `peer:identify` listener per running strand, beside the
|
|
502
|
+
// observer's and the backfill's — see the backfill's NOTE below for when to fold
|
|
503
|
+
// them into one dispatcher.
|
|
504
|
+
if (config.strandPeerBook) {
|
|
505
|
+
const swap = new StrandPeerBookSwap({
|
|
506
|
+
strandId,
|
|
507
|
+
libp2p: node,
|
|
508
|
+
protocolPrefix,
|
|
509
|
+
store: config.strandPeerBook,
|
|
510
|
+
privateKey: config.privateKey,
|
|
511
|
+
linkRoundTripMs: config.network?.linkRoundTripMs
|
|
512
|
+
});
|
|
513
|
+
this.peerBookSwaps.set(strandId, swap);
|
|
514
|
+
swap.start();
|
|
515
|
+
}
|
|
453
516
|
// One reservation supervisor PER RELAY, started now so the first attempts
|
|
454
517
|
// overlap the database bring-up below (the relay is never in a strand's
|
|
455
518
|
// Optimystic cohort — its protocol ids are namespaced `/optimystic/strand-<id>/…`
|
|
@@ -562,7 +625,7 @@ export class StrandInstanceManager {
|
|
|
562
625
|
// peer-join-backfill.ts for the strand-side argument (and why the control
|
|
563
626
|
// network, which DOES gate, is different).
|
|
564
627
|
//
|
|
565
|
-
// NOTE: cost is one PeerJoinBackfill object + one `
|
|
628
|
+
// NOTE: cost is one PeerJoinBackfill object + one `peer:identify` listener
|
|
566
629
|
// per running strand — linear in strand count, negligible at the handful a
|
|
567
630
|
// device or host runs today. If a node ever hosts strands by the hundred,
|
|
568
631
|
// move to one shared listener that dispatches by strand id.
|
|
@@ -600,7 +663,7 @@ export class StrandInstanceManager {
|
|
|
600
663
|
// failing here would only trade that for `StrandWatcher`'s full-rebuild retry.
|
|
601
664
|
//
|
|
602
665
|
// NOTE: a relay that is down costs this launch one full drive — the reservation budget
|
|
603
|
-
// counted from the declared link round trip,
|
|
666
|
+
// counted from the declared link round trip, 14 s at its default (`link-budget.ts`) — and
|
|
604
667
|
// `StrandWatcher` launches strands one at a time, so N strands cost N of those in
|
|
605
668
|
// bring-up during a relay outage, and MORE on a host that declared a slower link. If that
|
|
606
669
|
// ever matters, stop awaiting here (the circuit addr then lands after `active`) rather
|
|
@@ -657,6 +720,20 @@ export class StrandInstanceManager {
|
|
|
657
720
|
backfill.stop();
|
|
658
721
|
this.backfills.delete(instance.strandId);
|
|
659
722
|
}
|
|
723
|
+
// The peer-book observer goes with the node it listened on; a resume re-arms
|
|
724
|
+
// one on the rebuilt node.
|
|
725
|
+
const peerObserver = this.peerObservers.get(instance.strandId);
|
|
726
|
+
if (peerObserver) {
|
|
727
|
+
peerObserver.stop();
|
|
728
|
+
this.peerObservers.delete(instance.strandId);
|
|
729
|
+
}
|
|
730
|
+
// The book swap likewise: unsubscribed and its handler unregistered while the node is
|
|
731
|
+
// still up; a resume re-arms one, which re-signs the own entry over the new addresses.
|
|
732
|
+
const peerBookSwap = this.peerBookSwaps.get(instance.strandId);
|
|
733
|
+
if (peerBookSwap) {
|
|
734
|
+
this.peerBookSwaps.delete(instance.strandId);
|
|
735
|
+
await peerBookSwap.stop();
|
|
736
|
+
}
|
|
660
737
|
// The revoked-peer enforcer goes with the runtime it gated: a resume
|
|
661
738
|
// rebuilds it with a fresh (initially empty, fail-open) snapshot.
|
|
662
739
|
const revocationEnforcer = this.revocationEnforcers.get(instance.strandId);
|