@serfab/cadre-core 0.9.0 → 0.11.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 +255 -253
- package/dist/bootstrap-peer-store-file.d.ts +13 -0
- package/dist/bootstrap-peer-store-file.js +42 -0
- package/dist/bootstrap-peer-store-file.js.map +1 -0
- package/dist/bootstrap-peer-store.d.ts +73 -0
- package/dist/bootstrap-peer-store.js +128 -0
- package/dist/bootstrap-peer-store.js.map +1 -0
- package/dist/cadre-node.d.ts +1121 -81
- package/dist/cadre-node.js +2276 -262
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +93 -0
- package/dist/control-authorization.js +57 -0
- package/dist/control-authorization.js.map +1 -0
- package/dist/control-cohort.d.ts +26 -0
- package/dist/control-cohort.js +26 -0
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +959 -54
- package/dist/control-database.js +1421 -148
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +90 -10
- package/dist/control-formation-recorder.js +130 -14
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +651 -72
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +38 -4
- package/dist/control-stream.js +70 -4
- package/dist/control-stream.js.map +1 -1
- package/dist/control-write-retry.d.ts +161 -0
- package/dist/control-write-retry.js +412 -0
- package/dist/control-write-retry.js.map +1 -0
- package/dist/delegate-admission.d.ts +134 -0
- package/dist/delegate-admission.js +244 -0
- package/dist/delegate-admission.js.map +1 -0
- package/dist/device-token.d.ts +11 -11
- package/dist/device-token.js +14 -13
- package/dist/device-token.js.map +1 -1
- package/dist/ed25519-key.d.ts +26 -0
- package/dist/ed25519-key.js +56 -1
- package/dist/ed25519-key.js.map +1 -1
- package/dist/file-durable-slot.d.ts +20 -0
- package/dist/file-durable-slot.js +53 -0
- package/dist/file-durable-slot.js.map +1 -0
- package/dist/formation-approval.d.ts +157 -0
- package/dist/formation-approval.js +375 -0
- package/dist/formation-approval.js.map +1 -0
- package/dist/fs-atomic.d.ts +38 -0
- package/dist/fs-atomic.js +97 -0
- package/dist/fs-atomic.js.map +1 -0
- package/dist/identity-key.d.ts +49 -0
- package/dist/identity-key.js +81 -0
- package/dist/identity-key.js.map +1 -0
- package/dist/index.d.ts +17 -7
- package/dist/index.js +40 -6
- package/dist/index.js.map +1 -1
- package/dist/key-store-file.d.ts +2 -24
- package/dist/key-store-file.js +10 -77
- package/dist/key-store-file.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +146 -0
- package/dist/membership-connection-gater.js +180 -0
- package/dist/membership-connection-gater.js.map +1 -0
- package/dist/node-local-snapshot.d.ts +109 -0
- package/dist/node-local-snapshot.js +182 -0
- package/dist/node-local-snapshot.js.map +1 -0
- package/dist/peer-addr-book.d.ts +92 -0
- package/dist/peer-addr-book.js +220 -0
- package/dist/peer-addr-book.js.map +1 -0
- package/dist/peer-authorization.d.ts +135 -18
- package/dist/peer-authorization.js +152 -21
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-record.d.ts +21 -8
- package/dist/peer-record.js +26 -10
- package/dist/peer-record.js.map +1 -1
- package/dist/relay-addrs.d.ts +45 -0
- package/dist/relay-addrs.js +103 -0
- package/dist/relay-addrs.js.map +1 -0
- package/dist/relay-reservation.d.ts +266 -0
- package/dist/relay-reservation.js +613 -0
- package/dist/relay-reservation.js.map +1 -0
- package/dist/seed-bootstrap.d.ts +228 -56
- package/dist/seed-bootstrap.js +340 -198
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +47 -21
- package/dist/seed-trust-policy.js +24 -15
- package/dist/seed-trust-policy.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +42 -13
- package/dist/strand-addr-protocol.js +58 -52
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-backfill.d.ts +109 -0
- package/dist/strand-backfill.js +279 -0
- package/dist/strand-backfill.js.map +1 -0
- package/dist/strand-cohort.d.ts +4 -37
- package/dist/strand-cohort.js +4 -15
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +11 -16
- package/dist/strand-database.js +19 -7
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-formation-manager.d.ts +36 -7
- package/dist/strand-formation-manager.js +115 -15
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +103 -4
- package/dist/strand-formation-protocol.js +232 -4
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +88 -19
- package/dist/strand-instance-manager.js +178 -34
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-registry.d.ts +5 -5
- package/dist/strand-member-registry.js +18 -10
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +484 -88
- package/dist/strand-membership-writer.js +751 -128
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +99 -33
- package/dist/strand-solicitation.js +100 -52
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-transport-key.d.ts +40 -0
- package/dist/strand-transport-key.js +56 -0
- package/dist/strand-transport-key.js.map +1 -0
- package/dist/strand-wake-protocol.d.ts +9 -7
- package/dist/strand-wake-protocol.js +22 -46
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/strand-watcher.d.ts +17 -2
- package/dist/strand-watcher.js +47 -3
- package/dist/strand-watcher.js.map +1 -1
- package/dist/trusted-owner-store-file.d.ts +12 -0
- package/dist/trusted-owner-store-file.js +39 -0
- package/dist/trusted-owner-store-file.js.map +1 -0
- package/dist/trusted-owner-store.d.ts +64 -0
- package/dist/trusted-owner-store.js +126 -0
- package/dist/trusted-owner-store.js.map +1 -0
- package/dist/types.d.ts +307 -44
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -1
- package/package.json +20 -8
- package/src/arachnode-stub.ts +159 -159
- package/src/bootstrap-peer-store-file.ts +44 -0
- package/src/bootstrap-peer-store.ts +172 -0
- package/src/cadre-node.ts +5158 -2981
- package/src/canonical-json.ts +30 -30
- package/src/control-authorization.ts +114 -0
- package/src/control-cohort.ts +130 -103
- package/src/control-database.ts +2406 -948
- package/src/control-formation-recorder.ts +305 -151
- package/src/control-schema.ts +651 -72
- package/src/control-stream.ts +205 -128
- package/src/control-write-retry.ts +483 -0
- package/src/delegate-admission.ts +302 -0
- package/src/device-token.ts +94 -89
- package/src/diagnostics/connection-path.ts +277 -277
- package/src/diagnostics/webrtc-turn-tracker.ts +222 -222
- package/src/ed25519-key.ts +66 -1
- package/src/enrollment.ts +199 -199
- package/src/file-durable-slot.ts +61 -0
- package/src/formation-approval.ts +604 -0
- package/src/fs-atomic.ts +107 -0
- package/src/hibernation-manager.ts +437 -437
- package/src/identity-key.ts +109 -0
- package/src/index.ts +137 -5
- package/src/key-store-file.ts +132 -203
- package/src/key-store.ts +99 -99
- package/src/membership-connection-gater.ts +204 -0
- package/src/node-local-snapshot.ts +260 -0
- package/src/peer-addr-book.ts +250 -0
- package/src/peer-authorization.ts +259 -81
- package/src/peer-record.ts +164 -146
- package/src/push-credentials.ts +84 -84
- package/src/push-node.ts +64 -64
- package/src/push-notifier-apns.ts +247 -247
- package/src/push-notifier-fcm.ts +220 -220
- package/src/push-notifier-shared.ts +28 -28
- package/src/push-notifier.ts +52 -52
- package/src/relay-addrs.ts +111 -0
- package/src/relay-reservation.ts +823 -0
- package/src/seed-bootstrap.ts +409 -209
- package/src/seed-trust-policy.ts +140 -107
- package/src/strand-addr-protocol.ts +441 -410
- package/src/strand-backfill.ts +366 -0
- package/src/strand-cohort.ts +38 -77
- package/src/strand-database.ts +202 -204
- package/src/strand-formation-manager.ts +491 -368
- package/src/strand-formation-protocol.ts +692 -391
- package/src/strand-instance-manager.ts +201 -46
- package/src/strand-member-key.ts +39 -39
- package/src/strand-member-registry.ts +182 -177
- package/src/strand-membership-writer.ts +938 -146
- package/src/strand-solicitation.ts +472 -407
- package/src/strand-transport-key.ts +58 -0
- package/src/strand-wake-payload.ts +32 -32
- package/src/strand-wake-protocol.ts +314 -331
- package/src/strand-watcher.ts +314 -251
- package/src/trusted-owner-store-file.ts +41 -0
- package/src/trusted-owner-store.ts +171 -0
- package/src/types.ts +1309 -1032
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import { unwrapError } from '@quereus/quereus';
|
|
3
|
+
const log = debug('sereus:cadre:control-db');
|
|
4
|
+
/**
|
|
5
|
+
* Bounded retry for TRANSIENT control-write failures — the classifier and loop behind
|
|
6
|
+
* `ControlDatabase.lockedWithRetry`, the single funnel every local control write passes
|
|
7
|
+
* through.
|
|
8
|
+
*
|
|
9
|
+
* Why this exists: the control database replicates every block to the whole party, so a
|
|
10
|
+
* three-machine party commits control writes unanimously (`ceil(3 × 0.75) = 3` approvals) —
|
|
11
|
+
* and no `superMajorityThreshold` can lower that bar without violating Optimystic's
|
|
12
|
+
* partition-safety condition at the shipped admission fraction (the arithmetic lives in
|
|
13
|
+
* `docs/architecture.md` → "Replication cluster size"). Cadre therefore accepts unanimity
|
|
14
|
+
* and absorbs the transient half of its cost here: a write that failed because the cohort
|
|
15
|
+
* did not ANSWER (a stream reset mid-connection-formation, zero approvals) is re-presented
|
|
16
|
+
* a moment later, which the degraded-cohort-member scenario measured as safe (a failed
|
|
17
|
+
* write rolls back, nothing half-commits) and effective (the next write commits in ~1 s).
|
|
18
|
+
*
|
|
19
|
+
* A write somebody actually REJECTED is never retried — re-presenting it would re-present
|
|
20
|
+
* a spent signature against a cohort that already said no.
|
|
21
|
+
*
|
|
22
|
+
* TWO policies ship from here, and the split is deliberate: the default
|
|
23
|
+
* ({@link isRetriableControlWriteFailure}, {@link CONTROL_WRITE_ATTEMPTS}) sits under every
|
|
24
|
+
* control write, and {@link SCHEMA_INIT_RETRY_POLICY} is opted into by
|
|
25
|
+
* `ControlDatabase.loadSchema` alone. The widened one absorbs one extra failure class whose
|
|
26
|
+
* re-run safety holds for schema DDL and NOT for writes in general; anything added to the default
|
|
27
|
+
* list must be safe for all ~19 callers.
|
|
28
|
+
*
|
|
29
|
+
* Lives outside `control-database.ts` so a spec can drive the classifier against real engine
|
|
30
|
+
* errors without importing the whole database class.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Attempts allowed per control write: the first, plus two retries of a transiently-failed
|
|
34
|
+
* cluster commit. Bounded so a genuinely unreachable cohort terminates in the transactor's
|
|
35
|
+
* own error rather than spinning; two retries covers the observed failure (a stream still
|
|
36
|
+
* forming when the write fired) with margin.
|
|
37
|
+
*/
|
|
38
|
+
export const CONTROL_WRITE_ATTEMPTS = 3;
|
|
39
|
+
/**
|
|
40
|
+
* Attempts allowed for {@link SCHEMA_INIT_RETRY_POLICY}, the one call site that also absorbs a
|
|
41
|
+
* self-coordination refusal.
|
|
42
|
+
*
|
|
43
|
+
* Two more than {@link CONTROL_WRITE_ATTEMPTS} because what this retry buys is wall-clock for a
|
|
44
|
+
* cold-starting node's bootstrap dials to land, and because the refusal is cheap to re-provoke
|
|
45
|
+
* relative to a cluster round-trip: `findCoordinator` already spends its own bounded wait before
|
|
46
|
+
* raising (3 attempts, 500 ms apart, `db-p2p/src/libp2p-key-network.ts` ~422) and then decides
|
|
47
|
+
* LOCALLY that it may not elect itself, so no peer is contacted and no cohort deadline is
|
|
48
|
+
* consumed.
|
|
49
|
+
*
|
|
50
|
+
* Note that the ~1 s `findCoordinator` spends internally is real, on top of this policy's own
|
|
51
|
+
* backoff — which is exactly why {@link CONTROL_WRITE_RETRY_BUDGET_MS} and not this count is what
|
|
52
|
+
* actually terminates the loop in the worst case.
|
|
53
|
+
*/
|
|
54
|
+
export const SCHEMA_INIT_ATTEMPTS = 5;
|
|
55
|
+
/**
|
|
56
|
+
* Elapsed-time ceiling on the whole retry loop, measured from the START of the first
|
|
57
|
+
* attempt and checked after a failed attempt BEFORE sleeping.
|
|
58
|
+
*
|
|
59
|
+
* This budget — not the attempt count — is what makes the policy safe to sit under every
|
|
60
|
+
* control write. A transient failure (stream reset while a connection is still forming)
|
|
61
|
+
* surfaces in well under a second, so it is retried and the loop adds at most ~2.2 s. A
|
|
62
|
+
* genuinely silent cohort member fails at ~20 s (two 10 s `ClusterClient` response-deadline
|
|
63
|
+
* attempts, measured in `control-write-degraded-cohort-member.integration.ts`), which
|
|
64
|
+
* already exceeds this budget when attempt 1 returns — so that case is surfaced immediately
|
|
65
|
+
* and retry adds ZERO latency to the case where it cannot help.
|
|
66
|
+
*/
|
|
67
|
+
export const CONTROL_WRITE_RETRY_BUDGET_MS = 10000;
|
|
68
|
+
/**
|
|
69
|
+
* Backoff before retry N, jittered ±50% (so the first sleep is 125–375 ms — the jitter can
|
|
70
|
+
* never floor a delay to zero). Each single sleep is additionally capped at the LARGEST
|
|
71
|
+
* delay in the list, so a `stop()` racing a backoff is never held up by more than ~1 s.
|
|
72
|
+
* There is no `AbortSignal` at this seam to cancel a sleep outright; the cap is the
|
|
73
|
+
* substitute, deliberately not plumbing one through every write path.
|
|
74
|
+
*/
|
|
75
|
+
const CONTROL_WRITE_RETRY_DELAYS_MS = [250, 1000];
|
|
76
|
+
/**
|
|
77
|
+
* Backoff for {@link SCHEMA_INIT_RETRY_POLICY}, jittered and capped the same way. Worst case
|
|
78
|
+
* ~4.6 s of sleep across four retries (375 + 750 + 1500 + 2000, each capped at the largest base),
|
|
79
|
+
* which stays inside {@link CONTROL_WRITE_RETRY_BUDGET_MS} — the budget still terminates the loop
|
|
80
|
+
* first if any single attempt is itself slow.
|
|
81
|
+
*
|
|
82
|
+
* Sized against measured facts, not guessed:
|
|
83
|
+
*
|
|
84
|
+
* - the self-coordination guard clears the moment the node has ONE connection again (both
|
|
85
|
+
* branches that raise `grace-period-not-elapsed` require `getConnections().length === 0`), not
|
|
86
|
+
* when the 30 s grace period expires — so this does NOT need to cover the grace period, only
|
|
87
|
+
* the gap until a bootstrap dial completes;
|
|
88
|
+
* - the guard's 30 s `gracePeriodMs` cannot be tuned from this repo anyway: no caller in either
|
|
89
|
+
* repo passes a `SelfCoordinationConfig`, so the default is always in force;
|
|
90
|
+
* - a node that has NEVER connected is waved through as a bootstrap node (network high-water
|
|
91
|
+
* mark of 1), so this retry only ever delays a node that HAS seen peers. One whose peers are
|
|
92
|
+
* gone for good still fails, ~4.6 s later — which is why the budget stays at the shared
|
|
93
|
+
* ceiling rather than stretching to cover the full 30 s window.
|
|
94
|
+
*/
|
|
95
|
+
const SCHEMA_INIT_RETRY_DELAYS_MS = [250, 500, 1000, 2000];
|
|
96
|
+
/** The network transactor's aggregate, raised when some cohort peers gave no usable answer. */
|
|
97
|
+
const TRANSACTOR_AGGREGATE = /Some peers did not complete:/;
|
|
98
|
+
/**
|
|
99
|
+
* How the aggregate's per-batch details name a SINGLE-block batch — the shape both the block
|
|
100
|
+
* read (`get`) and phase 1 (`pend`) format, and the only shape safe to re-present.
|
|
101
|
+
*/
|
|
102
|
+
const SINGLE_BLOCK_BATCH_TOKEN = '[block:';
|
|
103
|
+
/**
|
|
104
|
+
* How the aggregate's per-batch details name a MULTI-block batch — the shape phase 2
|
|
105
|
+
* (`commitBlocks`) formats, and the marker of an outcome too indeterminate to retry.
|
|
106
|
+
*/
|
|
107
|
+
const COMMIT_BATCH_TOKEN = '[blocks:';
|
|
108
|
+
/**
|
|
109
|
+
* The cluster coordinator's super-majority shortfall (`db-p2p/src/repo/cluster-coordinator.ts`),
|
|
110
|
+
* matched ONLY with a ZERO rejection count. The same message with a non-zero count means a
|
|
111
|
+
* member actually voted no (that branch carries `membership-not-admitted` rejections), and
|
|
112
|
+
* retrying it would re-present a spent signature to a cohort that already refused it. This one
|
|
113
|
+
* is raised while collecting PROMISES, before any commit, so re-presenting it is safe. A
|
|
114
|
+
* decisive rejection is a different error entirely (`ValidatorRejectionError`,
|
|
115
|
+
* `Transaction rejected by validators`), which this classifier also never matches.
|
|
116
|
+
*/
|
|
117
|
+
const SUPER_MAJORITY_SHORTFALL_UNANSWERED = /Failed to get super-majority: \d+\/\d+ approvals \(needed \d+, 0 rejections\)/;
|
|
118
|
+
/**
|
|
119
|
+
* Is this a transactor aggregate from a phase where nothing can have committed yet?
|
|
120
|
+
*
|
|
121
|
+
* The transactor raises `Some peers did not complete:` from three sites
|
|
122
|
+
* (`db-core/src/transactor/network-transactor.ts`), and only two of them are safe to
|
|
123
|
+
* re-present:
|
|
124
|
+
*
|
|
125
|
+
* - `get` (a block read, which a write body also performs) and `pend` (phase 1) fail before
|
|
126
|
+
* anything commits, so the write is known not to have landed;
|
|
127
|
+
* - `commitBlocks` (phase 2) is REACHABLE here and NOT safe. `commitBlock` throws the
|
|
128
|
+
* aggregate when a header/tail commit got NO response at all — precisely the transient
|
|
129
|
+
* class this retry targets — and that escapes `commit()`, survives `TransactorSource`'s
|
|
130
|
+
* cancel-and-rethrow (cancel reaches PENDING actions only; a peer that already committed
|
|
131
|
+
* stays committed) and `Collection.syncInternal` (which retries StaleFailure RETURN values,
|
|
132
|
+
* not throws), reaching this funnel QuereusError-wrapped. The tail commit is one batch to
|
|
133
|
+
* one coordinator running consensus internally, so a no-response there is INDETERMINATE:
|
|
134
|
+
* the commit may have completed with only the response lost. Re-running the write body over
|
|
135
|
+
* a write that landed turns a success into a constraint failure (e.g.
|
|
136
|
+
* `UNIQUE constraint failed: CadrePeer.PeerId`). Surfacing the transient error instead lets
|
|
137
|
+
* the caller re-read committed state and decide. Commit-phase messages are vetoed by
|
|
138
|
+
* {@link reportsIndeterminateCommit}, not here.
|
|
139
|
+
*
|
|
140
|
+
* The `cause` chain does not discriminate the phases — a commit-phase aggregate carries the
|
|
141
|
+
* same stream-reset/dial error a pend-phase one does. The per-batch DETAIL text does: `get`
|
|
142
|
+
* and `pend` format `<peerId>[block:<id>](<status>)`, `commitBlocks` formats
|
|
143
|
+
* `<peerId>[blocks:<count>](<status>)`. `[block:` cannot occur inside `[blocks:`, so the two
|
|
144
|
+
* tokens are disjoint and separate the phases.
|
|
145
|
+
*
|
|
146
|
+
* NOTE: the discriminator is a formatting detail of another repo. If Optimystic ever reformats
|
|
147
|
+
* those per-batch details, this fails CLOSED — the aggregate stops matching and control writes
|
|
148
|
+
* simply stop being retried, silently losing the absorption rather than doing anything unsafe.
|
|
149
|
+
* The guard against that is live: `control-write-degraded-cohort-member.integration.ts` asserts
|
|
150
|
+
* this classifier against the real degraded-cohort failure object and asserts the `[block:`
|
|
151
|
+
* token on the live aggregate; if it reddens there, an upstream reformat is the first thing to
|
|
152
|
+
* check.
|
|
153
|
+
*
|
|
154
|
+
* An aggregate whose details came out EMPTY (possible when `formatBatchStatuses` has no
|
|
155
|
+
* batches to format) carries neither token, matches nothing here, and is not retried — an
|
|
156
|
+
* unattributable failure is not a proven non-commit.
|
|
157
|
+
*/
|
|
158
|
+
function isUncommittedTransactorAggregate(message) {
|
|
159
|
+
return TRANSACTOR_AGGREGATE.test(message) && message.includes(SINGLE_BLOCK_BATCH_TOKEN);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Does ANY message in the failure's `cause` chain describe a commit-phase batch — i.e. an
|
|
163
|
+
* outcome nobody can call committed or not?
|
|
164
|
+
*
|
|
165
|
+
* Vetoes the whole chain rather than the one message carrying the token, and vetoes it
|
|
166
|
+
* regardless of which matcher would otherwise have claimed it. The narrower per-message rule
|
|
167
|
+
* would already catch every shape the transactor is known to build (a commit-phase aggregate
|
|
168
|
+
* emits `[blocks:` for every batch it formats, and embeds its cause's message inline as
|
|
169
|
+
* `root: …`, so both tokens land in one string) — but "known to build" is an argument about
|
|
170
|
+
* another repo's error assembly, and this way an indeterminate commit anywhere in the chain
|
|
171
|
+
* costs a retry we could have had instead of risking a re-run over a write that landed.
|
|
172
|
+
*/
|
|
173
|
+
function reportsIndeterminateCommit(messages) {
|
|
174
|
+
return messages.some(message => message.includes(COMMIT_BATCH_TOKEN));
|
|
175
|
+
}
|
|
176
|
+
/** The coordinator's shortfall, raised pre-commit, with nobody having voted no. */
|
|
177
|
+
function isUnansweredSuperMajorityShortfall(message) {
|
|
178
|
+
return SUPER_MAJORITY_SHORTFALL_UNANSWERED.test(message);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Optimystic refusing to let a node elect ITSELF coordinator because its last connection dropped
|
|
182
|
+
* less than the guard's grace period ago (`Libp2pKeyPeerNetwork.shouldAllowSelfCoordination` →
|
|
183
|
+
* `findCoordinator`, `db-p2p/src/libp2p-key-network.ts`).
|
|
184
|
+
*
|
|
185
|
+
* Matched ONLY with the `grace-period-not-elapsed` reason. The same sentence is raised with three
|
|
186
|
+
* other reasons and none of them is worth a retry: `disabled` is static configuration, and
|
|
187
|
+
* `partition-detected` / `suspicious-shrinkage` describe a network condition that will not resolve
|
|
188
|
+
* inside a few seconds of backoff. The grace-period one WILL — it clears as soon as the node holds
|
|
189
|
+
* one connection again, which a cold-starting node dialling its bootstrap peers is actively
|
|
190
|
+
* working on.
|
|
191
|
+
*
|
|
192
|
+
* NOTE: matched by TEXT, not by type, and that is forced rather than chosen.
|
|
193
|
+
* `FindCoordinatorError` (`code: 'SELF_COORDINATION_BLOCKED'`) is exported from `@optimystic/db-p2p`
|
|
194
|
+
* and this package depends on it, but the error OBJECT does not survive the trip:
|
|
195
|
+
* `OptimysticVirtualTable.initialize` catches and rethrows as `new Error(message)` with no `cause`
|
|
196
|
+
* (`quereus-plugin-optimystic/src/optimystic-module.ts`), so only the text reaches this repo. Like
|
|
197
|
+
* every matcher here this fails CLOSED — a rewording upstream stops the retry engaging, it never
|
|
198
|
+
* makes it unsafe.
|
|
199
|
+
*/
|
|
200
|
+
const SELF_COORDINATION_GRACE_REFUSAL = /Self-coordination blocked: grace-period-not-elapsed\. No coordinator available for key\./;
|
|
201
|
+
/** Self-coordination refused because the node's last connection dropped moments ago. */
|
|
202
|
+
function isSelfCoordinationGraceRefusal(message) {
|
|
203
|
+
return SELF_COORDINATION_GRACE_REFUSAL.test(message);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Every message in the failure's `cause` chain.
|
|
207
|
+
*
|
|
208
|
+
* `unwrapError` declares its `message` as `string`, but it follows `.cause` without checking what
|
|
209
|
+
* that holds — a chain link that is not an `Error` (a stream rejected with a bare string reason,
|
|
210
|
+
* an `AbortSignal.reason` that is a plain object) yields `undefined` there, and calling
|
|
211
|
+
* `.includes` on it would throw a `TypeError` out of this classifier, INSIDE `retryControlWrite`'s
|
|
212
|
+
* catch, replacing the real control-write failure with a confusing one. Non-strings are dropped:
|
|
213
|
+
* a link nobody can read is a link that matches nothing, which is already the conservative answer.
|
|
214
|
+
*
|
|
215
|
+
* NOTE: a cause chain with a CYCLE would spin forever inside `unwrapError` itself. No error in
|
|
216
|
+
* this repo builds one; if a hang ever localises to a control-write failure path, look there.
|
|
217
|
+
*/
|
|
218
|
+
function chainMessages(error) {
|
|
219
|
+
return unwrapError(error)
|
|
220
|
+
.map(({ message }) => message)
|
|
221
|
+
.filter(message => typeof message === 'string');
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The error messages that mean "the cohort did not answer AND nothing committed", and nothing
|
|
225
|
+
* else. The observed one-shot failure was `registerSelf()` racing a connection still forming:
|
|
226
|
+
* a read/pend-phase transactor aggregate with `cause=The stream has been reset`.
|
|
227
|
+
*
|
|
228
|
+
* Deliberately NOT matched: every constraint / authorization failure
|
|
229
|
+
* (`CHECK constraint failed:`, `UNIQUE constraint failed:`) — those reach the same funnel
|
|
230
|
+
* and must be retried zero times: the cohort REFUSED the write, so re-presenting it
|
|
231
|
+
* re-presents a spent signature and can never commit.
|
|
232
|
+
*/
|
|
233
|
+
const RETRIABLE_CONTROL_WRITE_MATCHERS = [
|
|
234
|
+
isUncommittedTransactorAggregate,
|
|
235
|
+
isUnansweredSuperMajorityShortfall,
|
|
236
|
+
];
|
|
237
|
+
/**
|
|
238
|
+
* The above PLUS the self-coordination grace refusal — the classifier for schema init ONLY
|
|
239
|
+
* ({@link SCHEMA_INIT_RETRY_POLICY}), deliberately not folded into the list above.
|
|
240
|
+
*
|
|
241
|
+
* Why this class is safe HERE and not everywhere: a write refused at coordinator SELECTION never
|
|
242
|
+
* reached a peer, so re-presenting it is a proven non-commit. But `findCoordinator` is also
|
|
243
|
+
* reached from `NetworkTransactor.commitBlock` during PHASE 2 (`resolveCoordinator`,
|
|
244
|
+
* `db-core/src/transactor/network-transactor.ts`), and `commit()` commits the header block before
|
|
245
|
+
* the rest — so a general control write refused there can be refused AFTER something already
|
|
246
|
+
* committed. Worse, that error carries no `[blocks:` batch token, so
|
|
247
|
+
* {@link reportsIndeterminateCommit} would not veto it, and re-running an insert body over a write
|
|
248
|
+
* that landed is exactly the `UNIQUE constraint failed: CadrePeer.PeerId` failure that veto exists
|
|
249
|
+
* to prevent.
|
|
250
|
+
*
|
|
251
|
+
* Schema init does not have that problem: `apply schema` is a diff rather than a replay and a
|
|
252
|
+
* failed `create table` leaves the catalog clean, so a re-run emits only the tables that did not
|
|
253
|
+
* land (the full argument is at the `loadSchema` call site in `control-database.ts`).
|
|
254
|
+
*/
|
|
255
|
+
const RETRIABLE_SCHEMA_INIT_MATCHERS = [
|
|
256
|
+
...RETRIABLE_CONTROL_WRITE_MATCHERS,
|
|
257
|
+
isSelfCoordinationGraceRefusal,
|
|
258
|
+
];
|
|
259
|
+
/**
|
|
260
|
+
* Did this control write fail because the cluster cohort did not ANSWER — i.e. is
|
|
261
|
+
* re-presenting the SAME signed write a moment later the right response?
|
|
262
|
+
*
|
|
263
|
+
* Classifies by MESSAGE, walking the `cause` chain with Quereus' own {@link unwrapError} —
|
|
264
|
+
* the failure surface is not recognisable by type. The transactor and the coordinator both throw bare `Error`s, and by
|
|
265
|
+
* the time one reaches `ControlDatabase` it is wrapped in a `QuereusError` (the real chain
|
|
266
|
+
* is `QuereusError` → `Error` → `Error`), so the match must work at any depth. Anything
|
|
267
|
+
* that is not an `Error` is never retried.
|
|
268
|
+
*
|
|
269
|
+
* {@link reportsIndeterminateCommit} vetoes the whole chain before any matcher runs, so a
|
|
270
|
+
* failure that reports a commit-phase batch anywhere is never retried on the strength of some
|
|
271
|
+
* other level looking transient.
|
|
272
|
+
*
|
|
273
|
+
* NOTE: this depends on engine/transactor error TEXT. The messages producible without a
|
|
274
|
+
* network are driven from the REAL engine in `control-formation-seat-budget.spec.ts`, so
|
|
275
|
+
* a rewording reddens a spec rather than silently disabling the retry. The two RETRIABLE
|
|
276
|
+
* messages need a real multi-node cluster to produce; the degraded-cohort scenario
|
|
277
|
+
* (`control-write-degraded-cohort-member.integration.ts`) asserts this classifier against
|
|
278
|
+
* both LIVE — the super-majority shortfall from a real silent member, and the transactor
|
|
279
|
+
* aggregate from a real stream reset, which the retry there absorbs end to end.
|
|
280
|
+
*/
|
|
281
|
+
export function isRetriableControlWriteFailure(error) {
|
|
282
|
+
return matchesRetriableMessage(error, RETRIABLE_CONTROL_WRITE_MATCHERS);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* {@link isRetriableControlWriteFailure} widened by exactly one class — the self-coordination
|
|
286
|
+
* grace refusal ({@link RETRIABLE_SCHEMA_INIT_MATCHERS}) — for `ControlDatabase.loadSchema`.
|
|
287
|
+
*
|
|
288
|
+
* Opt-in per call site, via {@link SCHEMA_INIT_RETRY_POLICY}. Every other control write keeps the
|
|
289
|
+
* default classifier untouched.
|
|
290
|
+
*/
|
|
291
|
+
export function isRetriableSchemaInitFailure(error) {
|
|
292
|
+
return matchesRetriableMessage(error, RETRIABLE_SCHEMA_INIT_MATCHERS);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Shared body of both classifiers: walk the `cause` chain, let
|
|
296
|
+
* {@link reportsIndeterminateCommit} veto it, then ask `matchers`. The veto runs BEFORE any
|
|
297
|
+
* matcher for every policy — a chain reporting a commit-phase batch is never retried, however
|
|
298
|
+
* transient some other level looks.
|
|
299
|
+
*/
|
|
300
|
+
function matchesRetriableMessage(error, matchers) {
|
|
301
|
+
if (!(error instanceof Error)) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
const messages = chainMessages(error);
|
|
305
|
+
if (reportsIndeterminateCommit(messages)) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
return messages.some(message => matchers.some(matches => matches(message)));
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* The one non-default retry policy: `ControlDatabase.loadSchema`'s distributed DDL, which
|
|
312
|
+
* additionally absorbs optimystic's self-coordination grace refusal and gets more attempts over a
|
|
313
|
+
* longer backoff to do it (see {@link SCHEMA_INIT_ATTEMPTS} and
|
|
314
|
+
* {@link SCHEMA_INIT_RETRY_DELAYS_MS}). Still bounded by the shared
|
|
315
|
+
* {@link CONTROL_WRITE_RETRY_BUDGET_MS}.
|
|
316
|
+
*
|
|
317
|
+
* NOTE: exactly one call site opts in today, and the re-run safety this policy assumes is that
|
|
318
|
+
* site's, not a general property — `apply schema` is a diff and a failed `create table` leaves the
|
|
319
|
+
* catalog clean. A second opt-in must re-derive that argument for its own write body first; if this
|
|
320
|
+
* policy ever grows a third consumer, rename it for what the callers share rather than widening it
|
|
321
|
+
* by default.
|
|
322
|
+
*/
|
|
323
|
+
export const SCHEMA_INIT_RETRY_POLICY = {
|
|
324
|
+
attempts: SCHEMA_INIT_ATTEMPTS,
|
|
325
|
+
delaysMs: SCHEMA_INIT_RETRY_DELAYS_MS,
|
|
326
|
+
isRetriable: isRetriableSchemaInitFailure,
|
|
327
|
+
};
|
|
328
|
+
/**
|
|
329
|
+
* Run `attempt` up to {@link CONTROL_WRITE_ATTEMPTS} times (or `options.attempts`), retrying only
|
|
330
|
+
* failures the policy's classifier — {@link isRetriableControlWriteFailure} by default —
|
|
331
|
+
* calls transient, and only while {@link CONTROL_WRITE_RETRY_BUDGET_MS} has not elapsed.
|
|
332
|
+
*
|
|
333
|
+
* `attempt` must be atomic and re-runnable: a failed cluster write rolls back (nothing is
|
|
334
|
+
* half-applied), and re-running the body re-runs its reads too — `ControlDatabase`'s
|
|
335
|
+
* locked write bodies all satisfy this (see the contract on
|
|
336
|
+
* `ControlDatabase.withWriteLock`). The backoff sleeps happen with NO lock held; the caller
|
|
337
|
+
* takes and releases its lock inside `attempt`.
|
|
338
|
+
*
|
|
339
|
+
* On exhaustion (attempts or budget) the LAST error is rethrown unchanged — never wrapped,
|
|
340
|
+
* so the exact messages downstream code and the degraded-cohort-member scenario assert on
|
|
341
|
+
* survive. A non-retriable failure propagates from the attempt that raised it.
|
|
342
|
+
*/
|
|
343
|
+
export async function retryControlWrite(attempt, options = {}) {
|
|
344
|
+
const attempts = Math.max(1, options.attempts ?? CONTROL_WRITE_ATTEMPTS);
|
|
345
|
+
const delays = options.delaysMs ?? CONTROL_WRITE_RETRY_DELAYS_MS;
|
|
346
|
+
const isRetriable = options.isRetriable ?? isRetriableControlWriteFailure;
|
|
347
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
348
|
+
const now = options.now ?? Date.now;
|
|
349
|
+
// Empty when unlabelled, so an unlabelled line is byte-identical to what this loop
|
|
350
|
+
// logged before labels existed.
|
|
351
|
+
const tag = options.label ? ` [${options.label}]` : '';
|
|
352
|
+
const start = now();
|
|
353
|
+
let lastError;
|
|
354
|
+
let attemptsMade = 0;
|
|
355
|
+
for (let attemptNumber = 1; attemptNumber <= attempts; attemptNumber++) {
|
|
356
|
+
attemptsMade = attemptNumber;
|
|
357
|
+
try {
|
|
358
|
+
const result = await attempt();
|
|
359
|
+
if (attemptNumber > 1) {
|
|
360
|
+
log('Control write%s committed on attempt %d/%d', tag, attemptNumber, attempts);
|
|
361
|
+
}
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
if (!isRetriable(error)) {
|
|
366
|
+
// The ONLY trace that this funnel saw a failure and declined it. Without it the
|
|
367
|
+
// log is silent either way, so "the classifier vetoed this one" is
|
|
368
|
+
// indistinguishable from "the retry is not wired into this path at all".
|
|
369
|
+
log('Control write%s failed non-transiently on attempt %d/%d, not retried here: %s', tag, attemptNumber, attempts, error);
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
lastError = error;
|
|
373
|
+
if (attemptNumber === attempts) {
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
const elapsed = now() - start;
|
|
377
|
+
if (elapsed >= CONTROL_WRITE_RETRY_BUDGET_MS) {
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
const delay = jitteredDelay(delays, attemptNumber);
|
|
381
|
+
log('Control write%s failed transiently (attempt %d/%d), retrying in %d ms: %s', tag, attemptNumber, attempts, delay, error);
|
|
382
|
+
await sleep(delay);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
log('Control write%s failed after %d/%d attempt(s): %s', tag, attemptsMade, attempts, lastError);
|
|
386
|
+
throw lastError;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Backoff for the retry that follows attempt `attemptNumber`: the matching base delay
|
|
390
|
+
* (last entry repeats), jittered ±50%, then capped at the list's largest base so no single
|
|
391
|
+
* sleep exceeds it (see {@link CONTROL_WRITE_RETRY_DELAYS_MS} for why). `Math.random` is
|
|
392
|
+
* fine here — the jitter only de-synchronizes concurrent retriers, nothing is derived
|
|
393
|
+
* from it.
|
|
394
|
+
*
|
|
395
|
+
* NOTE: the cap bites on the LAST delay, whose base IS the largest, so ~half of those sleeps
|
|
396
|
+
* land exactly on the cap rather than spread — de-synchronization is only partial there.
|
|
397
|
+
* Harmless while retriers are a handful of party nodes whose attempts already start seconds
|
|
398
|
+
* apart; if a party ever retries in a tight synchronized herd, raise the cap above the
|
|
399
|
+
* largest base instead of jittering about it.
|
|
400
|
+
*/
|
|
401
|
+
function jitteredDelay(delays, attemptNumber) {
|
|
402
|
+
if (delays.length === 0) {
|
|
403
|
+
return 0;
|
|
404
|
+
}
|
|
405
|
+
const base = delays[Math.min(attemptNumber - 1, delays.length - 1)];
|
|
406
|
+
const jittered = base * (0.5 + Math.random());
|
|
407
|
+
return Math.min(jittered, Math.max(...delays));
|
|
408
|
+
}
|
|
409
|
+
function defaultSleep(ms) {
|
|
410
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
411
|
+
}
|
|
412
|
+
//# sourceMappingURL=control-write-retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control-write-retry.js","sourceRoot":"","sources":["../src/control-write-retry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegate-peer admission grants for a party control node that also runs the
|
|
3
|
+
* circuit-relay server.
|
|
4
|
+
*
|
|
5
|
+
* A strand node runs as its own libp2p instance with its own transport peerId,
|
|
6
|
+
* derived from the cadre identity key + strandId (`strand-transport-key.ts`).
|
|
7
|
+
* That derivation uses the member's PRIVATE seed by design, so no third party —
|
|
8
|
+
* including a co-cadre relay — can recompute it. When a NAT'd member's strand
|
|
9
|
+
* node tries to reserve a circuit-relay slot on a sibling's control node, the
|
|
10
|
+
* relay's membership connection gater sees an unknown peerId and denies the
|
|
11
|
+
* connection, which kills the reservation and fails the strand's
|
|
12
|
+
* `libp2p.start()` outright.
|
|
13
|
+
*
|
|
14
|
+
* The resolution (docs/strands.md → "Relays and admission"): a party control
|
|
15
|
+
* node running a relay is party-private infrastructure — it relays for its own
|
|
16
|
+
* party's nodes, including the extra transport identities its members' strand
|
|
17
|
+
* nodes run as, and for nobody else. The mechanism is a **member-announced
|
|
18
|
+
* delegate grant**: before a member's control node starts a strand node, it
|
|
19
|
+
* tells its siblings (over the already-authenticated `/sereus/strand-addr/1.0.0`
|
|
20
|
+
* RPC) the derived peerId that strand node will use, and the receiver holds a
|
|
21
|
+
* short-lived, in-memory admission grant for exactly that peerId. The grant is
|
|
22
|
+
* consulted ONLY by the connection-level gate
|
|
23
|
+
* (`CadreNode.admitInboundControlConnection`); the fail-closed per-stream gate
|
|
24
|
+
* (`authorizeInboundControlStream`) never honors it, so a delegate gets the
|
|
25
|
+
* connection (all a circuit-relay `hop` reservation needs) and nothing more.
|
|
26
|
+
*
|
|
27
|
+
* A grant is a delegation of trust to an already-trusted member: the receiver
|
|
28
|
+
* cannot verify that the announced peerId really is the member's strand node,
|
|
29
|
+
* so an authorized member can hand CONNECTION-level access to a peerId of its
|
|
30
|
+
* choosing for up to {@link DELEGATE_GRANT_TTL_MS}. The caps below bound how
|
|
31
|
+
* far a buggy or compromised member can stretch that.
|
|
32
|
+
*
|
|
33
|
+
* The receiver side is {@link DelegateAdmissionStore}. The announcer side keeps
|
|
34
|
+
* its own throttle state (a "last announced at" map that `CadreNode` owns) and
|
|
35
|
+
* decides from it, per reconcile pass, which relays are due a re-announce —
|
|
36
|
+
* {@link dueRelayAnnounces} and {@link pruneStoppedStrandAnnounces} are that
|
|
37
|
+
* decision, pure and testable, kept here beside the TTL they are derived from.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* How long one announced delegate grant stays valid without a re-announce.
|
|
41
|
+
* Announcers refresh at half this (`CadreNode`'s reconcile pass), so a live
|
|
42
|
+
* strand's grant never lapses while its control node can still reach the relay.
|
|
43
|
+
*/
|
|
44
|
+
export declare const DELEGATE_GRANT_TTL_MS: number;
|
|
45
|
+
/** Cap on live grants per announcing member (soonest-expiry evicted first). */
|
|
46
|
+
export declare const MAX_DELEGATE_GRANTS_PER_MEMBER = 32;
|
|
47
|
+
/** Cap on live grants in total (soonest-expiry evicted first). */
|
|
48
|
+
export declare const MAX_DELEGATE_GRANTS = 256;
|
|
49
|
+
/**
|
|
50
|
+
* Composite map key for the per-(peer, strand) state on both sides: the
|
|
51
|
+
* receiver's replace-per-(announcer, strand) grant, and the announcer's
|
|
52
|
+
* per-(target, strand) throttle timestamp.
|
|
53
|
+
*/
|
|
54
|
+
export declare function peerStrandKey(peerId: string, strandId: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* In-memory store of delegate admission grants, keyed by (announcer, strandId)
|
|
57
|
+
* so a re-announce REPLACES the previous delegate peerId rather than
|
|
58
|
+
* accumulating — a restarted strand cannot leak grants. Expired entries are
|
|
59
|
+
* pruned lazily on every read/write. Injectable `now` keeps expiry testable
|
|
60
|
+
* without fake timers.
|
|
61
|
+
*/
|
|
62
|
+
export declare class DelegateAdmissionStore {
|
|
63
|
+
private readonly grants;
|
|
64
|
+
private readonly ttlMs;
|
|
65
|
+
constructor(ttlMs?: number);
|
|
66
|
+
/** Number of live (unexpired at last prune) grants — test/diagnostic surface. */
|
|
67
|
+
get size(): number;
|
|
68
|
+
/**
|
|
69
|
+
* Record (or refresh) a grant: `delegatePeerId` acts on behalf of
|
|
70
|
+
* `announcerPeerId` for `strandId`, valid for the store's TTL from `now`.
|
|
71
|
+
* Enforces {@link MAX_DELEGATE_GRANTS_PER_MEMBER} and
|
|
72
|
+
* {@link MAX_DELEGATE_GRANTS}, evicting the soonest-expiry grant first —
|
|
73
|
+
* biased against the stalest delegation, never against the fresh announce.
|
|
74
|
+
*/
|
|
75
|
+
grant(announcerPeerId: string, strandId: string, delegatePeerId: string, now?: number): void;
|
|
76
|
+
/**
|
|
77
|
+
* Drop every grant. Called when the owning node stops: grants are scoped to
|
|
78
|
+
* the session that recorded them, and outliving it would admit delegates
|
|
79
|
+
* announced against a node that no longer exists.
|
|
80
|
+
*/
|
|
81
|
+
clear(): void;
|
|
82
|
+
/** Is `remotePeerId` covered by a live grant? Prunes expired entries as it goes. */
|
|
83
|
+
has(remotePeerId: string, now?: number): boolean;
|
|
84
|
+
/** Drop every grant whose expiry has passed. */
|
|
85
|
+
private prune;
|
|
86
|
+
/**
|
|
87
|
+
* If the grants matching `filter` are at `cap`, evict the soonest-expiry one
|
|
88
|
+
* among them to make room for the insert the caller is about to do.
|
|
89
|
+
*/
|
|
90
|
+
private evictAtCap;
|
|
91
|
+
}
|
|
92
|
+
/** A circuit relay named by a multiaddr, as an announce target. */
|
|
93
|
+
export interface CircuitRelayTarget {
|
|
94
|
+
/** The relay's peerId — the `<relay>` in `…/p2p/<relay>/p2p-circuit…`. */
|
|
95
|
+
relayPeerId: string;
|
|
96
|
+
/**
|
|
97
|
+
* The relay's DIRECT dial multiaddr (the prefix before `/p2p-circuit`),
|
|
98
|
+
* usable as a dial fallback when no connection to the relay is open yet.
|
|
99
|
+
*/
|
|
100
|
+
relayAddr: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Extract the circuit relays named by a multiaddr list — for each
|
|
104
|
+
* `…/p2p/<relay>/p2p-circuit…` entry, the relay's peerId and its direct dial
|
|
105
|
+
* prefix. A bare `/p2p-circuit` (no relay named), a non-circuit addr, an
|
|
106
|
+
* unparsable addr, and an unparsable relay peerId are all skipped. A trailing
|
|
107
|
+
* `/p2p/<dst>` after the circuit component names the DESTINATION, never the
|
|
108
|
+
* relay, and is ignored. Deduplicated by relay peerId (first addr wins).
|
|
109
|
+
*/
|
|
110
|
+
export declare function extractCircuitRelayTargets(addrs: string[]): CircuitRelayTarget[];
|
|
111
|
+
/**
|
|
112
|
+
* Of `relays`, the ones whose grant for `strandId` is due a re-announce: never
|
|
113
|
+
* announced to, or last announced at least half the TTL ago. Half, so a failed
|
|
114
|
+
* refresh has one more attempt before the grant lapses — a lapsed grant means
|
|
115
|
+
* the relay denies the strand node's reservation re-dial.
|
|
116
|
+
*
|
|
117
|
+
* `announceAt` is the announcer-side "last announced at" map, keyed by
|
|
118
|
+
* {@link peerStrandKey} on the peer announced TO.
|
|
119
|
+
*/
|
|
120
|
+
export declare function dueRelayAnnounces(announceAt: ReadonlyMap<string, number>, relays: readonly CircuitRelayTarget[], strandId: string, now: number, ttlMs?: number): CircuitRelayTarget[];
|
|
121
|
+
/**
|
|
122
|
+
* Drop `announceAt` entries whose strand is no longer running — a stopped
|
|
123
|
+
* strand needs no refresh, and without this the map grows for the node's
|
|
124
|
+
* lifetime. Mutates in place.
|
|
125
|
+
*/
|
|
126
|
+
export declare function pruneStoppedStrandAnnounces(announceAt: Map<string, number>, runningStrandIds: ReadonlySet<string>): void;
|
|
127
|
+
/**
|
|
128
|
+
* The circuit relay named by `addr`, throwing when `addr` is unparsable or names
|
|
129
|
+
* no relay. The throwing form exists for CONFIG-facing callers (`relay-addrs.ts`),
|
|
130
|
+
* where a typo must fail loudly rather than cost the node its reachability;
|
|
131
|
+
* {@link extractCircuitRelayTargets} reads runtime-discovered addrs instead and
|
|
132
|
+
* logs-and-skips.
|
|
133
|
+
*/
|
|
134
|
+
export declare function circuitRelayTargetOrThrow(addr: string): CircuitRelayTarget;
|