@serfab/cadre-core 0.13.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -6
- package/dist/bootstrap-peer-store-file.js +2 -2
- package/dist/bootstrap-peer-store.d.ts +32 -12
- package/dist/bootstrap-peer-store.js +41 -15
- package/dist/bootstrap-peer-store.js.map +1 -1
- package/dist/cadre-node.d.ts +558 -80
- package/dist/cadre-node.js +1060 -170
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +2 -2
- package/dist/control-authorization.js +1 -0
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +13 -26
- package/dist/control-cohort.js +0 -26
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +299 -44
- package/dist/control-database.js +469 -69
- package/dist/control-database.js.map +1 -1
- package/dist/control-read-retry.d.ts +40 -11
- package/dist/control-read-retry.js +48 -15
- package/dist/control-read-retry.js.map +1 -1
- package/dist/control-retry.d.ts +58 -3
- package/dist/control-retry.js +58 -3
- package/dist/control-retry.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +103 -19
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +80 -11
- package/dist/control-write-retry.js +176 -29
- package/dist/control-write-retry.js.map +1 -1
- package/dist/enrolled-machine-store.js +2 -2
- package/dist/index.d.ts +14 -7
- package/dist/index.js +33 -6
- package/dist/index.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +15 -7
- package/dist/node-local-snapshot.js +23 -8
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/peer-dial.d.ts +143 -0
- package/dist/peer-dial.js +176 -0
- package/dist/peer-dial.js.map +1 -0
- package/dist/relay-addrs.d.ts +90 -52
- package/dist/relay-addrs.js +91 -59
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +71 -3
- package/dist/relay-reservation.js +116 -8
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +30 -1
- package/dist/seed-bootstrap.js +62 -17
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/storage-scope.d.ts +62 -0
- package/dist/storage-scope.js +75 -0
- package/dist/storage-scope.js.map +1 -0
- package/dist/strand-database.d.ts +34 -16
- package/dist/strand-database.js +38 -24
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +152 -0
- package/dist/strand-first-sync-gate.js +208 -0
- package/dist/strand-first-sync-gate.js.map +1 -0
- package/dist/strand-formation-manager.d.ts +87 -5
- package/dist/strand-formation-manager.js +109 -5
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +21 -1
- package/dist/strand-formation-protocol.js +30 -0
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +243 -6
- package/dist/strand-instance-manager.js +470 -37
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-key.d.ts +20 -12
- package/dist/strand-member-key.js +20 -12
- package/dist/strand-member-key.js.map +1 -1
- package/dist/strand-member-registry.js +3 -0
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +361 -0
- package/dist/strand-membership-reconciler.js +548 -0
- package/dist/strand-membership-reconciler.js.map +1 -0
- package/dist/strand-membership-writer.d.ts +131 -11
- package/dist/strand-membership-writer.js +396 -134
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-network-config.d.ts +46 -14
- package/dist/strand-network-config.js +79 -23
- package/dist/strand-network-config.js.map +1 -1
- package/dist/strand-solicitation.d.ts +10 -1
- package/dist/strand-solicitation.js +5 -0
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +11 -7
- package/dist/strand-wake-protocol.js +20 -52
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/strand-watcher.js +6 -0
- package/dist/strand-watcher.js.map +1 -1
- package/dist/timed-step.d.ts +12 -0
- package/dist/timed-step.js +28 -0
- package/dist/timed-step.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +19 -0
- package/dist/timeout-scheduler.js +22 -0
- package/dist/timeout-scheduler.js.map +1 -0
- package/dist/types.d.ts +262 -30
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/bootstrap-peer-store-file.ts +2 -2
- package/src/bootstrap-peer-store.ts +69 -27
- package/src/cadre-node.ts +7301 -6353
- package/src/control-authorization.ts +2 -1
- package/src/control-cohort.ts +122 -130
- package/src/control-database.ts +537 -69
- package/src/control-read-retry.ts +202 -167
- package/src/control-retry.ts +297 -190
- package/src/control-schema.ts +103 -19
- package/src/control-write-retry.ts +533 -383
- package/src/enrolled-machine-store.ts +2 -2
- package/src/index.ts +83 -6
- package/src/node-local-snapshot.ts +25 -8
- package/src/peer-dial.ts +215 -0
- package/src/relay-addrs.ts +458 -436
- package/src/relay-reservation.ts +951 -828
- package/src/seed-bootstrap.ts +76 -18
- package/src/storage-scope.ts +78 -0
- package/src/strand-database.ts +50 -27
- package/src/strand-first-sync-gate.ts +258 -0
- package/src/strand-formation-manager.ts +140 -5
- package/src/strand-formation-protocol.ts +43 -1
- package/src/strand-instance-manager.ts +577 -37
- package/src/strand-member-key.ts +20 -12
- package/src/strand-member-registry.ts +3 -0
- package/src/strand-membership-reconciler.ts +680 -0
- package/src/strand-membership-writer.ts +527 -189
- package/src/strand-network-config.ts +102 -24
- package/src/strand-solicitation.ts +16 -1
- package/src/strand-wake-protocol.ts +21 -66
- package/src/strand-watcher.ts +6 -0
- package/src/timed-step.ts +28 -0
- package/src/timeout-scheduler.ts +28 -0
- package/src/types.ts +1789 -1554
|
@@ -19,16 +19,32 @@
|
|
|
19
19
|
* The reachability model this rests on: **a strand node is not separately dialable
|
|
20
20
|
* at a published fixed address.** It is reached through (a) its ephemeral direct
|
|
21
21
|
* listener plus the addresses peers observe for it, and (b) circuit relay —
|
|
22
|
-
* `network.relayAddrs` resolves to
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* `network.relayAddrs` resolves to ONE bare `/p2p-circuit` SEARCH listen entry PER
|
|
23
|
+
* relay, and the strand runtime (`strand-instance-manager.ts`) runs one
|
|
24
|
+
* reservation supervisor per relay over the dial addrs this module returns beside
|
|
25
|
+
* them ({@link StrandNodeAddrs.relayAddrs}). The same route the control node takes,
|
|
26
|
+
* for the same reason the configured `<relay>/p2p-circuit` shape was retired
|
|
27
|
+
* everywhere (`relay-addrs.ts`): a configured listener loses its address on a relay
|
|
28
|
+
* restart, on either side hanging up, and on libp2p's own reservation refresh, and
|
|
29
|
+
* recovers from none of them. One search entry per relay rather than one for all,
|
|
30
|
+
* because a search listener registers exactly ONE pending reservation and libp2p
|
|
31
|
+
* fills a pending reservation with exactly one relay — so N relays need N listeners,
|
|
32
|
+
* each with a supervisor that owns it.
|
|
25
33
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
34
|
+
* A hand-written `<relay>/p2p-circuit` entry in `listenAddrs` counts as a relay
|
|
35
|
+
* (its dial prefix is supervised like a `relayAddrs` entry) — the control node
|
|
36
|
+
* rejects that entry outright, so in production it never reaches here, but a
|
|
37
|
+
* strand built straight from such a config must not get an unsupervised configured
|
|
38
|
+
* listener either. A hand-written BARE `/p2p-circuit` (the browser shape,
|
|
39
|
+
* `reference-app-web`) passes through unchanged when no relay is named, and is
|
|
40
|
+
* absorbed by the per-relay entries when one is.
|
|
41
|
+
*
|
|
42
|
+
* Everything else in `NetworkConfig` — `transports`, `connectionGater`,
|
|
43
|
+
* `enableRelay` — is inherited by the caller; this module only owns the two
|
|
44
|
+
* host-endpoint fields above and the relay listen shape. One caveat on
|
|
45
|
+
* `connectionGater`: an OPEN strand's node gets it unchanged, while a CLOSED strand's
|
|
46
|
+
* node composes revoked-peer denial onto it in `strand-instance-manager.ts` (every
|
|
47
|
+
* supplied hook still honored — see `strand-revocation-enforcer.ts`).
|
|
32
48
|
*
|
|
33
49
|
* It does carry one non-address option out with them: the WebSocket transport switch
|
|
34
50
|
* a `/ws` listen entry implies (`relay-addrs.ts` → `resolveTransportOptions`). That is
|
|
@@ -38,7 +54,14 @@
|
|
|
38
54
|
*/
|
|
39
55
|
|
|
40
56
|
import { multiaddr, type Component } from '@multiformats/multiaddr';
|
|
41
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
DEFAULT_DIRECT_LISTEN_ADDR,
|
|
59
|
+
RELAY_SEARCH_LISTEN_ADDR,
|
|
60
|
+
isConfiguredCircuitListenAddr,
|
|
61
|
+
relayCircuitAddrs,
|
|
62
|
+
resolveTransportOptions
|
|
63
|
+
} from './relay-addrs.js';
|
|
64
|
+
import { extractCircuitRelayTargets } from './delegate-admission.js';
|
|
42
65
|
import type { NetworkConfig } from './types.js';
|
|
43
66
|
|
|
44
67
|
/**
|
|
@@ -59,7 +82,9 @@ export interface StrandNodeAddrs {
|
|
|
59
82
|
/**
|
|
60
83
|
* Listen entries for this strand node, or `undefined` when the operator
|
|
61
84
|
* configured neither `listenAddrs` nor `relayAddrs` — in which case the caller
|
|
62
|
-
* omits the option and inherits `@optimystic/db-p2p`'s own default.
|
|
85
|
+
* omits the option and inherits `@optimystic/db-p2p`'s own default. Carries one
|
|
86
|
+
* bare `/p2p-circuit` entry per entry of {@link relayAddrs}, deliberately NOT
|
|
87
|
+
* deduplicated: each one is its own listener with its own pending reservation.
|
|
63
88
|
*/
|
|
64
89
|
listenAddrs?: string[];
|
|
65
90
|
/**
|
|
@@ -70,14 +95,28 @@ export interface StrandNodeAddrs {
|
|
|
70
95
|
* `/ws` strand listen entry binds nothing and libp2p reports nothing.
|
|
71
96
|
*/
|
|
72
97
|
wsPort?: number;
|
|
98
|
+
/**
|
|
99
|
+
* The relays this strand node reserves through, as DIRECT dial addrs
|
|
100
|
+
* (`<host>/p2p/<relayPeerId>`, no `/p2p-circuit`), one per relay, in config order,
|
|
101
|
+
* deduplicated by relay peer id — the union of `network.relayAddrs` and any
|
|
102
|
+
* hand-written `<relay>/p2p-circuit` entry in `network.listenAddrs`. Present only
|
|
103
|
+
* when at least one relay is named.
|
|
104
|
+
*
|
|
105
|
+
* NOT a `createLibp2pNode` option: the caller (`strand-instance-manager.ts`)
|
|
106
|
+
* destructures it off before spreading the rest, and starts one
|
|
107
|
+
* `superviseRelayReservation` per entry over the node it built.
|
|
108
|
+
*/
|
|
109
|
+
relayAddrs?: string[];
|
|
73
110
|
}
|
|
74
111
|
|
|
75
112
|
/**
|
|
76
|
-
* The strand-node view of `network`: the
|
|
77
|
-
*
|
|
113
|
+
* The strand-node view of `network`: the operator's direct listen entries with every
|
|
114
|
+
* fixed port rewritten to an ephemeral one, one bare `/p2p-circuit` search entry per
|
|
115
|
+
* relay, the relay dial addrs those entries are supervised against, and no announce
|
|
116
|
+
* fields.
|
|
78
117
|
*
|
|
79
|
-
* Validation is
|
|
80
|
-
* malformed `relayAddrs` entry
|
|
118
|
+
* Validation is fail-fast exactly as it is for the control node — `relayCircuitAddrs`
|
|
119
|
+
* throws here on a malformed `relayAddrs` entry.
|
|
81
120
|
*
|
|
82
121
|
* NOTE: announce entries are no longer validated on this path at all; the CONTROL
|
|
83
122
|
* node's own build (`cadre-node.ts`) is the only thing that parses them, and it runs
|
|
@@ -87,22 +126,61 @@ export interface StrandNodeAddrs {
|
|
|
87
126
|
* would then go unreported until the control node's own build.
|
|
88
127
|
*/
|
|
89
128
|
export function strandNodeAddrs(network: NetworkConfig | undefined): StrandNodeAddrs {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// and leave every NAT'd strand node undialable. See `relay-addrs.ts`.
|
|
94
|
-
const listenAddrs = resolveListenAddrs(network);
|
|
95
|
-
if (!listenAddrs) {
|
|
129
|
+
const relayAddrs = strandRelayAddrs(network);
|
|
130
|
+
const configured = network?.listenAddrs;
|
|
131
|
+
if (!configured && relayAddrs.length === 0) {
|
|
96
132
|
return {};
|
|
97
133
|
}
|
|
98
134
|
// An explicitly empty `listenAddrs` (the React Native "cannot listen" case) stays
|
|
99
135
|
// empty — rewriting must never resurrect a direct listener that was opted out of.
|
|
100
|
-
|
|
136
|
+
// Naming a relay with no `listenAddrs` keeps the direct listener the control node
|
|
137
|
+
// keeps in that case (`relay-addrs.ts`), so a relay ADDS reachability rather than
|
|
138
|
+
// replacing it.
|
|
139
|
+
const direct = configured ?? [DEFAULT_DIRECT_LISTEN_ADDR];
|
|
140
|
+
// Every circuit entry — the configured shape (now a relay in `relayAddrs`) and a
|
|
141
|
+
// hand-written bare search entry alike — is replaced by the per-relay search entries
|
|
142
|
+
// once a relay is named; with none, the operator's entries pass through as written.
|
|
143
|
+
const kept = relayAddrs.length > 0 ? direct.filter((addr) => !isCircuitListenAddr(addr)) : direct;
|
|
144
|
+
// The search entries are appended AFTER the dedupe on purpose: they are identical
|
|
145
|
+
// strings, and each one must survive as its own listener (see the module doc).
|
|
146
|
+
//
|
|
147
|
+
// NOTE: every bare entry also makes libp2p start its own relay discovery whenever
|
|
148
|
+
// that listener's slot is empty (launch, and every loss). Measured on a node with
|
|
149
|
+
// no peer router: one peer-store scan plus a random walk that fails at once
|
|
150
|
+
// (`NoPeerRoutersError`) per loss event — no dial churn. If strand nodes ever get
|
|
151
|
+
// a peer router (a DHT), that walk turns into real dials on every relay loss;
|
|
152
|
+
// cap or disable `circuitRelayTransport`'s discovery then.
|
|
153
|
+
const listenAddrs = [
|
|
154
|
+
...dedupe(kept.map(ephemeralPortListenAddr)),
|
|
155
|
+
...relayAddrs.map(() => RELAY_SEARCH_LISTEN_ADDR)
|
|
156
|
+
];
|
|
101
157
|
// Classified AFTER the ephemeral rewrite, so the check reads what this strand node
|
|
102
158
|
// will actually bind. Zeroing a port cannot change an entry's transport — only the
|
|
103
159
|
// `tcp`/`udp` component's value moves — so `/ip4/0.0.0.0/tcp/4002/ws` still resolves
|
|
104
|
-
// to WebSocket as `/ip4/0.0.0.0/tcp/0/ws`.
|
|
105
|
-
return {
|
|
160
|
+
// to WebSocket as `/ip4/0.0.0.0/tcp/0/ws`; a bare `/p2p-circuit` is `'circuit'`.
|
|
161
|
+
return {
|
|
162
|
+
listenAddrs,
|
|
163
|
+
...resolveTransportOptions(network, listenAddrs),
|
|
164
|
+
...(relayAddrs.length > 0 && { relayAddrs })
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The relays a strand node built from `network` reserves through, as direct dial
|
|
170
|
+
* addrs: every `relayAddrs` entry (validated — a malformed one throws, naming the
|
|
171
|
+
* field) plus the relay named by any hand-written `<relay>/p2p-circuit` listen entry,
|
|
172
|
+
* deduplicated by relay PEER ID with the first spelling winning — two spellings of one
|
|
173
|
+
* relay would otherwise get two supervisors fighting over one reservation slot.
|
|
174
|
+
*/
|
|
175
|
+
function strandRelayAddrs(network: NetworkConfig | undefined): string[] {
|
|
176
|
+
const configured = relayCircuitAddrs(network?.relayAddrs ?? []);
|
|
177
|
+
const handWritten = (network?.listenAddrs ?? []).filter(isConfiguredCircuitListenAddr);
|
|
178
|
+
return extractCircuitRelayTargets([...configured, ...handWritten]).map((relay) => relay.relayAddr);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Any `/p2p-circuit` listen entry — bare or configured; an unparsable entry is not one. */
|
|
182
|
+
function isCircuitListenAddr(listenAddr: string): boolean {
|
|
183
|
+
return listenAddr === RELAY_SEARCH_LISTEN_ADDR || isConfiguredCircuitListenAddr(listenAddr);
|
|
106
184
|
}
|
|
107
185
|
|
|
108
186
|
/**
|
|
@@ -7,7 +7,8 @@ import { sign } from '@optimystic/quereus-plugin-crypto';
|
|
|
7
7
|
import type {
|
|
8
8
|
OpenInvitation,
|
|
9
9
|
FormStrandResult,
|
|
10
|
-
StrandFormationDisclosure
|
|
10
|
+
StrandFormationDisclosure,
|
|
11
|
+
StrandMembershipInvite
|
|
11
12
|
} from './types.js';
|
|
12
13
|
// control-database does not import this service, so these imports introduce no cycle.
|
|
13
14
|
import { generateStampId, formationConsentMessage } from './control-database.js';
|
|
@@ -199,6 +200,14 @@ export interface StrandSolicitationServiceOptions {
|
|
|
199
200
|
* by `CadreNode`; unwired, the responder discloses no strand addresses.
|
|
200
201
|
*/
|
|
201
202
|
resolveStrandAddrs?: (strandId: string) => string[];
|
|
203
|
+
/**
|
|
204
|
+
* Issue a single-use strand membership invitation against the live host strand a
|
|
205
|
+
* bound closed-strand redemption resolved to (responder side). Wired by `CadreNode`;
|
|
206
|
+
* see `StrandFormationManagerOptions.issueMembershipInvite` for the full contract
|
|
207
|
+
* (null = open strand, throw = reject the redemption retryably, unwired = mock/test
|
|
208
|
+
* posture: approve with no invitation).
|
|
209
|
+
*/
|
|
210
|
+
issueMembershipInvite?: (strandId: string) => Promise<StrandMembershipInvite | null>;
|
|
202
211
|
/** Configuration for the formation manager */
|
|
203
212
|
formationConfig?: StrandFormationManagerConfig;
|
|
204
213
|
}
|
|
@@ -224,6 +233,7 @@ export class StrandSolicitationService {
|
|
|
224
233
|
private readonly partyId: string;
|
|
225
234
|
private readonly cadrePeerAddrs: string[];
|
|
226
235
|
private readonly resolveStrandAddrs?: (strandId: string) => string[];
|
|
236
|
+
private readonly issueMembershipInvite?: (strandId: string) => Promise<StrandMembershipInvite | null>;
|
|
227
237
|
private formationManager?: StrandFormationManager;
|
|
228
238
|
private readonly formationConfig?: StrandFormationManagerConfig;
|
|
229
239
|
/**
|
|
@@ -247,6 +257,7 @@ export class StrandSolicitationService {
|
|
|
247
257
|
this.partyId = options?.partyId ?? `party-${Date.now()}`;
|
|
248
258
|
this.cadrePeerAddrs = options?.cadrePeerAddrs ?? [];
|
|
249
259
|
this.resolveStrandAddrs = options?.resolveStrandAddrs;
|
|
260
|
+
this.issueMembershipInvite = options?.issueMembershipInvite;
|
|
250
261
|
this.formationConfig = options?.formationConfig;
|
|
251
262
|
log('StrandSolicitationService created for party: %s', this.partyId);
|
|
252
263
|
}
|
|
@@ -265,6 +276,7 @@ export class StrandSolicitationService {
|
|
|
265
276
|
partyId: this.partyId,
|
|
266
277
|
cadrePeerAddrs: this.cadrePeerAddrs,
|
|
267
278
|
resolveStrandAddrs: this.resolveStrandAddrs,
|
|
279
|
+
issueMembershipInvite: this.issueMembershipInvite,
|
|
268
280
|
config: this.formationConfig
|
|
269
281
|
});
|
|
270
282
|
}
|
|
@@ -353,6 +365,9 @@ export class StrandSolicitationService {
|
|
|
353
365
|
// Carry the host strand's membership key delivered over the protocol (closed-strand
|
|
354
366
|
// provision-then-record). invitePrivateKey stays the initiator's generated signing key.
|
|
355
367
|
memberPrivateKey: result.memberPrivateKey,
|
|
368
|
+
// The joiner's own single-use membership invitation into a closed host strand
|
|
369
|
+
// (already shape-checked by the manager); absent for open/unbound.
|
|
370
|
+
membershipInvite: result.membershipInvite,
|
|
356
371
|
// The responder's live strand-network addresses, or `[]` when it had none to give.
|
|
357
372
|
strandAddrs: result.strandAddrs
|
|
358
373
|
};
|
|
@@ -30,7 +30,8 @@ import type { Libp2p, Connection } from '@libp2p/interface';
|
|
|
30
30
|
import type { Multiaddr } from '@multiformats/multiaddr';
|
|
31
31
|
import type { StrandInstance, WakeRequest, WakeAck } from './types.js';
|
|
32
32
|
import { decodeLengthPrefixedFrame } from './seed-bootstrap.js';
|
|
33
|
-
import { type ControlStream, writeFrame,
|
|
33
|
+
import { type ControlStream, writeFrame, exchangeFrame, readStreamToEnd } from './control-stream.js';
|
|
34
|
+
import { tryAddrsInTurn } from './peer-dial.js';
|
|
34
35
|
|
|
35
36
|
const log = debug('sereus:cadre:strand-wake');
|
|
36
37
|
|
|
@@ -60,8 +61,10 @@ const DEFAULT_WAKE_TIMEOUT_MS = 10_000;
|
|
|
60
61
|
*
|
|
61
62
|
* This makes the TARGET PEER the unit rather than the address — the same
|
|
62
63
|
* decision, for the same reason, as
|
|
63
|
-
* `DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS` in `
|
|
64
|
-
*
|
|
64
|
+
* `DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS` in `peer-dial.ts`, though not the same
|
|
65
|
+
* number: that one is sized to fit several dead addresses ahead of a working one,
|
|
66
|
+
* while a wake target is expected to be awake and reachable on its first or second
|
|
67
|
+
* address. 20 s is deliberately wider than one attempt timeout so a
|
|
65
68
|
* reachable peer whose signaling address is stale still gets a genuine try at
|
|
66
69
|
* its direct one; a peer needing longer than that is not "asleep and reachable",
|
|
67
70
|
* which is the only case a wake is for. The last attempt inside the budget gets
|
|
@@ -271,12 +274,6 @@ export interface DialWakeOptions {
|
|
|
271
274
|
protocolId?: string;
|
|
272
275
|
}
|
|
273
276
|
|
|
274
|
-
/** One candidate's outcome, kept so the thrown error can name every attempt. */
|
|
275
|
-
interface WakeDialFailure {
|
|
276
|
-
addr: Multiaddr;
|
|
277
|
-
error: Error;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
277
|
/**
|
|
281
278
|
* Sender side: dial a target's control-network address(es), send a
|
|
282
279
|
* {@link WakeRequest} over `WAKE_PROTOCOL`, and return the peer's {@link WakeAck}.
|
|
@@ -289,16 +286,18 @@ interface WakeDialFailure {
|
|
|
289
286
|
* dropped.
|
|
290
287
|
*
|
|
291
288
|
* Throws if no address is dialable — with an error naming EVERY candidate and
|
|
292
|
-
* why it failed, not merely the last one. That
|
|
293
|
-
* with only the last candidate's message the
|
|
294
|
-
* signaling address, tried first) is
|
|
295
|
-
*
|
|
289
|
+
* why it failed, not merely the last one ({@link tryAddrsInTurn}). That
|
|
290
|
+
* distinction is not cosmetic: with only the last candidate's message the
|
|
291
|
+
* failure that mattered (usually the signaling address, tried first) is
|
|
292
|
+
* invisible outside a debug log, and the surfaced message points at whichever
|
|
293
|
+
* address happened to be tried last.
|
|
296
294
|
*
|
|
297
295
|
* The candidate loop is deliberately explicit rather than one
|
|
298
296
|
* `dialProtocol(addrs)` call. libp2p sorts any multi-address dial with
|
|
299
297
|
* `defaultAddressSorter`, whose `circuitRelayAddressesLast` pass would demote
|
|
300
298
|
* exactly the signaling address this ordering puts first — silently inverting
|
|
301
|
-
* it, with no per-dial sorter override to opt out of.
|
|
299
|
+
* it, with no per-dial sorter override to opt out of. For the same reason this
|
|
300
|
+
* does not use `dialPeerAddrs`, which puts relayed addresses last.
|
|
302
301
|
*/
|
|
303
302
|
export async function dialWake(
|
|
304
303
|
node: Libp2p,
|
|
@@ -310,58 +309,14 @@ export async function dialWake(
|
|
|
310
309
|
throw new Error('No dialable address for wake target');
|
|
311
310
|
}
|
|
312
311
|
const protocolId = options.protocolId ?? WAKE_PROTOCOL;
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const remaining = deadline - Date.now();
|
|
322
|
-
if (remaining <= 0) {
|
|
323
|
-
failures.push({ addr, error: new Error(`not tried — the ${budgetMs}ms wake dial budget was spent on earlier addresses`) });
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
const attemptMs = Math.min(timeoutMs, remaining);
|
|
327
|
-
try {
|
|
328
|
-
// One deadline per attempt: its signal aborts the in-flight dialProtocol and
|
|
329
|
-
// resets the live stream, so neither the connect nor the ack-read leaks.
|
|
330
|
-
return await withDeadline(
|
|
331
|
-
attemptMs,
|
|
332
|
-
`Wake dial ${addr.toString()}`,
|
|
333
|
-
(signal) => sendWake(node, addr, protocolId, request, attemptMs, signal),
|
|
334
|
-
);
|
|
335
|
-
} catch (err) {
|
|
336
|
-
failures.push({ addr, error: err instanceof Error ? err : new Error(String(err)) });
|
|
337
|
-
log('Wake dial to %s failed: %o', addr.toString(), err);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
throw wakeDialError(failures);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Fold every candidate's failure into the one error {@link dialWake} throws.
|
|
345
|
-
*
|
|
346
|
-
* A single candidate throws its own error unchanged, so a one-address wake reads
|
|
347
|
-
* exactly as it always has (and `rejects.toThrow(/timed out/)` still means what
|
|
348
|
-
* it says). Several candidates produce one message naming each address and its
|
|
349
|
-
* cause, with the FIRST failure as `cause` — the first candidate is the
|
|
350
|
-
* signaling address, which is the one that matters when a relayed peer goes
|
|
351
|
-
* unreachable.
|
|
352
|
-
*/
|
|
353
|
-
function wakeDialError(failures: WakeDialFailure[]): Error {
|
|
354
|
-
if (failures.length === 0) {
|
|
355
|
-
return new Error('Wake dial failed');
|
|
356
|
-
}
|
|
357
|
-
if (failures.length === 1) {
|
|
358
|
-
return failures[0].error;
|
|
359
|
-
}
|
|
360
|
-
const detail = failures.map((f) => `${f.addr.toString()} — ${f.error.message}`).join('; ');
|
|
361
|
-
return new Error(
|
|
362
|
-
`Wake dial failed for all ${failures.length} candidate addresses: ${detail}`,
|
|
363
|
-
{ cause: failures[0].error },
|
|
364
|
-
);
|
|
312
|
+
const budget = {
|
|
313
|
+
perAddressMs: options.timeoutMs ?? DEFAULT_WAKE_TIMEOUT_MS,
|
|
314
|
+
totalMs: options.budgetMs ?? DEFAULT_WAKE_DIAL_BUDGET_MS,
|
|
315
|
+
};
|
|
316
|
+
// The attempt's signal aborts the in-flight dialProtocol and resets the live
|
|
317
|
+
// stream, so neither the connect nor the ack-read leaks.
|
|
318
|
+
return await tryAddrsInTurn(addrs, budget, 'Wake dial', (addr, signal, attemptMs) =>
|
|
319
|
+
sendWake(node, addr, protocolId, request, attemptMs, signal));
|
|
365
320
|
}
|
|
366
321
|
|
|
367
322
|
/**
|
package/src/strand-watcher.ts
CHANGED
|
@@ -244,6 +244,12 @@ export class StrandWatcher {
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
} catch (error) {
|
|
247
|
+
// NOTE: a machine cut off from its party before it ever received the `Strand` block
|
|
248
|
+
// fails this read `cohort-unreachable` on every poll and logs here (observed in
|
|
249
|
+
// `control-cohort-edge-carries-data`). Harmless: nothing was ever launched from a block
|
|
250
|
+
// this machine never held, and this catch keeps the known set. Do not answer it as
|
|
251
|
+
// empty the way `ControlDatabase.queryRevokedStamps` does for `Revocation` — an empty
|
|
252
|
+
// answer here runs the removed-strand loop above. Revisit if the log volume matters.
|
|
247
253
|
log('Error polling strands: %o', error);
|
|
248
254
|
}
|
|
249
255
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
|
|
3
|
+
const timing = debug('sereus:cadre:timing');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Await one step of a longer operation between two `sereus:cadre:timing` lines:
|
|
7
|
+
* `[<scope>:<id>] <step>: start` before it, then `<step>: <n>ms` — or
|
|
8
|
+
* `<step>: failed after <n>ms` — once it settles. The start line is what matters on a
|
|
9
|
+
* device: a step that never settles leaves a start with no matching end, so a trace
|
|
10
|
+
* names the step that hung rather than only the last one that finished.
|
|
11
|
+
*
|
|
12
|
+
* The text is built here rather than passed as `%s`/`%d` arguments: `debug`'s browser
|
|
13
|
+
* build, which React Native bundles, leaves placeholders for the console to fill, and
|
|
14
|
+
* RN's console prints them unfilled, so a device trace would read `[%s:%s] %s: start`.
|
|
15
|
+
*/
|
|
16
|
+
export async function timedStep<T>(scope: string, id: string, step: string, op: () => Promise<T>): Promise<T> {
|
|
17
|
+
const label = `[${scope}:${id}] ${step}`;
|
|
18
|
+
timing(`${label}: start`);
|
|
19
|
+
const t0 = performance.now();
|
|
20
|
+
try {
|
|
21
|
+
const result = await op();
|
|
22
|
+
timing(`${label}: ${Math.round(performance.now() - t0)}ms`);
|
|
23
|
+
return result;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
timing(`${label}: failed after ${Math.round(performance.now() - t0)}ms`);
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The timer seam shared by the strand bring-up loops that re-arm themselves one step at a
|
|
3
|
+
* time — the first-sync write gate's probe loop (`strand-first-sync-gate.ts`) and the
|
|
4
|
+
* membership reconciler's retry ladder (`strand-membership-reconciler.ts`). `setTimeout`
|
|
5
|
+
* rather than a repeating interval because the next step is armed only once the previous
|
|
6
|
+
* one settles, so a slow network read never stacks steps behind it, and because the delay
|
|
7
|
+
* can differ per step.
|
|
8
|
+
*
|
|
9
|
+
* Tests inject a hand-cranked clock; production omits it and gets real timers, `unref`'d so
|
|
10
|
+
* a loop that is only waiting never holds a Node process open (the call is optional —
|
|
11
|
+
* browsers and React Native return a plain handle with no `unref`).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Timer seam for a self-rescheduling loop; omit for real (unref'd) timeouts. */
|
|
15
|
+
export interface TimeoutScheduler {
|
|
16
|
+
setTimeout(fn: () => void, ms: number): unknown;
|
|
17
|
+
clearTimeout(handle: unknown): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Real timers, `unref`'d where the runtime supports it. */
|
|
21
|
+
export const defaultTimeoutScheduler: TimeoutScheduler = {
|
|
22
|
+
setTimeout: (fn, ms) => {
|
|
23
|
+
const handle = setTimeout(fn, ms);
|
|
24
|
+
(handle as { unref?: () => void }).unref?.();
|
|
25
|
+
return handle;
|
|
26
|
+
},
|
|
27
|
+
clearTimeout: (handle) => clearTimeout(handle as Parameters<typeof clearTimeout>[0])
|
|
28
|
+
};
|