@serfab/cadre-core 1.14.0 → 1.15.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 +293 -274
- package/dist/cadre-invite-protocol.d.ts +64 -29
- package/dist/cadre-invite-protocol.js +207 -38
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +42 -0
- package/dist/cadre-join-request.js +144 -0
- package/dist/cadre-join-request.js.map +1 -0
- package/dist/cadre-node.d.ts +375 -79
- package/dist/cadre-node.js +758 -186
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-database.d.ts +92 -12
- package/dist/control-database.js +323 -142
- package/dist/control-database.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +19 -3
- package/dist/control-schema.js.map +1 -1
- package/dist/ed25519-key.d.ts +7 -9
- package/dist/ed25519-key.js +20 -10
- package/dist/ed25519-key.js.map +1 -1
- package/dist/fetch-impl.d.ts +12 -0
- package/dist/fetch-impl.js +23 -0
- package/dist/fetch-impl.js.map +1 -0
- package/dist/file-durable-slot.js +2 -1
- package/dist/file-durable-slot.js.map +1 -1
- package/dist/file-safe-name.d.ts +25 -0
- package/dist/file-safe-name.js +29 -0
- package/dist/file-safe-name.js.map +1 -0
- package/dist/formation-approval.js +5 -11
- package/dist/formation-approval.js.map +1 -1
- package/dist/fs-atomic.d.ts +0 -19
- package/dist/fs-atomic.js +3 -24
- package/dist/fs-atomic.js.map +1 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.js +31 -6
- package/dist/index.js.map +1 -1
- package/dist/invitation-bootstrap.d.ts +50 -12
- package/dist/invitation-bootstrap.js +76 -13
- package/dist/invitation-bootstrap.js.map +1 -1
- package/dist/joined-strand-store.d.ts +7 -0
- package/dist/joined-strand-store.js +9 -0
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store-file.js +2 -1
- package/dist/key-store-file.js.map +1 -1
- package/dist/link-budget.d.ts +9 -4
- package/dist/link-budget.js +9 -4
- package/dist/link-budget.js.map +1 -1
- package/dist/machine-removal.d.ts +78 -0
- package/dist/machine-removal.js +166 -0
- package/dist/machine-removal.js.map +1 -0
- package/dist/membership-connection-gater.d.ts +6 -5
- package/dist/membership-connection-gater.js +6 -5
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-claim-payload.d.ts +55 -0
- package/dist/node-claim-payload.js +108 -4
- package/dist/node-claim-payload.js.map +1 -1
- package/dist/node-default-transports.d.ts +27 -0
- package/dist/node-default-transports.js +35 -0
- package/dist/node-default-transports.js.map +1 -0
- package/dist/node-transports.d.ts +30 -0
- package/dist/node-transports.js +40 -0
- package/dist/node-transports.js.map +1 -0
- package/dist/peer-dial.d.ts +124 -27
- package/dist/peer-dial.js +254 -30
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.js +6 -0
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/peer-record.d.ts +6 -45
- package/dist/peer-record.js +8 -50
- package/dist/peer-record.js.map +1 -1
- package/dist/primary-lan-address.d.ts +8 -0
- package/dist/primary-lan-address.js +46 -0
- package/dist/primary-lan-address.js.map +1 -0
- package/dist/provider/client.d.ts +131 -0
- package/dist/provider/client.js +226 -0
- package/dist/provider/client.js.map +1 -0
- package/dist/provider/descriptor.d.ts +41 -0
- package/dist/provider/descriptor.js +103 -0
- package/dist/provider/descriptor.js.map +1 -0
- package/dist/provider/index.d.ts +9 -0
- package/dist/provider/index.js +10 -0
- package/dist/provider/index.js.map +1 -0
- package/dist/provider/oauth.d.ts +125 -0
- package/dist/provider/oauth.js +199 -0
- package/dist/provider/oauth.js.map +1 -0
- package/dist/provider/rent-node.d.ts +101 -0
- package/dist/provider/rent-node.js +220 -0
- package/dist/provider/rent-node.js.map +1 -0
- package/dist/relay-addrs.d.ts +9 -4
- package/dist/relay-addrs.js +9 -4
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +51 -16
- package/dist/relay-reservation.js +107 -24
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +22 -5
- package/dist/seed-bootstrap.js +48 -25
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/strand-commit-durability.d.ts +54 -0
- package/dist/strand-commit-durability.js +74 -0
- package/dist/strand-commit-durability.js.map +1 -0
- package/dist/strand-database.d.ts +23 -8
- package/dist/strand-database.js +41 -13
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +7 -1
- package/dist/strand-first-sync-gate.js +7 -1
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-protocol.js +4 -1
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-fret-addrs.d.ts +19 -1
- package/dist/strand-fret-addrs.js +27 -9
- package/dist/strand-fret-addrs.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +81 -19
- package/dist/strand-instance-manager.js +216 -105
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-move.d.ts +124 -0
- package/dist/strand-move.js +438 -0
- package/dist/strand-move.js.map +1 -0
- package/dist/strand-network-config.js +7 -6
- package/dist/strand-network-config.js.map +1 -1
- package/dist/strand-peer-redial.d.ts +113 -0
- package/dist/strand-peer-redial.js +236 -0
- package/dist/strand-peer-redial.js.map +1 -0
- package/dist/strand-wake-protocol.js +10 -7
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/types.d.ts +212 -4
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/package.json +18 -10
- package/src/cadre-invite-protocol.ts +233 -45
- package/src/cadre-join-request.ts +167 -0
- package/src/cadre-node.ts +10003 -9381
- package/src/control-database.ts +4663 -4451
- package/src/control-schema.ts +19 -3
- package/src/ed25519-key.ts +20 -10
- package/src/fetch-impl.ts +23 -0
- package/src/file-durable-slot.ts +64 -63
- package/src/file-safe-name.ts +32 -0
- package/src/formation-approval.ts +604 -610
- package/src/fs-atomic.ts +92 -117
- package/src/index.ts +789 -722
- package/src/invitation-bootstrap.ts +91 -13
- package/src/joined-strand-store.ts +508 -498
- package/src/key-store-file.ts +142 -141
- package/src/link-budget.ts +9 -4
- package/src/machine-removal.ts +216 -0
- package/src/membership-connection-gater.ts +638 -637
- package/src/node-claim-payload.ts +138 -4
- package/src/node-default-transports.ts +38 -0
- package/src/node-transports.ts +49 -0
- package/src/peer-dial.ts +604 -336
- package/src/peer-join-backfill.ts +6 -0
- package/src/peer-record.ts +159 -202
- package/src/primary-lan-address.ts +47 -0
- package/src/provider/client.ts +326 -0
- package/src/provider/descriptor.ts +129 -0
- package/src/provider/index.ts +59 -0
- package/src/provider/oauth.ts +285 -0
- package/src/provider/rent-node.ts +275 -0
- package/src/relay-addrs.ts +9 -4
- package/src/relay-reservation.ts +1211 -1121
- package/src/seed-bootstrap.ts +1502 -1471
- package/src/strand-commit-durability.ts +116 -0
- package/src/strand-database.ts +46 -14
- package/src/strand-first-sync-gate.ts +7 -1
- package/src/strand-formation-protocol.ts +1039 -1036
- package/src/strand-fret-addrs.ts +179 -145
- package/src/strand-instance-manager.ts +2026 -1883
- package/src/strand-move.ts +585 -0
- package/src/strand-network-config.ts +7 -6
- package/src/strand-peer-redial.ts +307 -0
- package/src/strand-wake-protocol.ts +10 -7
- package/src/types.ts +220 -4
package/src/relay-reservation.ts
CHANGED
|
@@ -1,1121 +1,1211 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Relay reservation for nodes that reserve through the SEARCH listen addr — the
|
|
3
|
-
* dial, the explicit reservation request, the retry loop that keeps it, and the
|
|
4
|
-
* (live) status derivation.
|
|
5
|
-
*
|
|
6
|
-
* `@libp2p/circuit-relay-v2`'s listener branches on the SHAPE of the listen
|
|
7
|
-
* address:
|
|
8
|
-
*
|
|
9
|
-
* | listen addr | libp2p behaviour | unreachable relay |
|
|
10
|
-
* | ------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------- |
|
|
11
|
-
* | `<dial addr>/p2p/<relayPeerId>/p2p-circuit` | CONFIGURED reservation: dial that exact relay and reserve, or fail | `listen()` throws |
|
|
12
|
-
* | bare `/p2p-circuit` | SEARCH mode: register a pending reservation, to be filled by relay discovery | nothing throws, no reserve |
|
|
13
|
-
*
|
|
14
|
-
* A search listener needs someone to (a) dial the relay and (b) fill the pending
|
|
15
|
-
* reservation it created, then report whether one actually landed. That is this
|
|
16
|
-
* module. It is **fail-soft by construction**: nothing here throws.
|
|
17
|
-
*
|
|
18
|
-
* EVERY cadre CONTROL node now takes that route — `network.relayAddrs` resolves
|
|
19
|
-
* to the bare search entry (`relay-addrs.ts` → `resolveListenAddrs`) and
|
|
20
|
-
* `CadreNode.start()` drives this module once the control database is up. The
|
|
21
|
-
* configured shape would dial the relay from inside `libp2p.start()`, putting a
|
|
22
|
-
* sibling in this node's cohort before its own control database existed, and a
|
|
23
|
-
* sibling that has not yet replicated this node's `CadrePeer` row correctly
|
|
24
|
-
* refuses its control-DB streams — which killed `start()` outright.
|
|
25
|
-
*
|
|
26
|
-
* So the two shapes are no longer alternative ROUTES, only alternative failure
|
|
27
|
-
* postures on top of the same drive: a node that names `network.relayAddrs` gets
|
|
28
|
-
* a `RelayReservationFailedError` out of `start()` when the first attempt lands
|
|
29
|
-
* nothing (fail-fast, the operator asked for this relay), while a node that calls
|
|
30
|
-
* `CadreNode.reserveRelays()` itself gets a non-`reserved` status and stays up
|
|
31
|
-
* (fail-soft, the browser-tab posture). `network.requireRelay: false` gives a
|
|
32
|
-
* CONFIGURED relay the fail-soft posture too, for a node that must boot with no
|
|
33
|
-
* network at all — same drive, same supervisor, only the throw is dropped.
|
|
34
|
-
* Configuring both is now redundant, not fatal.
|
|
35
|
-
*
|
|
36
|
-
* STRAND nodes take the same route, ONE supervisor PER RELAY
|
|
37
|
-
* (`strand-instance-manager.ts` → `buildStrandRuntime`): each configured relay
|
|
38
|
-
* gets its own bare `/p2p-circuit` listener (`strand-network-config.ts`) and its
|
|
39
|
-
* own {@link superviseRelayReservation} over just that relay's addr, fail-soft
|
|
40
|
-
* like the browser tab. The configured shape they used to take
|
|
41
|
-
* (`<relay>/p2p-circuit`, reserved from inside `libp2p.start()`) is gone from
|
|
42
|
-
* cadre-built nodes entirely, because it loses its address and never recovers in
|
|
43
|
-
* three situations — the relay restarts, either side hangs up the relay
|
|
44
|
-
* connection, and libp2p's OWN reservation refresh (which removes the reservation
|
|
45
|
-
* before re-creating it, and a configured listener republishes only from inside
|
|
46
|
-
* its own `listen()`). The last one needs no network event at all: at the relay
|
|
47
|
-
* default TTL of two hours every configured-shape node went undialable ~1 h 55 min
|
|
48
|
-
* after reserving. A search listener republishes on `relay:created-reservation`
|
|
49
|
-
* for its own pending id, which the refresh re-queues, and the supervisor covers
|
|
50
|
-
* the other two.
|
|
51
|
-
*
|
|
52
|
-
* Because a strand node runs one supervisor per relay, "is the reservation held"
|
|
53
|
-
* is asked PER RELAY ({@link circuitMultiaddrsVia}): relay X's supervisor must not
|
|
54
|
-
* be satisfied by relay Y's circuit addr, or it would never re-drive X.
|
|
55
|
-
*
|
|
56
|
-
* ⚠️ WHY THE RESERVATION IS REQUESTED EXPLICITLY RATHER THAN LEFT TO DISCOVERY.
|
|
57
|
-
* libp2p fills a search listener's pending reservation from `RelayDiscovery`,
|
|
58
|
-
* which nominates a peer only once it can see the relay-hop protocol id in that
|
|
59
|
-
* peer's PEER-STORE protocol list — and that list is written exclusively by the
|
|
60
|
-
* IDENTIFY handshake. `@optimystic/db-p2p` gives every cadre node a
|
|
61
|
-
* network-namespaced identify protocol id (`/optimystic/control-<partyId>/id/1.0.0`)
|
|
62
|
-
* while the relay `ops/docker/libp2p-infra` deploys runs stock identify
|
|
63
|
-
* (`/ipfs/id/1.0.0`), so the two never identify each other, the peer store stays
|
|
64
|
-
* empty of protocols, and discovery never nominates the relay. The TCP/WebSocket
|
|
65
|
-
* connection itself is fine, which is why leaving it to discovery surfaced as a
|
|
66
|
-
* generic timeout.
|
|
67
|
-
*
|
|
68
|
-
* Identify is incidental to relaying, though: the reservation
|
|
69
|
-
* (`/libp2p/circuit/relay/0.2.0/hop`), a third peer's CONNECT through the relay,
|
|
70
|
-
* and the STOP handler on this node all use STOCK, un-namespaced protocol ids the
|
|
71
|
-
* deployed relay already serves. Discovery's only job is to GUESS which connected
|
|
72
|
-
* peer is a relay, and this module does not need to guess — it was handed the
|
|
73
|
-
* relay's address. So it asks the relay for a slot directly, via the
|
|
74
|
-
* circuit-relay transport's own reservation store.
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
*
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
return
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
readonly
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
private
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
private
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
private
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
this.
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Relay reservation for nodes that reserve through the SEARCH listen addr — the
|
|
3
|
+
* dial, the explicit reservation request, the retry loop that keeps it, and the
|
|
4
|
+
* (live) status derivation.
|
|
5
|
+
*
|
|
6
|
+
* `@libp2p/circuit-relay-v2`'s listener branches on the SHAPE of the listen
|
|
7
|
+
* address:
|
|
8
|
+
*
|
|
9
|
+
* | listen addr | libp2p behaviour | unreachable relay |
|
|
10
|
+
* | ------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------- |
|
|
11
|
+
* | `<dial addr>/p2p/<relayPeerId>/p2p-circuit` | CONFIGURED reservation: dial that exact relay and reserve, or fail | `listen()` throws |
|
|
12
|
+
* | bare `/p2p-circuit` | SEARCH mode: register a pending reservation, to be filled by relay discovery | nothing throws, no reserve |
|
|
13
|
+
*
|
|
14
|
+
* A search listener needs someone to (a) dial the relay and (b) fill the pending
|
|
15
|
+
* reservation it created, then report whether one actually landed. That is this
|
|
16
|
+
* module. It is **fail-soft by construction**: nothing here throws.
|
|
17
|
+
*
|
|
18
|
+
* EVERY cadre CONTROL node now takes that route — `network.relayAddrs` resolves
|
|
19
|
+
* to the bare search entry (`relay-addrs.ts` → `resolveListenAddrs`) and
|
|
20
|
+
* `CadreNode.start()` drives this module once the control database is up. The
|
|
21
|
+
* configured shape would dial the relay from inside `libp2p.start()`, putting a
|
|
22
|
+
* sibling in this node's cohort before its own control database existed, and a
|
|
23
|
+
* sibling that has not yet replicated this node's `CadrePeer` row correctly
|
|
24
|
+
* refuses its control-DB streams — which killed `start()` outright.
|
|
25
|
+
*
|
|
26
|
+
* So the two shapes are no longer alternative ROUTES, only alternative failure
|
|
27
|
+
* postures on top of the same drive: a node that names `network.relayAddrs` gets
|
|
28
|
+
* a `RelayReservationFailedError` out of `start()` when the first attempt lands
|
|
29
|
+
* nothing (fail-fast, the operator asked for this relay), while a node that calls
|
|
30
|
+
* `CadreNode.reserveRelays()` itself gets a non-`reserved` status and stays up
|
|
31
|
+
* (fail-soft, the browser-tab posture). `network.requireRelay: false` gives a
|
|
32
|
+
* CONFIGURED relay the fail-soft posture too, for a node that must boot with no
|
|
33
|
+
* network at all — same drive, same supervisor, only the throw is dropped.
|
|
34
|
+
* Configuring both is now redundant, not fatal.
|
|
35
|
+
*
|
|
36
|
+
* STRAND nodes take the same route, ONE supervisor PER RELAY
|
|
37
|
+
* (`strand-instance-manager.ts` → `buildStrandRuntime`): each configured relay
|
|
38
|
+
* gets its own bare `/p2p-circuit` listener (`strand-network-config.ts`) and its
|
|
39
|
+
* own {@link superviseRelayReservation} over just that relay's addr, fail-soft
|
|
40
|
+
* like the browser tab. The configured shape they used to take
|
|
41
|
+
* (`<relay>/p2p-circuit`, reserved from inside `libp2p.start()`) is gone from
|
|
42
|
+
* cadre-built nodes entirely, because it loses its address and never recovers in
|
|
43
|
+
* three situations — the relay restarts, either side hangs up the relay
|
|
44
|
+
* connection, and libp2p's OWN reservation refresh (which removes the reservation
|
|
45
|
+
* before re-creating it, and a configured listener republishes only from inside
|
|
46
|
+
* its own `listen()`). The last one needs no network event at all: at the relay
|
|
47
|
+
* default TTL of two hours every configured-shape node went undialable ~1 h 55 min
|
|
48
|
+
* after reserving. A search listener republishes on `relay:created-reservation`
|
|
49
|
+
* for its own pending id, which the refresh re-queues, and the supervisor covers
|
|
50
|
+
* the other two.
|
|
51
|
+
*
|
|
52
|
+
* Because a strand node runs one supervisor per relay, "is the reservation held"
|
|
53
|
+
* is asked PER RELAY ({@link circuitMultiaddrsVia}): relay X's supervisor must not
|
|
54
|
+
* be satisfied by relay Y's circuit addr, or it would never re-drive X.
|
|
55
|
+
*
|
|
56
|
+
* ⚠️ WHY THE RESERVATION IS REQUESTED EXPLICITLY RATHER THAN LEFT TO DISCOVERY.
|
|
57
|
+
* libp2p fills a search listener's pending reservation from `RelayDiscovery`,
|
|
58
|
+
* which nominates a peer only once it can see the relay-hop protocol id in that
|
|
59
|
+
* peer's PEER-STORE protocol list — and that list is written exclusively by the
|
|
60
|
+
* IDENTIFY handshake. `@optimystic/db-p2p` gives every cadre node a
|
|
61
|
+
* network-namespaced identify protocol id (`/optimystic/control-<partyId>/id/1.0.0`)
|
|
62
|
+
* while the relay `ops/docker/libp2p-infra` deploys runs stock identify
|
|
63
|
+
* (`/ipfs/id/1.0.0`), so the two never identify each other, the peer store stays
|
|
64
|
+
* empty of protocols, and discovery never nominates the relay. The TCP/WebSocket
|
|
65
|
+
* connection itself is fine, which is why leaving it to discovery surfaced as a
|
|
66
|
+
* generic timeout.
|
|
67
|
+
*
|
|
68
|
+
* Identify is incidental to relaying, though: the reservation
|
|
69
|
+
* (`/libp2p/circuit/relay/0.2.0/hop`), a third peer's CONNECT through the relay,
|
|
70
|
+
* and the STOP handler on this node all use STOCK, un-namespaced protocol ids the
|
|
71
|
+
* deployed relay already serves. Discovery's only job is to GUESS which connected
|
|
72
|
+
* peer is a relay, and this module does not need to guess — it was handed the
|
|
73
|
+
* relay's address. So it asks the relay for a slot directly, via the
|
|
74
|
+
* circuit-relay transport's own reservation store.
|
|
75
|
+
*
|
|
76
|
+
* ⚠️ WHY LIBP2P'S RELAY DISCOVERY IS SWITCHED OFF. Discovery does reach SOME
|
|
77
|
+
* peers: every connected peer whose identify lists the relay-hop protocol, and a
|
|
78
|
+
* storage-profile member's strand node runs the relay server in the same
|
|
79
|
+
* `/optimystic/strand-<id>/…` namespace as this node's strand node (control nodes
|
|
80
|
+
* share `/optimystic/control-<partyId>/…` the same way). Discovery reserves on such
|
|
81
|
+
* a peer as soon as it connects, and that takes one of the slots the configured
|
|
82
|
+
* relays' supervisors were going to fill. Worse, the reservation store runs one
|
|
83
|
+
* request at a time and, when a request takes the last free slot, clears its queue
|
|
84
|
+
* with `@libp2p/utils`' `Queue.clear()`, which drops a queued job WITHOUT ever
|
|
85
|
+
* settling its promise — so a supervisor whose `addRelay` was queued behind it
|
|
86
|
+
* waits out its whole deadline (gotchoices/sereus#34). A reservation discovery picks is
|
|
87
|
+
* also unmanaged: no supervisor re-drives it, the status does not count it, and a
|
|
88
|
+
* member's strand node goes away whenever that member hibernates. So every node
|
|
89
|
+
* whose bare search entries cadre generated for configured relays is built through
|
|
90
|
+
* {@link withoutRelayDiscovery} (`node-transports.ts`), and its slots are filled by
|
|
91
|
+
* the supervisors alone — given a transport list to wrap, which React Native and
|
|
92
|
+
* browser embedders always pass and a Node host passes as
|
|
93
|
+
* `network.defaultTransports` (cadre-cli does). A hand-written bare `/p2p-circuit`
|
|
94
|
+
* with no configured relay keeps discovery, since nothing else would fill that slot.
|
|
95
|
+
*
|
|
96
|
+
* Status is derived from the node's LIVE multiaddrs on every read rather than
|
|
97
|
+
* cached at drive time: when a relay restarts or the connection drops, libp2p's
|
|
98
|
+
* listener clears its listening addrs and the circuit multiaddr disappears, so a
|
|
99
|
+
* cached snapshot would keep claiming `reserved` for a node nothing can dial.
|
|
100
|
+
*
|
|
101
|
+
* And because discovery is either off or blind to the relay, nothing would ever
|
|
102
|
+
* ASK again either —
|
|
103
|
+
* so {@link driveRelayReservation} stays a single-shot primitive and
|
|
104
|
+
* {@link superviseRelayReservation} owns the retry cadence on top of it. That
|
|
105
|
+
* loop is what makes a lost reservation recover without a page reload. Single-shot
|
|
106
|
+
* but not unstoppable: the primitive takes an `AbortSignal` and the loop trips it
|
|
107
|
+
* from `stop()`, because a drive left running against a node being torn down both
|
|
108
|
+
* logs failures that read as real and — through the timers it still holds — keeps
|
|
109
|
+
* the process alive for the rest of its `timeoutMs`.
|
|
110
|
+
*
|
|
111
|
+
* What switching discovery off costs: libp2p 3.1.3's `connection.newStream` records
|
|
112
|
+
* every protocol OUR OWN outbound stream negotiated into the peer store, so the
|
|
113
|
+
* explicit reservation request writes the relay-hop protocol id against the relay
|
|
114
|
+
* itself, and with discovery on it refilled a slot freed by a hangup (while the
|
|
115
|
+
* relay stays up) within ~25 ms, measured. With it off that refill waits for the
|
|
116
|
+
* supervisor's next liveness check ({@link DEFAULT_RELAY_CHECK_MS}, 5 s while held).
|
|
117
|
+
* A relay RESTART always needed the supervisor, which un-poisons the store's
|
|
118
|
+
* `relayFilter` before every attempt ({@link clearRelayFilterEntry}). The specs
|
|
119
|
+
* that build their own clients keep discovery on and disable its refill
|
|
120
|
+
* (`forgetRelayProtocol` in `relay-reservation.spec.ts`) so they prove the
|
|
121
|
+
* supervisor's.
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
import debug from 'debug';
|
|
125
|
+
import { multiaddr } from '@multiformats/multiaddr';
|
|
126
|
+
import { peerIdFromString } from '@libp2p/peer-id';
|
|
127
|
+
import type { Libp2p, PeerId } from '@libp2p/interface';
|
|
128
|
+
import type { Libp2pTransports } from '@optimystic/db-p2p';
|
|
129
|
+
import { trailingPeerId } from './peer-dial.js';
|
|
130
|
+
import { relayReservationBudgetMs } from './link-budget.js';
|
|
131
|
+
|
|
132
|
+
const log = debug('sereus:cadre:relay-reservation');
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Relay-reservation posture for a node that reserves through the SEARCH listen
|
|
136
|
+
* addr (bare `/p2p-circuit`) — every cadre control node, whether its relays came
|
|
137
|
+
* from `network.relayAddrs` or from an explicit `CadreNode.reserveRelays()` call.
|
|
138
|
+
* - `none` — no relay addrs supplied; the node is undialable by design.
|
|
139
|
+
* - `dialing` — a drive is in flight.
|
|
140
|
+
* - `reserved` — the node currently holds at least one `/p2p-circuit` addr.
|
|
141
|
+
* - `retrying` — no reservation right now, but a {@link RelayReservationSupervisor}
|
|
142
|
+
* has the next attempt scheduled. Recoverable without a caller.
|
|
143
|
+
* - `error` — no reservation, and NOBODY IS GOING TO TRY AGAIN: no supervisor
|
|
144
|
+
* is running (or there is no node at all).
|
|
145
|
+
*/
|
|
146
|
+
export type RelayReservationStatus = 'none' | 'dialing' | 'reserved' | 'retrying' | 'error';
|
|
147
|
+
|
|
148
|
+
export interface RelayReservationState {
|
|
149
|
+
status: RelayReservationStatus;
|
|
150
|
+
/** The relay multiaddrs this node was asked to reserve through. */
|
|
151
|
+
addrs: string[];
|
|
152
|
+
/** LIVE `/p2p-circuit` multiaddrs, recomputed on every read. */
|
|
153
|
+
circuitAddrs: string[];
|
|
154
|
+
error: string | null;
|
|
155
|
+
/**
|
|
156
|
+
* Epoch ms of the supervisor's next scheduled tick, or `null` when a drive is
|
|
157
|
+
* in flight, no supervisor is running, or no addrs were supplied. A UI can turn
|
|
158
|
+
* this into "reconnecting, next try in 8s" rather than showing a bare error for
|
|
159
|
+
* a node that is in fact recovering.
|
|
160
|
+
*/
|
|
161
|
+
retryAtMs: number | null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* How long {@link driveRelayReservation} waits for a reservation to appear, at the DEFAULT
|
|
166
|
+
* declared link round trip: `RELAY_RESERVATION_ROUND_TRIPS` (4) x `DECLARED_LINK_ROUND_TRIP_MS`
|
|
167
|
+
* (3500 ms), plus 2 x `ADMISSION_DECISION_TIMEOUT_MS` (2000 ms) for a party-run relay's
|
|
168
|
+
* connection and reservation decisions, = 18 000 ms (`relayReservationBudgetMs`), where it was a
|
|
169
|
+
* fixed 10_000 before it was derived.
|
|
170
|
+
*
|
|
171
|
+
* Counted rather than chosen because this one deadline bounds the whole drive — dial the relay,
|
|
172
|
+
* request the reservation, wait — and each of those costs a fixed number of exchanges, not a
|
|
173
|
+
* fixed number of milliseconds. `link-budget.ts` carries the counts, the measurement behind
|
|
174
|
+
* them, and what still fails at the supported link whatever this says.
|
|
175
|
+
*
|
|
176
|
+
* A host on a slower link moves it by declaring `NetworkConfig.linkRoundTripMs`, which both
|
|
177
|
+
* call sites thread in (`CadreNode.reserveRelays` for the control node,
|
|
178
|
+
* `StrandInstanceManager`'s per-relay supervisors for a strand). This constant is the fallback
|
|
179
|
+
* for a caller that declares nothing.
|
|
180
|
+
*/
|
|
181
|
+
export const DEFAULT_RELAY_RESERVE_TIMEOUT_MS = relayReservationBudgetMs();
|
|
182
|
+
export const DEFAULT_RELAY_RESERVE_POLL_MS = 250;
|
|
183
|
+
|
|
184
|
+
export interface RelayReserveOptions {
|
|
185
|
+
timeoutMs?: number;
|
|
186
|
+
pollMs?: number;
|
|
187
|
+
/**
|
|
188
|
+
* Ends an in-flight drive early. Aborting is not a failure: the drive returns
|
|
189
|
+
* `{ error: null, cancelled: true }` and clears every timer it holds, so the
|
|
190
|
+
* process is free to exit immediately instead of trailing the teardown that
|
|
191
|
+
* cancelled it.
|
|
192
|
+
*/
|
|
193
|
+
signal?: AbortSignal;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** What one {@link driveRelayReservation} came back with. */
|
|
197
|
+
export interface RelayReserveResult {
|
|
198
|
+
/** Why no reservation landed; `null` when one did — or when the drive was cancelled. */
|
|
199
|
+
error: string | null;
|
|
200
|
+
/**
|
|
201
|
+
* The drive ended because its caller's `signal` tripped, not because of anything
|
|
202
|
+
* the relay did. Separate from `error` because `error: null` means "a reservation
|
|
203
|
+
* landed" and a cancelled drive landed nothing: folded together, a caller would
|
|
204
|
+
* read cancellation as success. `cancelled: true` always implies `error: null`.
|
|
205
|
+
*/
|
|
206
|
+
cancelled: boolean;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The slice of `@libp2p/utils`' `Filter` (a cuckoo filter) that the reservation
|
|
211
|
+
* store uses to remember relays whose reservation request failed. `remove` is
|
|
212
|
+
* OPTIONAL on that interface, so every use here has to cope with its absence —
|
|
213
|
+
* see {@link clearRelayFilterEntry}.
|
|
214
|
+
*/
|
|
215
|
+
export interface RelayFilterLike {
|
|
216
|
+
has(item: Uint8Array): boolean;
|
|
217
|
+
remove?(item: Uint8Array): boolean;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The slice of `@libp2p/circuit-relay-v2`'s `ReservationStore` this module drives.
|
|
222
|
+
* Declared structurally because the class is internal to that package: naming only
|
|
223
|
+
* the members we call keeps the coupling small, explicit and greppable, and lets
|
|
224
|
+
* {@link findCircuitRelayTransport} duck-type rather than brand-match.
|
|
225
|
+
*
|
|
226
|
+
* `relayFilter` is optional because it is not required to DRIVE a reservation,
|
|
227
|
+
* only to RE-drive one (see {@link clearRelayFilterEntry}); a libp2p version that
|
|
228
|
+
* renames or drops it must degrade, not break.
|
|
229
|
+
*/
|
|
230
|
+
export interface RelayReservationStoreLike {
|
|
231
|
+
addRelay(peerId: PeerId, type: 'discovered' | 'configured'): Promise<unknown>;
|
|
232
|
+
hasReservation(peerId: PeerId): boolean;
|
|
233
|
+
relayFilter?: RelayFilterLike;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The slice of libp2p's circuit-relay transport that owns the reservation store. */
|
|
237
|
+
export interface CircuitRelayTransportLike {
|
|
238
|
+
reservationStore: RelayReservationStoreLike;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** `components` is a real field on libp2p's node class but not on the `Libp2p` interface. */
|
|
242
|
+
interface Libp2pComponentsLike {
|
|
243
|
+
components?: {
|
|
244
|
+
transportManager?: {
|
|
245
|
+
getTransports?: () => readonly unknown[];
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The node's `/p2p-circuit` multiaddrs — the addresses a peer can dial it at
|
|
252
|
+
* while it holds a relay reservation. Empty means "not dialable via a relay
|
|
253
|
+
* right now", which is exactly what makes {@link resolveRelayReservationState}
|
|
254
|
+
* live rather than a stale snapshot.
|
|
255
|
+
*/
|
|
256
|
+
export function circuitMultiaddrs(node: Libp2p): string[] {
|
|
257
|
+
return node
|
|
258
|
+
.getMultiaddrs()
|
|
259
|
+
.map((ma) => ma.toString())
|
|
260
|
+
.filter((addr) => addr.includes('/p2p-circuit'));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The node's `/p2p-circuit` multiaddrs THROUGH the relays `addrs` name — the ones
|
|
265
|
+
* whose `/p2p/<relayPeerId>/p2p-circuit` component names one of them. This is the
|
|
266
|
+
* "held" question a supervisor over a SUBSET of the node's relays has to ask: a
|
|
267
|
+
* strand node runs one supervisor per relay, and relay X's supervisor must not be
|
|
268
|
+
* satisfied by relay Y's addr, or a lost X is never re-driven.
|
|
269
|
+
*
|
|
270
|
+
* Falls back to EVERY circuit addr when any entry names no relay peer id (no
|
|
271
|
+
* `/p2p/` component, or unparsable): such an addr can still be dialed and reserved
|
|
272
|
+
* through, but the addr it earns cannot be attributed to it, so "any circuit addr"
|
|
273
|
+
* is the only answer that does not mark a live reservation as lost. The control
|
|
274
|
+
* node passes its whole relay list, so its answer is the same either way.
|
|
275
|
+
*/
|
|
276
|
+
export function circuitMultiaddrsVia(node: Libp2p, addrs: readonly string[]): string[] {
|
|
277
|
+
const held = circuitMultiaddrs(node);
|
|
278
|
+
const relayIds = addrs.map(relayPeerIdOf);
|
|
279
|
+
if (relayIds.some((id) => id === null)) {
|
|
280
|
+
return held;
|
|
281
|
+
}
|
|
282
|
+
return held.filter((circuit) => relayIds.includes(relayPeerIdOf(circuit)));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The NORMALIZED peer id of the relay `addr` names: the `/p2p/` component just
|
|
287
|
+
* before `/p2p-circuit` when there is one, else the trailing `/p2p/` component
|
|
288
|
+
* (a relay DIAL addr). `null` when there is none, or `addr` does not parse.
|
|
289
|
+
*
|
|
290
|
+
* Normalized through `peerIdFromString(…).toString()` so a relay written in a
|
|
291
|
+
* different encoding than libp2p publishes (a CID form, say) still matches.
|
|
292
|
+
*/
|
|
293
|
+
function relayPeerIdOf(addr: string): string | null {
|
|
294
|
+
try {
|
|
295
|
+
const components = multiaddr(addr).getComponents();
|
|
296
|
+
const circuitIdx = components.findIndex((c) => c.name === 'p2p-circuit');
|
|
297
|
+
const relayScope = circuitIdx >= 0 ? components.slice(0, circuitIdx) : components;
|
|
298
|
+
const raw = trailingPeerId(multiaddr(relayScope));
|
|
299
|
+
return raw === null ? null : peerIdFromString(raw).toString();
|
|
300
|
+
} catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The running node's circuit-relay transport, or `null` when it has none.
|
|
307
|
+
*
|
|
308
|
+
* There is no public libp2p API for "reserve on THIS specific relay", so this
|
|
309
|
+
* reaches through `node.components.transportManager` — libp2p's internal layout.
|
|
310
|
+
* That coupling is deliberate and bounded: every step is optional-chained and the
|
|
311
|
+
* whole thing returns `null` rather than throwing, and a spec pins the seam so a
|
|
312
|
+
* libp2p upgrade that moves it fails loudly instead of silently reverting this
|
|
313
|
+
* module to the discovery wait that never fires.
|
|
314
|
+
*/
|
|
315
|
+
export function findCircuitRelayTransport(node: Libp2p): CircuitRelayTransportLike | null {
|
|
316
|
+
for (const transport of nodeTransports(node)) {
|
|
317
|
+
const store = (transport as { reservationStore?: unknown }).reservationStore;
|
|
318
|
+
if (isReservationStore(store)) {
|
|
319
|
+
return { reservationStore: store };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function nodeTransports(node: Libp2p): readonly unknown[] {
|
|
326
|
+
const manager = (node as Libp2p & Libp2pComponentsLike).components?.transportManager;
|
|
327
|
+
if (typeof manager?.getTransports !== 'function') {
|
|
328
|
+
log('No transportManager.getTransports() on this node — libp2p internals moved?');
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
return manager.getTransports();
|
|
333
|
+
} catch (err) {
|
|
334
|
+
log('transportManager.getTransports() threw: %o', err);
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function isReservationStore(value: unknown): value is RelayReservationStoreLike {
|
|
340
|
+
if (typeof value !== 'object' || value === null) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
const store = value as Partial<RelayReservationStoreLike>;
|
|
344
|
+
return typeof store.addRelay === 'function' && typeof store.hasReservation === 'function';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* `transports` with libp2p's relay discovery switched off in the circuit-relay transport any of
|
|
349
|
+
* them builds, for a node whose bare `/p2p-circuit` slots cadre's own supervisors fill (see the
|
|
350
|
+
* module header, "WHY LIBP2P'S RELAY DISCOVERY IS SWITCHED OFF"). Every other transport is built
|
|
351
|
+
* untouched.
|
|
352
|
+
*
|
|
353
|
+
* Each factory is wrapped rather than the built node patched afterwards: libp2p calls the
|
|
354
|
+
* factories in its constructor, and `@optimystic/db-p2p` builds with `start: false`, so the
|
|
355
|
+
* switch lands before anything starts. Patching the running node instead races — db-p2p starts
|
|
356
|
+
* the node before `createLibp2pNode` returns, and a cohort-seed connection in that window has
|
|
357
|
+
* already queued a discovered reservation.
|
|
358
|
+
*/
|
|
359
|
+
export function withoutRelayDiscovery(transports: Libp2pTransports): Libp2pTransports {
|
|
360
|
+
return transports.map((factory) => (components) => switchOffRelayDiscovery(factory(components)));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Logged once per process, not per node: every node in it runs the same `@libp2p/circuit-relay-v2`. */
|
|
364
|
+
let reportedDiscoveryFieldMissing = false;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Clear `transport`'s `discovery` when it is the circuit-relay transport — duck-typed by its
|
|
368
|
+
* reservation store, as {@link findCircuitRelayTransport} finds it.
|
|
369
|
+
*
|
|
370
|
+
* `@libp2p/circuit-relay-v2` 4.2.13 has no init option for this, but declares the field optional
|
|
371
|
+
* and guards every use (`this.discovery?.startDiscovery()`, `start`/`stop` skip a non-startable
|
|
372
|
+
* value), so `undefined` before `start()` registers no relay-hop topology and never runs a
|
|
373
|
+
* peer-store scan.
|
|
374
|
+
*/
|
|
375
|
+
function switchOffRelayDiscovery<T>(transport: T): T {
|
|
376
|
+
if (typeof transport !== 'object' || transport === null) {
|
|
377
|
+
return transport;
|
|
378
|
+
}
|
|
379
|
+
const candidate = transport as { reservationStore?: unknown; discovery?: unknown };
|
|
380
|
+
if (!isReservationStore(candidate.reservationStore)) {
|
|
381
|
+
return transport;
|
|
382
|
+
}
|
|
383
|
+
if (!Object.prototype.hasOwnProperty.call(candidate, 'discovery')) {
|
|
384
|
+
// Not thrown: a node with discovery left on still works, it is only exposed to the slot theft
|
|
385
|
+
// again — which the `withoutRelayDiscovery` spec in `relay-reservation.spec.ts` reproduces.
|
|
386
|
+
if (!reportedDiscoveryFieldMissing) {
|
|
387
|
+
reportedDiscoveryFieldMissing = true;
|
|
388
|
+
console.warn(
|
|
389
|
+
'cadre: the circuit-relay transport has no `discovery` field to switch off — the ' +
|
|
390
|
+
'@libp2p/circuit-relay-v2 shape changed. libp2p relay discovery stays on, and it can take a ' +
|
|
391
|
+
'configured relay\'s reservation slot.'
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return transport;
|
|
395
|
+
}
|
|
396
|
+
candidate.discovery = undefined;
|
|
397
|
+
return transport;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Sentinel resolved by the wait arm of {@link requestOneReservation}'s race. */
|
|
401
|
+
const WAIT_OVER = Symbol('relay-reservation-wait-over');
|
|
402
|
+
|
|
403
|
+
/** A relay that answered its dial, paired with the address it was dialed at. */
|
|
404
|
+
interface ConnectedRelay {
|
|
405
|
+
addr: string;
|
|
406
|
+
peerId: PeerId;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Whether `signal` has been aborted.
|
|
411
|
+
*
|
|
412
|
+
* A function rather than an inline `signal?.aborted` because a signal's `aborted` flag
|
|
413
|
+
* is a readonly boolean: one `if (signal?.aborted)` narrows every LATER read of it
|
|
414
|
+
* to `false`, and TypeScript then rejects the re-check as a comparison that can
|
|
415
|
+
* never hold. Re-checking after each await is exactly what cancellation is, so the
|
|
416
|
+
* narrowing is the thing that is wrong, not the check.
|
|
417
|
+
*/
|
|
418
|
+
function aborted(signal: AbortSignal | undefined): boolean {
|
|
419
|
+
return signal !== undefined && signal.aborted;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* `setTimeout` as a promise, cut short by `signal`.
|
|
424
|
+
*
|
|
425
|
+
* EVERY wait inside a drive routes through here, because a wait that merely LOSES
|
|
426
|
+
* a `Promise.race` leaves its timer pending — and a pending timer keeps a Node
|
|
427
|
+
* process alive for the rest of its duration, which is exactly the delay a
|
|
428
|
+
* cancelled drive exists to shed. So the timer is cleared on both endings, not
|
|
429
|
+
* only on its own.
|
|
430
|
+
*
|
|
431
|
+
* NOTE: no spec catches a wait that skips this helper. The cancellation specs
|
|
432
|
+
* assert prompt RETURN, which a `Promise.race` gives with its losing timer still
|
|
433
|
+
* pending — only the process actually exiting proves the clearing, and that was
|
|
434
|
+
* measured by hand rather than pinned. If a wait is ever added outside this
|
|
435
|
+
* helper, pin it with a spawned-child process-exit test instead.
|
|
436
|
+
*/
|
|
437
|
+
function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
438
|
+
if (aborted(signal)) {
|
|
439
|
+
return Promise.resolve();
|
|
440
|
+
}
|
|
441
|
+
return new Promise((resolve) => {
|
|
442
|
+
const finish = (): void => {
|
|
443
|
+
clearTimeout(timer);
|
|
444
|
+
signal?.removeEventListener('abort', finish);
|
|
445
|
+
resolve();
|
|
446
|
+
};
|
|
447
|
+
const timer = setTimeout(finish, ms);
|
|
448
|
+
signal?.addEventListener('abort', finish, { once: true });
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Relay `from`'s abort into `to`, and hand back the detach that must run on every
|
|
454
|
+
* exit path.
|
|
455
|
+
*
|
|
456
|
+
* By hand rather than through `AbortSignal.any`, for the same reason this module
|
|
457
|
+
* builds its deadline from an explicit `AbortController` rather than
|
|
458
|
+
* `AbortSignal.timeout`: React Native/Hermes runs this same code, and `any` is
|
|
459
|
+
* newer still — its polyfill also leaks listeners onto inputs that never abort,
|
|
460
|
+
* which is what the returned detach avoids here.
|
|
461
|
+
*/
|
|
462
|
+
function linkAbort(from: AbortSignal | undefined, to: AbortController): () => void {
|
|
463
|
+
if (from === undefined) {
|
|
464
|
+
return () => { /* nothing was attached */ };
|
|
465
|
+
}
|
|
466
|
+
if (from.aborted) {
|
|
467
|
+
to.abort();
|
|
468
|
+
return () => { /* nothing was attached */ };
|
|
469
|
+
}
|
|
470
|
+
const forward = (): void => to.abort();
|
|
471
|
+
from.addEventListener('abort', forward, { once: true });
|
|
472
|
+
return () => from.removeEventListener('abort', forward);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Dial every relay in `addrs`, ask the first one that answers for a reservation
|
|
477
|
+
* slot, then wait until a `/p2p-circuit` address appears.
|
|
478
|
+
*
|
|
479
|
+
* Never throws — a dial rejection, a rejected reservation, a MALFORMED address,
|
|
480
|
+
* or a timeout all come back as an `error` string. A dial that throws does NOT
|
|
481
|
+
* cost the rest of the list: one dead relay among two must not cost the
|
|
482
|
+
* reservation on the live one, so every addr is dialed and the first error (in
|
|
483
|
+
* list order) is kept. A reservation that lands wins over any earlier error
|
|
484
|
+
* (`error: null`).
|
|
485
|
+
*
|
|
486
|
+
* `timeoutMs` bounds the WHOLE drive — dials, reservation requests and the wait
|
|
487
|
+
* share one deadline. `opts.signal` bounds it from the other side: every phase
|
|
488
|
+
* below returns as soon as it trips, and the drive reports `cancelled` rather than
|
|
489
|
+
* inventing a failure for a relay that was never given a chance to answer.
|
|
490
|
+
*/
|
|
491
|
+
export async function driveRelayReservation(
|
|
492
|
+
node: Libp2p,
|
|
493
|
+
addrs: readonly string[],
|
|
494
|
+
opts?: RelayReserveOptions
|
|
495
|
+
): Promise<RelayReserveResult> {
|
|
496
|
+
if (addrs.length === 0) {
|
|
497
|
+
return { error: null, cancelled: false };
|
|
498
|
+
}
|
|
499
|
+
const signal = opts?.signal;
|
|
500
|
+
if (aborted(signal)) {
|
|
501
|
+
return { error: null, cancelled: true };
|
|
502
|
+
}
|
|
503
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_RELAY_RESERVE_TIMEOUT_MS;
|
|
504
|
+
// Clamped: a caller-supplied 0 would spin the poll loop hot until the deadline.
|
|
505
|
+
const pollMs = Math.max(1, opts?.pollMs ?? DEFAULT_RELAY_RESERVE_POLL_MS);
|
|
506
|
+
const deadline = Date.now() + timeoutMs;
|
|
507
|
+
|
|
508
|
+
const { connected, error: dialError } = await dialRelays(node, addrs, deadline, signal);
|
|
509
|
+
const attempt = await requestReservation(node, connected, deadline, signal);
|
|
510
|
+
// NOTE: a REJECTED reservation still spends the rest of the deadline polling,
|
|
511
|
+
// because on a node that kept libp2p's relay discovery (a hand-written bare
|
|
512
|
+
// `/p2p-circuit` driven through `reserveRelays`) discovery may independently land
|
|
513
|
+
// one (only the no-transport case is `fatal`). So a misconfigured node reports its
|
|
514
|
+
// now-legible reason a full `timeoutMs` late. If startup latency on that path ever
|
|
515
|
+
// matters, shorten the wait once every connected relay has rejected — do not skip
|
|
516
|
+
// it outright.
|
|
517
|
+
if (!attempt.fatal && (await waitForCircuitReservation(node, addrs, deadline, pollMs, signal))) {
|
|
518
|
+
return { error: null, cancelled: false };
|
|
519
|
+
}
|
|
520
|
+
// Checked AFTER the success above, so a reservation that landed in the same turn
|
|
521
|
+
// the signal tripped is still reported as landed. Below it, the reasons the
|
|
522
|
+
// phases assembled describe the cancellation rather than the relay, so they are
|
|
523
|
+
// dropped: a cancelled drive has nothing to say about the relay's health.
|
|
524
|
+
if (aborted(signal)) {
|
|
525
|
+
return { error: null, cancelled: true };
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
error: attempt.error ?? dialError ?? `no circuit reservation within ${timeoutMs}ms`,
|
|
529
|
+
cancelled: false
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Dial every relay CONCURRENTLY under the drive's shared deadline. Reports the
|
|
535
|
+
* relays that answered (in list order, for the reservation step) and the first
|
|
536
|
+
* failure in LIST order (`null` when every dial connected).
|
|
537
|
+
*
|
|
538
|
+
* Concurrent and deadline-bound because this whole drive is awaited on a browser
|
|
539
|
+
* tab's startup path: dialing serially with libp2p's own (much longer) per-dial
|
|
540
|
+
* timeout meant N unreachable relays cost N dial timeouts before the reservation
|
|
541
|
+
* request even began, so `timeoutMs` bounded only the tail of the operation.
|
|
542
|
+
*
|
|
543
|
+
* Aborting a dial at the deadline is not a lost reservation — the steps that
|
|
544
|
+
* follow have no time left either, so the drive would report `error` regardless.
|
|
545
|
+
*
|
|
546
|
+
* The caller's `signal` aborts the same dials, through the same controller: a
|
|
547
|
+
* hanging dial is the longest thing a cancelled drive can be sitting in.
|
|
548
|
+
*/
|
|
549
|
+
async function dialRelays(
|
|
550
|
+
node: Libp2p,
|
|
551
|
+
addrs: readonly string[],
|
|
552
|
+
deadline: number,
|
|
553
|
+
signal?: AbortSignal
|
|
554
|
+
): Promise<{ connected: ConnectedRelay[]; error: string | null }> {
|
|
555
|
+
// An explicit controller, not `AbortSignal.timeout` — the latter is not
|
|
556
|
+
// reliably present on React Native/Hermes, which runs this same module.
|
|
557
|
+
const controller = new AbortController();
|
|
558
|
+
const timer = setTimeout(() => controller.abort(), Math.max(0, deadline - Date.now()));
|
|
559
|
+
const unlink = linkAbort(signal, controller);
|
|
560
|
+
try {
|
|
561
|
+
const results = await Promise.allSettled(
|
|
562
|
+
addrs.map((addr) => dialOne(node, addr, controller.signal))
|
|
563
|
+
);
|
|
564
|
+
return { connected: connectedRelays(addrs, results), error: firstDialError(addrs, results) };
|
|
565
|
+
} finally {
|
|
566
|
+
clearTimeout(timer);
|
|
567
|
+
unlink();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* One dial, with the address PARSED INSIDE the promise.
|
|
573
|
+
*
|
|
574
|
+
* `multiaddr()` throws SYNCHRONOUSLY on a malformed string, and a throw out of
|
|
575
|
+
* the `map` callback escapes `Promise.allSettled` altogether — it would reject
|
|
576
|
+
* the whole drive (breaking the never-throws contract) and discard the dials to
|
|
577
|
+
* the other, perfectly good, addrs. `async` demotes it to a settled rejection,
|
|
578
|
+
* which is already exactly how a dial failure is reported.
|
|
579
|
+
*/
|
|
580
|
+
async function dialOne(
|
|
581
|
+
node: Libp2p,
|
|
582
|
+
addr: string,
|
|
583
|
+
signal: AbortSignal
|
|
584
|
+
): Promise<{ remotePeer: PeerId }> {
|
|
585
|
+
return node.dial(multiaddr(addr), { signal });
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function connectedRelays(
|
|
589
|
+
addrs: readonly string[],
|
|
590
|
+
results: readonly PromiseSettledResult<{ remotePeer: PeerId }>[]
|
|
591
|
+
): ConnectedRelay[] {
|
|
592
|
+
const connected: ConnectedRelay[] = [];
|
|
593
|
+
results.forEach((result, index) => {
|
|
594
|
+
if (result.status !== 'fulfilled') return;
|
|
595
|
+
connected.push({ addr: addrs[index], peerId: result.value.remotePeer });
|
|
596
|
+
});
|
|
597
|
+
return connected;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** Log every dial rejection; return the first one in list order. */
|
|
601
|
+
function firstDialError(
|
|
602
|
+
addrs: readonly string[],
|
|
603
|
+
results: readonly PromiseSettledResult<unknown>[]
|
|
604
|
+
): string | null {
|
|
605
|
+
let first: string | null = null;
|
|
606
|
+
results.forEach((result, index) => {
|
|
607
|
+
if (result.status !== 'rejected') return;
|
|
608
|
+
const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
609
|
+
log('Relay dial failed (%s): %s', addrs[index], message);
|
|
610
|
+
first ??= message;
|
|
611
|
+
});
|
|
612
|
+
return first;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Outcome of the explicit reservation request.
|
|
617
|
+
*
|
|
618
|
+
* `fatal` means no `/p2p-circuit` address can EVER appear (the node has no
|
|
619
|
+
* circuit-relay transport at all), so the caller must report immediately rather
|
|
620
|
+
* than spend the rest of the deadline polling for something that cannot happen.
|
|
621
|
+
* A rejected request is NOT fatal: on a node that kept relay discovery, discovery
|
|
622
|
+
* may still land a reservation for a relay that does happen to share this node's
|
|
623
|
+
* identify prefix.
|
|
624
|
+
*/
|
|
625
|
+
interface ReservationAttempt {
|
|
626
|
+
error: string | null;
|
|
627
|
+
fatal: boolean;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Ask the connected relays, in list order, for a reservation slot; stop at the
|
|
632
|
+
* first success.
|
|
633
|
+
*
|
|
634
|
+
* `'discovered'`, not `'configured'`: the search listener's
|
|
635
|
+
* `relay:created-reservation` handler ignores `configured` reservations outright,
|
|
636
|
+
* so the `/p2p-circuit` listen address would never be published and
|
|
637
|
+
* `getMultiaddrs()` would stay empty even though the reservation succeeded.
|
|
638
|
+
* `'discovered'` consumes the pending-reservation id the bare `/p2p-circuit`
|
|
639
|
+
* listener registered, which is exactly what that handler matches on.
|
|
640
|
+
*
|
|
641
|
+
* Sequential, not concurrent: a bare `/p2p-circuit` listener registers exactly
|
|
642
|
+
* ONE pending reservation, so a second concurrent `addRelay` would be rejected
|
|
643
|
+
* with `HadEnoughRelaysError` — noise, not a failure. Discovery naming the SAME
|
|
644
|
+
* relay is not a conflict (`addRelay` dedupes through its own peer queue); discovery
|
|
645
|
+
* naming a DIFFERENT peer is, which is why it is off on every node cadre supervises
|
|
646
|
+
* (module header).
|
|
647
|
+
*/
|
|
648
|
+
async function requestReservation(
|
|
649
|
+
node: Libp2p,
|
|
650
|
+
connected: readonly ConnectedRelay[],
|
|
651
|
+
deadline: number,
|
|
652
|
+
signal?: AbortSignal
|
|
653
|
+
): Promise<ReservationAttempt> {
|
|
654
|
+
if (connected.length === 0) {
|
|
655
|
+
// Nothing answered — the dial error is the whole story.
|
|
656
|
+
return { error: null, fatal: false };
|
|
657
|
+
}
|
|
658
|
+
const transport = findCircuitRelayTransport(node);
|
|
659
|
+
if (transport === null) {
|
|
660
|
+
return {
|
|
661
|
+
error: 'node has no circuit-relay transport — add circuitRelayTransport() to its transports',
|
|
662
|
+
fatal: true
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
let firstError: string | null = null;
|
|
666
|
+
for (const relay of connected) {
|
|
667
|
+
if (transport.reservationStore.hasReservation(relay.peerId)) {
|
|
668
|
+
return { error: null, fatal: false };
|
|
669
|
+
}
|
|
670
|
+
// A cancelled drive stops here rather than asking the next relay: the caller is
|
|
671
|
+
// tearing this node down, so another hop request against it can only fail.
|
|
672
|
+
if (Date.now() >= deadline || aborted(signal)) {
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
const failure = await requestOneReservation(
|
|
676
|
+
transport.reservationStore,
|
|
677
|
+
relay,
|
|
678
|
+
deadline,
|
|
679
|
+
signal
|
|
680
|
+
);
|
|
681
|
+
if (failure === null) {
|
|
682
|
+
return { error: null, fatal: false };
|
|
683
|
+
}
|
|
684
|
+
firstError ??= failure;
|
|
685
|
+
}
|
|
686
|
+
return { error: firstError, fatal: false };
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* One `addRelay` call, bounded by the drive's deadline and by the caller's signal.
|
|
691
|
+
* Returns `null` on success, or the reason it stopped waiting.
|
|
692
|
+
*
|
|
693
|
+
* Raced rather than passed a signal because `addRelay` accepts none and runs on
|
|
694
|
+
* libp2p's own (much longer) reservation timeout: without the race a drive given
|
|
695
|
+
* 1.5 s would sit on a silent relay for libp2p's timeout instead. The abandoned
|
|
696
|
+
* promise stays handled — `Promise.race` attaches its own handlers — so a later
|
|
697
|
+
* rejection is not unhandled, and is not logged either: a request abandoned at a
|
|
698
|
+
* cancellation must not print a relay failure the caller would read as real.
|
|
699
|
+
*
|
|
700
|
+
* `waitOver` ends the wait on BOTH routes — the caller's abort, relayed in, and the
|
|
701
|
+
* race settling some other way, aborted in the `finally`. Out-racing the deadline
|
|
702
|
+
* timer is not enough: the loser stays pending, and a pending timer is what holds
|
|
703
|
+
* a Node process open past the stop that asked for it.
|
|
704
|
+
*
|
|
705
|
+
* NOTE: the reservation store's `Queue.clear()` drops a queued job unsettled whenever
|
|
706
|
+
* a reservation takes the last pending slot. With relay discovery off, the slots never
|
|
707
|
+
* outnumber the supervised relays, so a first request cannot be dropped that way. A
|
|
708
|
+
* REFRESH of a held reservation that is queued at the moment another relay's first
|
|
709
|
+
* reservation takes the last slot still is, and that reservation then lapses at the
|
|
710
|
+
* relay unrefreshed. If that is ever observed, or anything besides the supervisors
|
|
711
|
+
* gains a way to fill these slots, also end this wait on the store's
|
|
712
|
+
* `relay:found-enough-relays` event while this relay holds no reservation. The real
|
|
713
|
+
* fix — `#checkReservationCount` aborting queued jobs instead of clearing them —
|
|
714
|
+
* belongs to js-libp2p.
|
|
715
|
+
*/
|
|
716
|
+
async function requestOneReservation(
|
|
717
|
+
store: RelayReservationStoreLike,
|
|
718
|
+
relay: ConnectedRelay,
|
|
719
|
+
deadline: number,
|
|
720
|
+
signal?: AbortSignal
|
|
721
|
+
): Promise<string | null> {
|
|
722
|
+
const waitOver = new AbortController();
|
|
723
|
+
const unlink = linkAbort(signal, waitOver);
|
|
724
|
+
try {
|
|
725
|
+
const outcome = await Promise.race([
|
|
726
|
+
store.addRelay(relay.peerId, 'discovered'),
|
|
727
|
+
delay(Math.max(0, deadline - Date.now()), waitOver.signal).then(() => WAIT_OVER)
|
|
728
|
+
]);
|
|
729
|
+
if (outcome !== WAIT_OVER) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
if (aborted(signal)) {
|
|
733
|
+
return `relay reservation request to ${relay.addr} was cancelled`;
|
|
734
|
+
}
|
|
735
|
+
log('Relay reservation request (%s) still pending at the deadline', relay.addr);
|
|
736
|
+
return `relay reservation request to ${relay.addr} did not complete before the deadline`;
|
|
737
|
+
} catch (err) {
|
|
738
|
+
const message = describeReservationFailure(err, relay.addr);
|
|
739
|
+
log('Relay reservation failed (%s): %s', relay.addr, message);
|
|
740
|
+
return message;
|
|
741
|
+
} finally {
|
|
742
|
+
unlink();
|
|
743
|
+
waitOver.abort();
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Turn libp2p's reservation rejection into a reason that names the actual cause.
|
|
749
|
+
*
|
|
750
|
+
* `HadEnoughRelaysError` needs translating rather than passing through: its own
|
|
751
|
+
* message ("we do not need any more relays") says the opposite of what happened.
|
|
752
|
+
* A `'discovered'` reservation is refused when the node holds NO pending
|
|
753
|
+
* reservation id, and a node holds one only while it listens on the bare
|
|
754
|
+
* `/p2p-circuit` search address — so the real cause is a missing listen address.
|
|
755
|
+
*/
|
|
756
|
+
function describeReservationFailure(err: unknown, addr: string): string {
|
|
757
|
+
// NOTE: libp2p adds a peer to the reservation store's `relayFilter` when a
|
|
758
|
+
// request fails with `DialError` or `UnsupportedProtocolError`, and the failure
|
|
759
|
+
// path does not reset that filter (it only resets when a reservation is actually
|
|
760
|
+
// removed). So a SECOND drive against the same relay in the same process reports
|
|
761
|
+
// `ListenError: The relay was previously invalid` even if the relay has since
|
|
762
|
+
// recovered. `RelayReservationLoop` clears the entry before every attempt (see
|
|
763
|
+
// {@link clearRelayFilterEntry}); a caller that re-drives WITHOUT the supervisor
|
|
764
|
+
// still has to deal with the filter itself.
|
|
765
|
+
const name = err instanceof Error ? err.name : '';
|
|
766
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
767
|
+
switch (name) {
|
|
768
|
+
case 'HadEnoughRelaysError':
|
|
769
|
+
return `relay reservation on ${addr} was not requested: this node holds no pending circuit reservation — it is not listening on the bare /p2p-circuit address`;
|
|
770
|
+
case 'UnsupportedProtocolError':
|
|
771
|
+
return `peer at ${addr} does not speak the circuit-relay hop protocol — it is not a relay`;
|
|
772
|
+
default:
|
|
773
|
+
return `relay reservation on ${addr} failed: ${message}`;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Poll until the node advertises a `/p2p-circuit` address through one of `addrs`'
|
|
779
|
+
* relays ({@link circuitMultiaddrsVia}), the deadline passes, or the drive is
|
|
780
|
+
* cancelled. This is the phase a cancellation has to reach: whatever the dial and
|
|
781
|
+
* the reservation request shed, an abandoned drive used to spend here instead,
|
|
782
|
+
* polling a node whose transports were already being torn down.
|
|
783
|
+
*
|
|
784
|
+
* Still a poll even though the reservation is now requested explicitly: `addRelay`
|
|
785
|
+
* resolving means the RELAY accepted, while the listen address is published a tick
|
|
786
|
+
* later by the listener's `relay:created-reservation` handler, and a reservation
|
|
787
|
+
* that discovery lands independently has no return value to await at all.
|
|
788
|
+
*
|
|
789
|
+
* NOTE: polls rather than subscribing to libp2p's `self:peer:update` — it keeps
|
|
790
|
+
* this free of libp2p event-name coupling. If 250 ms of reservation latency ever
|
|
791
|
+
* matters, switch to the event and keep the poll as a fallback.
|
|
792
|
+
*/
|
|
793
|
+
async function waitForCircuitReservation(
|
|
794
|
+
node: Libp2p,
|
|
795
|
+
addrs: readonly string[],
|
|
796
|
+
deadline: number,
|
|
797
|
+
pollMs: number,
|
|
798
|
+
signal?: AbortSignal
|
|
799
|
+
): Promise<boolean> {
|
|
800
|
+
for (;;) {
|
|
801
|
+
if (circuitMultiaddrsVia(node, addrs).length > 0) {
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
if (Date.now() >= deadline || aborted(signal)) {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
await delay(Math.min(pollMs, Math.max(0, deadline - Date.now())), signal);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/** Gap between liveness checks while a reservation is held. */
|
|
812
|
+
export const DEFAULT_RELAY_CHECK_MS = 5_000;
|
|
813
|
+
/** Backoff before the first re-drive after a failed attempt. */
|
|
814
|
+
export const DEFAULT_RELAY_MIN_BACKOFF_MS = 2_000;
|
|
815
|
+
/** Ceiling the backoff doubles up to. */
|
|
816
|
+
export const DEFAULT_RELAY_MAX_BACKOFF_MS = 60_000;
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Everything one drive takes, minus its `signal`, plus the loop's own cadence.
|
|
820
|
+
*
|
|
821
|
+
* `signal` is omitted rather than inherited because these options are handed
|
|
822
|
+
* straight to every drive: a caller-supplied one would cancel each attempt without
|
|
823
|
+
* the loop knowing, leaving it to reschedule drives that return immediately and
|
|
824
|
+
* forever. The way to cancel a supervisor is
|
|
825
|
+
* {@link RelayReservationSupervisor.stop}, which owns the signal it passes down.
|
|
826
|
+
*/
|
|
827
|
+
export interface RelayReservationSupervisorOptions extends Omit<RelayReserveOptions, 'signal'> {
|
|
828
|
+
/** Gap between liveness checks while a reservation is held. Default 5_000. */
|
|
829
|
+
checkMs?: number;
|
|
830
|
+
/** Backoff before the first re-drive after a failure. Default 2_000. */
|
|
831
|
+
minBackoffMs?: number;
|
|
832
|
+
/** Backoff ceiling. Default 60_000. */
|
|
833
|
+
maxBackoffMs?: number;
|
|
834
|
+
/**
|
|
835
|
+
* Awaited before every drive AFTER the first — never before the first attempt,
|
|
836
|
+
* which the caller has typically prepared for already. A hook that throws or
|
|
837
|
+
* rejects is logged and the drive still runs: the hook is preparation, and a
|
|
838
|
+
* failed preparation must not cost the attempt itself.
|
|
839
|
+
*
|
|
840
|
+
* What a strand node uses it for: re-announcing its delegate peer id to the relay
|
|
841
|
+
* about to be re-dialed. A party control node running the relay server admits a
|
|
842
|
+
* strand node on an in-memory delegate grant, and a relay restart drops every
|
|
843
|
+
* grant it held — so a re-drive that did not re-announce first would be denied
|
|
844
|
+
* at the relay's connection gate.
|
|
845
|
+
*/
|
|
846
|
+
beforeRedrive?: () => Promise<void> | void;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* A running retry loop for one node + relay list. Obtained from
|
|
851
|
+
* {@link superviseRelayReservation}; its only imperative is {@link stop}.
|
|
852
|
+
*/
|
|
853
|
+
export interface RelayReservationSupervisor {
|
|
854
|
+
/**
|
|
855
|
+
* Resolves once the FIRST attempt has settled — so a caller can await startup
|
|
856
|
+
* exactly as it awaited a single {@link driveRelayReservation}, and the retries
|
|
857
|
+
* carry on in the background afterwards. Also resolves on {@link stop}, so an
|
|
858
|
+
* awaiting caller is never left hanging.
|
|
859
|
+
*/
|
|
860
|
+
readonly firstAttempt: Promise<void>;
|
|
861
|
+
/** True while a drive is in flight. */
|
|
862
|
+
readonly driving: boolean;
|
|
863
|
+
/** Epoch ms of the next scheduled tick; `null` while a drive is running. */
|
|
864
|
+
readonly retryAtMs: number | null;
|
|
865
|
+
/** Reason the last drive produced no reservation; `null` once one is held. */
|
|
866
|
+
readonly lastError: string | null;
|
|
867
|
+
/**
|
|
868
|
+
* Idempotent. Clears the timer so no further drive is scheduled, and CANCELS the
|
|
869
|
+
* drive already in flight: it stops dialing or polling, clears every timer it
|
|
870
|
+
* holds and reports `cancelled`, so a teardown does not trail an attempt against
|
|
871
|
+
* the node it is dismantling. That result is discarded — a cancelled drive is
|
|
872
|
+
* not a failure — and nothing follows it.
|
|
873
|
+
*/
|
|
874
|
+
stop(): void;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Keep asking for a relay reservation until one is held, then keep watching that
|
|
879
|
+
* it still is — the thing that makes a lost reservation recover on its own.
|
|
880
|
+
*
|
|
881
|
+
* Needed because nothing else re-drives: libp2p re-fills a search listener's
|
|
882
|
+
* freed slot from relay DISCOVERY, which cannot see a stock-identify relay from a
|
|
883
|
+
* cadre node's namespaced identify, and which is switched off outright on every
|
|
884
|
+
* node whose slots cadre supervises (see the module header). Without this loop a
|
|
885
|
+
* relay restart left a browser tab undialable until the user reloaded the page.
|
|
886
|
+
*
|
|
887
|
+
* Per tick:
|
|
888
|
+
* - a reservation is held → reset the backoff, clear the error, re-check in
|
|
889
|
+
* `checkMs`. No drive: a healthy node must not re-request every few seconds.
|
|
890
|
+
* - otherwise → un-poison the reservation store's relay filter (see
|
|
891
|
+
* {@link clearRelayFilterEntry}) and run ONE {@link driveRelayReservation}.
|
|
892
|
+
* If that lands a reservation, rejoin the healthy path above; if not, sleep
|
|
893
|
+
* the current backoff and double it up to `maxBackoffMs`.
|
|
894
|
+
*
|
|
895
|
+
* "Held" is judged PER RELAY — a circuit addr through one of `addrs`' relays
|
|
896
|
+
* ({@link circuitMultiaddrsVia}) — so several loops over one node, one relay each
|
|
897
|
+
* (a strand node), each re-drive exactly the relay they own. Every drive after the
|
|
898
|
+
* first is preceded by the optional `beforeRedrive` hook
|
|
899
|
+
* ({@link RelayReservationSupervisorOptions}).
|
|
900
|
+
*
|
|
901
|
+
* Starts immediately; the first tick runs before this returns.
|
|
902
|
+
*/
|
|
903
|
+
export function superviseRelayReservation(
|
|
904
|
+
node: Libp2p,
|
|
905
|
+
addrs: readonly string[],
|
|
906
|
+
opts?: RelayReservationSupervisorOptions
|
|
907
|
+
): RelayReservationSupervisor {
|
|
908
|
+
return new RelayReservationLoop(node, [...addrs], opts ?? {});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Forget that `addr`'s peer ever failed a reservation request, so the next
|
|
913
|
+
* attempt is actually made rather than refused out of hand.
|
|
914
|
+
*
|
|
915
|
+
* `ReservationStore` records a peer in `relayFilter` when its reservation request
|
|
916
|
+
* fails with `DialError` or `UnsupportedProtocolError`, and NOTHING on that path
|
|
917
|
+
* ever clears it: the reset lives in `#checkReservationCount`, which only runs
|
|
918
|
+
* when a reservation is genuinely removed, and `#removeReservation` early-returns
|
|
919
|
+
* when there was never one. So a relay that was briefly not-a-relay (or died
|
|
920
|
+
* between our dial and the hop request) stays permanently rejected with
|
|
921
|
+
* `The relay was previously invalid` — measured, not inferred.
|
|
922
|
+
*
|
|
923
|
+
* Fails soft in every direction: no `relayFilter`, no `remove`, an addr with no
|
|
924
|
+
* peer id, a malformed addr — all mean "no un-poisoning", never a throw. The
|
|
925
|
+
* retry loop still recovers the common case without this, because a relay that is
|
|
926
|
+
* simply DOWN fails our own dial first and never reaches the filter.
|
|
927
|
+
*
|
|
928
|
+
* Returns whether an entry was actually removed (for specs; callers ignore it).
|
|
929
|
+
*/
|
|
930
|
+
export function clearRelayFilterEntry(store: RelayReservationStoreLike, addr: string): boolean {
|
|
931
|
+
const filter = store.relayFilter;
|
|
932
|
+
if (typeof filter?.remove !== 'function') {
|
|
933
|
+
log('Reservation store has no removable relayFilter — cannot un-poison %s', addr);
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
try {
|
|
937
|
+
// The peer id is in the multiaddr, so this costs no dial and can run before one.
|
|
938
|
+
const peerId = trailingPeerId(multiaddr(addr));
|
|
939
|
+
if (peerId === null) {
|
|
940
|
+
// NOTE: an addr with no `/p2p/` component can still be dialed and reserved
|
|
941
|
+
// through (the peer id comes off the connection), but it can never be
|
|
942
|
+
// un-poisoned — so for THAT addr a poisoned filter is permanent again. No
|
|
943
|
+
// caller supplies such an addr today; if one ever does, resolve the peer id
|
|
944
|
+
// from the live connection instead of the multiaddr.
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
return filter.remove(peerIdFromString(peerId).toMultihash().bytes);
|
|
948
|
+
} catch (err) {
|
|
949
|
+
log('Could not clear the relayFilter entry for %s: %o', addr, err);
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/** The self-rescheduling loop behind {@link superviseRelayReservation}. */
|
|
955
|
+
class RelayReservationLoop implements RelayReservationSupervisor {
|
|
956
|
+
readonly firstAttempt: Promise<void>;
|
|
957
|
+
|
|
958
|
+
private readonly checkMs: number;
|
|
959
|
+
private readonly minBackoffMs: number;
|
|
960
|
+
private readonly maxBackoffMs: number;
|
|
961
|
+
private backoffMs: number;
|
|
962
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
963
|
+
/** Tripped by {@link stop}; the only thing that cancels a drive of this loop's. */
|
|
964
|
+
private readonly cancel = new AbortController();
|
|
965
|
+
private stopped = false;
|
|
966
|
+
private inFlight = false;
|
|
967
|
+
/** Drives started so far — the `beforeRedrive` hook runs from the second one on. */
|
|
968
|
+
private drives = 0;
|
|
969
|
+
private nextTickAtMs: number | null = null;
|
|
970
|
+
private failure: string | null = null;
|
|
971
|
+
private settleFirstAttempt: () => void = () => {};
|
|
972
|
+
private firstAttemptSettled = false;
|
|
973
|
+
|
|
974
|
+
constructor(
|
|
975
|
+
private readonly node: Libp2p,
|
|
976
|
+
private readonly addrs: readonly string[],
|
|
977
|
+
private readonly opts: RelayReservationSupervisorOptions
|
|
978
|
+
) {
|
|
979
|
+
// Clamped away from 0: a caller-supplied 0 would spin the loop hot.
|
|
980
|
+
this.checkMs = Math.max(1, opts.checkMs ?? DEFAULT_RELAY_CHECK_MS);
|
|
981
|
+
this.minBackoffMs = Math.max(1, opts.minBackoffMs ?? DEFAULT_RELAY_MIN_BACKOFF_MS);
|
|
982
|
+
this.maxBackoffMs = Math.max(
|
|
983
|
+
this.minBackoffMs,
|
|
984
|
+
opts.maxBackoffMs ?? DEFAULT_RELAY_MAX_BACKOFF_MS
|
|
985
|
+
);
|
|
986
|
+
this.backoffMs = this.minBackoffMs;
|
|
987
|
+
this.firstAttempt = new Promise((resolve) => {
|
|
988
|
+
this.settleFirstAttempt = resolve;
|
|
989
|
+
});
|
|
990
|
+
void this.tick();
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
get driving(): boolean {
|
|
994
|
+
return this.inFlight;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
get retryAtMs(): number | null {
|
|
998
|
+
return this.nextTickAtMs;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
get lastError(): string | null {
|
|
1002
|
+
return this.failure;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
stop(): void {
|
|
1006
|
+
if (this.stopped) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
this.stopped = true;
|
|
1010
|
+
// The scheduled attempt and the running one are two different things to end:
|
|
1011
|
+
// the timer covers the first, the signal the second.
|
|
1012
|
+
this.cancel.abort();
|
|
1013
|
+
if (this.timer !== null) {
|
|
1014
|
+
clearTimeout(this.timer);
|
|
1015
|
+
this.timer = null;
|
|
1016
|
+
}
|
|
1017
|
+
this.nextTickAtMs = null;
|
|
1018
|
+
this.settleFirst();
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
private async tick(): Promise<void> {
|
|
1022
|
+
if (this.stopped) {
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
this.nextTickAtMs = null;
|
|
1026
|
+
if (this.reservationHeld()) {
|
|
1027
|
+
this.onReservationHeld();
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
await this.driveOnce();
|
|
1031
|
+
if (this.stopped) {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
this.settleFirst();
|
|
1035
|
+
// A drive that LANDED a reservation resumes the healthy cadence immediately.
|
|
1036
|
+
// Backing off here instead would leave the first liveness check after a long
|
|
1037
|
+
// outage a fully grown backoff away (up to `maxBackoffMs`), so a reservation
|
|
1038
|
+
// lost again right after recovery would go unnoticed for that whole window.
|
|
1039
|
+
if (this.reservationHeld()) {
|
|
1040
|
+
this.onReservationHeld();
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
this.scheduleBackoff();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/** Per relay: only a circuit addr THROUGH one of this loop's relays counts. */
|
|
1047
|
+
private reservationHeld(): boolean {
|
|
1048
|
+
return circuitMultiaddrsVia(this.node, this.addrs).length > 0;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/** Healthy tick: nothing to request, so only reset and re-check later. */
|
|
1052
|
+
private onReservationHeld(): void {
|
|
1053
|
+
this.backoffMs = this.minBackoffMs;
|
|
1054
|
+
this.failure = null;
|
|
1055
|
+
this.settleFirst();
|
|
1056
|
+
this.schedule(this.checkMs);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
private async driveOnce(): Promise<void> {
|
|
1060
|
+
this.inFlight = true;
|
|
1061
|
+
try {
|
|
1062
|
+
if (this.drives > 0) {
|
|
1063
|
+
await this.runBeforeRedrive();
|
|
1064
|
+
// A stop that arrived while the hook ran: nothing follows it, so do not dial
|
|
1065
|
+
// a node that is being torn down.
|
|
1066
|
+
if (this.stopped) {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
this.drives += 1;
|
|
1071
|
+
this.unpoisonRelayFilter();
|
|
1072
|
+
const { error, cancelled } = await driveRelayReservation(this.node, this.addrs, {
|
|
1073
|
+
...this.opts,
|
|
1074
|
+
signal: this.cancel.signal
|
|
1075
|
+
});
|
|
1076
|
+
// A cancelled drive landed nothing AND failed at nothing, so it has no status
|
|
1077
|
+
// to report: recording it would surface a teardown artifact through
|
|
1078
|
+
// `getRelayReservationState()` as if the relay had refused.
|
|
1079
|
+
if (!this.stopped && !cancelled) {
|
|
1080
|
+
this.failure = error;
|
|
1081
|
+
}
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
// `driveRelayReservation` is fail-soft by contract, so reaching here means
|
|
1084
|
+
// that contract broke. The loop must survive it anyway: an escaping
|
|
1085
|
+
// rejection would leave `firstAttempt` pending FOREVER (hanging every
|
|
1086
|
+
// caller that awaits a reservation at startup) and schedule no further
|
|
1087
|
+
// attempt, which is the failure this whole supervisor exists to prevent.
|
|
1088
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1089
|
+
log('Relay reservation drive threw, which it should not: %s', message);
|
|
1090
|
+
if (!this.stopped) {
|
|
1091
|
+
this.failure = message;
|
|
1092
|
+
}
|
|
1093
|
+
} finally {
|
|
1094
|
+
this.inFlight = false;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
private scheduleBackoff(): void {
|
|
1099
|
+
const wait = this.backoffMs;
|
|
1100
|
+
this.backoffMs = Math.min(this.backoffMs * 2, this.maxBackoffMs);
|
|
1101
|
+
this.schedule(wait);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/** The caller's `beforeRedrive` hook, contained: a throw or rejection is logged, never propagated. */
|
|
1105
|
+
private async runBeforeRedrive(): Promise<void> {
|
|
1106
|
+
const hook = this.opts.beforeRedrive;
|
|
1107
|
+
if (hook === undefined) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
await hook();
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
log('beforeRedrive hook failed (%o); driving the reservation anyway: %o', this.addrs, err);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
private unpoisonRelayFilter(): void {
|
|
1118
|
+
const transport = findCircuitRelayTransport(this.node);
|
|
1119
|
+
if (transport === null) {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
for (const addr of this.addrs) {
|
|
1123
|
+
clearRelayFilterEntry(transport.reservationStore, addr);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
private schedule(ms: number): void {
|
|
1128
|
+
if (this.stopped) {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
this.nextTickAtMs = Date.now() + ms;
|
|
1132
|
+
this.timer = setTimeout(() => {
|
|
1133
|
+
this.timer = null;
|
|
1134
|
+
void this.tick();
|
|
1135
|
+
}, ms);
|
|
1136
|
+
// Node keeps the process alive while a timer is pending, so an un-unref'd
|
|
1137
|
+
// supervisor would hang every CLI run and vitest worker that ever reserved.
|
|
1138
|
+
// Cast + optional-chain: browsers and React Native have no `unref`.
|
|
1139
|
+
(this.timer as { unref?: () => void }).unref?.();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
private settleFirst(): void {
|
|
1143
|
+
if (this.firstAttemptSettled) {
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
this.firstAttemptSettled = true;
|
|
1147
|
+
this.settleFirstAttempt();
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Derive the reservation posture, reading the node's circuit addrs LIVE. Pure
|
|
1153
|
+
* apart from that read, so the precedence is testable on its own:
|
|
1154
|
+
*
|
|
1155
|
+
* | condition | status |
|
|
1156
|
+
* | ------------------------ | ----------------------------------------------- |
|
|
1157
|
+
* | no addrs supplied | `none` |
|
|
1158
|
+
* | live circuit addrs held | `reserved` (`error: null`) |
|
|
1159
|
+
* | a drive is in flight | `dialing` |
|
|
1160
|
+
* | a retry is scheduled | `retrying` |
|
|
1161
|
+
* | otherwise | `error` |
|
|
1162
|
+
*
|
|
1163
|
+
* `reserved` is checked BEFORE `error` on purpose: the circuit addrs are read
|
|
1164
|
+
* live, so a reservation that lands by ANY route supersedes a stale error string
|
|
1165
|
+
* without a second drive.
|
|
1166
|
+
*
|
|
1167
|
+
* `retrying` sits between the two so that `error` carries a sharper meaning:
|
|
1168
|
+
* NOBODY IS GOING TO TRY AGAIN. A lost reservation does come back on its own now,
|
|
1169
|
+
* as long as a {@link RelayReservationSupervisor} is running — that loop is the
|
|
1170
|
+
* only thing that re-drives, since libp2p re-fills a search listener's freed slot
|
|
1171
|
+
* from relay DISCOVERY, which cannot see a stock-identify relay and is switched off
|
|
1172
|
+
* on every node cadre supervises (see the module header). So `error` means the
|
|
1173
|
+
* supervisor was stopped, was never started, or there is no node at all.
|
|
1174
|
+
*
|
|
1175
|
+
* `retryAtMs` is the supervisor's next scheduled tick (`null` while a drive runs
|
|
1176
|
+
* or when there is no supervisor); it both selects `retrying` and is passed
|
|
1177
|
+
* through so a UI can count down to the next attempt.
|
|
1178
|
+
*/
|
|
1179
|
+
export function resolveRelayReservationState(
|
|
1180
|
+
node: Libp2p | null,
|
|
1181
|
+
addrs: readonly string[],
|
|
1182
|
+
lastError: string | null,
|
|
1183
|
+
driving: boolean,
|
|
1184
|
+
retryAtMs: number | null
|
|
1185
|
+
): RelayReservationState {
|
|
1186
|
+
if (addrs.length === 0) {
|
|
1187
|
+
return { status: 'none', addrs: [], circuitAddrs: [], error: null, retryAtMs: null };
|
|
1188
|
+
}
|
|
1189
|
+
const supplied = [...addrs];
|
|
1190
|
+
const circuitAddrs = node ? circuitMultiaddrs(node) : [];
|
|
1191
|
+
if (circuitAddrs.length > 0) {
|
|
1192
|
+
return { status: 'reserved', addrs: supplied, circuitAddrs, error: null, retryAtMs };
|
|
1193
|
+
}
|
|
1194
|
+
if (driving) {
|
|
1195
|
+
return {
|
|
1196
|
+
status: 'dialing',
|
|
1197
|
+
addrs: supplied,
|
|
1198
|
+
circuitAddrs: [],
|
|
1199
|
+
error: lastError,
|
|
1200
|
+
retryAtMs: null
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
// `driveRelayReservation` always reports a reason when it finishes without a
|
|
1204
|
+
// reservation, so `lastError` is normally set below. The fallback only covers
|
|
1205
|
+
// addrs recorded without a completed drive.
|
|
1206
|
+
const error = lastError ?? 'no circuit reservation held';
|
|
1207
|
+
if (retryAtMs !== null) {
|
|
1208
|
+
return { status: 'retrying', addrs: supplied, circuitAddrs: [], error, retryAtMs };
|
|
1209
|
+
}
|
|
1210
|
+
return { status: 'error', addrs: supplied, circuitAddrs: [], error, retryAtMs: null };
|
|
1211
|
+
}
|