@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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The strand peer book's OBSERVATION writer: on one strand libp2p node, every peer
|
|
3
|
+
* whose identify result names this strand's block-transfer protocol — a strand peer,
|
|
4
|
+
* never the circuit relay or a bootstrap node ({@link speaksBlockTransfer}, the
|
|
5
|
+
* same gate `PeerJoinBackfill` schedules on) — is reported with its dialable
|
|
6
|
+
* addresses, so `CadreNode` can merge an unsigned book entry for it
|
|
7
|
+
* (`strand-peer-book.ts`). That is what lets a restarted machine dial the peers it
|
|
8
|
+
* was talking to, instead of only the ones a formation once carried back.
|
|
9
|
+
*
|
|
10
|
+
* Dialable addresses are the peer's announced `listenAddrs` from the identify
|
|
11
|
+
* result, plus the open connection's `remoteAddr` when it is relayed
|
|
12
|
+
* (`/p2p-circuit`) — a relay-only peer announces its circuit listener, but the
|
|
13
|
+
* address this node actually reached it on is the one proven to work. Every
|
|
14
|
+
* address is bound to the peer id (`withTrailingPeerId`), ordered signaling-first
|
|
15
|
+
* and capped like a formation's list, so the book files it under the right peer.
|
|
16
|
+
*
|
|
17
|
+
* Throttled to one report per peer per {@link STRAND_PEER_OBSERVE_THROTTLE_MS}
|
|
18
|
+
* unless the address set changed, so a flapping relayed connection — identify
|
|
19
|
+
* fires on every reconnect, every 14 s at worst — does not rewrite the node-local
|
|
20
|
+
* slot each time. Best-effort throughout: nothing here throws into a libp2p event
|
|
21
|
+
* handler.
|
|
22
|
+
*
|
|
23
|
+
* Lifecycle: one observer per running strand, armed in
|
|
24
|
+
* `StrandInstanceManager.buildStrandRuntime` right after the libp2p node exists
|
|
25
|
+
* (the bootstrap dials that follow `libp2p.start()` are exactly the peers worth
|
|
26
|
+
* observing) and stopped in `releaseRuntime`, so a hibernation wake re-arms it on
|
|
27
|
+
* the rebuilt node. `start` also walks the peers already connected, because
|
|
28
|
+
* identify may have completed for an early peer before this object existed.
|
|
29
|
+
*/
|
|
30
|
+
import debug from 'debug';
|
|
31
|
+
import type { Connection, IdentifyResult, Libp2p, PeerId } from '@libp2p/interface';
|
|
32
|
+
import { multiaddr } from '@multiformats/multiaddr';
|
|
33
|
+
import { speaksBlockTransfer } from './peer-join-backfill.js';
|
|
34
|
+
import { isSignalingAddr, orderSignalingFirst, withTrailingPeerId } from './peer-record.js';
|
|
35
|
+
import { MAX_STRAND_ADDRS } from './strand-formation-protocol.js';
|
|
36
|
+
|
|
37
|
+
const log = debug('sereus:cadre:strand-peer-observer');
|
|
38
|
+
|
|
39
|
+
/** Minimum gap between two reports of ONE peer with an unchanged address set. */
|
|
40
|
+
export const STRAND_PEER_OBSERVE_THROTTLE_MS = 10 * 60 * 1000;
|
|
41
|
+
|
|
42
|
+
/** One strand peer seen live: its strand transport peer id and the addresses it is dialable at. */
|
|
43
|
+
export interface StrandPeerObservation {
|
|
44
|
+
peerId: string;
|
|
45
|
+
/** Bound to `peerId`, signaling-first, at most `MAX_STRAND_ADDRS`. */
|
|
46
|
+
addrs: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface StrandPeerObserverDeps {
|
|
50
|
+
/** For logs (the strand id). */
|
|
51
|
+
label: string;
|
|
52
|
+
/** The strand's libp2p node. */
|
|
53
|
+
libp2p: Libp2p;
|
|
54
|
+
/** This strand network's `/optimystic/<networkName>` prefix — the block-transfer gate. */
|
|
55
|
+
protocolPrefix: string;
|
|
56
|
+
/** Called for every (throttled) observation; must not throw. */
|
|
57
|
+
onObserved: (observation: StrandPeerObservation) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface StrandPeerObserverOptions {
|
|
61
|
+
/** Default {@link STRAND_PEER_OBSERVE_THROTTLE_MS}. */
|
|
62
|
+
throttleMs?: number;
|
|
63
|
+
/** Clock, for tests. Default `Date.now`. */
|
|
64
|
+
now?: () => number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface LastReport {
|
|
68
|
+
at: number;
|
|
69
|
+
/** The address set reported, canonicalised for comparison. */
|
|
70
|
+
key: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Anything that prints as a multiaddr. libp2p's identify result and peer store hand
|
|
75
|
+
* back its nested `@multiformats/multiaddr` copy, a structurally different type from
|
|
76
|
+
* this package's, so addresses are taken by their string form and re-parsed here.
|
|
77
|
+
*/
|
|
78
|
+
export interface AddrLike {
|
|
79
|
+
toString(): string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One connected peer as the peer store knows it: what identify recorded, plus its open connections. */
|
|
83
|
+
export interface ConnectedIdentifiedPeer {
|
|
84
|
+
peerId: PeerId;
|
|
85
|
+
protocols: string[];
|
|
86
|
+
/** The stored addresses: identify-announced, plus whatever else was merged in. */
|
|
87
|
+
addrs: AddrLike[];
|
|
88
|
+
connections: Connection[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Every peer `libp2p` holds a connection to AND has already identified (it is in the
|
|
93
|
+
* peer store), grouped per peer. A connected peer whose identify has not finished is
|
|
94
|
+
* left out: its `peer:identify` event is still to come, and every listener that walks
|
|
95
|
+
* this list at start subscribes to that event too. Shared by the peer-book observer
|
|
96
|
+
* and the book swap (`strand-peer-book-swap.ts`), which both arm right after the
|
|
97
|
+
* node exists and may already have missed an early peer's identify.
|
|
98
|
+
*/
|
|
99
|
+
export async function connectedIdentifiedPeers(libp2p: Libp2p, label: string): Promise<ConnectedIdentifiedPeer[]> {
|
|
100
|
+
const byPeer = new Map<string, { peerId: PeerId; connections: Connection[] }>();
|
|
101
|
+
for (const connection of libp2p.getConnections()) {
|
|
102
|
+
const key = connection.remotePeer.toString();
|
|
103
|
+
const group = byPeer.get(key) ?? { peerId: connection.remotePeer, connections: [] };
|
|
104
|
+
group.connections.push(connection);
|
|
105
|
+
byPeer.set(key, group);
|
|
106
|
+
}
|
|
107
|
+
const identified = await Promise.all([...byPeer.values()].map(async ({ peerId, connections }): Promise<ConnectedIdentifiedPeer | undefined> => {
|
|
108
|
+
try {
|
|
109
|
+
const peer = await libp2p.peerStore.get(peerId);
|
|
110
|
+
return { peerId, protocols: peer.protocols, addrs: peer.addresses.map((a) => a.multiaddr), connections };
|
|
111
|
+
} catch (error) {
|
|
112
|
+
// NotFoundError: identify has not finished; the event handler will see it.
|
|
113
|
+
if ((error as Error).name !== 'NotFoundError') {
|
|
114
|
+
log('[%s] peer store read for %s failed — skipping it: %o', label, peerId.toString(), error);
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}));
|
|
119
|
+
return identified.filter((peer): peer is ConnectedIdentifiedPeer => peer !== undefined);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class StrandPeerObserver {
|
|
123
|
+
private readonly throttleMs: number;
|
|
124
|
+
private readonly now: () => number;
|
|
125
|
+
private readonly lastReports = new Map<string, LastReport>();
|
|
126
|
+
private readonly onPeerIdentify: (evt: CustomEvent<IdentifyResult>) => void;
|
|
127
|
+
private started = false;
|
|
128
|
+
private stopped = false;
|
|
129
|
+
|
|
130
|
+
constructor(private readonly deps: StrandPeerObserverDeps, options?: StrandPeerObserverOptions) {
|
|
131
|
+
this.throttleMs = options?.throttleMs ?? STRAND_PEER_OBSERVE_THROTTLE_MS;
|
|
132
|
+
this.now = options?.now ?? Date.now;
|
|
133
|
+
this.onPeerIdentify = (evt) => {
|
|
134
|
+
const { peerId, protocols, listenAddrs, connection } = evt.detail;
|
|
135
|
+
if (speaksBlockTransfer(protocols, this.deps.protocolPrefix)) {
|
|
136
|
+
this.observe(peerId, listenAddrs, [connection]);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Subscribe to `peer:identify` and report the peers already connected and identified. */
|
|
142
|
+
start(): void {
|
|
143
|
+
if (this.started || this.stopped) return;
|
|
144
|
+
this.started = true;
|
|
145
|
+
this.deps.libp2p.addEventListener('peer:identify', this.onPeerIdentify);
|
|
146
|
+
void this.observeConnectedPeers().then((seen) => {
|
|
147
|
+
log('[%s] started (%d peer(s) already connected)', this.deps.label, seen);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Unsubscribe; later identify results are ignored. */
|
|
152
|
+
stop(): void {
|
|
153
|
+
if (this.stopped) return;
|
|
154
|
+
this.stopped = true;
|
|
155
|
+
if (this.started) {
|
|
156
|
+
this.deps.libp2p.removeEventListener('peer:identify', this.onPeerIdentify);
|
|
157
|
+
}
|
|
158
|
+
this.lastReports.clear();
|
|
159
|
+
log('[%s] stopped', this.deps.label);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Report every connected peer the peer store already knows speaks this strand's
|
|
164
|
+
* protocol, with its stored addresses. A peer not yet in the store (identify has
|
|
165
|
+
* not finished) is left to the `peer:identify` handler. Returns the number of
|
|
166
|
+
* identified peers considered.
|
|
167
|
+
*
|
|
168
|
+
* NOTE: the stored addresses include what `CadreNode.mergeStrandPeerAddrs` merged
|
|
169
|
+
* INTO the peer store from the book, so this walk can re-vouch a dead address the
|
|
170
|
+
* book already held, until the peer's next `peer:identify` replaces the set with
|
|
171
|
+
* what it announces now. Bounded by the address cap; if a dead address ever shows
|
|
172
|
+
* up surviving across resumes, read only the peer store's identify-sourced
|
|
173
|
+
* addresses here rather than shortening the age.
|
|
174
|
+
*/
|
|
175
|
+
private async observeConnectedPeers(): Promise<number> {
|
|
176
|
+
const peers = await connectedIdentifiedPeers(this.deps.libp2p, this.deps.label);
|
|
177
|
+
for (const { peerId, protocols, addrs, connections } of peers) {
|
|
178
|
+
if (!this.stopped && speaksBlockTransfer(protocols, this.deps.protocolPrefix)) {
|
|
179
|
+
this.observe(peerId, addrs, connections);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return peers.length;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Shape, throttle and report one peer. Never throws. */
|
|
186
|
+
private observe(peerId: PeerId, listenAddrs: AddrLike[], connections: Connection[]): void {
|
|
187
|
+
if (this.stopped) return;
|
|
188
|
+
const id = peerId.toString();
|
|
189
|
+
// Identify is about remotes, so self never arrives here; belt and braces, since
|
|
190
|
+
// a node must never file its own addresses as a peer to dial.
|
|
191
|
+
if (id === this.deps.libp2p.peerId.toString()) return;
|
|
192
|
+
try {
|
|
193
|
+
const addrs = dialableAddrs(id, listenAddrs, connections);
|
|
194
|
+
if (!this.shouldReport(id, addrs)) return;
|
|
195
|
+
log('[%s] observed strand peer %s at %d addr(s)', this.deps.label, id, addrs.length);
|
|
196
|
+
this.deps.onObserved({ peerId: id, addrs });
|
|
197
|
+
} catch (error) {
|
|
198
|
+
log('[%s] observing %s failed (ignored): %o', this.deps.label, id, error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** One report per peer per throttle window, unless the address set changed. */
|
|
203
|
+
private shouldReport(peerId: string, addrs: string[]): boolean {
|
|
204
|
+
const now = this.now();
|
|
205
|
+
const key = [...addrs].sort().join('\n');
|
|
206
|
+
const last = this.lastReports.get(peerId);
|
|
207
|
+
if (last && last.key === key && now - last.at < this.throttleMs) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
this.lastReports.set(peerId, { at: now, key });
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The addresses to file for `peerId`: its announced listen addrs plus every relayed
|
|
217
|
+
* connection addr, each bound to `peerId`, de-duplicated, signaling-first, capped.
|
|
218
|
+
* Re-parsed through this package's own `multiaddr` (see {@link AddrLike}).
|
|
219
|
+
*/
|
|
220
|
+
export function dialableAddrs(peerId: string, listenAddrs: AddrLike[], connections: Connection[]): string[] {
|
|
221
|
+
const candidates = [
|
|
222
|
+
...listenAddrs.map((ma) => ma.toString()),
|
|
223
|
+
...connections.map((c) => c.remoteAddr.toString()).filter(isSignalingAddr)
|
|
224
|
+
];
|
|
225
|
+
const bound = new Set<string>();
|
|
226
|
+
for (const candidate of candidates) {
|
|
227
|
+
const addr = withTrailingPeerId(multiaddr(candidate), peerId);
|
|
228
|
+
if (addr === null) {
|
|
229
|
+
log('dropping addr %s — it names a peer other than %s', candidate, peerId);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
bound.add(addr.toString());
|
|
233
|
+
}
|
|
234
|
+
return orderSignalingFirst([...bound]).slice(0, MAX_STRAND_ADDRS);
|
|
235
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -10,7 +10,9 @@ import type { SeedTrustPolicy } from './seed-trust-policy.js';
|
|
|
10
10
|
import type { KeyStore, KeyId } from './key-store.js';
|
|
11
11
|
import type { TrustedOwnerStore, TrustSource } from './trusted-owner-store.js';
|
|
12
12
|
import type { BootstrapPeerStore } from './bootstrap-peer-store.js';
|
|
13
|
+
import type { StrandPeerBookStore } from './strand-peer-book.js';
|
|
13
14
|
import type { EnrolledMachineStore } from './enrolled-machine-store.js';
|
|
15
|
+
import type { JoinedStrandStore } from './joined-strand-store.js';
|
|
14
16
|
import type { PushNotifier } from './push-notifier.js';
|
|
15
17
|
import type { RevocableTable } from './control-authorization.js';
|
|
16
18
|
import type { ControlRetryAbandonment } from './control-retry.js';
|
|
@@ -317,7 +319,7 @@ export interface NetworkConfig {
|
|
|
317
319
|
* It buys a node that BOOTS, not a node that boots fast: `start()` still waits
|
|
318
320
|
* out that first attempt, which costs the drive's whole timeout
|
|
319
321
|
* (`DEFAULT_RELAY_RESERVE_TIMEOUT_MS`, four link round trips at
|
|
320
|
-
* {@link linkRoundTripMs} —
|
|
322
|
+
* {@link linkRoundTripMs} — 14 s at its default) against a relay that is unreachable
|
|
321
323
|
* rather than merely refusing (`relay-reservation.ts` polls to the deadline, in
|
|
322
324
|
* case libp2p's own discovery lands a reservation independently).
|
|
323
325
|
*
|
|
@@ -434,22 +436,25 @@ export interface NetworkConfig {
|
|
|
434
436
|
* The round trip this node assumes between itself and another machine, in milliseconds, for
|
|
435
437
|
* the control node and every strand node — as {@link cohortQueryTimeoutMs} is, and for the
|
|
436
438
|
* same reason: the setting describes the LINK, and a phone's control node and its strand
|
|
437
|
-
* nodes ride the same one. Omitted takes {@link DECLARED_LINK_ROUND_TRIP_MS} (
|
|
439
|
+
* nodes ride the same one. Omitted takes {@link DECLARED_LINK_ROUND_TRIP_MS} (3500 ms).
|
|
438
440
|
*
|
|
439
441
|
* This is NOT a timeout. It is the one stated assumption that cadre's own dial and
|
|
440
442
|
* reservation deadlines are DERIVED from, each by the number of round trips that operation
|
|
441
443
|
* was measured to cost: a peer-join catch-up's dial to one peer, its push response, one relay
|
|
442
|
-
* reservation drive,
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
444
|
+
* reservation drive, the control-cohort dial budgets, and libp2p's own `dialTimeout` and
|
|
445
|
+
* `inboundUpgradeTimeout` on every node. Reaching another machine through a relay costs a
|
|
446
|
+
* fixed number of exchanges, so a deadline written as milliseconds has a link speed above
|
|
447
|
+
* which it can never open a connection — which is the defect this declaration exists to make
|
|
448
|
+
* impossible to reintroduce one budget at a time. The counts, the measurement behind them,
|
|
449
|
+
* and what still fails at the supported link are in `link-budget.ts`.
|
|
447
450
|
*
|
|
448
|
-
*
|
|
449
|
-
* the ordinary cost of longer deadlines, a peer
|
|
450
|
-
* that
|
|
451
|
-
*
|
|
452
|
-
*
|
|
451
|
+
* The default covers the slowest link sereus supports, a 3-second round trip through a relay.
|
|
452
|
+
* Raise it for a link slower still; the cost is the ordinary cost of longer deadlines, a peer
|
|
453
|
+
* that is genuinely gone holding each operation that much longer before it is abandoned and
|
|
454
|
+
* retried. Lower it only if EVERY machine of the party is that close: this node is also the
|
|
455
|
+
* listener for connections other machines open to it, and its `inboundUpgradeTimeout` comes
|
|
456
|
+
* from this value, so a node declaring a faster link than its peers discards their
|
|
457
|
+
* half-built connections — silently, from the dialer's side.
|
|
453
458
|
*
|
|
454
459
|
* Refused where the libp2p node is built — inside `CadreNode.start()` for the control
|
|
455
460
|
* network, inside `CadreNode.addStrand` for a strand — if it is not a finite number above
|
|
@@ -569,6 +574,12 @@ export interface NetworkConfig {
|
|
|
569
574
|
* kept it (the same pair aborted at a 200ms stall only when the deadline was 300ms). An
|
|
570
575
|
* interval strictly above the deadline is what makes the 30 seconds real.
|
|
571
576
|
*
|
|
577
|
+
* WHAT THE LINK NEEDS OF IT. One ping opens a fresh stream and echoes over it: a protocol
|
|
578
|
+
* negotiation plus the echo, two link round trips by `link-budget.ts`'s counts (a `newStream`
|
|
579
|
+
* over a relayed circuit measured 3016 ms at 1500 ms one-way). At the slowest link sereus
|
|
580
|
+
* supports, a 3-second relayed round trip, that is about 6 s — well inside the 30 s deadline,
|
|
581
|
+
* which exists for the phone's CPU rather than the link.
|
|
582
|
+
*
|
|
572
583
|
* WHAT IT COSTS. A dead peer is reclaimed 30 to 65 seconds after it stops answering — the
|
|
573
584
|
* deadline, plus up to one interval of waiting for the ping that will fail — where
|
|
574
585
|
* libp2p's defaults took about 5 to 15 seconds. `db-p2p` caps a node at 16 connections,
|
|
@@ -807,11 +818,11 @@ export interface CadreNodeConfig {
|
|
|
807
818
|
* `timeoutMs` bounds how long {@link CadreNode.addStrand} waits before rejecting with
|
|
808
819
|
* `StrandAwaitingFirstSyncError` (retryable — the launch stays up and keeps probing);
|
|
809
820
|
* `pollIntervalMs` is the probe cadence. Omit for the defaults (`DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS`,
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
* only copy of
|
|
813
|
-
* deliberately no way to disable the gate: a machine
|
|
814
|
-
* gated, so nothing that works today is blocked by it.
|
|
821
|
+
* sized for a machine re-attaching through a relay on a slow link, the slowest attach
|
|
822
|
+
* measured; that constant's doc comment carries the number, the measurement behind it and
|
|
823
|
+
* what the budget costs, and is the only copy of them; `DEFAULT_STRAND_FIRST_SYNC_POLL_MS`,
|
|
824
|
+
* 500 ms). There is deliberately no way to disable the gate: a machine whose store already
|
|
825
|
+
* holds the Header is never gated, so nothing that works today is blocked by it.
|
|
815
826
|
*/
|
|
816
827
|
strandFirstSync?: StrandFirstSyncConfig;
|
|
817
828
|
|
|
@@ -938,6 +949,54 @@ export interface CadreNodeConfig {
|
|
|
938
949
|
store?: EnrolledMachineStore;
|
|
939
950
|
};
|
|
940
951
|
|
|
952
|
+
/**
|
|
953
|
+
* Node-local record of the strands this node joined from ANOTHER party (see
|
|
954
|
+
* `joined-strand-store.ts`): id, type and the closed strand's read secret, recorded
|
|
955
|
+
* by `formStrand` and by a joining (not `founder: true`) `addStrand` whose row is
|
|
956
|
+
* not in this party's control database, and re-offered on every start as `strand:discovered` — so an embedding
|
|
957
|
+
* app keeps no list of its own.
|
|
958
|
+
*
|
|
959
|
+
* Absent ⇒ a `KeyStoreJoinedStrandStore` over {@link keyStore} when one is
|
|
960
|
+
* configured (the record carries a secret, and the KeyStore is where every platform
|
|
961
|
+
* already keeps secrets), else an in-memory store that warns once and forgets every
|
|
962
|
+
* join on restart. An embedder that passes {@link privateKey} rather than a
|
|
963
|
+
* `keyStore` injects a durable store here, e.g.
|
|
964
|
+
* `new KeyStoreJoinedStrandStore(new FileKeyStore(dir), partyId)`.
|
|
965
|
+
*/
|
|
966
|
+
joinedStrands?: {
|
|
967
|
+
/** Its `partyId` must match `controlNetwork.partyId`; start() fails closed on a mismatch. */
|
|
968
|
+
store?: JoinedStrandStore;
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Node-local strand peer book (see `strand-peer-book.ts`): per strand, the strand
|
|
973
|
+
* peers this node has met — the other parties' strand transport peers a formation
|
|
974
|
+
* carried back, and every strand peer observed on a live connection — with their
|
|
975
|
+
* last-known addresses. Read on every launch, hibernation resume and periodic
|
|
976
|
+
* address refresh, so a restarted machine dials the peers it was talking to before
|
|
977
|
+
* anything else, instead of coming back up alone (gotchoices/sereus#18). Sibling
|
|
978
|
+
* of {@link bootstrapPeers} — same NON-replicated, per-party, injected-backend
|
|
979
|
+
* shape — and, like it, nothing here is trust-bearing: dial hints only, each
|
|
980
|
+
* bound to its peer id. Entries age out after 14 days (`STRAND_PEER_MAX_AGE_MS`,
|
|
981
|
+
* overridable per store via `StrandPeerBookOptions.maxAgeMs`).
|
|
982
|
+
*
|
|
983
|
+
* Absent ⇒ an in-memory store is created at start() (ephemeral: the book dies
|
|
984
|
+
* with the process, and a cross-party strand whose only contact was the other
|
|
985
|
+
* party does not re-mesh after a restart until a fresh formation). Every
|
|
986
|
+
* reference embedder injects a durable backend over the same slot kind as its
|
|
987
|
+
* bootstrap-peer store. A new platform needs no new store class: supply a
|
|
988
|
+
* `DurableSlot` and inject `PersistentStrandPeerBookStore.open(slot, partyId)`.
|
|
989
|
+
*/
|
|
990
|
+
strandPeers?: {
|
|
991
|
+
/**
|
|
992
|
+
* Injected store instance — e.g. a `FileStrandPeerBookStore` from the
|
|
993
|
+
* Node-only subpath `@serfab/cadre-core/strand-peer-book-file`, persisted in
|
|
994
|
+
* the node's state directory. Its `partyId` must match
|
|
995
|
+
* `controlNetwork.partyId`; start() fails closed on a mismatch.
|
|
996
|
+
*/
|
|
997
|
+
store?: StrandPeerBookStore;
|
|
998
|
+
};
|
|
999
|
+
|
|
941
1000
|
/**
|
|
942
1001
|
* Platform push-delivery for suspended mobile peers. When present, the node's
|
|
943
1002
|
* server fan-out can deliver strand-wake data messages over the platform push
|
|
@@ -1385,6 +1444,10 @@ export interface CadreNodeEvents {
|
|
|
1385
1444
|
* re-admits this party, so a second removal is reported again — and a
|
|
1386
1445
|
* hibernation wake rebuilds the gate, which may re-emit for a strand still
|
|
1387
1446
|
* revoked.
|
|
1447
|
+
*
|
|
1448
|
+
* For a strand joined from another party, the node also forgets its remembered
|
|
1449
|
+
* join (see {@link CadreNodeConfig.joinedStrands}): it keeps running this session
|
|
1450
|
+
* and is not re-offered after the next start.
|
|
1388
1451
|
*/
|
|
1389
1452
|
'strand:revoked': { strandId: string };
|
|
1390
1453
|
/**
|
|
@@ -1418,6 +1481,12 @@ export interface CadreNodeEvents {
|
|
|
1418
1481
|
* Carries the full {@link StrandRow} so the app can join without re-querying
|
|
1419
1482
|
* the control DB.
|
|
1420
1483
|
*
|
|
1484
|
+
* Also emitted for a strand this node joined from ANOTHER party, which no control
|
|
1485
|
+
* row names: the node remembers every such join (see
|
|
1486
|
+
* {@link CadreNodeConfig.joinedStrands}) and offers it here on each start, as a row
|
|
1487
|
+
* with `FounderOwnerKey: null` carrying the closed strand's `MemberPrivateKey`. That
|
|
1488
|
+
* row is the product of the formation's consent, so claiming it needs no second one.
|
|
1489
|
+
*
|
|
1421
1490
|
* **Fired once per strand per session, and it can fire before your listener is
|
|
1422
1491
|
* attached.** The strand watcher's first poll runs inside `CadreNode.start()`
|
|
1423
1492
|
* (100 ms after the watcher starts), so every strand already stored for this
|