@serfab/cadre-core 1.3.0 → 1.5.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.
Files changed (90) hide show
  1. package/README.md +5 -3
  2. package/dist/cadre-node.d.ts +70 -34
  3. package/dist/cadre-node.js +197 -60
  4. package/dist/cadre-node.js.map +1 -1
  5. package/dist/control-database.d.ts +24 -10
  6. package/dist/control-database.js +48 -16
  7. package/dist/control-database.js.map +1 -1
  8. package/dist/control-formation-recorder.d.ts +4 -4
  9. package/dist/control-formation-recorder.js +6 -6
  10. package/dist/control-formation-recorder.js.map +1 -1
  11. package/dist/control-read-retry.d.ts +12 -0
  12. package/dist/control-read-retry.js +12 -0
  13. package/dist/control-read-retry.js.map +1 -1
  14. package/dist/control-write-retry.d.ts +5 -4
  15. package/dist/control-write-retry.js +24 -7
  16. package/dist/control-write-retry.js.map +1 -1
  17. package/dist/index.d.ts +5 -4
  18. package/dist/index.js +10 -5
  19. package/dist/index.js.map +1 -1
  20. package/dist/link-budget.d.ts +182 -0
  21. package/dist/link-budget.js +195 -0
  22. package/dist/link-budget.js.map +1 -0
  23. package/dist/membership-connection-gater.d.ts +18 -5
  24. package/dist/membership-connection-gater.js +50 -10
  25. package/dist/membership-connection-gater.js.map +1 -1
  26. package/dist/peer-dial.d.ts +36 -17
  27. package/dist/peer-dial.js +37 -17
  28. package/dist/peer-dial.js.map +1 -1
  29. package/dist/peer-join-backfill.d.ts +58 -4
  30. package/dist/peer-join-backfill.js +165 -20
  31. package/dist/peer-join-backfill.js.map +1 -1
  32. package/dist/relay-reservation.d.ts +59 -11
  33. package/dist/relay-reservation.js +169 -43
  34. package/dist/relay-reservation.js.map +1 -1
  35. package/dist/seed-bootstrap.js +2 -1
  36. package/dist/seed-bootstrap.js.map +1 -1
  37. package/dist/storage-scope.d.ts +50 -12
  38. package/dist/storage-scope.js +85 -12
  39. package/dist/storage-scope.js.map +1 -1
  40. package/dist/strand-first-sync-gate.d.ts +43 -5
  41. package/dist/strand-first-sync-gate.js +55 -7
  42. package/dist/strand-first-sync-gate.js.map +1 -1
  43. package/dist/strand-formation-manager.js +2 -1
  44. package/dist/strand-formation-manager.js.map +1 -1
  45. package/dist/strand-id.d.ts +35 -0
  46. package/dist/strand-id.js +46 -0
  47. package/dist/strand-id.js.map +1 -0
  48. package/dist/strand-instance-manager.d.ts +27 -2
  49. package/dist/strand-instance-manager.js +75 -15
  50. package/dist/strand-instance-manager.js.map +1 -1
  51. package/dist/strand-membership-reconciler.d.ts +200 -45
  52. package/dist/strand-membership-reconciler.js +302 -86
  53. package/dist/strand-membership-reconciler.js.map +1 -1
  54. package/dist/strand-membership-writer.d.ts +3 -2
  55. package/dist/strand-membership-writer.js +67 -90
  56. package/dist/strand-membership-writer.js.map +1 -1
  57. package/dist/strand-network-config.d.ts +3 -3
  58. package/dist/strand-network-config.js +3 -3
  59. package/dist/strand-solicitation.js +2 -1
  60. package/dist/strand-solicitation.js.map +1 -1
  61. package/dist/strand-watcher.d.ts +45 -0
  62. package/dist/strand-watcher.js +74 -13
  63. package/dist/strand-watcher.js.map +1 -1
  64. package/dist/types.d.ts +190 -24
  65. package/dist/types.js +73 -4
  66. package/dist/types.js.map +1 -1
  67. package/package.json +8 -8
  68. package/src/cadre-node.ts +7445 -7302
  69. package/src/control-database.ts +48 -16
  70. package/src/control-formation-recorder.ts +6 -6
  71. package/src/control-read-retry.ts +214 -202
  72. package/src/control-write-retry.ts +24 -7
  73. package/src/index.ts +29 -2
  74. package/src/link-budget.ts +217 -0
  75. package/src/membership-connection-gater.ts +56 -11
  76. package/src/peer-dial.ts +39 -17
  77. package/src/peer-join-backfill.ts +192 -23
  78. package/src/relay-reservation.ts +1119 -951
  79. package/src/seed-bootstrap.ts +2 -1
  80. package/src/storage-scope.ts +163 -78
  81. package/src/strand-first-sync-gate.ts +55 -7
  82. package/src/strand-formation-manager.ts +647 -646
  83. package/src/strand-id.ts +48 -0
  84. package/src/strand-instance-manager.ts +1516 -1440
  85. package/src/strand-membership-reconciler.ts +334 -92
  86. package/src/strand-membership-writer.ts +1962 -1987
  87. package/src/strand-network-config.ts +250 -250
  88. package/src/strand-solicitation.ts +500 -499
  89. package/src/strand-watcher.ts +388 -325
  90. package/src/types.ts +192 -23
package/dist/index.js CHANGED
@@ -51,8 +51,10 @@ export { MemoryBootstrapPeerStore, PersistentBootstrapPeerStore } from './bootst
51
51
  export { MemoryEnrolledMachineStore, PersistentEnrolledMachineStore } from './enrolled-machine-store.js';
52
52
  // Storage scope keys — what `CadreNodeConfig.storage.provider` is called with. A
53
53
  // strand's key is its strand id; the control database's key carries the party id,
54
- // so two parties on one device never share a control store.
55
- export { controlStorageScope, isControlStorageScope } from './storage-scope.js';
54
+ // so two parties on one device never share a control store. Every key stays within
55
+ // `[A-Za-z0-9._-]`: the control key by base64url encoding, a strand's by
56
+ // `assertStrandScopeKey`, which every strand launch runs.
57
+ export { controlStorageScope, isControlStorageScope, isValidStrandScopeKey, assertStrandScopeKey, InvalidStrandIdError } from './storage-scope.js';
56
58
  // Strand database
57
59
  export { StrandDatabase } from './strand-database.js';
58
60
  // Strand management
@@ -65,7 +67,7 @@ export { StrandAwaitingFirstSyncError, DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS, DEF
65
67
  // Peer-join block catch-up (push this network's own blocks to each newly
66
68
  // connected peer, so a late joiner physically holds pre-join blocks). Shared
67
69
  // by the strand networks (ungated) and the control network (membership-gated).
68
- export { PeerJoinBackfill, DEFAULT_PEER_JOIN_BACKFILL, MAX_BLOCK_MESSAGE_BYTES } from './peer-join-backfill.js';
70
+ export { PeerJoinBackfill, DEFAULT_PEER_JOIN_BACKFILL, MAX_BLOCK_MESSAGE_BYTES, PEER_JOIN_BACKFILL_WARN_AFTER_FAILURES } from './peer-join-backfill.js';
69
71
  // Closed-strand revoked-peer enforcement (deny the network nodes of removed
70
72
  // members at the stream and connection layers, and hang up the sessions they
71
73
  // already hold — see the module doc for the settled deny-list design, the
@@ -134,7 +136,10 @@ export { PushFanoutService, DEFAULT_PUSH_COOLDOWN_MS, DEFAULT_PUSH_DEBOUNCE_MS }
134
136
  export { selectControlCohortDials, DEFAULT_CONTROL_COHORT_RECONCILE_MS, DEFAULT_CONTROL_COHORT_TARGET_DEGREE } from './control-cohort.js';
135
137
  // Dialing one peer from several candidate addresses, each under its own time
136
138
  // limit, so an address that never answers cannot starve the rest.
137
- export { dialPeerAddrs, tryAddrsInTurn, directBeforeRelayed, DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS, DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS, DEFAULT_PEER_DIAL_BUDGET } from './peer-dial.js';
139
+ export { dialPeerAddrs, tryAddrsInTurn, directBeforeRelayed, DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS, DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS, CONTROL_COHORT_DIAL_ADDRESS_ATTEMPTS, DEFAULT_PEER_DIAL_BUDGET } from './peer-dial.js';
140
+ // Dial and reservation budgets counted in link round trips rather than fixed milliseconds, so
141
+ // one declared assumption about the link moves them all — see `NetworkConfig.linkRoundTripMs`.
142
+ export { DECLARED_LINK_ROUND_TRIP_MS, RELAYED_DIAL_ROUND_TRIPS, RELAY_RESERVATION_ROUND_TRIPS, CIRCUIT_REQUEST_ROUND_TRIPS, PUSH_TRANSFER_ALLOWANCE_MS, resolveLinkRoundTripMs, relayedDialBudgetMs, relayReservationBudgetMs, circuitRequestBudgetMs, peerJoinPushBudget } from './link-budget.js';
138
143
  // Seed trust policy (trust anchor for incoming seeds)
139
144
  export { anchoredTrustPolicy, pinnedKeyTrustPolicy, tofuTrustPolicy } from './seed-trust-policy.js';
140
145
  // Schema Verification
@@ -144,7 +149,7 @@ export { FormationListener, dialFormation, isValidResponderCreatesResult, isWell
144
149
  // Strand Formation manager (drives the native transport)
145
150
  export { StrandFormationManager, createStrandFormationManager, MEMBERSHIP_INVITE_TTL_MS, MEMBERSHIP_INVITE_UNAVAILABLE_REASON, HOST_STRAND_MUST_BE_RECREATED_REASON } from './strand-formation-manager.js';
146
151
  // Control-network inbound connection gate (membership defense-in-depth)
147
- export { createMembershipConnectionGater, STRANGER_OPEN_PROTOCOLS, DEFAULT_ENROLLMENT_WINDOW_MS, ADMISSION_DECISION_TIMEOUT_MS, RELAY_ADMISSION_RESERVE_DEADLINE_MS, MAX_UNAUTHORIZED_RELAY_RESERVATIONS, UNAUTHORIZED_RESERVATION_TTL_MS, UnauthorizedReservationBudget } from './membership-connection-gater.js';
152
+ export { createMembershipConnectionGater, STRANGER_OPEN_PROTOCOLS, DEFAULT_ENROLLMENT_WINDOW_MS, ADMISSION_DECISION_TIMEOUT_MS, RELAY_ADMISSION_RESERVE_DEADLINE_MS, RELAY_ADMISSION_CLOSE_TIMEOUT_MS, MAX_UNAUTHORIZED_RELAY_RESERVATIONS, UNAUTHORIZED_RESERVATION_TTL_MS, UnauthorizedReservationBudget } from './membership-connection-gater.js';
148
153
  // Thrown out of `CadreNode.start()` when a `network.relayAddrs` reservation does
149
154
  // not land on its first attempt — the fail-fast posture of that config field now
150
155
  // that the control node reserves AFTER control-database bring-up (relay-addrs.ts).
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Dial and reservation budgets derived from ONE declared link round trip, instead of fixed
3
+ * millisecond numbers chosen on a fast local network.
4
+ *
5
+ * Reaching another machine through a relay costs a FIXED NUMBER OF EXCHANGES, not a fixed
6
+ * number of milliseconds. So every budget written as milliseconds has a link speed above which
7
+ * it can never open a connection at all — and the slower the link, the more of them fall over,
8
+ * silently, because an abandoned dial looks the same as a peer that is not there. Counting round
9
+ * trips instead is what stops that class of defect coming back: the next person to widen a
10
+ * budget changes ONE declaration, and the next person to add a dial writes down its round-trip
11
+ * count beside the arithmetic.
12
+ *
13
+ * ── The instrument ──
14
+ *
15
+ * Every count and every number below comes from
16
+ * `packages/integration-tests/src/scenarios/relayed-dial-cost-by-latency.integration.ts`, which
17
+ * is committed and opt-in (`RELAY_DIAL_COST=1`). **Its doc comment is the single home of the
18
+ * measurement**; this module holds only what the derivation needs. Re-run it before changing
19
+ * anything here.
20
+ *
21
+ * ── The unit ──
22
+ *
23
+ * ONE LINK ROUND TRIP is a message from one machine reaching the other and a reply coming
24
+ * back, over the path those two machines actually use — through the relay, when that is the
25
+ * only way they can reach each other. In the instrument, which injects a constant ONE-WAY
26
+ * frame delay `d`, one link round trip is `2d`.
27
+ *
28
+ * A leg between a node and the RELAY is half a link round trip, because it crosses one of the
29
+ * two hops rather than both. That is why the reservation drive's cost reads as "four
30
+ * node-to-relay round trips" in one telling and "two link round trips" in another: same
31
+ * quantity, different unit. This module uses the LINK round trip throughout, so there is one
32
+ * unit to reason in.
33
+ *
34
+ * ── The counts, and what they cost ──
35
+ *
36
+ * | operation | link round trips | at the declared default |
37
+ * | ---------------------------------------------------- | ---------------- | ----------------------- |
38
+ * | open a relayed connection to another machine | 4 | 8 000 ms |
39
+ * | dial the relay itself | 1 | 2 000 ms |
40
+ * | request a reservation on an open relay connection | 1 | 2 000 ms |
41
+ * | negotiate a protocol over an established circuit | 1 | 2 000 ms |
42
+ *
43
+ * **Measured** 2026-09-26, one Windows machine, loopback dedicated relay, re-run for this
44
+ * change: a relayed dial took 20-25 ms at no delay, **7 255-7 279 ms at 900 ms one-way**, and
45
+ * **12 066-12 094 ms at 1 500 ms one-way**. Four link round trips of pure delay would be 7 200
46
+ * and 12 000, so the real cost sits about 60-95 ms ABOVE the arithmetic — the handshakes and a
47
+ * phone's pure-JS crypto, which no delay figure contains. That residue is small but it is why
48
+ * {@link DECLARED_LINK_ROUND_TRIP_MS} carries headroom over the band sereus supports rather
49
+ * than matching it exactly: a declaration equal to the measured round trip would derive a
50
+ * budget marginally BELOW the dial it has to contain.
51
+ *
52
+ * ── The ceiling this does NOT lift ──
53
+ *
54
+ * Above roughly 1 250 ms one-way — a 2.5-second link round trip — no relayed connection can be
55
+ * established by this stack whatever cadre declares here. Two budgets of 10 000 ms each inside
56
+ * libp2p bound the same dial and cannot be reached from sereus at all:
57
+ * `connectionManager.dialTimeout` (libp2p's own default, which `@optimystic/db-p2p`'s
58
+ * `libp2p-node-base.ts` neither sets nor exposes) and `connectionManager.inboundUpgradeTimeout`
59
+ * (which it sets to 10 000). Both appear in the same re-run: at 1 500 ms one-way the dial under
60
+ * libp2p's default failed with `The operation was aborted due to timeout`, and with the shipped
61
+ * inbound-upgrade budget the DIALER's own 12 094 ms dial resolved while the LISTENER had
62
+ * already thrown the half-built connection away — so the first stream over it died with
63
+ * `Unexpected EOF - stream closed while reading 0/1 bytes` and the listener reported no peer at
64
+ * all. That is why a too-slow link produces silence rather than an error.
65
+ *
66
+ * Two consequences for a reader choosing a declaration. Below that ceiling, the DEFAULT
67
+ * declaration of 2 000 ms does not reach it: four round trips of 2 000 ms is 8 000 ms, which
68
+ * covers a one-way delay up to 1 000 ms, so a deployment somewhere between 1 000 and 1 250 ms
69
+ * one-way has to declare its own (about 2 200-2 500) to use the band libp2p still allows. And
70
+ * declaring much ABOVE 2 500 buys nothing: the budgets here grow, and
71
+ * the connection still fails inside libp2p. Lifting that needs an upstream change and a
72
+ * decision about how slow a link sereus intends to carry —
73
+ * `tickets/blocked/how-slow-a-relayed-link-does-sereus-carry`. A reader who raised these
74
+ * numbers and still cannot connect at 1 500 ms one-way has met that ceiling, not this module.
75
+ */
76
+ /**
77
+ * The link round trip cadre assumes when a host declares none, in milliseconds. A deployment
78
+ * that knows its own link states it with `NetworkConfig.linkRoundTripMs` instead.
79
+ *
80
+ * **Why 2 000.** It states the slowest link sereus already claims to support, and two other
81
+ * budgets fix that claim independently: `COHORT_READ_DEADLINE_MS`
82
+ * (`quereus-plugin-sereus/src/cluster-size.ts`, 5 000 ms) and
83
+ * `DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS` (`strand-first-sync-gate.ts`, 120 000 ms) were both
84
+ * sized against two parties reaching each other only through a public relay at a round trip
85
+ * near 1.8 s. Declaring 2 000 rounds that band up and leaves about 720 ms over the worst
86
+ * relayed dial measured at it (7 279 ms against a derived 8 000 ms).
87
+ *
88
+ * Raising it is how a deployment that KNOWS its link is slower moves every derived budget at
89
+ * once. The cost is the ordinary cost of a longer deadline: a peer that is genuinely gone holds
90
+ * the operation for that much longer before it is abandoned and retried. Above about 2 500 it
91
+ * buys nothing — see the module doc's ceiling paragraph.
92
+ */
93
+ export declare const DECLARED_LINK_ROUND_TRIP_MS = 2000;
94
+ /**
95
+ * Link round trips one relayed connection setup costs: transport, encryption and multiplexer
96
+ * handshakes to the relay, the circuit open, then both handshakes again end to end through it.
97
+ * Measured at 8 one-way delays, which is 4 link round trips.
98
+ */
99
+ export declare const RELAYED_DIAL_ROUND_TRIPS = 4;
100
+ /**
101
+ * Link round trips one whole reservation drive is budgeted for
102
+ * (`relay-reservation.ts`'s `driveRelayReservation`, whose ONE deadline covers the dial, the
103
+ * reservation request and the wait together).
104
+ *
105
+ * The protocol work measured is 2 — dial the relay (1) plus request the reservation (1). It is
106
+ * budgeted at 4, the relayed-dial count, because the wait afterwards may instead be satisfied
107
+ * by libp2p's own relay discovery, which repeats both legs after identify has run; 4 is
108
+ * therefore the largest thing this one deadline can be asked to contain, and the poll interval
109
+ * (`DEFAULT_RELAY_RESERVE_POLL_MS`) rounds up on top of it.
110
+ */
111
+ export declare const RELAY_RESERVATION_ROUND_TRIPS = 4;
112
+ /**
113
+ * Link round trips one request-and-answer over an ALREADY-OPEN circuit costs: the protocol
114
+ * negotiation (1, measured at 2 one-way delays) plus the request and its response (1). It does
115
+ * not include a dial — a caller that may have to open the connection budgets
116
+ * {@link RELAYED_DIAL_ROUND_TRIPS} separately, which is exactly what the two-field shape of
117
+ * Optimystic's `dialTimeoutMs` / `responseTimeoutMs` pair is for.
118
+ */
119
+ export declare const CIRCUIT_REQUEST_ROUND_TRIPS = 2;
120
+ /**
121
+ * What a block-transfer push message's BYTES get to cross, on top of the latency
122
+ * {@link CIRCUIT_REQUEST_ROUND_TRIPS} covers, in milliseconds.
123
+ *
124
+ * This part is bandwidth, not latency, so it does not scale with the declared round trip: a
125
+ * chunk is capped at `PeerJoinBackfillConfig.maxChunkBytes` (1 MiB by default) and crossing it
126
+ * costs whatever the link's throughput costs.
127
+ *
128
+ * NOTE: 6 000 ms is not a measurement of any throughput — it is the residue of the 10 000 ms
129
+ * this deadline shipped as before it was split into a latency part and a transfer part, chosen
130
+ * so that at the default declaration the derived value is the same 10 000 and no fast link got
131
+ * slower. Nobody has measured how long 1 MiB takes to cross a relayed mobile link. If pushes
132
+ * ever time out with the transfer only part done, measure that and raise THIS, not the declared
133
+ * round trip.
134
+ */
135
+ export declare const PUSH_TRANSFER_ALLOWANCE_MS = 6000;
136
+ /**
137
+ * Resolve a host's declared link round trip, falling back to
138
+ * {@link DECLARED_LINK_ROUND_TRIP_MS}.
139
+ *
140
+ * Validated here rather than downstream because nothing downstream would: every consumer
141
+ * multiplies this value into a `setTimeout` deadline, where a zero silently turns a budget into
142
+ * "give up immediately" and a `NaN` turns it into "never time out".
143
+ *
144
+ * Both node bring-up paths call this EAGERLY — `CadreNode.start()`, and
145
+ * `StrandInstanceManager.buildStrandRuntime` behind `addStrand`/`resumeStrand` — so a bad
146
+ * declaration fails the same start that Optimystic's own check on `cohortQueryTimeoutMs` fails.
147
+ * The eager call is what makes that true: every budget derived here is behind a condition (a
148
+ * node with no control storage builds no catch-up, a node with no relay addrs drives no
149
+ * reservation), so waiting for a first consumer would let a broken declaration boot and then
150
+ * throw inside a best-effort path that logs and carries on.
151
+ */
152
+ export declare function resolveLinkRoundTripMs(linkRoundTripMs?: number): number;
153
+ /**
154
+ * Deadline for OPENING a connection to another machine that may only be reachable through a
155
+ * relay: {@link RELAYED_DIAL_ROUND_TRIPS} at the declared link round trip.
156
+ */
157
+ export declare function relayedDialBudgetMs(linkRoundTripMs?: number): number;
158
+ /**
159
+ * Deadline for one whole relay reservation drive: {@link RELAY_RESERVATION_ROUND_TRIPS} at the
160
+ * declared link round trip.
161
+ */
162
+ export declare function relayReservationBudgetMs(linkRoundTripMs?: number): number;
163
+ /**
164
+ * Deadline for one request and its answer over a circuit that is already open —
165
+ * {@link CIRCUIT_REQUEST_ROUND_TRIPS} at the declared link round trip, plus `transferAllowanceMs`
166
+ * for the payload's own bytes.
167
+ */
168
+ export declare function circuitRequestBudgetMs(linkRoundTripMs?: number, transferAllowanceMs?: number): number;
169
+ /** The two deadlines one peer-join catch-up push needs, both derived from the declared link. */
170
+ export interface PeerJoinPushBudget {
171
+ /** Opening the connection to the peer being caught up — a relayed dial, in the worst case. */
172
+ dialTimeoutMs: number;
173
+ /** The push request and its answer over that connection, including the chunk's own bytes. */
174
+ responseTimeoutMs: number;
175
+ }
176
+ /**
177
+ * The dial and response deadlines a peer-join catch-up push needs at a declared link. One
178
+ * helper rather than two call-site expressions, because the two construction sites
179
+ * (`CadreNode`'s control catch-up and `StrandInstanceManager`'s per-strand one) and
180
+ * `DEFAULT_PEER_JOIN_BACKFILL` must not drift apart.
181
+ */
182
+ export declare function peerJoinPushBudget(linkRoundTripMs?: number): PeerJoinPushBudget;
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Dial and reservation budgets derived from ONE declared link round trip, instead of fixed
3
+ * millisecond numbers chosen on a fast local network.
4
+ *
5
+ * Reaching another machine through a relay costs a FIXED NUMBER OF EXCHANGES, not a fixed
6
+ * number of milliseconds. So every budget written as milliseconds has a link speed above which
7
+ * it can never open a connection at all — and the slower the link, the more of them fall over,
8
+ * silently, because an abandoned dial looks the same as a peer that is not there. Counting round
9
+ * trips instead is what stops that class of defect coming back: the next person to widen a
10
+ * budget changes ONE declaration, and the next person to add a dial writes down its round-trip
11
+ * count beside the arithmetic.
12
+ *
13
+ * ── The instrument ──
14
+ *
15
+ * Every count and every number below comes from
16
+ * `packages/integration-tests/src/scenarios/relayed-dial-cost-by-latency.integration.ts`, which
17
+ * is committed and opt-in (`RELAY_DIAL_COST=1`). **Its doc comment is the single home of the
18
+ * measurement**; this module holds only what the derivation needs. Re-run it before changing
19
+ * anything here.
20
+ *
21
+ * ── The unit ──
22
+ *
23
+ * ONE LINK ROUND TRIP is a message from one machine reaching the other and a reply coming
24
+ * back, over the path those two machines actually use — through the relay, when that is the
25
+ * only way they can reach each other. In the instrument, which injects a constant ONE-WAY
26
+ * frame delay `d`, one link round trip is `2d`.
27
+ *
28
+ * A leg between a node and the RELAY is half a link round trip, because it crosses one of the
29
+ * two hops rather than both. That is why the reservation drive's cost reads as "four
30
+ * node-to-relay round trips" in one telling and "two link round trips" in another: same
31
+ * quantity, different unit. This module uses the LINK round trip throughout, so there is one
32
+ * unit to reason in.
33
+ *
34
+ * ── The counts, and what they cost ──
35
+ *
36
+ * | operation | link round trips | at the declared default |
37
+ * | ---------------------------------------------------- | ---------------- | ----------------------- |
38
+ * | open a relayed connection to another machine | 4 | 8 000 ms |
39
+ * | dial the relay itself | 1 | 2 000 ms |
40
+ * | request a reservation on an open relay connection | 1 | 2 000 ms |
41
+ * | negotiate a protocol over an established circuit | 1 | 2 000 ms |
42
+ *
43
+ * **Measured** 2026-09-26, one Windows machine, loopback dedicated relay, re-run for this
44
+ * change: a relayed dial took 20-25 ms at no delay, **7 255-7 279 ms at 900 ms one-way**, and
45
+ * **12 066-12 094 ms at 1 500 ms one-way**. Four link round trips of pure delay would be 7 200
46
+ * and 12 000, so the real cost sits about 60-95 ms ABOVE the arithmetic — the handshakes and a
47
+ * phone's pure-JS crypto, which no delay figure contains. That residue is small but it is why
48
+ * {@link DECLARED_LINK_ROUND_TRIP_MS} carries headroom over the band sereus supports rather
49
+ * than matching it exactly: a declaration equal to the measured round trip would derive a
50
+ * budget marginally BELOW the dial it has to contain.
51
+ *
52
+ * ── The ceiling this does NOT lift ──
53
+ *
54
+ * Above roughly 1 250 ms one-way — a 2.5-second link round trip — no relayed connection can be
55
+ * established by this stack whatever cadre declares here. Two budgets of 10 000 ms each inside
56
+ * libp2p bound the same dial and cannot be reached from sereus at all:
57
+ * `connectionManager.dialTimeout` (libp2p's own default, which `@optimystic/db-p2p`'s
58
+ * `libp2p-node-base.ts` neither sets nor exposes) and `connectionManager.inboundUpgradeTimeout`
59
+ * (which it sets to 10 000). Both appear in the same re-run: at 1 500 ms one-way the dial under
60
+ * libp2p's default failed with `The operation was aborted due to timeout`, and with the shipped
61
+ * inbound-upgrade budget the DIALER's own 12 094 ms dial resolved while the LISTENER had
62
+ * already thrown the half-built connection away — so the first stream over it died with
63
+ * `Unexpected EOF - stream closed while reading 0/1 bytes` and the listener reported no peer at
64
+ * all. That is why a too-slow link produces silence rather than an error.
65
+ *
66
+ * Two consequences for a reader choosing a declaration. Below that ceiling, the DEFAULT
67
+ * declaration of 2 000 ms does not reach it: four round trips of 2 000 ms is 8 000 ms, which
68
+ * covers a one-way delay up to 1 000 ms, so a deployment somewhere between 1 000 and 1 250 ms
69
+ * one-way has to declare its own (about 2 200-2 500) to use the band libp2p still allows. And
70
+ * declaring much ABOVE 2 500 buys nothing: the budgets here grow, and
71
+ * the connection still fails inside libp2p. Lifting that needs an upstream change and a
72
+ * decision about how slow a link sereus intends to carry —
73
+ * `tickets/blocked/how-slow-a-relayed-link-does-sereus-carry`. A reader who raised these
74
+ * numbers and still cannot connect at 1 500 ms one-way has met that ceiling, not this module.
75
+ */
76
+ /**
77
+ * The link round trip cadre assumes when a host declares none, in milliseconds. A deployment
78
+ * that knows its own link states it with `NetworkConfig.linkRoundTripMs` instead.
79
+ *
80
+ * **Why 2 000.** It states the slowest link sereus already claims to support, and two other
81
+ * budgets fix that claim independently: `COHORT_READ_DEADLINE_MS`
82
+ * (`quereus-plugin-sereus/src/cluster-size.ts`, 5 000 ms) and
83
+ * `DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS` (`strand-first-sync-gate.ts`, 120 000 ms) were both
84
+ * sized against two parties reaching each other only through a public relay at a round trip
85
+ * near 1.8 s. Declaring 2 000 rounds that band up and leaves about 720 ms over the worst
86
+ * relayed dial measured at it (7 279 ms against a derived 8 000 ms).
87
+ *
88
+ * Raising it is how a deployment that KNOWS its link is slower moves every derived budget at
89
+ * once. The cost is the ordinary cost of a longer deadline: a peer that is genuinely gone holds
90
+ * the operation for that much longer before it is abandoned and retried. Above about 2 500 it
91
+ * buys nothing — see the module doc's ceiling paragraph.
92
+ */
93
+ export const DECLARED_LINK_ROUND_TRIP_MS = 2000;
94
+ /**
95
+ * Link round trips one relayed connection setup costs: transport, encryption and multiplexer
96
+ * handshakes to the relay, the circuit open, then both handshakes again end to end through it.
97
+ * Measured at 8 one-way delays, which is 4 link round trips.
98
+ */
99
+ export const RELAYED_DIAL_ROUND_TRIPS = 4;
100
+ /**
101
+ * Link round trips one whole reservation drive is budgeted for
102
+ * (`relay-reservation.ts`'s `driveRelayReservation`, whose ONE deadline covers the dial, the
103
+ * reservation request and the wait together).
104
+ *
105
+ * The protocol work measured is 2 — dial the relay (1) plus request the reservation (1). It is
106
+ * budgeted at 4, the relayed-dial count, because the wait afterwards may instead be satisfied
107
+ * by libp2p's own relay discovery, which repeats both legs after identify has run; 4 is
108
+ * therefore the largest thing this one deadline can be asked to contain, and the poll interval
109
+ * (`DEFAULT_RELAY_RESERVE_POLL_MS`) rounds up on top of it.
110
+ */
111
+ export const RELAY_RESERVATION_ROUND_TRIPS = 4;
112
+ /**
113
+ * Link round trips one request-and-answer over an ALREADY-OPEN circuit costs: the protocol
114
+ * negotiation (1, measured at 2 one-way delays) plus the request and its response (1). It does
115
+ * not include a dial — a caller that may have to open the connection budgets
116
+ * {@link RELAYED_DIAL_ROUND_TRIPS} separately, which is exactly what the two-field shape of
117
+ * Optimystic's `dialTimeoutMs` / `responseTimeoutMs` pair is for.
118
+ */
119
+ export const CIRCUIT_REQUEST_ROUND_TRIPS = 2;
120
+ /**
121
+ * What a block-transfer push message's BYTES get to cross, on top of the latency
122
+ * {@link CIRCUIT_REQUEST_ROUND_TRIPS} covers, in milliseconds.
123
+ *
124
+ * This part is bandwidth, not latency, so it does not scale with the declared round trip: a
125
+ * chunk is capped at `PeerJoinBackfillConfig.maxChunkBytes` (1 MiB by default) and crossing it
126
+ * costs whatever the link's throughput costs.
127
+ *
128
+ * NOTE: 6 000 ms is not a measurement of any throughput — it is the residue of the 10 000 ms
129
+ * this deadline shipped as before it was split into a latency part and a transfer part, chosen
130
+ * so that at the default declaration the derived value is the same 10 000 and no fast link got
131
+ * slower. Nobody has measured how long 1 MiB takes to cross a relayed mobile link. If pushes
132
+ * ever time out with the transfer only part done, measure that and raise THIS, not the declared
133
+ * round trip.
134
+ */
135
+ export const PUSH_TRANSFER_ALLOWANCE_MS = 6000;
136
+ /**
137
+ * Resolve a host's declared link round trip, falling back to
138
+ * {@link DECLARED_LINK_ROUND_TRIP_MS}.
139
+ *
140
+ * Validated here rather than downstream because nothing downstream would: every consumer
141
+ * multiplies this value into a `setTimeout` deadline, where a zero silently turns a budget into
142
+ * "give up immediately" and a `NaN` turns it into "never time out".
143
+ *
144
+ * Both node bring-up paths call this EAGERLY — `CadreNode.start()`, and
145
+ * `StrandInstanceManager.buildStrandRuntime` behind `addStrand`/`resumeStrand` — so a bad
146
+ * declaration fails the same start that Optimystic's own check on `cohortQueryTimeoutMs` fails.
147
+ * The eager call is what makes that true: every budget derived here is behind a condition (a
148
+ * node with no control storage builds no catch-up, a node with no relay addrs drives no
149
+ * reservation), so waiting for a first consumer would let a broken declaration boot and then
150
+ * throw inside a best-effort path that logs and carries on.
151
+ */
152
+ export function resolveLinkRoundTripMs(linkRoundTripMs) {
153
+ if (linkRoundTripMs === undefined) {
154
+ return DECLARED_LINK_ROUND_TRIP_MS;
155
+ }
156
+ if (!Number.isFinite(linkRoundTripMs) || linkRoundTripMs <= 0) {
157
+ throw new Error(`network.linkRoundTripMs must be a finite number of milliseconds above zero, not ${String(linkRoundTripMs)}`);
158
+ }
159
+ return linkRoundTripMs;
160
+ }
161
+ /**
162
+ * Deadline for OPENING a connection to another machine that may only be reachable through a
163
+ * relay: {@link RELAYED_DIAL_ROUND_TRIPS} at the declared link round trip.
164
+ */
165
+ export function relayedDialBudgetMs(linkRoundTripMs) {
166
+ return RELAYED_DIAL_ROUND_TRIPS * resolveLinkRoundTripMs(linkRoundTripMs);
167
+ }
168
+ /**
169
+ * Deadline for one whole relay reservation drive: {@link RELAY_RESERVATION_ROUND_TRIPS} at the
170
+ * declared link round trip.
171
+ */
172
+ export function relayReservationBudgetMs(linkRoundTripMs) {
173
+ return RELAY_RESERVATION_ROUND_TRIPS * resolveLinkRoundTripMs(linkRoundTripMs);
174
+ }
175
+ /**
176
+ * Deadline for one request and its answer over a circuit that is already open —
177
+ * {@link CIRCUIT_REQUEST_ROUND_TRIPS} at the declared link round trip, plus `transferAllowanceMs`
178
+ * for the payload's own bytes.
179
+ */
180
+ export function circuitRequestBudgetMs(linkRoundTripMs, transferAllowanceMs = PUSH_TRANSFER_ALLOWANCE_MS) {
181
+ return CIRCUIT_REQUEST_ROUND_TRIPS * resolveLinkRoundTripMs(linkRoundTripMs) + transferAllowanceMs;
182
+ }
183
+ /**
184
+ * The dial and response deadlines a peer-join catch-up push needs at a declared link. One
185
+ * helper rather than two call-site expressions, because the two construction sites
186
+ * (`CadreNode`'s control catch-up and `StrandInstanceManager`'s per-strand one) and
187
+ * `DEFAULT_PEER_JOIN_BACKFILL` must not drift apart.
188
+ */
189
+ export function peerJoinPushBudget(linkRoundTripMs) {
190
+ return {
191
+ dialTimeoutMs: relayedDialBudgetMs(linkRoundTripMs),
192
+ responseTimeoutMs: circuitRequestBudgetMs(linkRoundTripMs)
193
+ };
194
+ }
195
+ //# sourceMappingURL=link-budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-budget.js","sourceRoot":"","sources":["../src/link-budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,IAAI,CAAC;AAEhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC;AAE1C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC;AAE/C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC;AAE7C;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;AAE/C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,sBAAsB,CAAC,eAAwB;IAC9D,IAAI,eAAe,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,2BAA2B,CAAC;IACpC,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,eAAe,IAAI,CAAC,EAAE,CAAC;QAC/D,MAAM,IAAI,KAAK,CACd,mFAAmF,MAAM,CAAC,eAAe,CAAC,EAAE,CAC5G,CAAC;IACH,CAAC;IACD,OAAO,eAAe,CAAC;AACxB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,eAAwB;IAC3D,OAAO,wBAAwB,GAAG,sBAAsB,CAAC,eAAe,CAAC,CAAC;AAC3E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,eAAwB;IAChE,OAAO,6BAA6B,GAAG,sBAAsB,CAAC,eAAe,CAAC,CAAC;AAChF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CACrC,eAAwB,EACxB,mBAAmB,GAAG,0BAA0B;IAEhD,OAAO,2BAA2B,GAAG,sBAAsB,CAAC,eAAe,CAAC,GAAG,mBAAmB,CAAC;AACpG,CAAC;AAUD;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,eAAwB;IAC1D,OAAO;QACN,aAAa,EAAE,mBAAmB,CAAC,eAAe,CAAC;QACnD,iBAAiB,EAAE,sBAAsB,CAAC,eAAe,CAAC;KAC1D,CAAC;AACH,CAAC"}
@@ -88,9 +88,10 @@
88
88
  * annex the party's relay capacity.
89
89
  * - An `'admit-for-relay'` connection that is NOT reserving is dropped: it has
90
90
  * {@link RELAY_ADMISSION_RESERVE_DEADLINE_MS} to get a reservation ADMITTED
91
- * at that hook, after which the underlying connection is aborted. The
92
- * guarantee above thus weakens on relay-enabled nodes from "never in the
93
- * conversation" to "in it briefly, and can speak nothing".
91
+ * at that hook, after which the gate CLOSES the underlying connection so
92
+ * both ends let go of the socket. The guarantee above thus weakens on
93
+ * relay-enabled nodes from "never in the conversation" to "in it briefly,
94
+ * and can speak nothing".
94
95
  *
95
96
  * The deadline clears on reservation ADMISSION, not on the reservation's own
96
97
  * success: this gate cannot observe the server-side `reserve()` outcome, so a
@@ -187,13 +188,25 @@ export declare const ADMISSION_DECISION_TIMEOUT_MS = 2000;
187
188
  /**
188
189
  * How long an `'admit-for-relay'` connection may exist without a relay
189
190
  * reservation being ADMITTED at the `denyInboundRelayReservation` hook, after
190
- * which the gate aborts the underlying connection. A reserving client asks for
191
+ * which the gate closes the underlying connection. A reserving client asks for
191
192
  * its slot immediately after the connection upgrades (`relay-reservation.ts`
192
193
  * dials and requests in one drive; a strand node's configured circuit listener
193
194
  * does the same from inside its own `listen()`), so a connection idle past this
194
195
  * deadline is not reserving.
195
196
  */
196
197
  export declare const RELAY_ADMISSION_RESERVE_DEADLINE_MS = 5000;
198
+ /**
199
+ * Bound on the graceful close of an expired relay-only connection. It exists
200
+ * because `AbstractMultiaddrConnection.close()` awaits an `idle`/`drain` event
201
+ * when the connection still has unsent bytes, and that wait has no timeout of
202
+ * its own — an unsignalled one never ends. This gate writes nothing to a
203
+ * stranger, so the wait is not reachable today; the bound is here so a timer
204
+ * callback can never hold an unending await.
205
+ *
206
+ * Applied through {@link withDeadline}, not `AbortSignal.timeout`: the latter is
207
+ * not reliably present on React Native/Hermes, which loads this same module.
208
+ */
209
+ export declare const RELAY_ADMISSION_CLOSE_TIMEOUT_MS = 2000;
197
210
  /**
198
211
  * Default cap on concurrent relay reservations held by peers the membership
199
212
  * check could not place (see {@link UnauthorizedReservationBudget}). Small on
@@ -311,7 +324,7 @@ export declare class UnauthorizedReservationBudget {
311
324
  *
312
325
  * An `'admit-for-relay'` verdict admits the connection and arms a
313
326
  * `reserveDeadlineMs` timer against it; the timer is disarmed when a
314
- * reservation for that peer is ADMITTED at the reservation hook, and aborts the
327
+ * reservation for that peer is ADMITTED at the reservation hook, and closes the
315
328
  * underlying `MultiaddrConnection` when it fires first.
316
329
  *
317
330
  * NOTE: `base` is spread, so a gater passed as a CLASS INSTANCE would lose its
@@ -88,9 +88,10 @@
88
88
  * annex the party's relay capacity.
89
89
  * - An `'admit-for-relay'` connection that is NOT reserving is dropped: it has
90
90
  * {@link RELAY_ADMISSION_RESERVE_DEADLINE_MS} to get a reservation ADMITTED
91
- * at that hook, after which the underlying connection is aborted. The
92
- * guarantee above thus weakens on relay-enabled nodes from "never in the
93
- * conversation" to "in it briefly, and can speak nothing".
91
+ * at that hook, after which the gate CLOSES the underlying connection so
92
+ * both ends let go of the socket. The guarantee above thus weakens on
93
+ * relay-enabled nodes from "never in the conversation" to "in it briefly,
94
+ * and can speak nothing".
94
95
  *
95
96
  * The deadline clears on reservation ADMISSION, not on the reservation's own
96
97
  * success: this gate cannot observe the server-side `reserve()` outcome, so a
@@ -156,6 +157,7 @@
156
157
  import debug from 'debug';
157
158
  import { SEED_PROTOCOL } from './seed-bootstrap.js';
158
159
  import { FORMATION_PROTOCOL } from './strand-formation-protocol.js';
160
+ import { withDeadline } from './control-stream.js';
159
161
  const log = debug('sereus:cadre:connection-gater');
160
162
  /**
161
163
  * The protocols a not-yet-authorized peer may legitimately speak on a control
@@ -190,13 +192,25 @@ export const ADMISSION_DECISION_TIMEOUT_MS = 2000;
190
192
  /**
191
193
  * How long an `'admit-for-relay'` connection may exist without a relay
192
194
  * reservation being ADMITTED at the `denyInboundRelayReservation` hook, after
193
- * which the gate aborts the underlying connection. A reserving client asks for
195
+ * which the gate closes the underlying connection. A reserving client asks for
194
196
  * its slot immediately after the connection upgrades (`relay-reservation.ts`
195
197
  * dials and requests in one drive; a strand node's configured circuit listener
196
198
  * does the same from inside its own `listen()`), so a connection idle past this
197
199
  * deadline is not reserving.
198
200
  */
199
201
  export const RELAY_ADMISSION_RESERVE_DEADLINE_MS = 5000;
202
+ /**
203
+ * Bound on the graceful close of an expired relay-only connection. It exists
204
+ * because `AbstractMultiaddrConnection.close()` awaits an `idle`/`drain` event
205
+ * when the connection still has unsent bytes, and that wait has no timeout of
206
+ * its own — an unsignalled one never ends. This gate writes nothing to a
207
+ * stranger, so the wait is not reachable today; the bound is here so a timer
208
+ * callback can never hold an unending await.
209
+ *
210
+ * Applied through {@link withDeadline}, not `AbortSignal.timeout`: the latter is
211
+ * not reliably present on React Native/Hermes, which loads this same module.
212
+ */
213
+ export const RELAY_ADMISSION_CLOSE_TIMEOUT_MS = 2000;
200
214
  /**
201
215
  * Default cap on concurrent relay reservations held by peers the membership
202
216
  * check could not place (see {@link UnauthorizedReservationBudget}). Small on
@@ -297,7 +311,7 @@ export class UnauthorizedReservationBudget {
297
311
  *
298
312
  * An `'admit-for-relay'` verdict admits the connection and arms a
299
313
  * `reserveDeadlineMs` timer against it; the timer is disarmed when a
300
- * reservation for that peer is ADMITTED at the reservation hook, and aborts the
314
+ * reservation for that peer is ADMITTED at the reservation hook, and closes the
301
315
  * underlying `MultiaddrConnection` when it fires first.
302
316
  *
303
317
  * NOTE: `base` is spread, so a gater passed as a CLASS INSTANCE would lose its
@@ -406,11 +420,11 @@ export async function decideWithinDeadline(decide, fallback, timeoutMs, what) {
406
420
  /**
407
421
  * The not-reserving deadlines for `'admit-for-relay'` connections, keyed by
408
422
  * remote peerId (a Set per peer — one peer can hold several in-flight
409
- * connections). `arm` starts a timer that aborts the connection; `disarm`
423
+ * connections). `arm` starts a timer that closes the connection; `disarm`
410
424
  * (called when a reservation for that peer is admitted) cancels every pending
411
425
  * timer for the peer. Timers are unref'd so an armed deadline never holds a
412
426
  * process open, and a timer that fires against an already-closed connection is
413
- * a swallowed no-op.
427
+ * a no-op (`close()` returns at once on any status but `open`).
414
428
  *
415
429
  * Disarming is final for the connections it cancelled: a peer that reserved
416
430
  * once and then lets its reservation lapse keeps a mute connection (the
@@ -453,12 +467,38 @@ class PendingReserveDeadlines {
453
467
  if (entries?.size === 0) {
454
468
  this.byPeer.delete(remotePeerId);
455
469
  }
456
- log('Relay-only admission expired for %s — no reservation admitted within %dms, aborting the connection', remotePeerId, this.deadlineMs);
470
+ log('Relay-only admission expired for %s — no reservation admitted within %dms, closing the connection', remotePeerId, this.deadlineMs);
471
+ void this.drop(remotePeerId, entry.maConn);
472
+ }
473
+ /**
474
+ * End an expired relay-only connection: a bounded graceful close, with
475
+ * `abort()` only as the escalation. The order cannot be reversed — `abort()`
476
+ * marks the connection `aborted`, and `close()` returns immediately on any
477
+ * status that is not `open`, so aborting first would make the close a silent
478
+ * no-op. Closing a connection the peer already dropped returns at once
479
+ * without throwing, so the fallback stays unreached in the ordinary case.
480
+ */
481
+ async drop(remotePeerId, maConn) {
482
+ try {
483
+ await withDeadline(RELAY_ADMISSION_CLOSE_TIMEOUT_MS, `relay-only close for ${remotePeerId}`, (signal) => maConn.close({ signal }));
484
+ return;
485
+ }
486
+ catch (error) {
487
+ log('Closing the expired relay-only connection from %s failed — aborting: %o', remotePeerId, error);
488
+ }
489
+ // NOTE: this fallback cannot actually free a WebSocket. `@libp2p/websockets`'
490
+ // `sendReset()` calls `websocket.close(1006)`, and 1006 is a reserved code
491
+ // RFC 6455 forbids an endpoint from sending, so `ws` throws and
492
+ // `AbstractMessageStream.abort()` swallows it — the local end flips to
493
+ // `aborted` while the socket stays up. Measured against
494
+ // @libp2p/websockets@10.1.3 (see tickets/blocked/report-libp2p-websockets-abort-close-code.md).
495
+ // `MultiaddrConnection` exposes no way down to the raw socket, so this is as
496
+ // far as this layer can go. Revisit — drop the fallback, or go back to a
497
+ // plain `abort()` — once that transport sends a legal reset code.
457
498
  try {
458
- entry.maConn.abort(new Error(`relay-only admission expired: no relay reservation admitted within ${this.deadlineMs}ms`));
499
+ maConn.abort(new Error(`relay-only admission expired: no relay reservation admitted within ${this.deadlineMs}ms`));
459
500
  }
460
501
  catch (error) {
461
- // Aborting a connection that already closed on its own is not a failure.
462
502
  log('Aborting the expired relay-only connection from %s threw: %o', remotePeerId, error);
463
503
  }
464
504
  }