@statewalker/httpeers-member 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -0
  3. package/dist/browser-platform.d.ts +30 -0
  4. package/dist/browser-platform.d.ts.map +1 -0
  5. package/dist/browser-platform.js +30 -0
  6. package/dist/browser-profile.d.ts +85 -0
  7. package/dist/browser-profile.d.ts.map +1 -0
  8. package/dist/browser-profile.js +120 -0
  9. package/dist/browser.d.ts +48 -0
  10. package/dist/browser.d.ts.map +1 -0
  11. package/dist/browser.js +51 -0
  12. package/dist/connection-kind.d.ts +36 -0
  13. package/dist/connection-kind.d.ts.map +1 -0
  14. package/dist/connection-kind.js +52 -0
  15. package/dist/describe-error.d.ts +18 -0
  16. package/dist/describe-error.d.ts.map +1 -0
  17. package/dist/describe-error.js +72 -0
  18. package/dist/edge-control.d.ts +50 -0
  19. package/dist/edge-control.d.ts.map +1 -0
  20. package/dist/edge-control.js +69 -0
  21. package/dist/edge-dispatch.d.ts +188 -0
  22. package/dist/edge-dispatch.d.ts.map +1 -0
  23. package/dist/edge-dispatch.js +142 -0
  24. package/dist/edge-guard.d.ts +45 -0
  25. package/dist/edge-guard.d.ts.map +1 -0
  26. package/dist/edge-guard.js +62 -0
  27. package/dist/edge.d.ts +97 -0
  28. package/dist/edge.d.ts.map +1 -0
  29. package/dist/edge.js +104 -0
  30. package/dist/gateway.d.ts +49 -0
  31. package/dist/gateway.d.ts.map +1 -0
  32. package/dist/gateway.js +102 -0
  33. package/dist/identity.d.ts +78 -0
  34. package/dist/identity.d.ts.map +1 -0
  35. package/dist/identity.js +117 -0
  36. package/dist/index.d.ts +39 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +37 -0
  39. package/dist/join-blob.d.ts +125 -0
  40. package/dist/join-blob.d.ts.map +1 -0
  41. package/dist/join-blob.js +195 -0
  42. package/dist/join.d.ts +343 -0
  43. package/dist/join.d.ts.map +1 -0
  44. package/dist/join.js +409 -0
  45. package/dist/kv.d.ts +20 -0
  46. package/dist/kv.d.ts.map +1 -0
  47. package/dist/kv.js +10 -0
  48. package/dist/mesh-config.d.ts +26 -0
  49. package/dist/mesh-config.d.ts.map +1 -0
  50. package/dist/mesh-config.js +39 -0
  51. package/dist/mesh-memory.d.ts +55 -0
  52. package/dist/mesh-memory.d.ts.map +1 -0
  53. package/dist/mesh-memory.js +74 -0
  54. package/dist/node.d.ts +29 -0
  55. package/dist/node.d.ts.map +1 -0
  56. package/dist/node.js +42 -0
  57. package/dist/page-wake.d.ts +24 -0
  58. package/dist/page-wake.d.ts.map +1 -0
  59. package/dist/page-wake.js +31 -0
  60. package/dist/peer-request.d.ts +29 -0
  61. package/dist/peer-request.d.ts.map +1 -0
  62. package/dist/peer-request.js +11 -0
  63. package/dist/reset.d.ts +56 -0
  64. package/dist/reset.d.ts.map +1 -0
  65. package/dist/reset.js +103 -0
  66. package/dist/session.d.ts +214 -0
  67. package/dist/session.d.ts.map +1 -0
  68. package/dist/session.js +403 -0
  69. package/dist/start-member.d.ts +176 -0
  70. package/dist/start-member.d.ts.map +1 -0
  71. package/dist/start-member.js +307 -0
  72. package/package.json +120 -0
  73. package/src/browser-platform.ts +53 -0
  74. package/src/browser-profile.ts +179 -0
  75. package/src/browser.ts +103 -0
  76. package/src/connection-kind.ts +53 -0
  77. package/src/describe-error.ts +76 -0
  78. package/src/edge-control.ts +94 -0
  79. package/src/edge-dispatch.ts +295 -0
  80. package/src/edge-guard.ts +67 -0
  81. package/src/edge.ts +175 -0
  82. package/src/gateway.ts +141 -0
  83. package/src/identity.ts +146 -0
  84. package/src/index.ts +95 -0
  85. package/src/join-blob.ts +229 -0
  86. package/src/join.ts +632 -0
  87. package/src/kv.ts +21 -0
  88. package/src/mesh-config.ts +41 -0
  89. package/src/mesh-memory.ts +104 -0
  90. package/src/node.ts +49 -0
  91. package/src/page-wake.ts +59 -0
  92. package/src/peer-request.ts +39 -0
  93. package/src/reset.ts +127 -0
  94. package/src/session.ts +579 -0
  95. package/src/start-member.ts +476 -0
@@ -0,0 +1,476 @@
1
+ /**
2
+ * `startMember` — the candidate `member` package's entry point.
3
+ *
4
+ * THIS IS `src/browser/peer-runtime.ts`'s `startBrowserPeer` WITH THREE
5
+ * COUPLINGS LIFTED OUT, and nothing else changed. Every step of the join is
6
+ * the proven one, imported from the stack rather than reimplemented:
7
+ * `dialRelay`, `reachHub`, `reachHubRelayed`, `createPeer`, `resumeMembership`,
8
+ * `redeemInvitation`, `reserveOnHub`, `superviseHubReservation`,
9
+ * `leaveRelay`, `startJoin`, `createRouteEnsurer`, `createEdgeDispatch`.
10
+ * If this file had to re-implement any of them, the seam would be in the
11
+ * wrong place and the rung would have failed.
12
+ *
13
+ * The three couplings, and why each is a platform boundary rather than a
14
+ * preference:
15
+ *
16
+ * 1. `createNode` — `createBrowserNode` hard-codes the browser transport
17
+ * set (webSockets + webRTC + circuitRelay) and reads the identity key
18
+ * out of IndexedDB. Node needs `tcp` as well, and its key comes from a
19
+ * file. The LISTEN ADDRESSES ARE NOT A PLATFORM CHOICE and are kept
20
+ * identical: a member listens on `/p2p-circuit` + `/webrtc` because it
21
+ * reserves on its hub, which is what `feat/hub-relay` established.
22
+ * 2. `mountEdge` — the ServiceWorker is how a *page* turns `fetch()` into
23
+ * a mesh call. Node has no ServiceWorker and needs none: the dispatch
24
+ * handler IS the edge, callable in process. So the edge is optional,
25
+ * and `MemberHandle.fetch` is always present — on both platforms it is
26
+ * the same `createEdgeDispatch` handler, which is the whole isomorphism
27
+ * claim in one field.
28
+ * 3. `watchWake` — `page-wake.ts` listens on `window`/`document`. A Node
29
+ * process has no such events; the supervisor's own backstop timer is
30
+ * what covers it, so the default is a no-op rather than a stub.
31
+ *
32
+ * `rules` replaces `policies`: `peer-runtime.ts` calls the app's `appRules`,
33
+ * which bakes `APP_RULES` — an application's vocabulary — into what ought to
34
+ * be library code. The caller passes a built `RuleSet` instead.
35
+ */
36
+
37
+ import type { Ed25519PrivateKey, Libp2p } from "@libp2p/interface";
38
+ import { peerIdFromString } from "@libp2p/peer-id";
39
+ import type { RuleSet } from "@statewalker/httpeers-access";
40
+ import { RevocationCache, withAccess } from "@statewalker/httpeers-access";
41
+ import type { FetchHandler, MeshView, Mounts, PeerIdStr } from "@statewalker/httpeers-core";
42
+ import { ANONYMOUS, forwardLocalOnly, lookupPeer } from "@statewalker/httpeers-core";
43
+ import {
44
+ dialRelay,
45
+ HubReservationError,
46
+ leaveRelay,
47
+ reachHub,
48
+ reachHubRelayed,
49
+ reserveOnHub,
50
+ servePeer,
51
+ superviseHubReservation,
52
+ } from "@statewalker/httpeers-libp2p";
53
+ import { type ConnectionKind, classifyConnection } from "./connection-kind.js";
54
+ import { createEdgeDispatch, type EdgeDispatchInit } from "./edge-dispatch.js";
55
+ import type { AdvertisementInput, PresenceRefusal } from "./join.js";
56
+ import {
57
+ createRouteEnsurer,
58
+ nextInitialSeq,
59
+ REVOCATION_MAX_STALENESS_MS,
60
+ redeemInvitation,
61
+ resumeMembership,
62
+ startJoin,
63
+ } from "./join.js";
64
+
65
+ /** The mesh a member joins: exactly `httpeers.json`'s two fields. */
66
+ export interface MeshConfig {
67
+ relayAddrs: string[];
68
+ hubPeerId: string;
69
+ }
70
+
71
+ /** What a platform's edge gives back. The browser's `mountEdge` already returns this shape. */
72
+ export interface MemberEdge {
73
+ /** Where this peer's own `fetch()` reaches the mesh, e.g. `https://origin/peers/`. */
74
+ baseUrl: string;
75
+ stop(): Promise<void>;
76
+ }
77
+
78
+ /** The platform adapters — one per thing that genuinely differs between Node and a page. */
79
+ export interface MemberPlatform {
80
+ /** Build the libp2p node. Browser: `createBrowserNode`. Node: `createNodeMemberNode` (`./node.ts`). */
81
+ createNode(init: { privateKey?: Ed25519PrivateKey }): Promise<Libp2p>;
82
+ /**
83
+ * Publish `dispatch` at a local URL. Browser: the ServiceWorker edge, via
84
+ * `@statewalker/webrun-http-browser`'s `SwHttpAdapter`. Omitted under Node,
85
+ * where `MemberHandle.fetch` is the edge.
86
+ */
87
+ mountEdge?(init: { key: string; dispatch: FetchHandler }): Promise<MemberEdge>;
88
+ /** Fires when the host wakes (online / visible / resumed) so the supervisor retries at once. Browser: `watchPageWake`. */
89
+ watchWake?(onWake: () => void): () => void;
90
+ }
91
+
92
+ export interface StartMemberInit {
93
+ /** Mount-prefix first segment, and the ServiceWorker adapter key where there is one. */
94
+ key: string;
95
+ /** This peer's own mount table, served to other members and through its own edge. */
96
+ mounts: Mounts;
97
+ /** This peer's rule set, already built. Replaces `peer-runtime.ts`'s `policies` + `appRules`. */
98
+ rules: RuleSet;
99
+ /** The mesh to join. A page may read it from `httpeers.json`; that fetch is the caller's, not this function's. */
100
+ config: MeshConfig;
101
+ platform: MemberPlatform;
102
+ invitationId?: string;
103
+ privateKey?: Ed25519PrivateKey;
104
+ advertisements?: () => AdvertisementInput[];
105
+ onPresenceRefused?: (refusal: PresenceRefusal) => void;
106
+ onState?: (state: MemberState) => void;
107
+ heartbeatIntervalMs?: number;
108
+ keepaliveIntervalMs?: number;
109
+ }
110
+
111
+ export type MemberState =
112
+ | "connecting-relay"
113
+ | "starting-peer"
114
+ | "dialing-hub"
115
+ | "resuming"
116
+ | "joining"
117
+ | "awaiting-reservation"
118
+ | "mounting-edge"
119
+ | "ready"
120
+ | "stopped";
121
+
122
+ export type JoinMethod = "resumed" | "redeemed";
123
+
124
+ /**
125
+ * How a member reaches its hub. `"direct"` is the WebRTC upgrade, with a
126
+ * reservation on the hub so other members can reach this one through it.
127
+ * `"relay"` is the fallback when that upgrade fails (a hub in Docker on a
128
+ * bridge network), or when it succeeds but the hub then refuses the
129
+ * reservation for want of capacity (see `fallsBackToRelay`): a KEPT limited
130
+ * circuit through the public relay, over which this member calls the hub and
131
+ * nothing else -- no reservation, so no other member can reach it.
132
+ *
133
+ * FIXED FOR A RUN. It is decided once, at join; a relay-mode member re-links
134
+ * over a new circuit and never retries the upgrade (no automatic re-upgrade in
135
+ * v1), so it never becomes `"direct"` without direct mode's reservation. A new
136
+ * join (a reconnect) decides again.
137
+ */
138
+ export type HubLink = "direct" | "relay";
139
+
140
+ export interface MemberHandle {
141
+ peerId: PeerIdStr;
142
+ hubPeerId: string;
143
+ relayAddr: string;
144
+ joinedBy: JoinMethod;
145
+ /**
146
+ * This member's edge, in process. The SAME handler the ServiceWorker
147
+ * serves in a page — `createEdgeDispatch`'s — so a Node caller writes
148
+ * `member.fetch(new Request(\`http://local/${key}/${peer}/x\`))` where a
149
+ * page writes `fetch(\`${baseUrl}${peer}/x\`)`, and both take the identical
150
+ * path from there on.
151
+ */
152
+ fetch: FetchHandler;
153
+ /** Present only where the platform mounted an edge (a page). */
154
+ baseUrl?: string;
155
+ meshView(): MeshView | null;
156
+ token(): string;
157
+ connectionKind(peerId: string): ConnectionKind;
158
+ /** How this member reaches its hub -- see `HubLink`. Constant for the life of this handle. */
159
+ hubLink(): HubLink;
160
+ /**
161
+ * Why this member is in relay mode although its WebRTC upgrade worked -- the
162
+ * hub refused it a reservation -- in words a page can show. Absent when
163
+ * there is nothing unexpected to say: a direct link, or a relay link because
164
+ * the upgrade itself failed (a known deployment shape, see `HubLink`).
165
+ */
166
+ hubLinkNote?: string;
167
+ /**
168
+ * The libp2p node, as an escape hatch.
169
+ *
170
+ * `startBrowserPeer` does not expose it and a page never needs it. It is
171
+ * here because everything that OBSERVES OR PERTURBS THE TRANSPORT has to
172
+ * reach it: `connectionKind` above is already such a thing, and claim 7
173
+ * of this rung — drop the hub link and watch the supervisor restore it —
174
+ * cannot be written without it. A library that hides the node entirely
175
+ * makes its own recovery behaviour untestable from outside.
176
+ */
177
+ node: Libp2p;
178
+ stop(): Promise<void>;
179
+ }
180
+
181
+ export class MemberJoinError extends Error {
182
+ constructor(
183
+ readonly reason:
184
+ | "not-a-member"
185
+ | "duplicate-identity"
186
+ | "presence-refused"
187
+ | "invitation-refused",
188
+ message: string,
189
+ options?: { cause?: unknown },
190
+ ) {
191
+ super(message, options);
192
+ this.name = "MemberJoinError";
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Does a refused reservation on the hub leave the join standing, in relay
198
+ * mode? Only when the refusal says nothing about THIS member:
199
+ *
200
+ * - `store-full` (`RESERVATION_REFUSED`) and `resource-limit`
201
+ * (`RESOURCE_LIMIT_EXCEEDED`) -- the hub is out of capacity. This is the
202
+ * production failure: a hub's relay grants a fixed number of
203
+ * reservations, and once they were spent every direct-linked page failed
204
+ * to join while relay-mode pages worked.
205
+ * - `no-answer` -- a timeout or a dropped link; nothing was refused.
206
+ *
207
+ * The reservation only lets OTHER members reach this one; everything the
208
+ * member does itself works in relay mode, so failing the join over it is
209
+ * strictly worse than degrading.
210
+ *
211
+ * NOT for `not-a-member` (`PERMISSION_DENIED`): the hub has just accepted
212
+ * this peer, so a hub that now denies it membership contradicts itself (a
213
+ * revocation in between, a gater reading a different store) -- a real fault
214
+ * to report, not a condition to route around. NOT for `no-relay`: a hub
215
+ * deployed without its relay service is a configuration error that would put
216
+ * every member in relay mode silently. NOT for any other status, which is a
217
+ * protocol fault. And not for errors that are not refusals at all.
218
+ */
219
+ export function fallsBackToRelay(err: unknown): boolean {
220
+ if (!(err instanceof HubReservationError)) return false;
221
+ return (
222
+ err.refusal === "store-full" || err.refusal === "resource-limit" || err.refusal === "no-answer"
223
+ );
224
+ }
225
+
226
+ export async function startMember(init: StartMemberInit): Promise<MemberHandle> {
227
+ const onState = init.onState ?? ((): void => {});
228
+ const relayAddr = init.config.relayAddrs[0];
229
+ if (relayAddr == null) throw new Error("startMember: config.relayAddrs is empty.");
230
+ const hubPeerId = init.config.hubPeerId;
231
+
232
+ onState("connecting-relay");
233
+ const node = await init.platform.createNode({ privateKey: init.privateKey });
234
+
235
+ const unwind: Array<() => Promise<void>> = [async () => await node.stop()];
236
+ const startFailed = async (): Promise<void> => {
237
+ for (const step of [...unwind].reverse()) await step().catch(() => {});
238
+ };
239
+
240
+ try {
241
+ await dialRelay(node, relayAddr);
242
+
243
+ onState("starting-peer");
244
+ const revocationCache = new RevocationCache({ maxStalenessMs: REVOCATION_MAX_STALENESS_MS });
245
+ // `createPeer` in the prototype did transport + binding + policy + router
246
+ // in one closed function. Here the three are separate packages that meet
247
+ // at a `FetchHandler`: `servePeer` owns the wire and the router,
248
+ // `withAccess` owns who may do what, and neither imports the other.
249
+ const peer = await servePeer({
250
+ node,
251
+ mounts: init.mounts,
252
+ // WITHOUT THIS A MEMBER CANNOT CALL ANOTHER MEMBER AT ALL. The edge
253
+ // hands `/{peerId}/{path}` to `peer.dispatch`, and the router refuses to
254
+ // forward unless asked — deny by default, which is right (R-2: relaying
255
+ // is its own capability). `createPeer` in the prototype supplied this
256
+ // policy; the extraction split it into `servePeer` + `withAccess` and
257
+ // dropped it, and every type check stayed green because a missing
258
+ // optional callback is not a type error. A real mesh found it in one
259
+ // call: 403 `this peer does not relay for you`, from the CALLER's own
260
+ // router.
261
+ allowForward: forwardLocalOnly,
262
+ // Calls TO THE HUB may ride a limited connection, and only those. Set
263
+ // before the link mode is known, and harmless when it turns out to be
264
+ // `"direct"`: the link prefers an unlimited connection whenever one is
265
+ // open. Serving stays closed to limited connections -- the hub's own
266
+ // relay limits are what protect member-to-member traffic.
267
+ callOnLimitedConnection: (peerId) => peerId === hubPeerId,
268
+ access: withAccess({
269
+ issuer: hubPeerId,
270
+ rules: init.rules,
271
+ selfPeer: node.peerId.toString(),
272
+ // What the TRANSPORT proved. `serveTransport` registers it on the
273
+ // request before dispatch, so reading it back here is the same fact,
274
+ // not a second opinion.
275
+ provenPeer: (req) => lookupPeer(req) ?? ANONYMOUS,
276
+ revocation: revocationCache,
277
+ }),
278
+ });
279
+ unwind.push(async () => await peer.stop());
280
+
281
+ // WEBRTC FIRST, ONCE PER JOIN, THEN THE RELAY CIRCUIT. Any failure of the
282
+ // upgrade counts: a hub that WebRTC cannot reach fails in more ways than
283
+ // one (no route, ICE timeout, no transport), and the fallback is the same
284
+ // for all.
285
+ onState("dialing-hub");
286
+ let hubLink: HubLink;
287
+ let hubLinkNote: string | undefined;
288
+ try {
289
+ await reachHub(node, relayAddr, hubPeerId);
290
+ hubLink = "direct";
291
+ } catch (upgradeErr) {
292
+ try {
293
+ await reachHubRelayed(node, relayAddr, hubPeerId);
294
+ hubLink = "relay";
295
+ } catch (relayErr) {
296
+ throw new Error(
297
+ `startMember: the hub (${hubPeerId}) could not be reached over WebRTC ` +
298
+ `(${String(upgradeErr)}) nor over a relay circuit (${String(relayErr)}).`,
299
+ { cause: relayErr },
300
+ );
301
+ }
302
+ }
303
+
304
+ onState("resuming");
305
+ const seq = nextInitialSeq();
306
+ const resume = await resumeMembership({
307
+ peer,
308
+ hubPeerId,
309
+ addrs: node.getMultiaddrs().map((addr) => addr.toString()),
310
+ advertisements: init.advertisements?.(),
311
+ seq,
312
+ });
313
+
314
+ let initialToken: string;
315
+ let joinedBy: JoinMethod;
316
+ if (resume.status === "resumed") {
317
+ initialToken = resume.token;
318
+ joinedBy = "resumed";
319
+ } else if (resume.refusal.kind === "duplicate-identity") {
320
+ throw new MemberJoinError(
321
+ "duplicate-identity",
322
+ `another live peer is already using this identity (${peer.peerId}) on this mesh.`,
323
+ );
324
+ } else if (resume.refusal.kind !== "not-a-member") {
325
+ throw new MemberJoinError(
326
+ "presence-refused",
327
+ `the hub (${hubPeerId}) refused this peer's first heartbeat: ${resume.refusal.message}`,
328
+ );
329
+ } else if (init.invitationId == null) {
330
+ throw new MemberJoinError(
331
+ "not-a-member",
332
+ `the hub (${hubPeerId}) does not list this peer (${peer.peerId}) as a member, and no ` +
333
+ "invitation was supplied.",
334
+ );
335
+ } else {
336
+ onState("joining");
337
+ try {
338
+ const redemption = await redeemInvitation(peer, hubPeerId, init.invitationId);
339
+ initialToken = redemption.token;
340
+ joinedBy = "redeemed";
341
+ } catch (err) {
342
+ throw new MemberJoinError(
343
+ "invitation-refused",
344
+ `the hub (${hubPeerId}) would not accept that invitation. ${String(err)}`,
345
+ { cause: err },
346
+ );
347
+ }
348
+ }
349
+
350
+ // A MEMBER RESERVES ON ITS HUB, NOT ON THE PUBLIC RELAY, and only once
351
+ // the hub has accepted it — `hub-relay.ts`'s membership gater refuses
352
+ // everyone else. This is the ordering `feat/hub-relay` established.
353
+ //
354
+ // NOT OVER THE RELAY CIRCUIT. There is nothing to reserve over (the
355
+ // circuit carries only this member's calls), and `leaveRelay` would hang
356
+ // up the circuit itself, since it runs through the relay.
357
+ if (hubLink === "direct") {
358
+ onState("awaiting-reservation");
359
+ const refused = await reserveOnHub(node, hubPeerId).then(
360
+ () => null,
361
+ (err: unknown) => err,
362
+ );
363
+ if (refused != null && !fallsBackToRelay(refused)) throw refused;
364
+ if (refused != null) {
365
+ // DOWN TO RELAY MODE, INTO THE SAME STATE AS A MEMBER WHOSE UPGRADE
366
+ // FAILED: one kept circuit, no supervisor, no `leaveRelay`, the
367
+ // relay-mode re-link below. The WebRTC connection goes, too -- kept, it
368
+ // would make this a "direct" member with none of direct mode's
369
+ // reservation duties, and the hub would report it as direct while this
370
+ // handle says relay. The failed circuit listener stays behind, empty
371
+ // (`reserveOnHub`'s note).
372
+ await node.hangUp(peerIdFromString(hubPeerId)).catch(() => {});
373
+ try {
374
+ await reachHubRelayed(node, relayAddr, hubPeerId);
375
+ } catch (relayErr) {
376
+ throw new Error(
377
+ `startMember: the hub (${hubPeerId}) refused a reservation (${String(refused)}), ` +
378
+ `and the relay circuit to it could not be opened (${String(relayErr)}).`,
379
+ { cause: relayErr },
380
+ );
381
+ }
382
+ hubLink = "relay";
383
+ hubLinkNote =
384
+ "Connected through the relay only: the hub refused this member a reservation, so " +
385
+ "other members cannot reach it; its own calls are unaffected. Reconnecting tries the " +
386
+ `direct link again. (${(refused as Error).message})`;
387
+ }
388
+ }
389
+ if (hubLink === "direct") {
390
+ const supervisor = superviseHubReservation({ node, relayAddr, hubPeerId });
391
+ const unwatchWake = init.platform.watchWake?.(() => supervisor.poke()) ?? ((): void => {});
392
+ unwind.push(async () => {
393
+ unwatchWake();
394
+ supervisor.stop();
395
+ });
396
+
397
+ await leaveRelay(node, relayAddr).catch(() => {});
398
+ }
399
+
400
+ const join = startJoin({
401
+ peer,
402
+ node,
403
+ hubPeerId,
404
+ relayAddr,
405
+ initialToken,
406
+ initialSeq: seq,
407
+ revocationCache,
408
+ advertisements: init.advertisements,
409
+ onPresenceRefused: init.onPresenceRefused,
410
+ heartbeatIntervalMs: init.heartbeatIntervalMs,
411
+ keepaliveIntervalMs: init.keepaliveIntervalMs,
412
+ // A relay-mode member re-links over a NEW CIRCUIT ONLY, and stays in
413
+ // relay mode: retrying WebRTC here could land it "direct" with none of
414
+ // the reservation duties above. One that joined direct keeps `reachHub`,
415
+ // with the supervisor restoring its reservation.
416
+ relinkHub:
417
+ hubLink === "relay"
418
+ ? async () => {
419
+ await reachHubRelayed(node, relayAddr, hubPeerId);
420
+ }
421
+ : undefined,
422
+ });
423
+ unwind.push(async () => join.stop());
424
+
425
+ onState("mounting-edge");
426
+ const dispatchInit: EdgeDispatchInit = {
427
+ dispatch: peer.dispatch,
428
+ key: init.key,
429
+ token: () => join.token(),
430
+ ensureRoute: createRouteEnsurer({
431
+ node,
432
+ relayAddr,
433
+ selfPeerId: peer.peerId,
434
+ hubPeerId,
435
+ meshView: () => join.meshView(),
436
+ }),
437
+ };
438
+ const edgeFetch = createEdgeDispatch(dispatchInit);
439
+ const edge = await init.platform.mountEdge?.({ key: init.key, dispatch: edgeFetch });
440
+ if (edge != null) unwind.push(async () => await edge.stop());
441
+
442
+ onState("ready");
443
+ return {
444
+ peerId: peer.peerId,
445
+ hubPeerId,
446
+ relayAddr,
447
+ joinedBy,
448
+ fetch: edgeFetch,
449
+ baseUrl: edge?.baseUrl,
450
+ node,
451
+ meshView: () => join.meshView(),
452
+ token: () => join.token(),
453
+ hubLink: () => hubLink,
454
+ hubLinkNote,
455
+ connectionKind(peerIdStr: string): ConnectionKind {
456
+ try {
457
+ return classifyConnection(
458
+ node
459
+ .getConnections(peerIdFromString(peerIdStr))
460
+ .filter((c) => c.status === "open")
461
+ .map((c) => c.remoteAddr.toString()),
462
+ );
463
+ } catch {
464
+ return "none";
465
+ }
466
+ },
467
+ async stop() {
468
+ for (const step of [...unwind].reverse()) await step().catch(() => {});
469
+ onState("stopped");
470
+ },
471
+ };
472
+ } catch (err) {
473
+ await startFailed();
474
+ throw err;
475
+ }
476
+ }