@optimystic/db-p2p 1.0.0-beta.3 → 1.0.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/dist/src/cluster/block-transfer-service.d.ts +73 -1
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +70 -3
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +16 -1
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +73 -72
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/client.d.ts +4 -6
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +8 -13
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +2 -1
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +126 -6
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +378 -50
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.js +2 -2
- package/dist/src/cluster/race-resolution.d.ts +51 -7
- package/dist/src/cluster/race-resolution.d.ts.map +1 -1
- package/dist/src/cluster/race-resolution.js +71 -7
- package/dist/src/cluster/race-resolution.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +67 -5
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +120 -26
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +34 -47
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +8 -6
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/membership-source.js +1 -1
- package/dist/src/cohort-topic/membership-source.js.map +1 -1
- package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/peer-codec.js +5 -2
- package/dist/src/cohort-topic/peer-codec.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +24 -3
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +36 -3
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/cohort-topic/topic-router.d.ts +9 -0
- package/dist/src/cohort-topic/topic-router.d.ts.map +1 -1
- package/dist/src/cohort-topic/topic-router.js +12 -2
- package/dist/src/cohort-topic/topic-router.js.map +1 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +15 -3
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +78 -17
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +234 -147
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +35 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +151 -26
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/libp2p-node-rn.d.ts +2 -2
- package/dist/src/libp2p-node-rn.d.ts.map +1 -1
- package/dist/src/libp2p-node-rn.js.map +1 -1
- package/dist/src/libp2p-node.d.ts +2 -2
- package/dist/src/libp2p-node.d.ts.map +1 -1
- package/dist/src/libp2p-node.js.map +1 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +6 -1
- package/dist/src/logger.js.map +1 -1
- package/dist/src/matchmaking/query-transport.d.ts +3 -4
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
- package/dist/src/matchmaking/query-transport.js +9 -9
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +10 -5
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +11 -2
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/network/relay-reservation.d.ts +170 -0
- package/dist/src/network/relay-reservation.d.ts.map +1 -0
- package/dist/src/network/relay-reservation.js +527 -0
- package/dist/src/network/relay-reservation.js.map +1 -0
- package/dist/src/pend-validation.d.ts +3 -2
- package/dist/src/pend-validation.d.ts.map +1 -1
- package/dist/src/pend-validation.js +3 -2
- package/dist/src/pend-validation.js.map +1 -1
- package/dist/src/protocol-client.d.ts +8 -1
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +11 -0
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/reactivity/forwarder-host.d.ts +2 -1
- package/dist/src/reactivity/forwarder-host.d.ts.map +1 -1
- package/dist/src/reactivity/forwarder-host.js +2 -1
- package/dist/src/reactivity/forwarder-host.js.map +1 -1
- package/dist/src/reactivity/origination-manager.d.ts +3 -3
- package/dist/src/reactivity/origination-manager.js +3 -3
- package/dist/src/reactivity/recover-transport.d.ts +22 -14
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +32 -19
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/reactivity/subscription-manager.d.ts +2 -2
- package/dist/src/reactivity/topic-bytes.d.ts +6 -7
- package/dist/src/reactivity/topic-bytes.d.ts.map +1 -1
- package/dist/src/reactivity/topic-bytes.js +8 -9
- package/dist/src/reactivity/topic-bytes.js.map +1 -1
- package/dist/src/repo/client.d.ts +5 -2
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +49 -40
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +77 -5
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +131 -20
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +238 -122
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +625 -472
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/i-under-replication-ledger.d.ts +87 -0
- package/dist/src/repo/i-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/i-under-replication-ledger.js +2 -0
- package/dist/src/repo/i-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts +91 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.js +287 -0
- package/dist/src/repo/kv-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/responsibility.d.ts +37 -0
- package/dist/src/repo/responsibility.d.ts.map +1 -0
- package/dist/src/repo/responsibility.js +37 -0
- package/dist/src/repo/responsibility.js.map +1 -0
- package/dist/src/repo/service.d.ts +44 -15
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +64 -27
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/repo/stuck-reservation.d.ts +127 -0
- package/dist/src/repo/stuck-reservation.d.ts.map +1 -0
- package/dist/src/repo/stuck-reservation.js +179 -0
- package/dist/src/repo/stuck-reservation.js.map +1 -0
- package/dist/src/repo/under-replication-drain.d.ts +208 -0
- package/dist/src/repo/under-replication-drain.d.ts.map +1 -0
- package/dist/src/repo/under-replication-drain.js +461 -0
- package/dist/src/repo/under-replication-drain.js.map +1 -0
- package/dist/src/rn.d.ts +5 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +5 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-latch.d.ts +3 -2
- package/dist/src/storage/block-latch.d.ts.map +1 -1
- package/dist/src/storage/block-latch.js +7 -4
- package/dist/src/storage/block-latch.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts +45 -3
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +156 -9
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +57 -5
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/pending-claim.d.ts +167 -0
- package/dist/src/storage/pending-claim.d.ts.map +1 -0
- package/dist/src/storage/pending-claim.js +156 -0
- package/dist/src/storage/pending-claim.js.map +1 -0
- package/dist/src/storage/raw-store-codec.d.ts +13 -0
- package/dist/src/storage/raw-store-codec.d.ts.map +1 -1
- package/dist/src/storage/raw-store-codec.js +24 -3
- package/dist/src/storage/raw-store-codec.js.map +1 -1
- package/dist/src/storage/restoration-coordinator.d.ts.map +1 -1
- package/dist/src/storage/restoration-coordinator.js +2 -1
- package/dist/src/storage/restoration-coordinator.js.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.js +3 -3
- package/dist/src/storage/ring-shift-coordinator.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +122 -5
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +245 -68
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/struct.d.ts +58 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +77 -11
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +226 -100
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +27 -3
- package/src/cluster/block-transfer-service.ts +132 -4
- package/src/cluster/block-transfer.ts +79 -84
- package/src/cluster/client.ts +115 -119
- package/src/cluster/cluster-policy.ts +2 -1
- package/src/cluster/cluster-repo.ts +422 -56
- package/src/cluster/commit-proof.ts +2 -2
- package/src/cluster/race-resolution.ts +76 -7
- package/src/cluster/rebalance-monitor.ts +677 -539
- package/src/cluster/spread-on-churn.ts +34 -54
- package/src/cohort-topic/host.ts +8 -6
- package/src/cohort-topic/membership-source.ts +1 -1
- package/src/cohort-topic/peer-codec.ts +34 -31
- package/src/cohort-topic/stream-util.ts +39 -4
- package/src/cohort-topic/topic-router.ts +12 -2
- package/src/dispute/client.ts +17 -7
- package/src/index.ts +64 -59
- package/src/libp2p-key-network.ts +1377 -1260
- package/src/libp2p-node-base.ts +186 -27
- package/src/libp2p-node-rn.ts +31 -30
- package/src/libp2p-node.ts +37 -36
- package/src/logger.ts +6 -1
- package/src/matchmaking/query-transport.ts +9 -9
- package/src/network/network-manager-service.ts +16 -5
- package/src/network/relay-reservation.ts +636 -0
- package/src/pend-validation.ts +3 -2
- package/src/protocol-client.ts +13 -1
- package/src/reactivity/forwarder-host.ts +2 -1
- package/src/reactivity/origination-manager.ts +3 -3
- package/src/reactivity/recover-transport.ts +39 -22
- package/src/reactivity/subscription-manager.ts +2 -2
- package/src/reactivity/topic-bytes.ts +35 -37
- package/src/repo/client.ts +172 -173
- package/src/repo/cluster-coordinator.ts +145 -19
- package/src/repo/coordinator-repo.ts +652 -505
- package/src/repo/i-under-replication-ledger.ts +89 -0
- package/src/repo/kv-under-replication-ledger.ts +302 -0
- package/src/repo/responsibility.ts +48 -0
- package/src/repo/service.ts +76 -39
- package/src/repo/stuck-reservation.ts +224 -0
- package/src/repo/under-replication-drain.ts +582 -0
- package/src/rn.ts +74 -69
- package/src/storage/block-latch.ts +7 -4
- package/src/storage/block-storage.ts +161 -10
- package/src/storage/i-block-storage.ts +60 -5
- package/src/storage/pending-claim.ts +200 -0
- package/src/storage/raw-store-codec.ts +26 -3
- package/src/storage/restoration-coordinator.ts +212 -211
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/storage-repo.ts +280 -72
- package/src/storage/struct.ts +58 -0
- package/src/testing/mesh-harness.ts +293 -112
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps a relay-only node's circuit reservation alive.
|
|
3
|
+
*
|
|
4
|
+
* A phone or browser reaches its group only through a relay server: it listens on
|
|
5
|
+
* `<relay address>/p2p/<relay id>/p2p-circuit`, holds a slot ("reservation") on that relay, and
|
|
6
|
+
* advertises the circuit address other peers dial. libp2p asks the relay for that slot once, from
|
|
7
|
+
* inside `start()`, and never again. The slot is lost, and the node stays unreachable until the
|
|
8
|
+
* app restarts, whenever the relay restarts, the connection to it drops, or libp2p's own routine
|
|
9
|
+
* renewal of the slot runs — all three verified against the installed `@libp2p/circuit-relay-v2`
|
|
10
|
+
* 4.1.3 (see the `NOTE:` at {@link findCircuitRelayTransport} for the exact mechanics).
|
|
11
|
+
*
|
|
12
|
+
* This module gives the node its own supervisor for every such relay, in two parts:
|
|
13
|
+
*
|
|
14
|
+
* 1. {@link planRelayListenAddrs} rewrites each relay-naming circuit listen address into a bare
|
|
15
|
+
* `/p2p-circuit` entry and records the relay it named. libp2p treats the bare shape as a
|
|
16
|
+
* "search" listener: it registers a pending slot and publishes any `discovered` reservation
|
|
17
|
+
* that fills it, including the re-created one after a renewal. A listener on the relay-naming
|
|
18
|
+
* ("configured") shape publishes only from inside its own `listen()`, so nothing done after
|
|
19
|
+
* start can ever bring its address back — which is why the rewrite is not optional.
|
|
20
|
+
* 2. {@link superviseRelayReservation} runs one supervisor per recorded relay. It dials the relay,
|
|
21
|
+
* asks the circuit-relay transport's reservation store for a `discovered` slot on it, waits
|
|
22
|
+
* until the node advertises a circuit address through it, and repeats that whenever the address
|
|
23
|
+
* goes away. "Held" is judged per relay ({@link routesThroughRelay}), so one relay's slot never
|
|
24
|
+
* satisfies another relay's supervisor.
|
|
25
|
+
*
|
|
26
|
+
* `createLibp2pNodeBase` applies the plan before building libp2p's options, starts the supervisors
|
|
27
|
+
* first thing after `node.start()`, and awaits their first drives so a relay that cannot be
|
|
28
|
+
* reserved still rejects node creation, as it did when libp2p reserved from `listen()`.
|
|
29
|
+
*
|
|
30
|
+
* A host that passes a bare `/p2p-circuit` itself is left alone and not supervised: it owns that
|
|
31
|
+
* reservation (Sereus runs its own supervisor over exactly that shape).
|
|
32
|
+
*
|
|
33
|
+
* Everything here is fail-soft: a drive never throws, a stopped supervisor never schedules
|
|
34
|
+
* anything, and every timer is `unref`'d so a pending retry cannot keep a Node process alive.
|
|
35
|
+
* Adapted from `sereus/packages/cadre-core/src/relay-reservation.ts`, reduced to what this node
|
|
36
|
+
* needs: one relay per supervisor, event-driven re-checks, and no status surface.
|
|
37
|
+
*/
|
|
38
|
+
import { multiaddr, type Component } from '@multiformats/multiaddr';
|
|
39
|
+
import { peerIdFromString } from '@libp2p/peer-id';
|
|
40
|
+
import type { Libp2p } from 'libp2p';
|
|
41
|
+
import type { PeerId } from '@libp2p/interface';
|
|
42
|
+
import { routesThroughRelay } from '../peer-address-book.js';
|
|
43
|
+
import { createLogger } from '../logger.js';
|
|
44
|
+
|
|
45
|
+
const log = createLogger('relay-reservation');
|
|
46
|
+
|
|
47
|
+
/** The bare "search" circuit listen address: one pending slot, filled by whoever asks for it. */
|
|
48
|
+
export const CIRCUIT_SEARCH_LISTEN_ADDR = '/p2p-circuit';
|
|
49
|
+
|
|
50
|
+
/** A relay this node must keep a reservation on itself. */
|
|
51
|
+
export interface SupervisedRelay {
|
|
52
|
+
/** Where to dial the relay: the listen entry without its trailing `/p2p-circuit`. */
|
|
53
|
+
readonly dialAddr: string;
|
|
54
|
+
/** The relay's peer id in the form `PeerId.toString()` produces, whatever spelling the listen entry used. */
|
|
55
|
+
readonly peerId: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** What {@link planRelayListenAddrs} hands back: the addresses libp2p listens on, and the relays to supervise. */
|
|
59
|
+
export interface RelayListenPlan {
|
|
60
|
+
readonly listenAddrs: string[];
|
|
61
|
+
readonly supervisedRelays: SupervisedRelay[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Split a node's listen addresses into what libp2p should listen on and which relays this node
|
|
66
|
+
* must supervise.
|
|
67
|
+
*
|
|
68
|
+
* A relay-naming circuit listen address — exactly one `p2p-circuit` component, last, with a `p2p`
|
|
69
|
+
* component (the relay's peer id) immediately before it — becomes one bare
|
|
70
|
+
* {@link CIRCUIT_SEARCH_LISTEN_ADDR} entry plus one supervised relay. libp2p keeps listen addresses
|
|
71
|
+
* as a plain array with no de-duplication, so N such entries give N bare listeners and N pending
|
|
72
|
+
* slots. Two entries naming one relay (by peer id, whatever the spelling) give one of each; the
|
|
73
|
+
* first entry's dial address wins. Everything else passes through untouched: a host's own bare
|
|
74
|
+
* `/p2p-circuit` (the host owns that reservation), TCP and WebSocket entries, and anything
|
|
75
|
+
* malformed or multi-hop, which libp2p rejects itself.
|
|
76
|
+
*/
|
|
77
|
+
export function planRelayListenAddrs(listenAddrs: readonly string[]): RelayListenPlan {
|
|
78
|
+
const listen: string[] = [];
|
|
79
|
+
const supervised: SupervisedRelay[] = [];
|
|
80
|
+
const seen = new Set<string>();
|
|
81
|
+
for (const addr of listenAddrs) {
|
|
82
|
+
const relay = relayNamedByListenAddr(addr);
|
|
83
|
+
if (relay === undefined) {
|
|
84
|
+
listen.push(addr);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (seen.has(relay.peerId)) continue;
|
|
88
|
+
seen.add(relay.peerId);
|
|
89
|
+
supervised.push(relay);
|
|
90
|
+
listen.push(CIRCUIT_SEARCH_LISTEN_ADDR);
|
|
91
|
+
}
|
|
92
|
+
return { listenAddrs: listen, supervisedRelays: supervised };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The relay a listen address names, or `undefined` when it is not a relay-naming circuit listen address. */
|
|
96
|
+
function relayNamedByListenAddr(addr: string): SupervisedRelay | undefined {
|
|
97
|
+
let components: Component[];
|
|
98
|
+
try {
|
|
99
|
+
components = multiaddr(addr).getComponents();
|
|
100
|
+
} catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const circuitHops = components.filter(c => c.name === 'p2p-circuit').length;
|
|
104
|
+
const last = components[components.length - 1];
|
|
105
|
+
const relayHop = components[components.length - 2];
|
|
106
|
+
if (circuitHops !== 1 || last?.name !== 'p2p-circuit' || relayHop?.name !== 'p2p' || relayHop.value === undefined) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
let peerId: string;
|
|
110
|
+
try {
|
|
111
|
+
peerId = peerIdFromString(relayHop.value).toString();
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return { dialAddr: multiaddr(components.slice(0, -1)).toString(), peerId };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The slice of `@libp2p/utils`' cuckoo `Filter` the reservation store remembers failed relays in.
|
|
120
|
+
* `remove` is optional on that interface, so its absence must degrade, never throw.
|
|
121
|
+
*/
|
|
122
|
+
export interface RelayFilterLike {
|
|
123
|
+
has(item: Uint8Array): boolean;
|
|
124
|
+
remove?(item: Uint8Array): boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The slice of `@libp2p/circuit-relay-v2`'s transport-side `ReservationStore` this module drives.
|
|
129
|
+
* Structural, because the class is internal to that package: naming only the members used keeps
|
|
130
|
+
* the coupling small and greppable, and lets {@link findCircuitRelayTransport} duck-type it.
|
|
131
|
+
*/
|
|
132
|
+
export interface RelayReservationStoreLike {
|
|
133
|
+
addRelay(peerId: PeerId, type: 'discovered' | 'configured'): Promise<unknown>;
|
|
134
|
+
hasReservation(peerId: PeerId): boolean;
|
|
135
|
+
relayFilter?: RelayFilterLike;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The slice of libp2p's circuit-relay transport that owns the reservation store. */
|
|
139
|
+
export interface CircuitRelayTransportLike {
|
|
140
|
+
reservationStore: RelayReservationStoreLike;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The running node's circuit-relay transport, or `null` when it has none.
|
|
145
|
+
*
|
|
146
|
+
* NOTE: the ONE place this package reaches libp2p internals for relay reservations, pinned against
|
|
147
|
+
* `libp2p` 3.1.3 and `@libp2p/circuit-relay-v2` 4.1.3 by `test/relay-reservation-seam.spec.ts`,
|
|
148
|
+
* which must fail loudly if an upgrade moves any of it. There is no public route: the `Libp2p`
|
|
149
|
+
* interface has no `listen`, and nothing public exposes the reservation store. `node.components`
|
|
150
|
+
* is a real public field on libp2p's node class, just not on the interface; `transportManager`
|
|
151
|
+
* is a component; the circuit-relay transport is the one whose `reservationStore` has `addRelay`
|
|
152
|
+
* and `hasReservation`. What the store does on 4.1.3, read from
|
|
153
|
+
* `node_modules/@libp2p/circuit-relay-v2/src/transport/`: `listener.ts` publishes a `configured`
|
|
154
|
+
* reservation only from inside `listen()` and ignores it on `relay:created-reservation`, while a
|
|
155
|
+
* bare listener publishes any `discovered` reservation carrying its pending id; and
|
|
156
|
+
* `reservation-store.ts` drops a reservation on `connection:close`, re-queues the pending id only
|
|
157
|
+
* for `discovered` ones, removes-then-recreates on refresh, and adds a relay whose request failed
|
|
158
|
+
* with `DialError` or `UnsupportedProtocolError` to the private `relayFilter`, refusing it
|
|
159
|
+
* afterwards with "The relay was previously invalid" until the filter is reset.
|
|
160
|
+
*/
|
|
161
|
+
export function findCircuitRelayTransport(node: Libp2p): CircuitRelayTransportLike | null {
|
|
162
|
+
for (const transport of nodeTransports(node)) {
|
|
163
|
+
const store = (transport as { reservationStore?: unknown }).reservationStore;
|
|
164
|
+
if (isReservationStore(store)) {
|
|
165
|
+
return { reservationStore: store };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** `components` is a real field on libp2p's node class but not on the `Libp2p` interface. */
|
|
172
|
+
interface Libp2pComponentsLike {
|
|
173
|
+
components?: {
|
|
174
|
+
transportManager?: {
|
|
175
|
+
getTransports?: () => readonly unknown[];
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function nodeTransports(node: Libp2p): readonly unknown[] {
|
|
181
|
+
const manager = (node as Libp2p & Libp2pComponentsLike).components?.transportManager;
|
|
182
|
+
if (typeof manager?.getTransports !== 'function') {
|
|
183
|
+
log.error('no transportManager.getTransports() on this node — libp2p internals moved?');
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
return manager.getTransports();
|
|
188
|
+
} catch (err) {
|
|
189
|
+
log.error('transportManager.getTransports() threw: %o', err);
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isReservationStore(value: unknown): value is RelayReservationStoreLike {
|
|
195
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
196
|
+
const store = value as Partial<RelayReservationStoreLike>;
|
|
197
|
+
return typeof store.addRelay === 'function' && typeof store.hasReservation === 'function';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Fail node creation, before anything starts, when a listen address names a relay but no transport
|
|
202
|
+
* could ever reserve on it. libp2p would reject the bare `/p2p-circuit` entry itself during
|
|
203
|
+
* `start()`, with a generic unsupported-listen-address error; this names the actual omission.
|
|
204
|
+
*/
|
|
205
|
+
export function assertCircuitRelayTransport(node: Libp2p, relays: readonly SupervisedRelay[]): void {
|
|
206
|
+
if (relays.length === 0) return;
|
|
207
|
+
if (findCircuitRelayTransport(node) !== null) return;
|
|
208
|
+
throw new Error(
|
|
209
|
+
`listen address names a relay (${relays[0]!.dialAddr}) but the node has no circuit-relay transport — add circuitRelayTransport() to transports`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Fail node creation, before anything is built, when a listen address names a relay but
|
|
215
|
+
* `announceAddrs` is set. libp2p advertises ONLY the announce set then (the readme's "replaces the
|
|
216
|
+
* advertised set entirely"), so the circuit address through the relay could never appear in
|
|
217
|
+
* `node.getMultiaddrs()`, nobody could learn it, and every supervisor drive would time out waiting
|
|
218
|
+
* for it: node creation would reject after a full drive deadline with a message about publishing
|
|
219
|
+
* rather than about the configuration. `appendAnnounceAddrs` keeps the listener addresses and is fine.
|
|
220
|
+
*/
|
|
221
|
+
export function assertRelayAddrsAdvertisable(relays: readonly SupervisedRelay[], announceAddrs: readonly string[] | undefined): void {
|
|
222
|
+
if (relays.length === 0 || announceAddrs === undefined || announceAddrs.length === 0) return;
|
|
223
|
+
throw new Error(
|
|
224
|
+
`listen address names a relay (${relays[0]!.dialAddr}) but announceAddrs replaces the advertised address set, so the circuit address through it could never be advertised — drop announceAddrs or use appendAnnounceAddrs`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Forget that a relay ever failed a reservation request, so the next request is actually made
|
|
230
|
+
* rather than refused with "The relay was previously invalid".
|
|
231
|
+
*
|
|
232
|
+
* The store resets its filter when a reservation is removed while a pending slot exists, which a
|
|
233
|
+
* bare listener always has, so this is a belt-and-braces step: the poisoning path this guards is a
|
|
234
|
+
* failed request from libp2p's own relay discovery while the relay was down. Fails soft in every
|
|
235
|
+
* direction; returns whether an entry was actually removed (for specs).
|
|
236
|
+
*/
|
|
237
|
+
export function clearRelayFilterEntry(store: RelayReservationStoreLike, relayPeerId: PeerId): boolean {
|
|
238
|
+
const filter = store.relayFilter;
|
|
239
|
+
if (typeof filter?.remove !== 'function') return false;
|
|
240
|
+
try {
|
|
241
|
+
return filter.remove(relayPeerId.toMultihash().bytes);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
log('could not clear the relayFilter entry for %s: %o', relayPeerId.toString(), err);
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Deadline for one drive: dial, reservation request and the wait for the address share it. */
|
|
249
|
+
export const DEFAULT_RELAY_DRIVE_TIMEOUT_MS = 10_000;
|
|
250
|
+
/** Gap between liveness checks while the reservation is held; a fallback in case an event is missed. */
|
|
251
|
+
export const DEFAULT_RELAY_CHECK_MS = 5_000;
|
|
252
|
+
/** Backoff before the first retry after a failed drive. */
|
|
253
|
+
export const DEFAULT_RELAY_MIN_BACKOFF_MS = 1_000;
|
|
254
|
+
/**
|
|
255
|
+
* Backoff ceiling. It bounds recovery time after a long relay outage, because nothing else
|
|
256
|
+
* reconnects a lone phone to its relay; a WebSocket dial every 30 s during an outage is cheap.
|
|
257
|
+
*/
|
|
258
|
+
export const DEFAULT_RELAY_MAX_BACKOFF_MS = 30_000;
|
|
259
|
+
/** How often a drive re-reads the address list while waiting for the address to be published. */
|
|
260
|
+
export const DEFAULT_RELAY_POLL_MS = 100;
|
|
261
|
+
|
|
262
|
+
export interface RelayReservationSupervisorOptions {
|
|
263
|
+
/** Default {@link DEFAULT_RELAY_DRIVE_TIMEOUT_MS}. */
|
|
264
|
+
driveTimeoutMs?: number;
|
|
265
|
+
/** Default {@link DEFAULT_RELAY_CHECK_MS}. */
|
|
266
|
+
checkMs?: number;
|
|
267
|
+
/** Default {@link DEFAULT_RELAY_MIN_BACKOFF_MS}. */
|
|
268
|
+
minBackoffMs?: number;
|
|
269
|
+
/** Default {@link DEFAULT_RELAY_MAX_BACKOFF_MS}. */
|
|
270
|
+
maxBackoffMs?: number;
|
|
271
|
+
/** Default {@link DEFAULT_RELAY_POLL_MS}. */
|
|
272
|
+
pollMs?: number;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** A running supervisor for one node and one relay. Obtained from {@link superviseRelayReservation}. */
|
|
276
|
+
export interface RelayReservationSupervisor {
|
|
277
|
+
readonly relay: SupervisedRelay;
|
|
278
|
+
/**
|
|
279
|
+
* Settles once the first drive has: `null` when the reservation was held, otherwise the reason
|
|
280
|
+
* it was not. Also settles on {@link stop}, so an awaiting caller is never left hanging.
|
|
281
|
+
*/
|
|
282
|
+
readonly firstDrive: Promise<string | null>;
|
|
283
|
+
/** Whether the node currently advertises a circuit address through this relay. Read live. */
|
|
284
|
+
readonly held: boolean;
|
|
285
|
+
/** True while a drive is in flight. */
|
|
286
|
+
readonly driving: boolean;
|
|
287
|
+
/** Epoch ms of the next scheduled check or retry; `null` while driving or once stopped. */
|
|
288
|
+
readonly retryAtMs: number | null;
|
|
289
|
+
/** Reason the last drive produced no reservation; `null` once one is held. */
|
|
290
|
+
readonly lastError: string | null;
|
|
291
|
+
/**
|
|
292
|
+
* Idempotent. Clears the pending timer, removes both event listeners, aborts an in-flight dial,
|
|
293
|
+
* and discards the result of an in-flight reservation request without starting anything after it.
|
|
294
|
+
*/
|
|
295
|
+
stop(): void;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Keep asking `relay` for a reservation until one is held, then keep watching that it still is.
|
|
300
|
+
*
|
|
301
|
+
* Drives run on: the first tick, right away; libp2p's `self:peer:update` (the listener withdrew or
|
|
302
|
+
* added addresses); `peer:connect` for this relay (a dial by anything on this node reconnected it,
|
|
303
|
+
* which turns a partner's dial through the relay into an immediate re-reserve, and resets the
|
|
304
|
+
* backoff); a liveness poll every `checkMs` while held; and a doubling backoff from `minBackoffMs`
|
|
305
|
+
* to `maxBackoffMs` after a failed drive. Drives are serialized per relay: a trigger that arrives
|
|
306
|
+
* mid-drive does nothing by itself, because every drive ends with the same re-check and reschedule.
|
|
307
|
+
*
|
|
308
|
+
* `HadEnoughRelaysError` from the reservation store means this node's pending slot was already
|
|
309
|
+
* filled with a reservation on a different relay (libp2p's relay discovery, see the accepted
|
|
310
|
+
* tradeoff in `libp2p-node-base.ts`); it is logged once per episode and retried at the backoff cap, since it
|
|
311
|
+
* recovers on its own only if that other reservation drops.
|
|
312
|
+
*/
|
|
313
|
+
export function superviseRelayReservation(
|
|
314
|
+
node: Libp2p,
|
|
315
|
+
relay: SupervisedRelay,
|
|
316
|
+
opts: RelayReservationSupervisorOptions = {}
|
|
317
|
+
): RelayReservationSupervisor {
|
|
318
|
+
return new RelayReservationLoop(node, relay, opts);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Every supervisor a node runs, with the two operations the node factory needs. */
|
|
322
|
+
export interface RelayReservationSupervisors {
|
|
323
|
+
readonly supervisors: readonly RelayReservationSupervisor[];
|
|
324
|
+
/** Resolves once every first drive held its reservation; rejects naming every relay that did not. */
|
|
325
|
+
awaitFirstDrives(): Promise<void>;
|
|
326
|
+
/** Stops every supervisor. Idempotent. */
|
|
327
|
+
stop(): void;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** One {@link superviseRelayReservation} per relay, started immediately. */
|
|
331
|
+
export function superviseRelayReservations(
|
|
332
|
+
node: Libp2p,
|
|
333
|
+
relays: readonly SupervisedRelay[],
|
|
334
|
+
opts: RelayReservationSupervisorOptions = {}
|
|
335
|
+
): RelayReservationSupervisors {
|
|
336
|
+
const supervisors = relays.map(relay => superviseRelayReservation(node, relay, opts));
|
|
337
|
+
return {
|
|
338
|
+
supervisors,
|
|
339
|
+
async awaitFirstDrives(): Promise<void> {
|
|
340
|
+
const outcomes = await Promise.all(supervisors.map(async s => ({ relay: s.relay, reason: await s.firstDrive })));
|
|
341
|
+
const failed = outcomes.filter(o => o.reason !== null);
|
|
342
|
+
if (failed.length === 0) return;
|
|
343
|
+
throw new Error(failed.map(f => `could not reserve a circuit on relay ${f.relay.dialAddr}: ${f.reason}`).join('; '));
|
|
344
|
+
},
|
|
345
|
+
stop(): void {
|
|
346
|
+
for (const s of supervisors) s.stop();
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Sentinel resolved by the deadline race around `addRelay`, which takes no signal. */
|
|
352
|
+
const DEADLINE_PASSED = Symbol('relay-reservation-deadline-passed');
|
|
353
|
+
|
|
354
|
+
/** Milliseconds left until `deadline`, never negative. */
|
|
355
|
+
const remaining = (deadline: number): number => Math.max(0, deadline - Date.now());
|
|
356
|
+
|
|
357
|
+
const errorMessage = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
358
|
+
|
|
359
|
+
/** A pending timer must not keep a stopped Node process alive; browsers and React Native have no `unref`. */
|
|
360
|
+
function unref(timer: ReturnType<typeof setTimeout>): void {
|
|
361
|
+
(timer as { unref?: () => void }).unref?.();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Resolve after `ms`, on an unref'd timer. */
|
|
365
|
+
function delay(ms: number): Promise<void> {
|
|
366
|
+
return new Promise(resolve => {
|
|
367
|
+
unref(setTimeout(resolve, ms));
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** The self-rescheduling loop behind {@link superviseRelayReservation}. */
|
|
372
|
+
class RelayReservationLoop implements RelayReservationSupervisor {
|
|
373
|
+
readonly firstDrive: Promise<string | null>;
|
|
374
|
+
|
|
375
|
+
private readonly relayPeerId: PeerId;
|
|
376
|
+
private readonly driveTimeoutMs: number;
|
|
377
|
+
private readonly checkMs: number;
|
|
378
|
+
private readonly minBackoffMs: number;
|
|
379
|
+
private readonly maxBackoffMs: number;
|
|
380
|
+
private readonly pollMs: number;
|
|
381
|
+
private backoffMs: number;
|
|
382
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
383
|
+
private stopped = false;
|
|
384
|
+
private inFlight = false;
|
|
385
|
+
private nextTickAtMs: number | null = null;
|
|
386
|
+
private failure: string | null = null;
|
|
387
|
+
private dialAbort: AbortController | null = null;
|
|
388
|
+
private slotTakenLogged = false;
|
|
389
|
+
private settleFirstDrive: (reason: string | null) => void = () => {};
|
|
390
|
+
private firstDriveSettled = false;
|
|
391
|
+
|
|
392
|
+
constructor(
|
|
393
|
+
private readonly node: Libp2p,
|
|
394
|
+
readonly relay: SupervisedRelay,
|
|
395
|
+
opts: RelayReservationSupervisorOptions
|
|
396
|
+
) {
|
|
397
|
+
this.relayPeerId = peerIdFromString(relay.peerId);
|
|
398
|
+
// Clamped away from 0: a caller-supplied 0 would spin the loop hot.
|
|
399
|
+
this.driveTimeoutMs = Math.max(1, opts.driveTimeoutMs ?? DEFAULT_RELAY_DRIVE_TIMEOUT_MS);
|
|
400
|
+
this.checkMs = Math.max(1, opts.checkMs ?? DEFAULT_RELAY_CHECK_MS);
|
|
401
|
+
this.minBackoffMs = Math.max(1, opts.minBackoffMs ?? DEFAULT_RELAY_MIN_BACKOFF_MS);
|
|
402
|
+
this.maxBackoffMs = Math.max(this.minBackoffMs, opts.maxBackoffMs ?? DEFAULT_RELAY_MAX_BACKOFF_MS);
|
|
403
|
+
this.pollMs = Math.max(1, opts.pollMs ?? DEFAULT_RELAY_POLL_MS);
|
|
404
|
+
this.backoffMs = this.minBackoffMs;
|
|
405
|
+
this.firstDrive = new Promise(resolve => {
|
|
406
|
+
this.settleFirstDrive = resolve;
|
|
407
|
+
});
|
|
408
|
+
node.addEventListener('self:peer:update', this.onSelfPeerUpdate);
|
|
409
|
+
node.addEventListener('peer:connect', this.onPeerConnect);
|
|
410
|
+
void this.tick('start');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
get held(): boolean {
|
|
414
|
+
return this.node.getMultiaddrs().some(addr => routesThroughRelay(addr.toString(), this.relay.peerId, log));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
get driving(): boolean {
|
|
418
|
+
return this.inFlight;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
get retryAtMs(): number | null {
|
|
422
|
+
return this.nextTickAtMs;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
get lastError(): string | null {
|
|
426
|
+
return this.failure;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
stop(): void {
|
|
430
|
+
if (this.stopped) return;
|
|
431
|
+
this.stopped = true;
|
|
432
|
+
this.clearTimer();
|
|
433
|
+
this.nextTickAtMs = null;
|
|
434
|
+
this.node.removeEventListener('self:peer:update', this.onSelfPeerUpdate);
|
|
435
|
+
this.node.removeEventListener('peer:connect', this.onPeerConnect);
|
|
436
|
+
this.dialAbort?.abort();
|
|
437
|
+
this.settleFirst('the relay reservation supervisor was stopped before its first drive settled');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private readonly onSelfPeerUpdate = (): void => {
|
|
441
|
+
this.wake('self:peer:update', false);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
private readonly onPeerConnect = (evt: CustomEvent<PeerId>): void => {
|
|
445
|
+
if (!evt.detail.equals(this.relayPeerId)) return;
|
|
446
|
+
this.wake('peer:connect', true);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Something may have changed the reservation: re-check now. Mid-drive, only the backoff reset
|
|
451
|
+
* takes effect — the drive's own tail re-checks and reschedules, so nothing is lost.
|
|
452
|
+
*/
|
|
453
|
+
private wake(trigger: string, resetBackoff: boolean): void {
|
|
454
|
+
if (this.stopped) return;
|
|
455
|
+
if (resetBackoff) this.backoffMs = this.minBackoffMs;
|
|
456
|
+
if (this.inFlight) return;
|
|
457
|
+
this.clearTimer();
|
|
458
|
+
void this.tick(trigger);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private async tick(trigger: string): Promise<void> {
|
|
462
|
+
if (this.stopped || this.inFlight) return;
|
|
463
|
+
this.nextTickAtMs = null;
|
|
464
|
+
if (this.held) {
|
|
465
|
+
this.onHeld();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
await this.driveOnce(trigger);
|
|
469
|
+
if (this.stopped) return;
|
|
470
|
+
// A drive that landed the reservation resumes the healthy cadence at once; backing off here
|
|
471
|
+
// would leave the first liveness check after a long outage a fully grown backoff away.
|
|
472
|
+
if (this.held) {
|
|
473
|
+
this.onHeld();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
this.scheduleBackoff();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Healthy tick: nothing to request, so only reset and re-check later. */
|
|
480
|
+
private onHeld(): void {
|
|
481
|
+
this.backoffMs = this.minBackoffMs;
|
|
482
|
+
this.failure = null;
|
|
483
|
+
// Once held, a later slot-taken episode is a new event and deserves its own log line.
|
|
484
|
+
this.slotTakenLogged = false;
|
|
485
|
+
this.settleFirst(null);
|
|
486
|
+
this.schedule(this.checkMs);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private async driveOnce(trigger: string): Promise<void> {
|
|
490
|
+
this.inFlight = true;
|
|
491
|
+
log('relay-reservation:drive relay=%s trigger=%s', this.relay.dialAddr, trigger);
|
|
492
|
+
try {
|
|
493
|
+
const reason = await this.drive();
|
|
494
|
+
if (this.stopped) return;
|
|
495
|
+
this.failure = reason;
|
|
496
|
+
if (reason === null) {
|
|
497
|
+
log('relay-reservation:held relay=%s', this.relay.dialAddr);
|
|
498
|
+
} else {
|
|
499
|
+
log.error('relay-reservation:failed relay=%s reason=%s', this.relay.dialAddr, reason);
|
|
500
|
+
}
|
|
501
|
+
} catch (err) {
|
|
502
|
+
// `drive` is fail-soft by contract, so reaching here means that contract broke. The loop
|
|
503
|
+
// must survive it anyway: an escaping rejection would leave `firstDrive` pending forever
|
|
504
|
+
// and schedule no further attempt, which is the failure this supervisor exists to prevent.
|
|
505
|
+
const message = errorMessage(err);
|
|
506
|
+
log.error('relay-reservation:drive-threw relay=%s err=%s', this.relay.dialAddr, message);
|
|
507
|
+
if (!this.stopped) this.failure = message;
|
|
508
|
+
} finally {
|
|
509
|
+
this.inFlight = false;
|
|
510
|
+
if (!this.stopped) this.settleFirst(this.failure);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** One attempt under one deadline. Never throws; returns `null` when the address is held afterwards. */
|
|
515
|
+
private async drive(): Promise<string | null> {
|
|
516
|
+
const transport = findCircuitRelayTransport(this.node);
|
|
517
|
+
if (transport === null) {
|
|
518
|
+
return 'the node has no circuit-relay transport — add circuitRelayTransport() to transports';
|
|
519
|
+
}
|
|
520
|
+
const deadline = Date.now() + this.driveTimeoutMs;
|
|
521
|
+
clearRelayFilterEntry(transport.reservationStore, this.relayPeerId);
|
|
522
|
+
const dialFailure = await this.dialRelay(deadline);
|
|
523
|
+
if (dialFailure !== null) return dialFailure;
|
|
524
|
+
if (this.stopped) return 'stopped';
|
|
525
|
+
const requestFailure = await this.requestReservation(transport.reservationStore, deadline);
|
|
526
|
+
if (requestFailure !== null) return requestFailure;
|
|
527
|
+
if (await this.waitUntilHeld(deadline)) return null;
|
|
528
|
+
return `relay ${this.relay.dialAddr} granted a reservation but no circuit address through it was published within ${this.driveTimeoutMs}ms`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Dial the relay, or return why that failed. An explicit controller rather than
|
|
533
|
+
* `AbortSignal.timeout`, which is unreliable on Hermes; {@link stop} aborts it too.
|
|
534
|
+
*/
|
|
535
|
+
private async dialRelay(deadline: number): Promise<string | null> {
|
|
536
|
+
const controller = new AbortController();
|
|
537
|
+
this.dialAbort = controller;
|
|
538
|
+
const timer = setTimeout(() => controller.abort(), remaining(deadline));
|
|
539
|
+
unref(timer);
|
|
540
|
+
try {
|
|
541
|
+
await this.node.dial(multiaddr(this.relay.dialAddr), { signal: controller.signal });
|
|
542
|
+
return null;
|
|
543
|
+
} catch (err) {
|
|
544
|
+
return `dial to relay ${this.relay.dialAddr} failed: ${errorMessage(err)}`;
|
|
545
|
+
} finally {
|
|
546
|
+
clearTimeout(timer);
|
|
547
|
+
this.dialAbort = null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Ask the store for a `discovered` reservation on the relay, raced against the deadline because
|
|
553
|
+
* `addRelay` takes no signal. `'discovered'`, never `'configured'`: only a `discovered`
|
|
554
|
+
* reservation is published by the bare listener the plan gave libp2p. A concurrent `addRelay`
|
|
555
|
+
* for the same relay from libp2p's own discovery joins the same queue job, so there is no
|
|
556
|
+
* double reservation. The abandoned promise stays handled — `Promise.race` attaches handlers.
|
|
557
|
+
*/
|
|
558
|
+
private async requestReservation(store: RelayReservationStoreLike, deadline: number): Promise<string | null> {
|
|
559
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
560
|
+
const expired = new Promise<typeof DEADLINE_PASSED>(resolve => {
|
|
561
|
+
timer = setTimeout(() => resolve(DEADLINE_PASSED), remaining(deadline));
|
|
562
|
+
unref(timer);
|
|
563
|
+
});
|
|
564
|
+
try {
|
|
565
|
+
const outcome = await Promise.race([store.addRelay(this.relayPeerId, 'discovered'), expired]);
|
|
566
|
+
if (outcome === DEADLINE_PASSED) {
|
|
567
|
+
return `reservation request to relay ${this.relay.dialAddr} did not complete within ${this.driveTimeoutMs}ms`;
|
|
568
|
+
}
|
|
569
|
+
return null;
|
|
570
|
+
} catch (err) {
|
|
571
|
+
return this.describeReservationFailure(err);
|
|
572
|
+
} finally {
|
|
573
|
+
clearTimeout(timer);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Turn the store's rejection into a reason that names the cause, and apply its consequence. */
|
|
578
|
+
private describeReservationFailure(err: unknown): string {
|
|
579
|
+
const name = err instanceof Error ? err.name : '';
|
|
580
|
+
switch (name) {
|
|
581
|
+
case 'HadEnoughRelaysError': {
|
|
582
|
+
// The pending slot the bare listener registered is already filled, by a reservation on
|
|
583
|
+
// some other relay. Nothing this supervisor does frees it, so retry only at the cap.
|
|
584
|
+
this.backoffMs = this.maxBackoffMs;
|
|
585
|
+
const reason = `this node's pending circuit slot is already filled by a reservation on another relay, so relay ${this.relay.dialAddr} was not asked; retrying every ${this.maxBackoffMs}ms until that reservation drops`;
|
|
586
|
+
if (!this.slotTakenLogged) {
|
|
587
|
+
this.slotTakenLogged = true;
|
|
588
|
+
log.error('relay-reservation:slot-taken relay=%s', this.relay.dialAddr);
|
|
589
|
+
}
|
|
590
|
+
return reason;
|
|
591
|
+
}
|
|
592
|
+
case 'UnsupportedProtocolError':
|
|
593
|
+
return `the peer at ${this.relay.dialAddr} does not serve the circuit-relay hop protocol, so it is not a relay`;
|
|
594
|
+
default:
|
|
595
|
+
return `reservation request to relay ${this.relay.dialAddr} failed: ${errorMessage(err)}`;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** `addRelay` resolving means the relay accepted; the listener publishes the address a tick later. */
|
|
600
|
+
private async waitUntilHeld(deadline: number): Promise<boolean> {
|
|
601
|
+
for (;;) {
|
|
602
|
+
if (this.held) return true;
|
|
603
|
+
if (this.stopped || Date.now() >= deadline) return false;
|
|
604
|
+
await delay(Math.min(this.pollMs, remaining(deadline)));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private scheduleBackoff(): void {
|
|
609
|
+
const wait = this.backoffMs;
|
|
610
|
+
this.backoffMs = Math.min(this.backoffMs * 2, this.maxBackoffMs);
|
|
611
|
+
this.schedule(wait);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private schedule(ms: number): void {
|
|
615
|
+
if (this.stopped) return;
|
|
616
|
+
this.clearTimer();
|
|
617
|
+
this.nextTickAtMs = Date.now() + ms;
|
|
618
|
+
this.timer = setTimeout(() => {
|
|
619
|
+
this.timer = null;
|
|
620
|
+
void this.tick('timer');
|
|
621
|
+
}, ms);
|
|
622
|
+
unref(this.timer);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
private clearTimer(): void {
|
|
626
|
+
if (this.timer === null) return;
|
|
627
|
+
clearTimeout(this.timer);
|
|
628
|
+
this.timer = null;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private settleFirst(reason: string | null): void {
|
|
632
|
+
if (this.firstDriveSettled) return;
|
|
633
|
+
this.firstDriveSettled = true;
|
|
634
|
+
this.settleFirstDrive(reason);
|
|
635
|
+
}
|
|
636
|
+
}
|
package/src/pend-validation.ts
CHANGED
|
@@ -38,8 +38,9 @@ export type PendValidationEvent =
|
|
|
38
38
|
*
|
|
39
39
|
* Two decisions live here:
|
|
40
40
|
*
|
|
41
|
-
* - **No `validation` pair** (
|
|
42
|
-
*
|
|
41
|
+
* - **No `validation` pair** (bare transforms, so there is nothing to re-execute: the
|
|
42
|
+
* single-collection `Collection.sync` shape, and the Quereus adapter's legacy multi-tree commit,
|
|
43
|
+
* whose coordinator is built with `pendValidation: 'none'`): an explicit, LOGGED policy branch on both arms, never a silent
|
|
43
44
|
* fall-through. `'accept'` admits it unchecked, preserving the historical behaviour; `'reject'`
|
|
44
45
|
* refuses with {@link PEND_NOT_VALIDATABLE}. The presence test is on the whole pair, so a sender
|
|
45
46
|
* cannot talk a receiver out of validating by omitting half of it.
|
package/src/protocol-client.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { pipe } from 'it-pipe';
|
|
2
2
|
import { encode as lpEncode, decode as lpDecode } from 'it-length-prefixed';
|
|
3
3
|
import type { Stream as Libp2pStream } from '@libp2p/interface';
|
|
4
|
-
import type { PeerId, IPeerNetwork } from '@optimystic/db-core';
|
|
4
|
+
import type { PeerId, IPeerNetwork, IKeyNetwork, BlockId } from '@optimystic/db-core';
|
|
5
|
+
import { routingKeyForBlock } from '@optimystic/db-core';
|
|
5
6
|
import { first } from './it-utility.js';
|
|
6
7
|
import { createLogger } from './logger.js';
|
|
7
8
|
import { MAX_BLOCK_MESSAGE_BYTES } from './protocol-limits.js';
|
|
@@ -48,6 +49,17 @@ export class ProtocolClient {
|
|
|
48
49
|
protected readonly peerNetwork: IPeerNetwork,
|
|
49
50
|
) { }
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Remember that `coordinator` coordinates `blockId`, so a follow-up op on that block dials it directly
|
|
54
|
+
* instead of being redirected again. Only a key network keeps coordinator hints — a plain
|
|
55
|
+
* {@link IPeerNetwork} omits `recordCoordinator`, and this is then a no-op. The hint is keyed on the
|
|
56
|
+
* block's routing key, the key a later `findCoordinator` looks it up by.
|
|
57
|
+
*/
|
|
58
|
+
protected recordCoordinatorHint(blockId: BlockId, coordinator: PeerId): void {
|
|
59
|
+
const hints = this.peerNetwork as IPeerNetwork & Partial<Pick<IKeyNetwork, 'recordCoordinator'>>;
|
|
60
|
+
hints.recordCoordinator?.(routingKeyForBlock(blockId), coordinator);
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
protected async processMessage<T>(
|
|
52
64
|
message: unknown,
|
|
53
65
|
protocol: string,
|
|
@@ -311,7 +311,8 @@ export class ReactivityForwarderHost {
|
|
|
311
311
|
* (`isDraining(now)`) returns its {@link RotationRedirectV1}; once `T_drain` closes, evicts the gate **and**
|
|
312
312
|
* the topic's served `PushState` (the old tail originates nothing further — this also reclaims the
|
|
313
313
|
* `served` / `ingestTails` maps the 12.31 review flagged as un-evicted on rotation) and returns `undefined`,
|
|
314
|
-
* so the next request
|
|
314
|
+
* so this member declines the next request and the subscriber's transport tries the next cohort member
|
|
315
|
+
* (re-walking / chain-reading only once every member declines).
|
|
315
316
|
*/
|
|
316
317
|
rotationRedirectFor(oldTopicId: Uint8Array, now: number): RotationRedirectV1 | undefined {
|
|
317
318
|
const key = this.topicKey(oldTopicId);
|