@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
|
@@ -0,0 +1,396 @@
|
|
|
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 { multiaddr } from "@multiformats/multiaddr";
|
|
28
|
+
import { requestRelayReservation } from "./hop-reserve.js";
|
|
29
|
+
import { lastPeerIdOf } from "./multiaddr-parts.js";
|
|
30
|
+
import { globalTimers } from "./timers.js";
|
|
31
|
+
/** 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`. */
|
|
32
|
+
export async function dialRelay(node, relayAddr) {
|
|
33
|
+
await node.dial(multiaddr(relayAddr));
|
|
34
|
+
}
|
|
35
|
+
/** How often `waitForCircuitReservation` re-checks `getMultiaddrs()`. */
|
|
36
|
+
export const RESERVATION_POLL_INTERVAL_MS = 250;
|
|
37
|
+
/** How many times it checks before giving up -- 40 x 250 ms = 10 s, the ceiling the validated relay test (`relay.test.ts`) used. */
|
|
38
|
+
export const RESERVATION_POLL_ATTEMPTS = 40;
|
|
39
|
+
/**
|
|
40
|
+
* Poll `node.getMultiaddrs()` until a `p2p-circuit` address appears,
|
|
41
|
+
* returning it as a string. See this module's own "READINESS IS POLLED,
|
|
42
|
+
* NOT AWAITED" note for why this exists instead of trusting `dialRelay`'s
|
|
43
|
+
* resolution: the reservation is granted by the relay asynchronously,
|
|
44
|
+
* after the dial itself has already resolved.
|
|
45
|
+
*/
|
|
46
|
+
export async function waitForCircuitReservation(node, init = {}) {
|
|
47
|
+
const intervalMs = init.intervalMs ?? RESERVATION_POLL_INTERVAL_MS;
|
|
48
|
+
const attempts = init.attempts ?? RESERVATION_POLL_ATTEMPTS;
|
|
49
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
50
|
+
const found = node.getMultiaddrs().find((addr) => addr.toString().includes("p2p-circuit"));
|
|
51
|
+
if (found != null)
|
|
52
|
+
return found.toString();
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`reservation: no p2p-circuit reservation appeared within ${attempts * intervalMs}ms of dialing the relay -- ` +
|
|
56
|
+
"the relay may be unreachable, or its reservation limits already exhausted.");
|
|
57
|
+
}
|
|
58
|
+
/** How long one restore attempt waits for the reservation after its dial resolved -- 20 x 250 ms = 5 s. */
|
|
59
|
+
const RESTORE_POLL_ATTEMPTS = 20;
|
|
60
|
+
/** The backstop check, for a loss no event reported. */
|
|
61
|
+
const SUPERVISOR_CHECK_INTERVAL_MS = 10_000;
|
|
62
|
+
/** How soon after starting the supervisor first asks the relay. See `scheduleRenewal`. */
|
|
63
|
+
const INITIAL_VERIFY_DELAY_MS = 1_000;
|
|
64
|
+
/**
|
|
65
|
+
* The floor and ceiling on the renewal cadence, and the cadence used when the
|
|
66
|
+
* relay has not told us a TTL yet.
|
|
67
|
+
*
|
|
68
|
+
* A QUARTER OF THE TTL is the rule (`renewalIntervalMs`). Against the relay
|
|
69
|
+
* this project runs -- `apps/relay/src/limits.ts`, TTL two hours, restated
|
|
70
|
+
* there precisely so a library change is visible -- that is thirty minutes,
|
|
71
|
+
* and it means three consecutive renewals can fail before the reservation
|
|
72
|
+
* would lapse on its own. A half would leave room for one, which is no margin
|
|
73
|
+
* at all; an eighth would double the traffic for a margin nobody needs.
|
|
74
|
+
*
|
|
75
|
+
* The CEILING is the same thirty minutes, so a relay that one day advertises a
|
|
76
|
+
* longer TTL does not stretch the cadence with it: the incident was not a
|
|
77
|
+
* reservation that expired, it was one that vanished, and how fast that is
|
|
78
|
+
* repaired is bounded by this number, not by the TTL. The FLOOR keeps a relay
|
|
79
|
+
* with an implausibly short TTL from being asked every few seconds.
|
|
80
|
+
*/
|
|
81
|
+
export const MIN_RENEWAL_INTERVAL_MS = 60_000;
|
|
82
|
+
export const MAX_RENEWAL_INTERVAL_MS = 30 * 60_000;
|
|
83
|
+
export const DEFAULT_RENEWAL_INTERVAL_MS = 20 * 60_000;
|
|
84
|
+
/** The renewal cadence for a reservation the relay says lasts `ttlMs`. See the constants above. */
|
|
85
|
+
export function renewalIntervalMs(ttlMs) {
|
|
86
|
+
if (ttlMs == null || !(ttlMs > 0))
|
|
87
|
+
return DEFAULT_RENEWAL_INTERVAL_MS;
|
|
88
|
+
return Math.min(Math.max(ttlMs / 4, MIN_RENEWAL_INTERVAL_MS), MAX_RENEWAL_INTERVAL_MS);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The actual wait before the next renewal: `intervalMs` jittered DOWN into its
|
|
92
|
+
* upper quarter, never up.
|
|
93
|
+
*
|
|
94
|
+
* Down, because the interval is already the deadline -- stretching it past a
|
|
95
|
+
* quarter of the TTL would spend the margin the quarter exists to buy. The
|
|
96
|
+
* spread is what keeps a fleet of hubs that started together, or came back
|
|
97
|
+
* together after a relay restart, from asking the relay in the same instant
|
|
98
|
+
* for ever after.
|
|
99
|
+
*/
|
|
100
|
+
export function renewalDelayMs(intervalMs, random = Math.random) {
|
|
101
|
+
return intervalMs * 0.75 + intervalMs * 0.25 * random();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* How long a lost reservation may go unrepaired before the hub should call
|
|
105
|
+
* itself unhealthy.
|
|
106
|
+
*
|
|
107
|
+
* TWO MINUTES. A restore normally completes in seconds: the loss is noticed
|
|
108
|
+
* either at once (the address disappears) or at the next renewal, the restore
|
|
109
|
+
* dials and waits at most five seconds for the reservation, and the retry
|
|
110
|
+
* backoff caps at thirty. Two minutes is therefore at least four attempts
|
|
111
|
+
* including capped retries -- long enough that a relay restart or a dropped
|
|
112
|
+
* link never shows up as an unhealthy appliance, short enough to be a small
|
|
113
|
+
* fraction of the two-hour TTL, so a hub that truly cannot get back is
|
|
114
|
+
* reported long before members would notice on their own.
|
|
115
|
+
*/
|
|
116
|
+
export const RESERVATION_LOSS_GRACE_MS = 120_000;
|
|
117
|
+
/** Is this state healthy -- reserved, or lost for less than `graceMs`? See `RESERVATION_LOSS_GRACE_MS`. */
|
|
118
|
+
export function reservationHealthy(state, graceMs = RESERVATION_LOSS_GRACE_MS, now = Date.now()) {
|
|
119
|
+
if (state.status === "reserved")
|
|
120
|
+
return true;
|
|
121
|
+
if (state.status === "stopped")
|
|
122
|
+
return false;
|
|
123
|
+
return state.lostSince != null && now - state.lostSince < graceMs;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Keep this node's reservation on the relay at `relayAddr`: renew it against
|
|
127
|
+
* the relay on a schedule, and re-dial whenever it is lost. Call once the
|
|
128
|
+
* first reservation has landed.
|
|
129
|
+
*/
|
|
130
|
+
export function superviseRelay(init) {
|
|
131
|
+
const { node, relayAddr } = init;
|
|
132
|
+
const minDelayMs = init.minRetryDelayMs ?? 1_000;
|
|
133
|
+
const maxDelayMs = init.maxRetryDelayMs ?? 30_000;
|
|
134
|
+
const relayPeerId = lastPeerIdOf(multiaddr(relayAddr)) ?? null;
|
|
135
|
+
const timers = init.timers ?? globalTimers;
|
|
136
|
+
const now = init.now ?? Date.now;
|
|
137
|
+
const random = init.random ?? Math.random;
|
|
138
|
+
const log = init.log ?? defaultRelayLog;
|
|
139
|
+
const verify = init.verify === false
|
|
140
|
+
? undefined
|
|
141
|
+
: (init.verify ??
|
|
142
|
+
(relayPeerId != null
|
|
143
|
+
? (options) => requestRelayReservation(node, relayPeerId, options)
|
|
144
|
+
: undefined));
|
|
145
|
+
const state = {
|
|
146
|
+
status: "reserved",
|
|
147
|
+
relayAddr,
|
|
148
|
+
relayPeerId,
|
|
149
|
+
verifiedAt: null,
|
|
150
|
+
expiresAt: null,
|
|
151
|
+
lostSince: null,
|
|
152
|
+
consecutiveFailures: 0,
|
|
153
|
+
renewals: 0,
|
|
154
|
+
restores: 0,
|
|
155
|
+
lastError: null,
|
|
156
|
+
};
|
|
157
|
+
let retryTimer;
|
|
158
|
+
let renewalTimer;
|
|
159
|
+
let restoring = false;
|
|
160
|
+
let renewing = false;
|
|
161
|
+
let stopped = false;
|
|
162
|
+
const snapshot = () => ({ ...state });
|
|
163
|
+
const report = (event, reason) => {
|
|
164
|
+
const where = relayPeerId ?? relayAddr;
|
|
165
|
+
log(`relay: ${event} on ${where}${reason != null ? ` -- ${reason}` : ""}` +
|
|
166
|
+
` (failures=${state.consecutiveFailures}, renewals=${state.renewals}, restores=${state.restores})`, { event, state: snapshot() });
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* The address check, used ONLY to prove a loss. `true` here means nothing:
|
|
170
|
+
* it is precisely the belief that was wrong during the incident.
|
|
171
|
+
*/
|
|
172
|
+
const addressGone = () => !node.getMultiaddrs().some((addr) => {
|
|
173
|
+
const s = addr.toString();
|
|
174
|
+
return relayPeerId == null
|
|
175
|
+
? s.includes("/p2p-circuit")
|
|
176
|
+
: s.includes(`/p2p/${relayPeerId}/p2p-circuit`);
|
|
177
|
+
});
|
|
178
|
+
const markReserved = (grant, event) => {
|
|
179
|
+
state.status = "reserved";
|
|
180
|
+
state.verifiedAt = now();
|
|
181
|
+
state.expiresAt = grant.expiresAt;
|
|
182
|
+
state.lostSince = null;
|
|
183
|
+
state.consecutiveFailures = 0;
|
|
184
|
+
if (event === "renewed")
|
|
185
|
+
state.renewals++;
|
|
186
|
+
if (event === "restored")
|
|
187
|
+
state.restores++;
|
|
188
|
+
report(event);
|
|
189
|
+
};
|
|
190
|
+
const markLost = (reason) => {
|
|
191
|
+
state.lastError = reason;
|
|
192
|
+
if (state.status === "lost")
|
|
193
|
+
return;
|
|
194
|
+
state.status = "lost";
|
|
195
|
+
state.lostSince = now();
|
|
196
|
+
report("lost", reason);
|
|
197
|
+
};
|
|
198
|
+
const scheduleRetry = (delayMs) => {
|
|
199
|
+
timers.clearTimeout(retryTimer);
|
|
200
|
+
retryTimer = timers.setTimeout(() => {
|
|
201
|
+
retryTimer = undefined;
|
|
202
|
+
void restore();
|
|
203
|
+
}, delayMs);
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* THE FIRST RENEWAL IS PROMPT, the rest are on the TTL's cadence.
|
|
207
|
+
*
|
|
208
|
+
* A supervisor is started the moment a reservation lands, so one request a
|
|
209
|
+
* second later costs a two-byte round trip and buys two things the caller
|
|
210
|
+
* cannot get otherwise: the relay's own expiry -- which is what every later
|
|
211
|
+
* interval is derived from, instead of the blind `DEFAULT_RENEWAL_INTERVAL_MS`
|
|
212
|
+
* -- and a `verifiedAt` an operator can read straight after a restart rather
|
|
213
|
+
* than twenty minutes into one.
|
|
214
|
+
*/
|
|
215
|
+
const scheduleRenewal = (first = false) => {
|
|
216
|
+
if (stopped || verify == null)
|
|
217
|
+
return;
|
|
218
|
+
timers.clearTimeout(renewalTimer);
|
|
219
|
+
const interval = init.renewalIntervalMs ?? renewalIntervalMs(ttlOf(state, now()));
|
|
220
|
+
const delayMs = first && init.renewalIntervalMs == null
|
|
221
|
+
? INITIAL_VERIFY_DELAY_MS
|
|
222
|
+
: renewalDelayMs(interval, random);
|
|
223
|
+
renewalTimer = timers.setTimeout(() => {
|
|
224
|
+
renewalTimer = undefined;
|
|
225
|
+
void renew();
|
|
226
|
+
}, delayMs);
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* One relay-verified renewal. This is the whole point of the module: the
|
|
230
|
+
* request renews a reservation that is there and re-creates one that is not,
|
|
231
|
+
* so the ordinary case never reaches `restore` at all.
|
|
232
|
+
*/
|
|
233
|
+
async function renew() {
|
|
234
|
+
if (stopped || verify == null || renewing || restoring) {
|
|
235
|
+
scheduleRenewal();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
renewing = true;
|
|
239
|
+
try {
|
|
240
|
+
const grant = await verify({});
|
|
241
|
+
if (stopped)
|
|
242
|
+
return;
|
|
243
|
+
markReserved(grant, state.status === "reserved" ? "renewed" : "restored");
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
if (stopped)
|
|
247
|
+
return;
|
|
248
|
+
state.consecutiveFailures++;
|
|
249
|
+
const reason = messageOf(error);
|
|
250
|
+
state.lastError = reason;
|
|
251
|
+
report("renewal-failed", reason);
|
|
252
|
+
markLost(reason);
|
|
253
|
+
scheduleRetry(0);
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
renewing = false;
|
|
257
|
+
scheduleRenewal();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Get the reservation back the hard way: re-dial (or the caller's `restore`),
|
|
262
|
+
* then ASK THE RELAY. A restore that only re-listens has proved nothing --
|
|
263
|
+
* that is the same local belief that failed before -- so when a `verify` is
|
|
264
|
+
* available, its answer is what ends the outage.
|
|
265
|
+
*/
|
|
266
|
+
async function restore() {
|
|
267
|
+
if (stopped || restoring)
|
|
268
|
+
return;
|
|
269
|
+
restoring = true;
|
|
270
|
+
report("restoring", state.lastError ?? undefined);
|
|
271
|
+
try {
|
|
272
|
+
if (init.restore != null) {
|
|
273
|
+
await init.restore();
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
await dialRelay(node, relayAddr);
|
|
277
|
+
await waitForCircuitReservation(node, { attempts: RESTORE_POLL_ATTEMPTS });
|
|
278
|
+
}
|
|
279
|
+
if (stopped)
|
|
280
|
+
return;
|
|
281
|
+
if (verify != null) {
|
|
282
|
+
markReserved(await verify({}), "restored");
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
if (addressGone())
|
|
286
|
+
throw new Error("restore resolved without a reservation");
|
|
287
|
+
markReserved({ expiresAt: null, ttlMs: null }, "restored");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
if (stopped)
|
|
292
|
+
return;
|
|
293
|
+
state.consecutiveFailures++;
|
|
294
|
+
const reason = messageOf(error);
|
|
295
|
+
state.lastError = reason;
|
|
296
|
+
markLost(reason);
|
|
297
|
+
report("restore-failed", reason);
|
|
298
|
+
scheduleRetry(retryDelayMs(state.consecutiveFailures - 1, minDelayMs, maxDelayMs, random));
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
restoring = false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/** The fast loss detector: an address that has gone is proof; one that is there is not. */
|
|
305
|
+
const check = () => {
|
|
306
|
+
if (stopped || restoring || renewing || retryTimer != null)
|
|
307
|
+
return;
|
|
308
|
+
if (!addressGone())
|
|
309
|
+
return;
|
|
310
|
+
markLost("the node's own circuit address is gone (the relay connection closed)");
|
|
311
|
+
scheduleRetry(0);
|
|
312
|
+
};
|
|
313
|
+
node.addEventListener("connection:close", check);
|
|
314
|
+
node.addEventListener("self:peer:update", check);
|
|
315
|
+
const interval = timers.setInterval(check, init.checkIntervalMs ?? SUPERVISOR_CHECK_INTERVAL_MS);
|
|
316
|
+
report("reserved");
|
|
317
|
+
scheduleRenewal(true);
|
|
318
|
+
return {
|
|
319
|
+
poke() {
|
|
320
|
+
if (stopped)
|
|
321
|
+
return;
|
|
322
|
+
timers.clearTimeout(retryTimer);
|
|
323
|
+
retryTimer = undefined;
|
|
324
|
+
state.consecutiveFailures = 0;
|
|
325
|
+
if (verify != null)
|
|
326
|
+
void renew();
|
|
327
|
+
else
|
|
328
|
+
check();
|
|
329
|
+
},
|
|
330
|
+
state: snapshot,
|
|
331
|
+
stop() {
|
|
332
|
+
if (stopped)
|
|
333
|
+
return;
|
|
334
|
+
stopped = true;
|
|
335
|
+
state.status = "stopped";
|
|
336
|
+
timers.clearTimeout(retryTimer);
|
|
337
|
+
timers.clearTimeout(renewalTimer);
|
|
338
|
+
retryTimer = undefined;
|
|
339
|
+
renewalTimer = undefined;
|
|
340
|
+
timers.clearInterval(interval);
|
|
341
|
+
node.removeEventListener("connection:close", check);
|
|
342
|
+
node.removeEventListener("self:peer:update", check);
|
|
343
|
+
report("stopped");
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/** What is left of the reservation the relay last granted, for the next renewal's cadence. */
|
|
348
|
+
function ttlOf(state, nowMs) {
|
|
349
|
+
return state.expiresAt != null ? state.expiresAt - nowMs : null;
|
|
350
|
+
}
|
|
351
|
+
function messageOf(error) {
|
|
352
|
+
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
353
|
+
}
|
|
354
|
+
/** One line per transition on stdout. Replaceable, but never absent — see the module comment. */
|
|
355
|
+
const defaultRelayLog = (message) => {
|
|
356
|
+
console.log(message);
|
|
357
|
+
};
|
|
358
|
+
/**
|
|
359
|
+
* The wait before retry number `failures` (0-based): doubling from
|
|
360
|
+
* `minDelayMs`, capped at `maxDelayMs`, then jittered into its upper half
|
|
361
|
+
* so a relay restart does not bring every peer back in the same instant.
|
|
362
|
+
*/
|
|
363
|
+
export function retryDelayMs(failures, minDelayMs, maxDelayMs, random = Math.random) {
|
|
364
|
+
const base = Math.min(maxDelayMs, minDelayMs * 2 ** failures);
|
|
365
|
+
return base / 2 + (random() * base) / 2;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Which of a node's addresses a peer should actually be told.
|
|
369
|
+
*
|
|
370
|
+
* After reserving, a node carries several multiaddrs at once and they are NOT
|
|
371
|
+
* interchangeable: its own transport address is not reachable through a relay
|
|
372
|
+
* at all, the bare circuit relays every byte, and the `/webrtc` circuit uses
|
|
373
|
+
* the relay for the handshake only and then talks directly. Handing out the
|
|
374
|
+
* wrong one is a connection that fails for a reason nobody can read from the
|
|
375
|
+
* error.
|
|
376
|
+
*
|
|
377
|
+
* Each assembly in the prototype picked one inline, and differently — the kind
|
|
378
|
+
* of triplication the extraction exists to end.
|
|
379
|
+
*/
|
|
380
|
+
export function circuitAddrs(node) {
|
|
381
|
+
const found = {};
|
|
382
|
+
for (const addr of node.getMultiaddrs()) {
|
|
383
|
+
const value = addr.toString();
|
|
384
|
+
// Not a circuit: the node's own transport address. A dialler reaching it
|
|
385
|
+
// needs no relay, so it does not belong in either field.
|
|
386
|
+
if (!value.includes("/p2p-circuit"))
|
|
387
|
+
continue;
|
|
388
|
+
// Order matters: every webrtc circuit also contains `/p2p-circuit`, so the
|
|
389
|
+
// narrower test comes first or every address lands in `bare`.
|
|
390
|
+
if (value.includes("/webrtc"))
|
|
391
|
+
found.webrtc ??= value;
|
|
392
|
+
else
|
|
393
|
+
found.bare ??= value;
|
|
394
|
+
}
|
|
395
|
+
return found;
|
|
396
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A mount table, served to the mesh.
|
|
3
|
+
*
|
|
4
|
+
* THE SEAM THREE ASSEMBLIES COLLAPSE INTO. The prototype builds a peer three
|
|
5
|
+
* times — the Node hub, the browser hub and the test hub — each wiring a node,
|
|
6
|
+
* a reservation, identity registration, a router and mounts in its own order,
|
|
7
|
+
* and the coverage audit found that the one symbol every assembly turns on was
|
|
8
|
+
* exported nowhere. This is it.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IT DOES NOT DO, and the omission is the design. It does not verify a
|
|
11
|
+
* token, evaluate a policy, or know what a hub is. `access` is a middleware
|
|
12
|
+
* the caller supplies — `withAccess` from `@statewalker/httpeers-access`, or
|
|
13
|
+
* nothing at all for a peer that trusts its transport — because this package
|
|
14
|
+
* must not learn about Biscuit and `httpeers-access` must not learn about
|
|
15
|
+
* libp2p. They meet at a `FetchHandler`, which is the only contract either
|
|
16
|
+
* needs.
|
|
17
|
+
*
|
|
18
|
+
* IDENTITY IS REGISTERED BEFORE ANYTHING DOWNSTREAM RUNS. `serveTransport`
|
|
19
|
+
* gives every inbound stream its own `Duplex` closing over that stream's
|
|
20
|
+
* Noise-proven `remotePeer`, and calls `registerPeer` on the request before
|
|
21
|
+
* the handler sees it. That ordering is the whole basis of the binding check:
|
|
22
|
+
* a token's subject is compared against what the TRANSPORT proved, never
|
|
23
|
+
* against anything the caller said.
|
|
24
|
+
*/
|
|
25
|
+
import type { Libp2p } from "@libp2p/interface";
|
|
26
|
+
import type { FetchHandler, Mounts, PeerIdStr } from "@statewalker/httpeers-core";
|
|
27
|
+
import type { CallOnLimitedConnection } from "./link.js";
|
|
28
|
+
export interface ServePeerInit {
|
|
29
|
+
node: Libp2p;
|
|
30
|
+
/**
|
|
31
|
+
* What this peer serves. A value, or a factory taking the peer's own id —
|
|
32
|
+
* a hub's endpoints need to know which mesh they are, and the alternative
|
|
33
|
+
* is for this package to learn what a hub is.
|
|
34
|
+
*/
|
|
35
|
+
mounts: Mounts | ((self: PeerIdStr) => Mounts);
|
|
36
|
+
/**
|
|
37
|
+
* Everything between the transport and the mounts: identity binding and
|
|
38
|
+
* policy. Omitted, requests reach the mounts with only the transport's
|
|
39
|
+
* proof — which is a legitimate shape for a peer whose mounts are public,
|
|
40
|
+
* and a serious mistake for one whose are not.
|
|
41
|
+
*
|
|
42
|
+
* Passed through to the router, which applies it to LOCAL handling only.
|
|
43
|
+
* That distinction is load-bearing and easy to lose: wrapping the whole
|
|
44
|
+
* router instead would run policy over FORWARDED requests too, and an
|
|
45
|
+
* outbound call is not this peer's business to authorize.
|
|
46
|
+
*/
|
|
47
|
+
access?: (next: FetchHandler) => FetchHandler;
|
|
48
|
+
/**
|
|
49
|
+
* May this request be forwarded to `target`? **Deny by default**, and the
|
|
50
|
+
* default is inherited rather than restated here.
|
|
51
|
+
*
|
|
52
|
+
* Relaying is a distinct capability, not a side effect of knowing how to
|
|
53
|
+
* route: without it any peer can make this one dial a third party and pump
|
|
54
|
+
* a stream on its behalf. The hole is invisible for a while because it
|
|
55
|
+
* fails CLOSED at the far end — the third party rejects the tokenless
|
|
56
|
+
* request, so every observable outcome looks right. The damage is the work
|
|
57
|
+
* done, not the answer given.
|
|
58
|
+
*/
|
|
59
|
+
allowForward?: (request: Request, target: PeerIdStr) => Promise<boolean>;
|
|
60
|
+
protocol?: string;
|
|
61
|
+
/** Forwarded to the transport; see `transport.ts` for what each bounds. */
|
|
62
|
+
maxInboundStreams?: number;
|
|
63
|
+
maxOutboundStreams?: number;
|
|
64
|
+
drainTimeoutMs?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Accept streams on limited connections (relay circuits) -- SERVING ONLY. A
|
|
67
|
+
* HUB sets it, so members that can reach it only through the public relay
|
|
68
|
+
* are served; a member never does, so the hub's relay limits still bound
|
|
69
|
+
* member-to-member traffic.
|
|
70
|
+
*/
|
|
71
|
+
serveOnLimitedConnection?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Open streams on limited connections to the peers this allows -- CALLING
|
|
74
|
+
* ONLY, and it covers every outbound path of this peer (`call`, and the
|
|
75
|
+
* router's forwarding behind `dispatch`). A member in relay mode passes
|
|
76
|
+
* `(peerId) => peerId === hubPeerId`. See `CallOnLimitedConnection`.
|
|
77
|
+
*/
|
|
78
|
+
callOnLimitedConnection?: CallOnLimitedConnection;
|
|
79
|
+
}
|
|
80
|
+
export interface Peer {
|
|
81
|
+
peerId: PeerIdStr;
|
|
82
|
+
/** Every multiaddr this node is reachable on, right now. */
|
|
83
|
+
addrs(): string[];
|
|
84
|
+
/** Inbound, after identity binding and policy. Exposed so an edge can reuse it locally. */
|
|
85
|
+
dispatch: FetchHandler;
|
|
86
|
+
/** Call another peer. The request's URL path is what the far side routes on. */
|
|
87
|
+
call(peerId: PeerIdStr, request: Request): Promise<Response>;
|
|
88
|
+
stop(): Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
export declare function servePeer(init: ServePeerInit): Promise<Peer>;
|
|
91
|
+
//# sourceMappingURL=serve-peer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve-peer.d.ts","sourceRoot":"","sources":["../src/serve-peer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAElF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAGzD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,SAAS,KAAK,MAAM,CAAC,CAAC;IAC/C;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,YAAY,CAAC;IAC9C;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;;;OAKG;IACH,uBAAuB,CAAC,EAAE,uBAAuB,CAAC;CACnD;AAED,MAAM,WAAW,IAAI;IACnB,MAAM,EAAE,SAAS,CAAC;IAClB,4DAA4D;IAC5D,KAAK,IAAI,MAAM,EAAE,CAAC;IAClB,2FAA2F;IAC3F,QAAQ,EAAE,YAAY,CAAC;IACvB,gFAAgF;IAChF,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7D,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA6ClE"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A mount table, served to the mesh.
|
|
3
|
+
*
|
|
4
|
+
* THE SEAM THREE ASSEMBLIES COLLAPSE INTO. The prototype builds a peer three
|
|
5
|
+
* times — the Node hub, the browser hub and the test hub — each wiring a node,
|
|
6
|
+
* a reservation, identity registration, a router and mounts in its own order,
|
|
7
|
+
* and the coverage audit found that the one symbol every assembly turns on was
|
|
8
|
+
* exported nowhere. This is it.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IT DOES NOT DO, and the omission is the design. It does not verify a
|
|
11
|
+
* token, evaluate a policy, or know what a hub is. `access` is a middleware
|
|
12
|
+
* the caller supplies — `withAccess` from `@statewalker/httpeers-access`, or
|
|
13
|
+
* nothing at all for a peer that trusts its transport — because this package
|
|
14
|
+
* must not learn about Biscuit and `httpeers-access` must not learn about
|
|
15
|
+
* libp2p. They meet at a `FetchHandler`, which is the only contract either
|
|
16
|
+
* needs.
|
|
17
|
+
*
|
|
18
|
+
* IDENTITY IS REGISTERED BEFORE ANYTHING DOWNSTREAM RUNS. `serveTransport`
|
|
19
|
+
* gives every inbound stream its own `Duplex` closing over that stream's
|
|
20
|
+
* Noise-proven `remotePeer`, and calls `registerPeer` on the request before
|
|
21
|
+
* the handler sees it. That ordering is the whole basis of the binding check:
|
|
22
|
+
* a token's subject is compared against what the TRANSPORT proved, never
|
|
23
|
+
* against anything the caller said.
|
|
24
|
+
*/
|
|
25
|
+
import { createPeerRouter } from "@statewalker/httpeers-core";
|
|
26
|
+
import { createRemote, PROTOCOL, serveTransport } from "./transport.js";
|
|
27
|
+
export async function servePeer(init) {
|
|
28
|
+
const peerId = init.node.peerId.toString();
|
|
29
|
+
const protocol = init.protocol ?? PROTOCOL;
|
|
30
|
+
const mounts = typeof init.mounts === "function" ? init.mounts(peerId) : init.mounts;
|
|
31
|
+
const remote = createRemote({
|
|
32
|
+
node: init.node,
|
|
33
|
+
protocol,
|
|
34
|
+
maxOutboundStreams: init.maxOutboundStreams,
|
|
35
|
+
callOnLimitedConnection: init.callOnLimitedConnection,
|
|
36
|
+
});
|
|
37
|
+
const dispatch = createPeerRouter({
|
|
38
|
+
selfPeerId: peerId,
|
|
39
|
+
mounts,
|
|
40
|
+
remote,
|
|
41
|
+
access: init.access,
|
|
42
|
+
allowForward: init.allowForward,
|
|
43
|
+
});
|
|
44
|
+
const stopServing = await serveTransport({
|
|
45
|
+
node: init.node,
|
|
46
|
+
dispatch,
|
|
47
|
+
protocol,
|
|
48
|
+
maxInboundStreams: init.maxInboundStreams,
|
|
49
|
+
maxOutboundStreams: init.maxOutboundStreams,
|
|
50
|
+
drainTimeoutMs: init.drainTimeoutMs,
|
|
51
|
+
runOnLimitedConnection: init.serveOnLimitedConnection,
|
|
52
|
+
});
|
|
53
|
+
let stopped = false;
|
|
54
|
+
return {
|
|
55
|
+
peerId,
|
|
56
|
+
addrs: () => init.node.getMultiaddrs().map((a) => a.toString()),
|
|
57
|
+
dispatch,
|
|
58
|
+
call: async (target, request) => remote(target, request),
|
|
59
|
+
async stop() {
|
|
60
|
+
// Idempotent: an assembly that tears down twice — a page unloading while
|
|
61
|
+
// a test also cleans up — must not see the second call throw.
|
|
62
|
+
if (stopped)
|
|
63
|
+
return;
|
|
64
|
+
stopped = true;
|
|
65
|
+
await stopServing();
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
package/dist/timers.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The timer seam: the four calls anything long-lived in this package schedules
|
|
3
|
+
* with, behind an interface instead of the global functions.
|
|
4
|
+
*
|
|
5
|
+
* WHY, AND IT IS NOT TESTABILITY. A browser throttles `setTimeout` and
|
|
6
|
+
* `setInterval` in a background tab -- to once a minute, and on some engines
|
|
7
|
+
* far less -- and a hub running in a tab is a hub whose relay reservation is
|
|
8
|
+
* renewed on the browser's schedule rather than on the relay's. The fix is
|
|
9
|
+
* `worker-timers`, which schedules from a Web Worker and is not throttled; the
|
|
10
|
+
* fix is NOT to shorten the interval, which changes nothing about what the
|
|
11
|
+
* browser does to it. That wiring is deliberately not here yet (the Node hub is
|
|
12
|
+
* what runs today); this seam is what makes it a one-line change later:
|
|
13
|
+
*
|
|
14
|
+
* import { clearInterval, clearTimeout, setInterval, setTimeout } from "worker-timers";
|
|
15
|
+
* const timers: Timers = { setTimeout, clearTimeout, setInterval, clearInterval };
|
|
16
|
+
*
|
|
17
|
+
* Tests get the seam for free, and that is the cheaper of the two reasons.
|
|
18
|
+
*
|
|
19
|
+
* `TimerHandle` IS `unknown` ON PURPOSE. Node's timers return an object,
|
|
20
|
+
* browsers return a number, `worker-timers` returns a number of its own that
|
|
21
|
+
* only its own `clearTimeout` understands. An implementation must therefore
|
|
22
|
+
* only ever hand its OWN handles back to its OWN clear functions -- which is
|
|
23
|
+
* exactly what an opaque type states and a `number` alias would quietly
|
|
24
|
+
* permit callers to violate.
|
|
25
|
+
*/
|
|
26
|
+
/** A handle from one `Timers` implementation, meaningful only to that same one. */
|
|
27
|
+
export type TimerHandle = unknown;
|
|
28
|
+
export interface Timers {
|
|
29
|
+
setTimeout(handler: () => void, delayMs: number): TimerHandle;
|
|
30
|
+
clearTimeout(handle: TimerHandle): void;
|
|
31
|
+
setInterval(handler: () => void, delayMs: number): TimerHandle;
|
|
32
|
+
clearInterval(handle: TimerHandle): void;
|
|
33
|
+
}
|
|
34
|
+
/** The global timers, which is what everything uses unless it is told otherwise. */
|
|
35
|
+
export declare const globalTimers: Timers;
|
|
36
|
+
//# sourceMappingURL=timers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timers.d.ts","sourceRoot":"","sources":["../src/timers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,mFAAmF;AACnF,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC;AAElC,MAAM,WAAW,MAAM;IACrB,UAAU,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,CAAC;IAC9D,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IACxC,WAAW,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,CAAC;IAC/D,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;CAC1C;AAED,oFAAoF;AACpF,eAAO,MAAM,YAAY,EAAE,MAS1B,CAAC"}
|
package/dist/timers.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The timer seam: the four calls anything long-lived in this package schedules
|
|
3
|
+
* with, behind an interface instead of the global functions.
|
|
4
|
+
*
|
|
5
|
+
* WHY, AND IT IS NOT TESTABILITY. A browser throttles `setTimeout` and
|
|
6
|
+
* `setInterval` in a background tab -- to once a minute, and on some engines
|
|
7
|
+
* far less -- and a hub running in a tab is a hub whose relay reservation is
|
|
8
|
+
* renewed on the browser's schedule rather than on the relay's. The fix is
|
|
9
|
+
* `worker-timers`, which schedules from a Web Worker and is not throttled; the
|
|
10
|
+
* fix is NOT to shorten the interval, which changes nothing about what the
|
|
11
|
+
* browser does to it. That wiring is deliberately not here yet (the Node hub is
|
|
12
|
+
* what runs today); this seam is what makes it a one-line change later:
|
|
13
|
+
*
|
|
14
|
+
* import { clearInterval, clearTimeout, setInterval, setTimeout } from "worker-timers";
|
|
15
|
+
* const timers: Timers = { setTimeout, clearTimeout, setInterval, clearInterval };
|
|
16
|
+
*
|
|
17
|
+
* Tests get the seam for free, and that is the cheaper of the two reasons.
|
|
18
|
+
*
|
|
19
|
+
* `TimerHandle` IS `unknown` ON PURPOSE. Node's timers return an object,
|
|
20
|
+
* browsers return a number, `worker-timers` returns a number of its own that
|
|
21
|
+
* only its own `clearTimeout` understands. An implementation must therefore
|
|
22
|
+
* only ever hand its OWN handles back to its OWN clear functions -- which is
|
|
23
|
+
* exactly what an opaque type states and a `number` alias would quietly
|
|
24
|
+
* permit callers to violate.
|
|
25
|
+
*/
|
|
26
|
+
/** The global timers, which is what everything uses unless it is told otherwise. */
|
|
27
|
+
export const globalTimers = {
|
|
28
|
+
setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
|
|
29
|
+
clearTimeout: (handle) => {
|
|
30
|
+
if (handle !== undefined)
|
|
31
|
+
clearTimeout(handle);
|
|
32
|
+
},
|
|
33
|
+
setInterval: (handler, delayMs) => setInterval(handler, delayMs),
|
|
34
|
+
clearInterval: (handle) => {
|
|
35
|
+
if (handle !== undefined)
|
|
36
|
+
clearInterval(handle);
|
|
37
|
+
},
|
|
38
|
+
};
|