@serfab/cadre-core 1.6.0 → 1.8.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 +8 -6
- package/dist/cadre-node.d.ts +548 -137
- package/dist/cadre-node.js +1208 -383
- 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 +6 -0
- package/dist/control-cohort.js +6 -0
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +46 -8
- package/dist/control-database.js +97 -18
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +37 -14
- package/dist/control-formation-recorder.js +60 -10
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-read-retry.d.ts +13 -12
- package/dist/control-read-retry.js +14 -12
- package/dist/control-read-retry.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +67 -5
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +9 -1
- package/dist/control-stream.js +24 -1
- package/dist/control-stream.js.map +1 -1
- package/dist/control-write-retry.d.ts +12 -2
- package/dist/control-write-retry.js +13 -2
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +11 -8
- package/dist/delegate-admission.js +19 -11
- package/dist/delegate-admission.js.map +1 -1
- package/dist/diagnostics/connection-path.d.ts +0 -7
- package/dist/diagnostics/connection-path.js +0 -7
- package/dist/diagnostics/connection-path.js.map +1 -1
- package/dist/formation-approval.d.ts +7 -0
- package/dist/formation-approval.js +9 -3
- package/dist/formation-approval.js.map +1 -1
- package/dist/fs-atomic.d.ts +11 -2
- package/dist/fs-atomic.js +13 -3
- package/dist/fs-atomic.js.map +1 -1
- package/dist/hibernation-manager.d.ts +111 -8
- package/dist/hibernation-manager.js +234 -49
- package/dist/hibernation-manager.js.map +1 -1
- package/dist/index.d.ts +13 -6
- package/dist/index.js +27 -5
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +177 -0
- package/dist/joined-strand-store.js +431 -0
- package/dist/joined-strand-store.js.map +1 -0
- package/dist/key-store-file.js +14 -5
- package/dist/key-store-file.js.map +1 -1
- package/dist/key-store.d.ts +6 -0
- package/dist/key-store.js.map +1 -1
- package/dist/link-budget.d.ts +367 -69
- package/dist/link-budget.js +375 -72
- package/dist/link-budget.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +30 -43
- package/dist/membership-connection-gater.js +31 -42
- package/dist/membership-connection-gater.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 +24 -4
- package/dist/peer-dial.js +74 -5
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +35 -15
- package/dist/peer-join-backfill.js +115 -55
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/push-fanout.d.ts +6 -0
- package/dist/push-fanout.js +6 -0
- package/dist/push-fanout.js.map +1 -1
- package/dist/relay-reservation.d.ts +3 -1
- package/dist/relay-reservation.js +3 -1
- package/dist/relay-reservation.js.map +1 -1
- package/dist/relay-server.d.ts +79 -0
- package/dist/relay-server.js +74 -0
- package/dist/relay-server.js.map +1 -0
- package/dist/seed-bootstrap.d.ts +57 -4
- package/dist/seed-bootstrap.js +107 -83
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/storage-scope.d.ts +33 -17
- package/dist/storage-scope.js +44 -23
- package/dist/storage-scope.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +58 -21
- package/dist/strand-addr-protocol.js +133 -85
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-database.d.ts +20 -2
- package/dist/strand-database.js +42 -5
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +59 -21
- package/dist/strand-first-sync-gate.js +58 -19
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +151 -0
- package/dist/strand-formation-deadlines.js +171 -0
- package/dist/strand-formation-deadlines.js.map +1 -0
- package/dist/strand-formation-manager.d.ts +63 -30
- package/dist/strand-formation-manager.js +67 -41
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +45 -35
- package/dist/strand-formation-protocol.js +59 -122
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +152 -6
- package/dist/strand-instance-manager.js +331 -42
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +6 -0
- package/dist/strand-membership-writer.js +11 -0
- package/dist/strand-membership-writer.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 +168 -0
- package/dist/strand-peer-book-protocol.js +356 -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 +361 -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/strand-revocation-enforcer.js +2 -1
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +36 -20
- package/dist/strand-solicitation.js +4 -4
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +80 -23
- package/dist/strand-wake-protocol.js +89 -64
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/types.d.ts +181 -33
- package/dist/types.js +4 -3
- package/dist/types.js.map +1 -1
- package/package.json +11 -7
- package/src/cadre-node.ts +1316 -398
- package/src/control-authorization.ts +2 -1
- package/src/control-cohort.ts +6 -0
- package/src/control-database.ts +122 -18
- package/src/control-formation-recorder.ts +354 -305
- package/src/control-read-retry.ts +14 -12
- package/src/control-schema.ts +67 -5
- package/src/control-stream.ts +25 -1
- package/src/control-write-retry.ts +13 -2
- package/src/delegate-admission.ts +23 -13
- package/src/diagnostics/connection-path.ts +0 -7
- package/src/formation-approval.ts +610 -604
- package/src/fs-atomic.ts +14 -4
- package/src/hibernation-manager.ts +638 -437
- package/src/index.ts +91 -6
- package/src/joined-strand-store.ts +498 -0
- package/src/key-store-file.ts +14 -5
- package/src/key-store.ts +6 -0
- package/src/link-budget.ts +393 -74
- package/src/membership-connection-gater.ts +31 -45
- package/src/node-local-snapshot.ts +293 -289
- package/src/peer-dial.ts +81 -7
- package/src/peer-join-backfill.ts +120 -57
- package/src/push-fanout.ts +6 -0
- package/src/relay-reservation.ts +3 -1
- package/src/relay-server.ts +98 -0
- package/src/seed-bootstrap.ts +133 -91
- package/src/storage-scope.ts +44 -23
- package/src/strand-addr-protocol.ts +547 -458
- package/src/strand-database.ts +287 -242
- package/src/strand-first-sync-gate.ts +58 -19
- package/src/strand-formation-deadlines.ts +227 -0
- package/src/strand-formation-manager.ts +700 -647
- package/src/strand-formation-protocol.ts +103 -150
- package/src/strand-instance-manager.ts +387 -45
- package/src/strand-membership-writer.ts +12 -0
- package/src/strand-peer-book-file.ts +39 -0
- package/src/strand-peer-book-protocol.ts +466 -0
- package/src/strand-peer-book-swap.ts +338 -0
- package/src/strand-peer-book.ts +475 -0
- package/src/strand-peer-observer.ts +235 -0
- package/src/strand-revocation-enforcer.ts +2 -1
- package/src/strand-solicitation.ts +519 -500
- package/src/strand-wake-protocol.ts +401 -355
- package/src/types.ts +185 -33
|
@@ -14,7 +14,7 @@ import { isUncommittedTransactorAggregate } from './control-write-retry.js';
|
|
|
14
14
|
* was absorbed). This module gives reads the same bounded re-presentation with a SHORTER
|
|
15
15
|
* deadline and a NARROWER failure set, because the tightest caller deadline over a control
|
|
16
16
|
* read is the inbound admission gate's 2 s fail-open timeout
|
|
17
|
-
* (`ADMISSION_DECISION_TIMEOUT_MS`, `
|
|
17
|
+
* (`ADMISSION_DECISION_TIMEOUT_MS`, `link-budget.ts`) — a read budget that
|
|
18
18
|
* does not fit inside it with headroom spends its retries after the gate has already
|
|
19
19
|
* admitted.
|
|
20
20
|
*
|
|
@@ -67,18 +67,20 @@ export const CONTROL_READ_RETRY_DELAYS_MS: readonly number[] = [100, 400];
|
|
|
67
67
|
* 2 s only because each stays well under it. Do not raise these numbers without
|
|
68
68
|
* re-checking against `ADMISSION_DECISION_TIMEOUT_MS`.
|
|
69
69
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* `clusterPolicy.cohortQueryTimeoutMs`,
|
|
73
|
-
* (`
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
70
|
+
* It cuts the loop off by design, so it does not grow with the link or with the number that
|
|
71
|
+
* dominates a slow attempt: an attempt whose cohort consult finds a silent peer costs
|
|
72
|
+
* `clusterPolicy.cohortQueryTimeoutMs`, two link round trips at the declared link
|
|
73
|
+
* (`cohortReadDeadlineMs` in `link-budget.ts`, 7 000 ms at the default declaration; the plugin's
|
|
74
|
+
* `COHORT_READ_DEADLINE_MS` is the same number). That is more than four times this budget, so
|
|
75
|
+
* such an attempt is never retried and the caller gets its error unchanged. That is deliberate:
|
|
76
|
+
* the caller this budget is sized for, the admission gate, has
|
|
77
|
+
* already taken its fail-open answer by then, and a retry would only spend time after it. What
|
|
78
|
+
* the budget still buys is the fast failures it was built for: the ~25 ms transactor read-phase
|
|
79
|
+
* aggregate off a stream still forming, and a `cohort-unreachable` read during bring-up, which
|
|
80
|
+
* fails fast because there is no connection to ask. Both retry twice well inside 1500 ms. See
|
|
81
|
+
* `docs/cadre-consistency.md` → "Deadlines Over Optimystic's Reads and Commits".
|
|
81
82
|
*/
|
|
83
|
+
// eslint-disable-next-line no-restricted-syntax -- cuts off by design: a slow attempt is not retried, because the admission gate this budget is sized for has already failed open by then; see docs/cadre-consistency.md → "Deadlines Over Optimystic's Reads and Commits"
|
|
82
84
|
export const CONTROL_READ_RETRY_BUDGET_MS = 1_500;
|
|
83
85
|
|
|
84
86
|
/**
|
package/src/control-schema.ts
CHANGED
|
@@ -365,6 +365,65 @@ declare schema CadreControl {
|
|
|
365
365
|
)
|
|
366
366
|
) with context (OwnerKey text, Signature text);
|
|
367
367
|
|
|
368
|
+
-- A strand THIS party joined from ANOTHER party. The founding party's control database
|
|
369
|
+
-- holds its Strand row, so without this table only the machine that joined would know of
|
|
370
|
+
-- it. Every machine of the party reads it (the strand watcher offers each row like a
|
|
371
|
+
-- Strand row), so a replica host keeps a copy and the party's other machines can attach.
|
|
372
|
+
-- A separate table rather than a Strand row with a provenance marker: Strand means "this
|
|
373
|
+
-- party's own strand" to every reader of it (founder derivation, FormationUsage.StrandExists,
|
|
374
|
+
-- unpublish, the consent branch of Strand.AuthorizedInsert), and each would need a
|
|
375
|
+
-- provenance check. MemberPrivateKey is the closed strand's shared read secret the
|
|
376
|
+
-- formation delivered — replicated in plaintext to every machine the party owns, under the
|
|
377
|
+
-- same accepted risk as Strand.MemberPrivateKey (docs/strands.md → "Closed-Strand Member
|
|
378
|
+
-- Key Handling"). Unlike that column it is not held only here: the founding party and
|
|
379
|
+
-- every member hold it, so a removed row is recoverable by re-forming, which is why this
|
|
380
|
+
-- table (unlike Strand) carries a REAP branch.
|
|
381
|
+
table JoinedStrand (
|
|
382
|
+
Id text primary key, -- the joined strand's id (the founder's Strand.Id)
|
|
383
|
+
Type text not null, -- 'o' | 'c', as Strand.Type
|
|
384
|
+
MemberPrivateKey text null, -- closed strand's shared read secret; null for open
|
|
385
|
+
StampId text not null unique, -- single-use authorization nonce (anti-replay).
|
|
386
|
+
-- \`unique\` holds over LIVE rows only; a removed row's stamp is
|
|
387
|
+
-- retired permanently into Revocation (NotRevoked below).
|
|
388
|
+
-- The approval that seated a removed row can never re-seat it. Same rationale as
|
|
389
|
+
-- OwnerKey.NotRevoked, stated in full there.
|
|
390
|
+
constraint NotRevoked check on insert (
|
|
391
|
+
not exists (select 1 from Revocation R where R.TableName = 'JoinedStrand' and R.StampId = new.StampId)
|
|
392
|
+
),
|
|
393
|
+
-- A delete must file its correctly named tombstone in the same transaction. Same
|
|
394
|
+
-- rationale as StrandPartyKey.RevocationRecorded.
|
|
395
|
+
constraint RevocationRecorded check on delete (
|
|
396
|
+
exists (select 1 from Revocation R where R.TableName = 'JoinedStrand' and R.StampId = old.StampId and R.RowKey = old.Id)
|
|
397
|
+
),
|
|
398
|
+
-- Insert + delete only: neither Authorized rule below covers update, so an in-place
|
|
399
|
+
-- rewrite would swap the read secret unsigned. Mirrors StrandPartyKey.NoUpdate.
|
|
400
|
+
constraint NoUpdate check on update (false),
|
|
401
|
+
-- Readers cast Type to 'o' | 'c' (ControlDatabase.queryJoinedStrands).
|
|
402
|
+
constraint KnownType check (new.Type = 'o' or new.Type = 'c'),
|
|
403
|
+
-- An open strand has no read gate, so no read secret. Same rule as Strand.MemberKeyClosedOnly.
|
|
404
|
+
constraint MemberKeyClosedOnly check (new.MemberPrivateKey is null or new.Type = 'c'),
|
|
405
|
+
constraint AuthorizedInsert check on insert (
|
|
406
|
+
-- Owners authorize by signing over THIS row (Id, Type, MemberPrivateKey, StampId),
|
|
407
|
+
-- MemberPrivateKey signing as '' when null, so a captured approval can only reproduce
|
|
408
|
+
-- the exact secret it approved. Owner-signed, deliberately NOT self-signable by an
|
|
409
|
+
-- enrolled CadrePeer: every always-on machine of the party downloads the strands this
|
|
410
|
+
-- table names, so a non-owner machine could otherwise make them all host an arbitrary
|
|
411
|
+
-- strand. A machine that is not an owner keeps its joins machine-local.
|
|
412
|
+
exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.JoinedStrand', 'add', new.Id, new.Type, coalesce(new.MemberPrivateKey, ''), new.StampId), context.Signature, A.Key, 'ed25519'))
|
|
413
|
+
),
|
|
414
|
+
constraint AuthorizedDelete check on delete (
|
|
415
|
+
-- 'remove'-tagged digest over the STORED row (Id, StampId), so the add approval can
|
|
416
|
+
-- never be replayed as a removal.
|
|
417
|
+
exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest('CadreControl.JoinedStrand', 'remove', old.Id, old.StampId), context.Signature, A.Key, 'ed25519'))
|
|
418
|
+
|
|
419
|
+
-- or REAP: a COMMITTED tombstone already retires this exact row incarnation.
|
|
420
|
+
-- Why committed.* and why the stamp is bound: stated in full on
|
|
421
|
+
-- CadrePeer.AuthorizedDelete. Allowed here, unlike Strand / StrandPartyKey,
|
|
422
|
+
-- because MemberPrivateKey is recoverable (see the table comment).
|
|
423
|
+
or exists (select 1 from committed.Revocation R where R.TableName = 'JoinedStrand' and R.RowKey = old.Id and R.StampId = old.StampId)
|
|
424
|
+
)
|
|
425
|
+
) with context (OwnerKey text, Signature text);
|
|
426
|
+
|
|
368
427
|
-- A peer (node) that is part of the cadre, carrying a self-published,
|
|
369
428
|
-- freshness-stamped, self-signed address record (see PeerAddressRecord in cadre-core).
|
|
370
429
|
-- The row IS the peer-address record: a resolver reads it, re-verifies Sig against
|
|
@@ -785,8 +844,8 @@ declare schema CadreControl {
|
|
|
785
844
|
-- thing to suspect.
|
|
786
845
|
index FormationUsageByToken on FormationUsage (Token);
|
|
787
846
|
|
|
788
|
-
-- Append-only retirement record for the one-off StampId nonces of removed
|
|
789
|
-
--
|
|
847
|
+
-- Append-only retirement record for the one-off StampId nonces of removed rows of every
|
|
848
|
+
-- guarded table (listed on TableName below). Without it a removal was undoable: the add
|
|
790
849
|
-- approval is a signature over
|
|
791
850
|
-- (row key, stamp) that never expires, and deleting the row freed the stamp, so anyone who
|
|
792
851
|
-- kept a copy of the approval could resurrect the row.
|
|
@@ -815,10 +874,12 @@ declare schema CadreControl {
|
|
|
815
874
|
-- its own guarded TableName, 'Revocation' is not one, and ControlDatabase.queryRevocations
|
|
816
875
|
-- skips the marker, so nothing reaps or re-issues it.
|
|
817
876
|
table Revocation (
|
|
818
|
-
TableName text, -- 'OwnerKey' | 'CadrePeer' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' |
|
|
819
|
-
-- or 'Revocation' for the ledger marker only
|
|
877
|
+
TableName text, -- 'OwnerKey' | 'CadrePeer' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' |
|
|
878
|
+
-- 'JoinedStrand' | 'DeviceToken', or 'Revocation' for the ledger marker only
|
|
879
|
+
-- (all confined by RowIsGone below)
|
|
820
880
|
RowKey text not null, -- primary key of the removed row: OwnerKey.Key / ValidationKey.Key /
|
|
821
|
-
-- CadrePeer.PeerId / DeviceToken.PeerId / Strand.Id / StrandPartyKey.Id
|
|
881
|
+
-- CadrePeer.PeerId / DeviceToken.PeerId / Strand.Id / StrandPartyKey.Id /
|
|
882
|
+
-- JoinedStrand.Id
|
|
822
883
|
-- Every guarded table's RevocationRecorded CHECK requires the
|
|
823
884
|
-- accompanying tombstone to carry the removed row's key here, so
|
|
824
885
|
-- a REMOVAL cannot file a misnamed tombstone. A tombstone with no
|
|
@@ -869,6 +930,7 @@ declare schema CadreControl {
|
|
|
869
930
|
or (new.TableName = 'ValidationKey' and not exists (select 1 from ValidationKey V where V.StampId = new.StampId))
|
|
870
931
|
or (new.TableName = 'Strand' and not exists (select 1 from Strand S where S.StampId = new.StampId))
|
|
871
932
|
or (new.TableName = 'StrandPartyKey' and not exists (select 1 from StrandPartyKey K where K.StampId = new.StampId))
|
|
933
|
+
or (new.TableName = 'JoinedStrand' and not exists (select 1 from JoinedStrand J where J.StampId = new.StampId))
|
|
872
934
|
or (new.TableName = 'DeviceToken' and not exists (select 1 from DeviceToken D where D.StampId = new.StampId))
|
|
873
935
|
or (new.TableName = 'Revocation' and new.RowKey = 'ledger' and new.StampId = 'opened')
|
|
874
936
|
),
|
package/src/control-stream.ts
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* deadline, write one frame, half-close, read one response, reset the stream on
|
|
14
14
|
* any failure — so that shape lives here too, as {@link withDeadline} +
|
|
15
15
|
* {@link exchangeFrame}; each protocol supplies only its dial options, its
|
|
16
|
-
* request object, and its response decoder.
|
|
16
|
+
* request object, and its response decoder. The receiver's half — one reply
|
|
17
|
+
* frame, then close — is {@link replyAndClose}.
|
|
17
18
|
*
|
|
18
19
|
* Dependency-free by design: it imports nothing from the protocol modules, so
|
|
19
20
|
* the import graph stays acyclic (`control-stream` ← `seed-bootstrap` ←
|
|
@@ -22,6 +23,10 @@
|
|
|
22
23
|
* `decodeLengthPrefixedFrame` guard.
|
|
23
24
|
*/
|
|
24
25
|
|
|
26
|
+
import debug from 'debug';
|
|
27
|
+
|
|
28
|
+
const log = debug('sereus:cadre:control-stream');
|
|
29
|
+
|
|
25
30
|
/**
|
|
26
31
|
* Minimal libp2p 3.x stream surface: AsyncIterable for reads, `send()` for
|
|
27
32
|
* writes, `close()` to half-close the write end (EOF), `abort()` to reset.
|
|
@@ -41,6 +46,25 @@ export function writeFrame(stream: ControlStream, obj: unknown): void {
|
|
|
41
46
|
stream.send(body);
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Receiver side: write one reply frame and close the stream, best-effort on both and never
|
|
51
|
+
* throwing. A peer that has already gone away cannot be answered, and nothing the receiver
|
|
52
|
+
* decided depends on whether it heard. The close is what releases the sender, which reads the
|
|
53
|
+
* reply to end-of-stream ({@link exchangeFrame}).
|
|
54
|
+
*/
|
|
55
|
+
export async function replyAndClose(stream: ControlStream, reply: unknown, label: string): Promise<void> {
|
|
56
|
+
try {
|
|
57
|
+
writeFrame(stream, reply);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
log('%s: failed to write reply: %o', label, error);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
await stream.close();
|
|
63
|
+
} catch (error) {
|
|
64
|
+
log('%s: failed to close stream: %o', label, error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
/**
|
|
45
69
|
* Reject if `op` does not settle within `ms`. On timeout, invoke `onTimeout`
|
|
46
70
|
* before rejecting — a hook for releasing whatever the op is parked on. A
|
|
@@ -77,11 +77,21 @@ export const SCHEMA_INIT_ATTEMPTS = 5;
|
|
|
77
77
|
* This budget — not the attempt count — is what makes the policy safe to sit under every
|
|
78
78
|
* control write. A transient failure (stream reset while a connection is still forming)
|
|
79
79
|
* surfaces in well under a second, so it is retried and the loop adds at most ~2.2 s. A
|
|
80
|
-
* genuinely silent cohort member fails at ~
|
|
81
|
-
* attempts,
|
|
80
|
+
* genuinely silent cohort member fails at ~21 s per pend round (two `ClusterClient`
|
|
81
|
+
* response-deadline attempts, 10.5 s each at the default declared link; a two-round write
|
|
82
|
+
* measured 42.2 s in `control-write-degraded-cohort-member.integration.ts`), which
|
|
82
83
|
* already exceeds this budget when attempt 1 returns — so that case is surfaced immediately
|
|
83
84
|
* and retry adds ZERO latency to the case where it cannot help.
|
|
84
85
|
*
|
|
86
|
+
* It cuts the loop off by design, so it does not grow with the link: its job is to end the
|
|
87
|
+
* retries of slow attempts, and the caller gets the last attempt's error unchanged. An attempt
|
|
88
|
+
* whose read phase consulted a silent peer ends at about one per-peer read deadline
|
|
89
|
+
* (`cohortReadDeadlineMs` in `link-budget.ts`: 7 s at the default declared link, 5 s before it
|
|
90
|
+
* was derived, 1 s at Optimystic's own default), so it still gets one retry inside this budget
|
|
91
|
+
* where it once got two; an attempt that runs into the `ClusterClient` response deadline
|
|
92
|
+
* (10.5 s at the default declared link) still gets none. See `docs/cadre-consistency.md` →
|
|
93
|
+
* "Deadlines Over Optimystic's Reads and Commits".
|
|
94
|
+
*
|
|
85
95
|
* NOTE: a failed commit attempt now also pays a cancel discharge before its error returns
|
|
86
96
|
* (optimystic `TransactorSource.transact`'s catch, added by upstream
|
|
87
97
|
* `1-a-failed-attempt-must-discharge-its-own-pend`) — bounded by six rounds and
|
|
@@ -93,6 +103,7 @@ export const SCHEMA_INIT_ATTEMPTS = 5;
|
|
|
93
103
|
* to do: if that case ever starts failing with `failed after 2/3 attempt(s)`, the cancel
|
|
94
104
|
* discharge is where the time went.
|
|
95
105
|
*/
|
|
106
|
+
// eslint-disable-next-line no-restricted-syntax -- cuts off by design: it ends the retries of slow attempts rather than waiting them out; see docs/cadre-consistency.md → "Deadlines Over Optimystic's Reads and Commits"
|
|
96
107
|
export const CONTROL_WRITE_RETRY_BUDGET_MS = 10_000;
|
|
97
108
|
|
|
98
109
|
/**
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* The receiver side is {@link DelegateAdmissionStore}. The announcer side keeps
|
|
36
36
|
* its own throttle state (a "last announced at" map that `CadreNode` owns) and
|
|
37
37
|
* decides from it, per reconcile pass, which relays are due a re-announce —
|
|
38
|
-
* {@link dueRelayAnnounces} and {@link
|
|
38
|
+
* {@link dueRelayAnnounces} and {@link prunePeerStrandKeys} are that
|
|
39
39
|
* decision, pure and testable, kept here beside the TTL they are derived from.
|
|
40
40
|
*/
|
|
41
41
|
|
|
@@ -71,14 +71,20 @@ interface DelegateGrant {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* Composite map key for
|
|
75
|
-
*
|
|
76
|
-
* per-(
|
|
74
|
+
* Composite map key for per-(peer, strand) state: the receiver's
|
|
75
|
+
* replace-per-(announcer, strand) grant, the announcer's per-(target, strand)
|
|
76
|
+
* throttle timestamp, and `CadreNode`'s per-(sibling, strand) strand-addr ask
|
|
77
|
+
* due time.
|
|
77
78
|
*/
|
|
78
79
|
export function peerStrandKey(peerId: string, strandId: string): string {
|
|
79
80
|
return `${peerId}\n${strandId}`;
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
/** The `peerId` half of a {@link peerStrandKey} (a peer id never contains a newline). */
|
|
84
|
+
function peerIdOfKey(key: string): string {
|
|
85
|
+
return key.slice(0, key.indexOf('\n'));
|
|
86
|
+
}
|
|
87
|
+
|
|
82
88
|
/** The `strandId` half of a {@link peerStrandKey}. */
|
|
83
89
|
function strandIdOfKey(key: string): string {
|
|
84
90
|
return key.slice(key.indexOf('\n') + 1);
|
|
@@ -233,17 +239,21 @@ export function dueRelayAnnounces(
|
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
/**
|
|
236
|
-
* Drop
|
|
237
|
-
*
|
|
238
|
-
*
|
|
242
|
+
* Drop the entries of a {@link peerStrandKey}-keyed map whose strand is no longer
|
|
243
|
+
* running, and — when `livePeerIds` is given — whose peer is not among them. A
|
|
244
|
+
* stopped strand needs no refresh, a departed peer is owed a fresh start when it
|
|
245
|
+
* returns, and without this the map grows for the node's lifetime. Mutates in
|
|
246
|
+
* place.
|
|
239
247
|
*/
|
|
240
|
-
export function
|
|
241
|
-
|
|
242
|
-
runningStrandIds: ReadonlySet<string
|
|
248
|
+
export function prunePeerStrandKeys(
|
|
249
|
+
byKey: Map<string, number>,
|
|
250
|
+
runningStrandIds: ReadonlySet<string>,
|
|
251
|
+
livePeerIds?: ReadonlySet<string>
|
|
243
252
|
): void {
|
|
244
|
-
for (const key of
|
|
245
|
-
|
|
246
|
-
|
|
253
|
+
for (const key of byKey.keys()) {
|
|
254
|
+
const peerGone = livePeerIds !== undefined && !livePeerIds.has(peerIdOfKey(key));
|
|
255
|
+
if (peerGone || !runningStrandIds.has(strandIdOfKey(key))) {
|
|
256
|
+
byKey.delete(key);
|
|
247
257
|
}
|
|
248
258
|
}
|
|
249
259
|
}
|
|
@@ -3,13 +3,6 @@
|
|
|
3
3
|
* (`/p2p-circuit`) vs direct, tag its transport, and summarise counts plus a
|
|
4
4
|
* "stuck on relay" condition.
|
|
5
5
|
*
|
|
6
|
-
* This is the **canonical** implementation. The web reference app keeps a
|
|
7
|
-
* deliberate, documented duplicate at
|
|
8
|
-
* `packages/reference-app-web/src/lib/connection-path.ts` (it does not depend on
|
|
9
|
-
* `@serfab/cadre-core`). Keep the function signatures and classification table
|
|
10
|
-
* identical across both copies so a future shared micro-package would be a
|
|
11
|
-
* drop-in.
|
|
12
|
-
*
|
|
13
6
|
* The whole signal is a pure function over `node.getConnections()` + a settle
|
|
14
7
|
* window + `Date.now()`. A connection's path is derivable from its `remoteAddr`
|
|
15
8
|
* multiaddr string alone — no I/O, no events, no stateful observer.
|