@statewalker/httpeers-libp2p 0.1.3
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/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/browser.d.ts +33 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +77 -0
- package/dist/duplex.d.ts +86 -0
- package/dist/duplex.d.ts.map +1 -0
- package/dist/duplex.js +101 -0
- package/dist/hop-reserve.d.ts +105 -0
- package/dist/hop-reserve.d.ts.map +1 -0
- package/dist/hop-reserve.js +269 -0
- package/dist/hub-link.d.ts +131 -0
- package/dist/hub-link.d.ts.map +1 -0
- package/dist/hub-link.js +217 -0
- package/dist/hub-relay.d.ts +129 -0
- package/dist/hub-relay.d.ts.map +1 -0
- package/dist/hub-relay.js +154 -0
- package/dist/identity.d.ts +84 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +91 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/link.d.ts +49 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +88 -0
- package/dist/multiaddr-parts.d.ts +21 -0
- package/dist/multiaddr-parts.d.ts.map +1 -0
- package/dist/multiaddr-parts.js +27 -0
- package/dist/node.d.ts +33 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +60 -0
- package/dist/reservation.d.ts +245 -0
- package/dist/reservation.d.ts.map +1 -0
- package/dist/reservation.js +396 -0
- package/dist/serve-peer.d.ts +91 -0
- package/dist/serve-peer.d.ts.map +1 -0
- package/dist/serve-peer.js +68 -0
- package/dist/timers.d.ts +36 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +38 -0
- package/dist/transport.d.ts +314 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +410 -0
- package/package.json +73 -0
- package/src/browser.ts +118 -0
- package/src/duplex.ts +172 -0
- package/src/hop-reserve.ts +295 -0
- package/src/hub-link.ts +281 -0
- package/src/hub-relay.ts +188 -0
- package/src/identity.ts +155 -0
- package/src/index.ts +114 -0
- package/src/link.ts +143 -0
- package/src/multiaddr-parts.ts +29 -0
- package/src/node.ts +63 -0
- package/src/reservation.ts +564 -0
- package/src/serve-peer.ts +141 -0
- package/src/timers.ts +47 -0
- package/src/transport.ts +538 -0
package/dist/link.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* libp2p, as a `PeerLink`.
|
|
3
|
+
*
|
|
4
|
+
* This is the whole of what makes the bridge libp2p-specific: how to open a
|
|
5
|
+
* duplex to a peer, and how to accept one. Everything the bridge does with
|
|
6
|
+
* those duplexes -- binding the proven peer, one stream per call, the permit,
|
|
7
|
+
* the timeout -- lives in `@statewalker/httpeers-bridge` and is shared with
|
|
8
|
+
* every other transport, including the MessagePort one the tests use.
|
|
9
|
+
*
|
|
10
|
+
* `serveConnections` is used rather than `node.handle` by hand because it
|
|
11
|
+
* already threads a `ConnectionContext`, and `context.remotePeer` is the
|
|
12
|
+
* answer the Noise handshake established. That value is what the bridge binds
|
|
13
|
+
* to every inbound request, and it is the reason a peer cannot lie about who
|
|
14
|
+
* it is no matter what headers it sets.
|
|
15
|
+
*/
|
|
16
|
+
import { peerIdFromString } from "@libp2p/peer-id";
|
|
17
|
+
import { multiaddr } from "@multiformats/multiaddr";
|
|
18
|
+
import { connect, serveConnections, } from "@statewalker/webrun-streams-libp2p";
|
|
19
|
+
import { DEFAULT_DRAIN_TIMEOUT_MS, DEFAULT_MAX_STREAMS, PROTOCOL } from "./transport.js";
|
|
20
|
+
/** Wrap a running libp2p node as the one seam the bridge needs. */
|
|
21
|
+
export function libp2pLink(init) {
|
|
22
|
+
const { node, protocol = PROTOCOL, drainTimeoutMs = DEFAULT_DRAIN_TIMEOUT_MS, maxInboundStreams = DEFAULT_MAX_STREAMS, maxOutboundStreams = DEFAULT_MAX_STREAMS, serveOnLimitedConnection, callOnLimitedConnection = false, } = init;
|
|
23
|
+
return {
|
|
24
|
+
async open(peerId) {
|
|
25
|
+
// A BARE `/p2p/<id>` dial, deliberately: a peer dialled once by full
|
|
26
|
+
// multiaddr is dialable again by id alone, and composing an address here
|
|
27
|
+
// would duplicate the routing decisions `reservation.ts` already made.
|
|
28
|
+
const limited = typeof callOnLimitedConnection === "function"
|
|
29
|
+
? callOnLimitedConnection(peerId)
|
|
30
|
+
: callOnLimitedConnection;
|
|
31
|
+
const conn = await connect({
|
|
32
|
+
node: limited ? keptCircuitFirst(node, peerId) : node,
|
|
33
|
+
peer: multiaddr(`/p2p/${peerId}`),
|
|
34
|
+
protocol,
|
|
35
|
+
drainTimeoutMs,
|
|
36
|
+
maxOutboundStreams,
|
|
37
|
+
...(limited ? { runOnLimitedConnection: true } : {}),
|
|
38
|
+
});
|
|
39
|
+
return { call: conn.call, close: async () => await conn.close() };
|
|
40
|
+
},
|
|
41
|
+
async serve(handlerFor) {
|
|
42
|
+
return serveConnections({
|
|
43
|
+
node,
|
|
44
|
+
protocol,
|
|
45
|
+
drainTimeoutMs,
|
|
46
|
+
maxInboundStreams,
|
|
47
|
+
maxOutboundStreams,
|
|
48
|
+
runOnLimitedConnection: serveOnLimitedConnection,
|
|
49
|
+
}, (context) => handlerFor(context.remotePeer.toString()));
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* `node`, except that a stream to `peerId` opens on the LIMITED connection
|
|
55
|
+
* already held to it when that is the only kind there is. Used only for a
|
|
56
|
+
* target `callOnLimitedConnection` allows.
|
|
57
|
+
*
|
|
58
|
+
* WHY THE FLAG ALONE IS NOT ENOUGH. libp2p 3.3.8 does not reuse a limited
|
|
59
|
+
* connection for a dial: `findExistingConnection`
|
|
60
|
+
* (`connection-manager/utils.js`) returns only connections with
|
|
61
|
+
* `limits == null`, so `dialProtocol` to a peer reachable only by a kept relay
|
|
62
|
+
* circuit dials AGAIN, from the peer store -- a second circuit per call, through
|
|
63
|
+
* whichever of the relay's addresses identify taught it. The
|
|
64
|
+
* relay-fallback conformance test measured exactly that (two new connections
|
|
65
|
+
* for two requests) before this existed. The kept connection is the one the
|
|
66
|
+
* caller chose; the stream goes there.
|
|
67
|
+
*
|
|
68
|
+
* An UNLIMITED connection still wins when there is one (a WebRTC upgrade that
|
|
69
|
+
* did come up): that case is left to libp2p's own dial, which reuses it.
|
|
70
|
+
*
|
|
71
|
+
* A NARROW STAND-IN, not a wrapper: `connect` (`webrun-streams-libp2p`) reads
|
|
72
|
+
* nothing from the node but `dialProtocol`, and giving that package a way to
|
|
73
|
+
* take a `Connection` would be a change in another repository for one caller.
|
|
74
|
+
*/
|
|
75
|
+
function keptCircuitFirst(node, peerId) {
|
|
76
|
+
const dialer = {
|
|
77
|
+
dialProtocol: async (peer, protocols, options) => {
|
|
78
|
+
const open = node.getConnections(peerIdFromString(peerId)).filter((c) => c.status === "open");
|
|
79
|
+
const kept = open.some((c) => c.limits == null)
|
|
80
|
+
? undefined
|
|
81
|
+
: open.find((c) => c.limits != null);
|
|
82
|
+
return kept != null
|
|
83
|
+
? kept.newStream(protocols, options)
|
|
84
|
+
: node.dialProtocol(peer, protocols, options);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
return dialer;
|
|
88
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a peer id out of a multiaddr, without `Array.prototype.findLast`.
|
|
3
|
+
*
|
|
4
|
+
* The prototype used `findLast`, which needs `lib: ES2023`. Raising this
|
|
5
|
+
* package's lib would raise the RUNTIME floor for every consumer — the emitted
|
|
6
|
+
* code calls the method regardless of what the types allow — for one line of
|
|
7
|
+
* convenience. A reverse loop costs nothing and keeps the floor at ES2022,
|
|
8
|
+
* matching every other package here.
|
|
9
|
+
*
|
|
10
|
+
* To reverse: set `"lib": ["ES2023"]` in tsconfig.json and inline
|
|
11
|
+
* `.getComponents().findLast((c) => c.name === "p2p")?.value` at the two call
|
|
12
|
+
* sites (`reservation.ts`, `hub-link.ts`).
|
|
13
|
+
*
|
|
14
|
+
* LAST, not first, and that is the whole point: a circuit address is
|
|
15
|
+
* `/…/p2p/<relay>/p2p-circuit/p2p/<target>`, so the FIRST `p2p` component is
|
|
16
|
+
* the relay and the last is whoever the address finally names. Taking the
|
|
17
|
+
* first would hang up on the relay when asked to hang up on a peer.
|
|
18
|
+
*/
|
|
19
|
+
import type { Multiaddr } from "@multiformats/multiaddr";
|
|
20
|
+
export declare function lastPeerIdOf(addr: Multiaddr): string | undefined;
|
|
21
|
+
//# sourceMappingURL=multiaddr-parts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multiaddr-parts.d.ts","sourceRoot":"","sources":["../src/multiaddr-parts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAOhE"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a peer id out of a multiaddr, without `Array.prototype.findLast`.
|
|
3
|
+
*
|
|
4
|
+
* The prototype used `findLast`, which needs `lib: ES2023`. Raising this
|
|
5
|
+
* package's lib would raise the RUNTIME floor for every consumer — the emitted
|
|
6
|
+
* code calls the method regardless of what the types allow — for one line of
|
|
7
|
+
* convenience. A reverse loop costs nothing and keeps the floor at ES2022,
|
|
8
|
+
* matching every other package here.
|
|
9
|
+
*
|
|
10
|
+
* To reverse: set `"lib": ["ES2023"]` in tsconfig.json and inline
|
|
11
|
+
* `.getComponents().findLast((c) => c.name === "p2p")?.value` at the two call
|
|
12
|
+
* sites (`reservation.ts`, `hub-link.ts`).
|
|
13
|
+
*
|
|
14
|
+
* LAST, not first, and that is the whole point: a circuit address is
|
|
15
|
+
* `/…/p2p/<relay>/p2p-circuit/p2p/<target>`, so the FIRST `p2p` component is
|
|
16
|
+
* the relay and the last is whoever the address finally names. Taking the
|
|
17
|
+
* first would hang up on the relay when asked to hang up on a peer.
|
|
18
|
+
*/
|
|
19
|
+
export function lastPeerIdOf(addr) {
|
|
20
|
+
const components = addr.getComponents();
|
|
21
|
+
for (let i = components.length - 1; i >= 0; i--) {
|
|
22
|
+
const component = components[i];
|
|
23
|
+
if (component?.name === "p2p")
|
|
24
|
+
return component.value;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node profile. Everything here is why the root export is not this.
|
|
3
|
+
*
|
|
4
|
+
* `tcp()` cannot run in a browser, and the prototype hard-coded it inside its
|
|
5
|
+
* node factory — so importing the transport module at all dragged a Node-only
|
|
6
|
+
* transport into a browser bundle, in a package whose whole claim is that one
|
|
7
|
+
* implementation runs on both. Separating the profiles is the fix, and the
|
|
8
|
+
* root's boundary test asserts `@libp2p/tcp` never appears there.
|
|
9
|
+
*/
|
|
10
|
+
import type { BytesStore } from "./identity.js";
|
|
11
|
+
import type { TransportFactory } from "./transport.js";
|
|
12
|
+
/**
|
|
13
|
+
* Transports for a server: TCP, and nothing a page could not also do.
|
|
14
|
+
*
|
|
15
|
+
* Listed as a function rather than a constant so a caller gets fresh factory
|
|
16
|
+
* instances — libp2p's are not reusable across nodes.
|
|
17
|
+
*/
|
|
18
|
+
export declare function nodeTransports(): TransportFactory[];
|
|
19
|
+
/**
|
|
20
|
+
* A `BytesStore` on the filesystem — where a server keeps its identity.
|
|
21
|
+
*
|
|
22
|
+
* WRITE-THEN-MOVE, not write-in-place. A key file truncated by a crash
|
|
23
|
+
* mid-write is not a corrupt file you notice, it is a mesh that silently
|
|
24
|
+
* re-founds itself on the next start: `identityStore.loadOrCreate` sees
|
|
25
|
+
* unreadable bytes, generates a new key, and every token the old identity
|
|
26
|
+
* issued stops verifying. A rename is atomic on POSIX, so the file is either
|
|
27
|
+
* the old key or the new one and never half of either.
|
|
28
|
+
*
|
|
29
|
+
* The prototype's snapshot store wrote in place; extraction prototype 04
|
|
30
|
+
* measured the consequence and this is the shape that survived it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function fileBytesStore(dir: string): BytesStore;
|
|
33
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,gBAAgB,EAAE,CAEnD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAuBtD"}
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node profile. Everything here is why the root export is not this.
|
|
3
|
+
*
|
|
4
|
+
* `tcp()` cannot run in a browser, and the prototype hard-coded it inside its
|
|
5
|
+
* node factory — so importing the transport module at all dragged a Node-only
|
|
6
|
+
* transport into a browser bundle, in a package whose whole claim is that one
|
|
7
|
+
* implementation runs on both. Separating the profiles is the fix, and the
|
|
8
|
+
* root's boundary test asserts `@libp2p/tcp` never appears there.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tcp } from "@libp2p/tcp";
|
|
13
|
+
/**
|
|
14
|
+
* Transports for a server: TCP, and nothing a page could not also do.
|
|
15
|
+
*
|
|
16
|
+
* Listed as a function rather than a constant so a caller gets fresh factory
|
|
17
|
+
* instances — libp2p's are not reusable across nodes.
|
|
18
|
+
*/
|
|
19
|
+
export function nodeTransports() {
|
|
20
|
+
return [tcp()];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A `BytesStore` on the filesystem — where a server keeps its identity.
|
|
24
|
+
*
|
|
25
|
+
* WRITE-THEN-MOVE, not write-in-place. A key file truncated by a crash
|
|
26
|
+
* mid-write is not a corrupt file you notice, it is a mesh that silently
|
|
27
|
+
* re-founds itself on the next start: `identityStore.loadOrCreate` sees
|
|
28
|
+
* unreadable bytes, generates a new key, and every token the old identity
|
|
29
|
+
* issued stops verifying. A rename is atomic on POSIX, so the file is either
|
|
30
|
+
* the old key or the new one and never half of either.
|
|
31
|
+
*
|
|
32
|
+
* The prototype's snapshot store wrote in place; extraction prototype 04
|
|
33
|
+
* measured the consequence and this is the shape that survived it.
|
|
34
|
+
*/
|
|
35
|
+
export function fileBytesStore(dir) {
|
|
36
|
+
return {
|
|
37
|
+
async get(key) {
|
|
38
|
+
try {
|
|
39
|
+
return new Uint8Array(await readFile(join(dir, key)));
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error.code === "ENOENT")
|
|
43
|
+
return undefined;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
async set(key, value) {
|
|
48
|
+
await mkdir(dir, { recursive: true });
|
|
49
|
+
const target = join(dir, key);
|
|
50
|
+
// Same directory, so the rename cannot cross a filesystem boundary —
|
|
51
|
+
// which is the one case where it would stop being atomic.
|
|
52
|
+
const staging = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
53
|
+
await writeFile(staging, value, { mode: 0o600 });
|
|
54
|
+
await rename(staging, target);
|
|
55
|
+
},
|
|
56
|
+
async delete(key) {
|
|
57
|
+
await rm(join(dir, key), { force: true });
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dialing the relay, and waiting for the reservation it grants afterwards.
|
|
3
|
+
*
|
|
4
|
+
* TRANSPORT-NEUTRAL ON PURPOSE, AND THAT IS WHY IT LIVES HERE. Both halves
|
|
5
|
+
* of this module touch nothing but `Libp2p.dial` and `Libp2p.getMultiaddrs`,
|
|
6
|
+
* so they are identical for a browser node (`browser/node-profile.ts`), a
|
|
7
|
+
* Node hub (`hub/node-profile.ts`) and a test peer (`tests/e2e/harness.ts`).
|
|
8
|
+
* They used to live in `browser/node-profile.ts`, whose top-level imports
|
|
9
|
+
* include `@libp2p/webrtc` -- which is why `tests/e2e/harness.ts` grew a
|
|
10
|
+
* second, hand-copied poll loop rather than import them (Task 14 reported
|
|
11
|
+
* that duplication as a finding). `@libp2p/webrtc` now loads under Node
|
|
12
|
+
* (its `node-datachannel` binary is installed, Task 20 Step 1), so the
|
|
13
|
+
* import would work today; the copy is still gone because ONE poll loop
|
|
14
|
+
* with one timing contract is the point, not because importing was
|
|
15
|
+
* impossible.
|
|
16
|
+
*
|
|
17
|
+
* READINESS IS POLLED, NOT AWAITED (design note 17 §4). `node.dial(relay)`
|
|
18
|
+
* resolving means the connection to the relay opened -- it says nothing
|
|
19
|
+
* about whether the relay has finished granting a reservation. That
|
|
20
|
+
* reservation lands asynchronously afterwards, so `waitForCircuitReservation`
|
|
21
|
+
* polls `getMultiaddrs()` until a `p2p-circuit` address actually appears, on
|
|
22
|
+
* the same schedule (250 ms x 40 attempts = 10 s ceiling) the validated
|
|
23
|
+
* relay test used (`prototypes/16-.../src/relay.test.ts`). Treating `dial`'s
|
|
24
|
+
* resolution as "ready" is exactly the race that note documents this project
|
|
25
|
+
* already got bitten by once.
|
|
26
|
+
*/
|
|
27
|
+
import type { Libp2p } from "@libp2p/interface";
|
|
28
|
+
import { type RelayReservationGrant } from "./hop-reserve.js";
|
|
29
|
+
import { type Timers } from "./timers.js";
|
|
30
|
+
/** Dial the relay named by `relayAddr` (`httpeers.json`'s `relayAddrs[0]`). Resolving means the link is up -- NOT that a circuit reservation exists yet; see `waitForCircuitReservation`. */
|
|
31
|
+
export declare function dialRelay(node: Libp2p, relayAddr: string): Promise<void>;
|
|
32
|
+
/** How often `waitForCircuitReservation` re-checks `getMultiaddrs()`. */
|
|
33
|
+
export declare const RESERVATION_POLL_INTERVAL_MS = 250;
|
|
34
|
+
/** How many times it checks before giving up -- 40 x 250 ms = 10 s, the ceiling the validated relay test (`relay.test.ts`) used. */
|
|
35
|
+
export declare const RESERVATION_POLL_ATTEMPTS = 40;
|
|
36
|
+
export interface WaitForCircuitReservationInit {
|
|
37
|
+
intervalMs?: number;
|
|
38
|
+
attempts?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Poll `node.getMultiaddrs()` until a `p2p-circuit` address appears,
|
|
42
|
+
* returning it as a string. See this module's own "READINESS IS POLLED,
|
|
43
|
+
* NOT AWAITED" note for why this exists instead of trusting `dialRelay`'s
|
|
44
|
+
* resolution: the reservation is granted by the relay asynchronously,
|
|
45
|
+
* after the dial itself has already resolved.
|
|
46
|
+
*/
|
|
47
|
+
export declare function waitForCircuitReservation(node: Libp2p, init?: WaitForCircuitReservationInit): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* WHAT `superviseRelay` NOW DOES, AND WHY IT IS NOT WHAT IT USED TO DO.
|
|
50
|
+
*
|
|
51
|
+
* The supervisor below used to answer "am I reserved?" from
|
|
52
|
+
* `node.getMultiaddrs()` -- the node's OWN belief, held by libp2p's transport
|
|
53
|
+
* reservation store. On 2026-09-19 that belief was wrong for hours: the relay
|
|
54
|
+
* held no reservation for the hub, the websocket to it was still ESTABLISHED,
|
|
55
|
+
* every member got `NO_RESERVATION`, and the supervisor saw a perfectly good
|
|
56
|
+
* `/p2p-circuit` address and did nothing. Only a container restart fixed it,
|
|
57
|
+
* and the root cause on the relay's side was never found -- which is the
|
|
58
|
+
* argument for repairing the CLASS of fault rather than that one cause.
|
|
59
|
+
*
|
|
60
|
+
* So the relay decides now. On a schedule well inside the reservation's TTL
|
|
61
|
+
* the supervisor asks the relay for a reservation over the connection it
|
|
62
|
+
* already holds (`./hop-reserve.ts`, which explains the wire-level choice).
|
|
63
|
+
* That single request is both the question and the repair: a relay's
|
|
64
|
+
* reservation store is keyed by peer, so the answer is OK whether it renewed
|
|
65
|
+
* an entry or created a missing one. The incident therefore heals on the next
|
|
66
|
+
* tick without anything having to notice it first -- and if the relay instead
|
|
67
|
+
* REFUSES, or cannot be asked at all, that is a loss, with the backoff and
|
|
68
|
+
* `restore` path below.
|
|
69
|
+
*
|
|
70
|
+
* THE LOCAL ADDRESS LIST STILL EARNS ITS KEEP, in one direction only: an
|
|
71
|
+
* address that has GONE is proof of a loss (libp2p removes it when the relay
|
|
72
|
+
* connection closes), and reacting to it turns a dropped link into a restore
|
|
73
|
+
* in milliseconds rather than at the next renewal. An address that is PRESENT
|
|
74
|
+
* proves nothing and can no longer put the supervisor back into `reserved` --
|
|
75
|
+
* `tests/reservation-supervisor.test.ts` pins exactly that asymmetry.
|
|
76
|
+
*
|
|
77
|
+
* NOTHING IS SWALLOWED. Every transition logs one line naming the relay and
|
|
78
|
+
* the reason. The old implementation had a bare `catch {}` around the whole
|
|
79
|
+
* restore, which is why the incident left no trace in the hub's own output.
|
|
80
|
+
*/
|
|
81
|
+
/** Where a supervised reservation stands. `stopped` is terminal. */
|
|
82
|
+
export type RelayReservationStatus = "reserved" | "lost" | "stopped";
|
|
83
|
+
/** Every transition the supervisor logs. There is no "gave up": it retries forever, with a capped backoff, and the health signal is what escalates. */
|
|
84
|
+
export type RelayReservationEvent = "reserved" | "renewed" | "renewal-failed" | "lost" | "restoring" | "restored" | "restore-failed" | "stopped";
|
|
85
|
+
/** A supervisor's current view, as `GET /hub/api/relay` reports it. */
|
|
86
|
+
export interface RelayReservationState {
|
|
87
|
+
status: RelayReservationStatus;
|
|
88
|
+
relayAddr: string;
|
|
89
|
+
/** The relay's peer id, or `null` when `relayAddr` names none. */
|
|
90
|
+
relayPeerId: string | null;
|
|
91
|
+
/** When the RELAY last confirmed the reservation (ms since the epoch); `null` until it first has. */
|
|
92
|
+
verifiedAt: number | null;
|
|
93
|
+
/** When the relay says the reservation expires (ms since the epoch); `null` when it did not say. */
|
|
94
|
+
expiresAt: number | null;
|
|
95
|
+
/** When the loss was first noticed (ms since the epoch); `null` while reserved. */
|
|
96
|
+
lostSince: number | null;
|
|
97
|
+
/** Renewals or restores that have failed in a row. Zero the moment the relay says OK. */
|
|
98
|
+
consecutiveFailures: number;
|
|
99
|
+
/** Relay-confirmed renewals since this supervisor started. */
|
|
100
|
+
renewals: number;
|
|
101
|
+
/** Recoveries completed since this supervisor started. */
|
|
102
|
+
restores: number;
|
|
103
|
+
/** The most recent failure, verbatim; kept after recovery so an operator can see what happened. */
|
|
104
|
+
lastError: string | null;
|
|
105
|
+
}
|
|
106
|
+
/** One line, plus the structured form of the same thing. */
|
|
107
|
+
export type RelayLog = (message: string, detail: {
|
|
108
|
+
event: RelayReservationEvent;
|
|
109
|
+
state: RelayReservationState;
|
|
110
|
+
}) => void;
|
|
111
|
+
export interface SuperviseRelayInit {
|
|
112
|
+
node: Libp2p;
|
|
113
|
+
/**
|
|
114
|
+
* The same string `dialRelay` was given. Its last `/p2p/<id>` names the
|
|
115
|
+
* relay whose reservation counts; with a custom `restore`, `/p2p/<id>`
|
|
116
|
+
* alone is enough. Without any `/p2p/<id>` there is nobody to ask, and
|
|
117
|
+
* proactive renewal is off.
|
|
118
|
+
*/
|
|
119
|
+
relayAddr: string;
|
|
120
|
+
/**
|
|
121
|
+
* How to get the reservation back. Defaults to dialling `relayAddr` and
|
|
122
|
+
* waiting for libp2p to reserve on it, which is right for a relay libp2p
|
|
123
|
+
* DISCOVERED (a `/p2p-circuit` listen address). A CONFIGURED relay -- a
|
|
124
|
+
* member's reservation on its hub -- is never re-reserved by libp2p, so its
|
|
125
|
+
* restore has to ask again itself; see `./hub-link.ts`.
|
|
126
|
+
*/
|
|
127
|
+
restore?: () => Promise<void>;
|
|
128
|
+
/** The first retry after a failed attempt. */
|
|
129
|
+
minRetryDelayMs?: number;
|
|
130
|
+
/** The ceiling the backoff grows to. */
|
|
131
|
+
maxRetryDelayMs?: number;
|
|
132
|
+
/**
|
|
133
|
+
* Ask the relay itself. Defaults to a circuit-relay v2 HOP `RESERVE` over
|
|
134
|
+
* the connection this node already holds (`requestRelayReservation`).
|
|
135
|
+
* `false` turns proactive renewal off entirely, which leaves only the
|
|
136
|
+
* address-disappeared detector -- i.e. the behaviour that missed the
|
|
137
|
+
* incident. It exists for tests, not for production.
|
|
138
|
+
*/
|
|
139
|
+
verify?: ((options: {
|
|
140
|
+
signal?: AbortSignal;
|
|
141
|
+
}) => Promise<RelayReservationGrant>) | false;
|
|
142
|
+
/**
|
|
143
|
+
* How long between renewals. Defaults to a quarter of the TTL the relay
|
|
144
|
+
* itself reported in its last grant -- see `renewalIntervalMs`.
|
|
145
|
+
*/
|
|
146
|
+
renewalIntervalMs?: number;
|
|
147
|
+
/** The backstop check for a loss no event reported. */
|
|
148
|
+
checkIntervalMs?: number;
|
|
149
|
+
/** Defaults to the global timers; a browser tab should pass `worker-timers` (see `./timers.ts`). */
|
|
150
|
+
timers?: Timers;
|
|
151
|
+
/** Defaults to one `console.log` per transition. A silent supervisor is what hid the incident. */
|
|
152
|
+
log?: RelayLog;
|
|
153
|
+
now?: () => number;
|
|
154
|
+
random?: () => number;
|
|
155
|
+
}
|
|
156
|
+
export interface RelaySupervisor {
|
|
157
|
+
/** Re-check now, and if the reservation is gone try at once rather than wait out the backoff. */
|
|
158
|
+
poke(): void;
|
|
159
|
+
/** What the relay last said, and when. Safe to call at any time; a copy, not the live object. */
|
|
160
|
+
state(): RelayReservationState;
|
|
161
|
+
stop(): void;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The floor and ceiling on the renewal cadence, and the cadence used when the
|
|
165
|
+
* relay has not told us a TTL yet.
|
|
166
|
+
*
|
|
167
|
+
* A QUARTER OF THE TTL is the rule (`renewalIntervalMs`). Against the relay
|
|
168
|
+
* this project runs -- `apps/relay/src/limits.ts`, TTL two hours, restated
|
|
169
|
+
* there precisely so a library change is visible -- that is thirty minutes,
|
|
170
|
+
* and it means three consecutive renewals can fail before the reservation
|
|
171
|
+
* would lapse on its own. A half would leave room for one, which is no margin
|
|
172
|
+
* at all; an eighth would double the traffic for a margin nobody needs.
|
|
173
|
+
*
|
|
174
|
+
* The CEILING is the same thirty minutes, so a relay that one day advertises a
|
|
175
|
+
* longer TTL does not stretch the cadence with it: the incident was not a
|
|
176
|
+
* reservation that expired, it was one that vanished, and how fast that is
|
|
177
|
+
* repaired is bounded by this number, not by the TTL. The FLOOR keeps a relay
|
|
178
|
+
* with an implausibly short TTL from being asked every few seconds.
|
|
179
|
+
*/
|
|
180
|
+
export declare const MIN_RENEWAL_INTERVAL_MS = 60000;
|
|
181
|
+
export declare const MAX_RENEWAL_INTERVAL_MS: number;
|
|
182
|
+
export declare const DEFAULT_RENEWAL_INTERVAL_MS: number;
|
|
183
|
+
/** The renewal cadence for a reservation the relay says lasts `ttlMs`. See the constants above. */
|
|
184
|
+
export declare function renewalIntervalMs(ttlMs: number | null | undefined): number;
|
|
185
|
+
/**
|
|
186
|
+
* The actual wait before the next renewal: `intervalMs` jittered DOWN into its
|
|
187
|
+
* upper quarter, never up.
|
|
188
|
+
*
|
|
189
|
+
* Down, because the interval is already the deadline -- stretching it past a
|
|
190
|
+
* quarter of the TTL would spend the margin the quarter exists to buy. The
|
|
191
|
+
* spread is what keeps a fleet of hubs that started together, or came back
|
|
192
|
+
* together after a relay restart, from asking the relay in the same instant
|
|
193
|
+
* for ever after.
|
|
194
|
+
*/
|
|
195
|
+
export declare function renewalDelayMs(intervalMs: number, random?: () => number): number;
|
|
196
|
+
/**
|
|
197
|
+
* How long a lost reservation may go unrepaired before the hub should call
|
|
198
|
+
* itself unhealthy.
|
|
199
|
+
*
|
|
200
|
+
* TWO MINUTES. A restore normally completes in seconds: the loss is noticed
|
|
201
|
+
* either at once (the address disappears) or at the next renewal, the restore
|
|
202
|
+
* dials and waits at most five seconds for the reservation, and the retry
|
|
203
|
+
* backoff caps at thirty. Two minutes is therefore at least four attempts
|
|
204
|
+
* including capped retries -- long enough that a relay restart or a dropped
|
|
205
|
+
* link never shows up as an unhealthy appliance, short enough to be a small
|
|
206
|
+
* fraction of the two-hour TTL, so a hub that truly cannot get back is
|
|
207
|
+
* reported long before members would notice on their own.
|
|
208
|
+
*/
|
|
209
|
+
export declare const RESERVATION_LOSS_GRACE_MS = 120000;
|
|
210
|
+
/** Is this state healthy -- reserved, or lost for less than `graceMs`? See `RESERVATION_LOSS_GRACE_MS`. */
|
|
211
|
+
export declare function reservationHealthy(state: Pick<RelayReservationState, "status" | "lostSince">, graceMs?: number, now?: number): boolean;
|
|
212
|
+
/**
|
|
213
|
+
* Keep this node's reservation on the relay at `relayAddr`: renew it against
|
|
214
|
+
* the relay on a schedule, and re-dial whenever it is lost. Call once the
|
|
215
|
+
* first reservation has landed.
|
|
216
|
+
*/
|
|
217
|
+
export declare function superviseRelay(init: SuperviseRelayInit): RelaySupervisor;
|
|
218
|
+
/**
|
|
219
|
+
* The wait before retry number `failures` (0-based): doubling from
|
|
220
|
+
* `minDelayMs`, capped at `maxDelayMs`, then jittered into its upper half
|
|
221
|
+
* so a relay restart does not bring every peer back in the same instant.
|
|
222
|
+
*/
|
|
223
|
+
export declare function retryDelayMs(failures: number, minDelayMs: number, maxDelayMs: number, random?: () => number): number;
|
|
224
|
+
/** The circuit addresses a node currently holds, split by kind. */
|
|
225
|
+
export interface CircuitAddrs {
|
|
226
|
+
/** `/…/p2p-circuit/p2p/<self>` — reachable through the relay, no WebRTC. */
|
|
227
|
+
bare?: string;
|
|
228
|
+
/** `/…/p2p-circuit/webrtc/p2p/<self>` — the relay carries only the handshake. */
|
|
229
|
+
webrtc?: string;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Which of a node's addresses a peer should actually be told.
|
|
233
|
+
*
|
|
234
|
+
* After reserving, a node carries several multiaddrs at once and they are NOT
|
|
235
|
+
* interchangeable: its own transport address is not reachable through a relay
|
|
236
|
+
* at all, the bare circuit relays every byte, and the `/webrtc` circuit uses
|
|
237
|
+
* the relay for the handshake only and then talks directly. Handing out the
|
|
238
|
+
* wrong one is a connection that fails for a reason nobody can read from the
|
|
239
|
+
* error.
|
|
240
|
+
*
|
|
241
|
+
* Each assembly in the prototype picked one inline, and differently — the kind
|
|
242
|
+
* of triplication the extraction exists to end.
|
|
243
|
+
*/
|
|
244
|
+
export declare function circuitAddrs(node: Libp2p): CircuitAddrs;
|
|
245
|
+
//# sourceMappingURL=reservation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reservation.d.ts","sourceRoot":"","sources":["../src/reservation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,KAAK,qBAAqB,EAA2B,MAAM,kBAAkB,CAAC;AAEvF,OAAO,EAAkC,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1E,6LAA6L;AAC7L,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9E;AAED,yEAAyE;AACzE,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAChD,oIAAoI;AACpI,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C,MAAM,WAAW,6BAA6B;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,6BAAkC,GACvC,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,oEAAoE;AACpE,MAAM,MAAM,sBAAsB,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS,CAAC;AAErE,uJAAuJ;AACvJ,MAAM,MAAM,qBAAqB,GAC7B,UAAU,GACV,SAAS,GACT,gBAAgB,GAChB,MAAM,GACN,WAAW,GACX,UAAU,GACV,gBAAgB,GAChB,SAAS,CAAC;AAEd,uEAAuE;AACvE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,sBAAsB,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,qGAAqG;IACrG,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,oGAAoG;IACpG,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mFAAmF;IACnF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yFAAyF;IACzF,mBAAmB,EAAE,MAAM,CAAC;IAC5B,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,mGAAmG;IACnG,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,4DAA4D;AAC5D,MAAM,MAAM,QAAQ,GAAG,CACrB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE;IAAE,KAAK,EAAE,qBAAqB,CAAC;IAAC,KAAK,EAAE,qBAAqB,CAAA;CAAE,KACnE,IAAI,CAAC;AAEV,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,8CAA8C;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,wCAAwC;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC,GAAG,KAAK,CAAC;IACzF;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uDAAuD;IACvD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oGAAoG;IACpG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,GAAG,CAAC,EAAE,QAAQ,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,iGAAiG;IACjG,IAAI,IAAI,IAAI,CAAC;IACb,iGAAiG;IACjG,KAAK,IAAI,qBAAqB,CAAC;IAC/B,IAAI,IAAI,IAAI,CAAC;CACd;AAWD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAC9C,eAAO,MAAM,uBAAuB,QAAc,CAAC;AACnD,eAAO,MAAM,2BAA2B,QAAc,CAAC;AAEvD,mGAAmG;AACnG,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAG1E;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,MAAoB,GAAG,MAAM,CAE7F;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,SAAU,CAAC;AAEjD,2GAA2G;AAC3G,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,IAAI,CAAC,qBAAqB,EAAE,QAAQ,GAAG,WAAW,CAAC,EAC1D,OAAO,GAAE,MAAkC,EAC3C,GAAG,GAAE,MAAmB,GACvB,OAAO,CAIT;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,kBAAkB,GAAG,eAAe,CAuNxE;AAgBD;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAM,MAAoB,GACjC,MAAM,CAGR;AAED,mEAAmE;AACnE,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAavD"}
|