@ckb-ccc/libp2p 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/barrel.d.mts +49 -25
- package/dist/barrel.d.mts.map +1 -1
- package/dist/barrel.mjs +1 -1
- package/dist/barrel.mjs.map +1 -1
- package/dist.commonjs/barrel.d.ts +49 -25
- package/dist.commonjs/barrel.d.ts.map +1 -1
- package/dist.commonjs/barrel.js +1 -1
- package/dist.commonjs/barrel.js.map +1 -1
- package/package.json +3 -3
- package/src/barrel.ts +1 -0
- package/src/jsonRpcService.ts +60 -9
- package/src/jsonRpcTransport.ts +10 -6
- package/src/pairingEndpoint.ts +21 -6
- package/src/pairingService.ts +4 -68
- package/src/relayConnection.ts +120 -0
package/src/jsonRpcService.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Registrar } from "@libp2p/interface-internal";
|
|
|
4
4
|
import { lpStream } from "@libp2p/utils";
|
|
5
5
|
|
|
6
6
|
const DEFAULT_MAX_MESSAGE_LENGTH = 1024 * 1024;
|
|
7
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
7
8
|
|
|
8
9
|
export type JsonRpcServiceComponents = {
|
|
9
10
|
registrar: Registrar;
|
|
@@ -12,12 +13,15 @@ export type JsonRpcServiceComponents = {
|
|
|
12
13
|
export type JsonRpcServiceConfig = {
|
|
13
14
|
protocol: string;
|
|
14
15
|
maxMessageLength?: number;
|
|
16
|
+
timeout?: number;
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
export type JsonRpcRequest = {
|
|
18
20
|
connection: Connection;
|
|
19
21
|
peerId: PeerId;
|
|
20
22
|
payload: ccc.JsonRpcPayload;
|
|
23
|
+
/** Aborts if the service times out or the request stream closes. */
|
|
24
|
+
signal: AbortSignal;
|
|
21
25
|
};
|
|
22
26
|
|
|
23
27
|
export type JsonRpcRequestHandler<
|
|
@@ -29,6 +33,7 @@ export abstract class JsonRpcService<
|
|
|
29
33
|
> {
|
|
30
34
|
private readonly errorListeners = new Set<(error: Error) => void>();
|
|
31
35
|
readonly maxMessageLength: number;
|
|
36
|
+
readonly timeout: number;
|
|
32
37
|
|
|
33
38
|
constructor(
|
|
34
39
|
readonly components: Components,
|
|
@@ -43,6 +48,13 @@ export abstract class JsonRpcService<
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
this.maxMessageLength = maxMessageLength;
|
|
51
|
+
|
|
52
|
+
const timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
53
|
+
if (!Number.isSafeInteger(timeout) || timeout <= 0) {
|
|
54
|
+
throw new Error("JSON-RPC request timeout must be a positive integer");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.timeout = timeout;
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
protected abstract handleRequest(request: JsonRpcRequest): unknown;
|
|
@@ -66,28 +78,57 @@ export abstract class JsonRpcService<
|
|
|
66
78
|
|
|
67
79
|
private async handleProtocol(stream: Stream, connection: Connection) {
|
|
68
80
|
let requestAborted = false;
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timeoutSignal = AbortSignal.timeout(this.timeout);
|
|
83
|
+
const abortRequest = (event: { error?: Error }) => {
|
|
84
|
+
requestAborted = true;
|
|
85
|
+
const error = new Error(event.error?.message ?? "JSON-RPC stream closed");
|
|
86
|
+
error.name = "AbortError";
|
|
87
|
+
controller.abort(error);
|
|
88
|
+
};
|
|
89
|
+
const abortTimeout = () => {
|
|
90
|
+
requestAborted = true;
|
|
91
|
+
const error = abortReason(timeoutSignal, "JSON-RPC request timed out");
|
|
92
|
+
controller.abort(error);
|
|
93
|
+
stream.abort(error);
|
|
94
|
+
};
|
|
95
|
+
stream.addEventListener("close", abortRequest, { once: true });
|
|
96
|
+
timeoutSignal.addEventListener("abort", abortTimeout, { once: true });
|
|
97
|
+
if (stream.status !== "open") {
|
|
98
|
+
abortRequest({});
|
|
99
|
+
}
|
|
69
100
|
|
|
70
101
|
try {
|
|
71
102
|
const rpcStream = lpStream(stream, {
|
|
72
103
|
maxDataLength: this.maxMessageLength,
|
|
73
104
|
});
|
|
74
105
|
const payload = parseJsonRpcRequest(
|
|
75
|
-
ccc.bytesTo(
|
|
106
|
+
ccc.bytesTo(
|
|
107
|
+
(await rpcStream.read({ signal: controller.signal })).subarray(),
|
|
108
|
+
"utf8",
|
|
109
|
+
),
|
|
76
110
|
);
|
|
77
111
|
|
|
78
112
|
let response: ccc.JsonRpcResponse;
|
|
79
113
|
try {
|
|
114
|
+
controller.signal.throwIfAborted();
|
|
80
115
|
response = {
|
|
81
116
|
jsonrpc: "2.0",
|
|
82
117
|
id: payload.id,
|
|
83
|
-
result: await
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
118
|
+
result: await Promise.race([
|
|
119
|
+
Promise.resolve(
|
|
120
|
+
this.handleRequest({
|
|
121
|
+
connection,
|
|
122
|
+
peerId: connection.remotePeer,
|
|
123
|
+
payload,
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
ccc.abortSignalToPromise(controller.signal),
|
|
128
|
+
]),
|
|
88
129
|
};
|
|
89
130
|
} catch (cause) {
|
|
90
|
-
requestAborted
|
|
131
|
+
requestAborted ||= isAbortError(cause);
|
|
91
132
|
const error = toJsonRpcError(cause);
|
|
92
133
|
response = {
|
|
93
134
|
jsonrpc: "2.0",
|
|
@@ -100,13 +141,19 @@ export abstract class JsonRpcService<
|
|
|
100
141
|
};
|
|
101
142
|
}
|
|
102
143
|
|
|
103
|
-
await rpcStream.write(ccc.bytesFrom(JSON.stringify(response), "utf8")
|
|
104
|
-
|
|
144
|
+
await rpcStream.write(ccc.bytesFrom(JSON.stringify(response), "utf8"), {
|
|
145
|
+
signal: controller.signal,
|
|
146
|
+
});
|
|
147
|
+
stream.removeEventListener("close", abortRequest);
|
|
148
|
+
await stream.close({ signal: controller.signal });
|
|
105
149
|
} catch (cause) {
|
|
106
150
|
if (requestAborted) {
|
|
107
151
|
return;
|
|
108
152
|
}
|
|
109
153
|
this.handleError(cause, stream);
|
|
154
|
+
} finally {
|
|
155
|
+
stream.removeEventListener("close", abortRequest);
|
|
156
|
+
timeoutSignal.removeEventListener("abort", abortTimeout);
|
|
110
157
|
}
|
|
111
158
|
}
|
|
112
159
|
|
|
@@ -147,6 +194,10 @@ function asJsonRpcError(cause: unknown) {
|
|
|
147
194
|
return cause instanceof Error ? cause : new Error("JSON-RPC request failed");
|
|
148
195
|
}
|
|
149
196
|
|
|
197
|
+
function abortReason(signal: AbortSignal, fallback: string) {
|
|
198
|
+
return signal.reason instanceof Error ? signal.reason : new Error(fallback);
|
|
199
|
+
}
|
|
200
|
+
|
|
150
201
|
function isAbortError(cause: unknown) {
|
|
151
202
|
return (
|
|
152
203
|
cause instanceof Error &&
|
package/src/jsonRpcTransport.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { lpStream } from "@libp2p/utils";
|
|
|
4
4
|
import { dialKnownAddresses } from "./dial.js";
|
|
5
5
|
|
|
6
6
|
const DEFAULT_MAX_MESSAGE_LENGTH = 1024 * 1024;
|
|
7
|
-
const DEFAULT_TIMEOUT =
|
|
7
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
8
8
|
|
|
9
9
|
export type JsonRpcTransportLibp2pConfig = {
|
|
10
10
|
protocol: string;
|
|
@@ -41,11 +41,15 @@ export class JsonRpcTransportLibp2p implements ccc.JsonRpcTransport {
|
|
|
41
41
|
this.timeout = timeout;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async request(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
async request(
|
|
45
|
+
payload: ccc.JsonRpcPayload,
|
|
46
|
+
options?: ccc.JsonRpcTransportRequestOptions,
|
|
47
|
+
): Promise<ccc.JsonRpcResponse> {
|
|
48
|
+
const timeoutSignal = AbortSignal.timeout(options?.timeout ?? this.timeout);
|
|
49
|
+
const signals = [this.config.signal, options?.signal, timeoutSignal].filter(
|
|
50
|
+
(signal): signal is AbortSignal => signal !== undefined,
|
|
51
|
+
);
|
|
52
|
+
const signal = ccc.abortSignalAny(signals);
|
|
49
53
|
|
|
50
54
|
let stream: Stream | undefined;
|
|
51
55
|
let response: ccc.JsonRpcResponse;
|
package/src/pairingEndpoint.ts
CHANGED
|
@@ -7,7 +7,14 @@ const MAX_DECOMPRESSED_ADDRESSES_LENGTH = 16 * 1024;
|
|
|
7
7
|
const MAX_ADDRESSES = 16;
|
|
8
8
|
const PAIRING_PARAMETER_NAMES = ["addresses", "role", "secret"] as const;
|
|
9
9
|
|
|
10
|
-
export class
|
|
10
|
+
export class PairingEndpointError extends Error {
|
|
11
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
12
|
+
super(message, options);
|
|
13
|
+
this.name = "PairingEndpointError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class PairingEndpointRoleError extends PairingEndpointError {
|
|
11
18
|
constructor(
|
|
12
19
|
readonly expectedRole: string,
|
|
13
20
|
readonly actualRole: string | undefined,
|
|
@@ -66,7 +73,14 @@ export async function decodePairingEndpoint(
|
|
|
66
73
|
endpoint: string,
|
|
67
74
|
expectedRole?: string,
|
|
68
75
|
): Promise<PairingTarget> {
|
|
69
|
-
|
|
76
|
+
let url: URL;
|
|
77
|
+
try {
|
|
78
|
+
url = new URL(endpoint.trim());
|
|
79
|
+
} catch (cause) {
|
|
80
|
+
throw new PairingEndpointError("Pairing endpoint is not a valid URL", {
|
|
81
|
+
cause,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
70
84
|
const { params } = fragmentParameters(url);
|
|
71
85
|
if (expectedRole) {
|
|
72
86
|
const actualRole = params.get("role")?.trim() || undefined;
|
|
@@ -78,7 +92,7 @@ export async function decodePairingEndpoint(
|
|
|
78
92
|
const secret = params.get("secret")?.trim();
|
|
79
93
|
|
|
80
94
|
if (!compressedAddresses || !secret) {
|
|
81
|
-
throw new
|
|
95
|
+
throw new PairingEndpointError("Pairing endpoint is incomplete");
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
const addresses = await decodeCompressedAddresses(compressedAddresses);
|
|
@@ -128,9 +142,10 @@ async function decodeCompressedAddresses(value: string): Promise<Multiaddr[]> {
|
|
|
128
142
|
}
|
|
129
143
|
return addresses;
|
|
130
144
|
} catch (cause) {
|
|
131
|
-
throw new
|
|
132
|
-
|
|
133
|
-
|
|
145
|
+
throw new PairingEndpointError(
|
|
146
|
+
"Pairing endpoint contains invalid compressed addresses",
|
|
147
|
+
{ cause },
|
|
148
|
+
);
|
|
134
149
|
}
|
|
135
150
|
}
|
|
136
151
|
|
package/src/pairingService.ts
CHANGED
|
@@ -5,19 +5,14 @@ import type {
|
|
|
5
5
|
PeerId,
|
|
6
6
|
Stream,
|
|
7
7
|
} from "@libp2p/interface";
|
|
8
|
-
import type {
|
|
9
|
-
AddressManager,
|
|
10
|
-
ConnectionManager,
|
|
11
|
-
Registrar,
|
|
12
|
-
} from "@libp2p/interface-internal";
|
|
8
|
+
import type { ConnectionManager, Registrar } from "@libp2p/interface-internal";
|
|
13
9
|
import { lpStream } from "@libp2p/utils";
|
|
14
|
-
import {
|
|
10
|
+
import type { Multiaddr } from "@multiformats/multiaddr";
|
|
15
11
|
|
|
16
12
|
const MAX_PAIRING_MESSAGE_LENGTH = 1024;
|
|
17
13
|
const UNPAIR_TIMEOUT_MS = 10_000;
|
|
18
14
|
|
|
19
15
|
export type PairingServiceComponents = {
|
|
20
|
-
addressManager?: Pick<AddressManager, "getAddresses">;
|
|
21
16
|
connectionManager: ConnectionManager;
|
|
22
17
|
registrar: Registrar;
|
|
23
18
|
};
|
|
@@ -39,8 +34,7 @@ export type PairingGuard<
|
|
|
39
34
|
> = (this: PairingService<Components>, peerId: PeerId) => boolean;
|
|
40
35
|
|
|
41
36
|
type PairingRequest =
|
|
42
|
-
|
|
43
|
-
| { type: "unpair" };
|
|
37
|
+
{ type: "pair"; secret: string; name?: string } | { type: "unpair" };
|
|
44
38
|
|
|
45
39
|
type PairingResponse =
|
|
46
40
|
{ ok: true; name?: string } | { ok: false; error: string };
|
|
@@ -147,7 +141,7 @@ export abstract class PairingService<
|
|
|
147
141
|
|
|
148
142
|
const response = await exchange(
|
|
149
143
|
stream,
|
|
150
|
-
createPairRequest(target.secret, this.config.name
|
|
144
|
+
createPairRequest(target.secret, this.config.name),
|
|
151
145
|
options,
|
|
152
146
|
);
|
|
153
147
|
options.signal?.throwIfAborted();
|
|
@@ -223,11 +217,6 @@ export abstract class PairingService<
|
|
|
223
217
|
return;
|
|
224
218
|
}
|
|
225
219
|
|
|
226
|
-
const addresses = parseAddresses(
|
|
227
|
-
request.addresses,
|
|
228
|
-
connection.remotePeer,
|
|
229
|
-
);
|
|
230
|
-
|
|
231
220
|
// Consume the credential before the next asynchronous boundary so two
|
|
232
221
|
// concurrent requests cannot both pair with the same endpoint.
|
|
233
222
|
this.rotateSecret();
|
|
@@ -241,7 +230,6 @@ export abstract class PairingService<
|
|
|
241
230
|
});
|
|
242
231
|
if (addedPeer) {
|
|
243
232
|
this.notifyPairedPeer(addedPeer, request.name);
|
|
244
|
-
void this.openFallbackConnection(addresses);
|
|
245
233
|
}
|
|
246
234
|
} catch (cause) {
|
|
247
235
|
if (addedPeer) {
|
|
@@ -266,22 +254,6 @@ export abstract class PairingService<
|
|
|
266
254
|
this.pairedListeners.forEach((listener) => listener(peerId, name));
|
|
267
255
|
}
|
|
268
256
|
|
|
269
|
-
private addresses() {
|
|
270
|
-
return this.components.addressManager?.getAddresses() ?? [];
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
private async openFallbackConnection(addresses: Multiaddr[]) {
|
|
274
|
-
if (addresses.length === 0) {
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
await this.components.connectionManager.openConnection(addresses);
|
|
280
|
-
} catch {
|
|
281
|
-
// The original pairing connection remains usable when the fallback fails.
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
257
|
private rotateSecret() {
|
|
286
258
|
this.currentSecret = createSecret();
|
|
287
259
|
this.secretChangedListeners.forEach((listener) =>
|
|
@@ -392,22 +364,12 @@ async function exchange(
|
|
|
392
364
|
function createPairRequest(
|
|
393
365
|
secret: string,
|
|
394
366
|
name: string | undefined,
|
|
395
|
-
addresses: Multiaddr[],
|
|
396
367
|
): PairingRequest {
|
|
397
|
-
const addressStrings = addresses.map((address) => address.toString());
|
|
398
368
|
const request: PairingRequest = {
|
|
399
369
|
type: "pair",
|
|
400
370
|
secret,
|
|
401
371
|
name,
|
|
402
|
-
addresses: addressStrings,
|
|
403
372
|
};
|
|
404
|
-
while (addressStrings.length > 0 && !pairingRequestFits(request)) {
|
|
405
|
-
addressStrings.pop();
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (!pairingRequestFits(request)) {
|
|
409
|
-
delete request.addresses;
|
|
410
|
-
}
|
|
411
373
|
if (!pairingRequestFits(request)) {
|
|
412
374
|
delete request.name;
|
|
413
375
|
}
|
|
@@ -446,7 +408,6 @@ function parsePairingRequest(data: Uint8Array): PairingRequest {
|
|
|
446
408
|
type: "pair",
|
|
447
409
|
secret: request.secret,
|
|
448
410
|
name: typeof request.name === "string" ? request.name : undefined,
|
|
449
|
-
addresses: parseAddressStrings(request.addresses),
|
|
450
411
|
};
|
|
451
412
|
}
|
|
452
413
|
|
|
@@ -457,31 +418,6 @@ function parsePairingRequest(data: Uint8Array): PairingRequest {
|
|
|
457
418
|
throw new Error("Invalid pairing request");
|
|
458
419
|
}
|
|
459
420
|
|
|
460
|
-
function parseAddressStrings(value: unknown): string[] | undefined {
|
|
461
|
-
if (!Array.isArray(value)) {
|
|
462
|
-
return undefined;
|
|
463
|
-
}
|
|
464
|
-
return value.filter(
|
|
465
|
-
(address): address is string =>
|
|
466
|
-
typeof address === "string" && address.length > 0,
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function parseAddresses(values: string[] | undefined, peerId: PeerId) {
|
|
471
|
-
return (values ?? []).flatMap((value) => {
|
|
472
|
-
try {
|
|
473
|
-
const address = multiaddr(value);
|
|
474
|
-
const peer = address
|
|
475
|
-
.getComponents()
|
|
476
|
-
.filter(({ code }) => code === CODE_P2P)
|
|
477
|
-
.at(-1)?.value;
|
|
478
|
-
return peer === peerId.toString() ? [address] : [];
|
|
479
|
-
} catch {
|
|
480
|
-
return [];
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
|
|
485
421
|
function parsePairingResponse(data: Uint8Array): PairingResponse {
|
|
486
422
|
const response = JSON.parse(
|
|
487
423
|
ccc.bytesTo(data, "utf8"),
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { ccc } from "@ckb-ccc/core";
|
|
2
|
+
import type { Connection, Libp2p } from "@libp2p/interface";
|
|
3
|
+
import { multiaddr, type Multiaddr } from "@multiformats/multiaddr";
|
|
4
|
+
|
|
5
|
+
const DIAL_TIMEOUT_MS = 10_000;
|
|
6
|
+
const RETRY_DELAYS_MS = [5_000, 10_000, 20_000] as const;
|
|
7
|
+
const RETRY_REPEAT_MS = 30_000;
|
|
8
|
+
|
|
9
|
+
export type RelayConnectionControllerOptions = {
|
|
10
|
+
onConnectionChange?: (connection?: Connection) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Maintains a connection to a relay until stopped. */
|
|
14
|
+
export class RelayConnectionController {
|
|
15
|
+
private readonly controller = new AbortController();
|
|
16
|
+
private readonly addresses: readonly Multiaddr[];
|
|
17
|
+
private selectedAddress?: Multiaddr;
|
|
18
|
+
private connection?: Connection;
|
|
19
|
+
private connecting?: Promise<Connection>;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly node: Libp2p,
|
|
23
|
+
addresses: readonly string[],
|
|
24
|
+
private readonly options: RelayConnectionControllerOptions = {},
|
|
25
|
+
) {
|
|
26
|
+
if (addresses.length === 0) {
|
|
27
|
+
throw new Error("At least one relay address is required");
|
|
28
|
+
}
|
|
29
|
+
this.addresses = addresses.map((address) => multiaddr(address.trim()));
|
|
30
|
+
node.addEventListener("connection:close", this.handleConnectionClose, {
|
|
31
|
+
signal: this.controller.signal,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
connect(): Promise<Connection> {
|
|
36
|
+
this.controller.signal.throwIfAborted();
|
|
37
|
+
if (this.connection?.status === "open") {
|
|
38
|
+
return Promise.resolve(this.connection);
|
|
39
|
+
}
|
|
40
|
+
if (this.connecting) {
|
|
41
|
+
return this.connecting;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.connection = undefined;
|
|
45
|
+
const connecting = ccc
|
|
46
|
+
.retry<Connection>(
|
|
47
|
+
RETRY_DELAYS_MS,
|
|
48
|
+
async ({ resolve }) => {
|
|
49
|
+
return resolve(await this.dialRelay());
|
|
50
|
+
},
|
|
51
|
+
{ repeat: RETRY_REPEAT_MS, signal: this.controller.signal },
|
|
52
|
+
)
|
|
53
|
+
.finally(() => {
|
|
54
|
+
if (this.connecting === connecting) {
|
|
55
|
+
this.connecting = undefined;
|
|
56
|
+
}
|
|
57
|
+
if (!this.controller.signal.aborted && !this.connection) {
|
|
58
|
+
void this.connect().catch(() => {});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
this.connecting = connecting;
|
|
62
|
+
return connecting;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async stop() {
|
|
66
|
+
if (this.controller.signal.aborted) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.controller.abort();
|
|
71
|
+
const connection = this.connection;
|
|
72
|
+
this.connection = undefined;
|
|
73
|
+
this.options.onConnectionChange?.();
|
|
74
|
+
await connection?.close();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async dialRelay() {
|
|
78
|
+
const addresses = this.selectedAddress
|
|
79
|
+
? [this.selectedAddress]
|
|
80
|
+
: this.addresses;
|
|
81
|
+
let lastError: unknown;
|
|
82
|
+
|
|
83
|
+
for (const address of addresses) {
|
|
84
|
+
await ccc.waitForAvailability(this.controller.signal);
|
|
85
|
+
let connection: Connection | undefined;
|
|
86
|
+
try {
|
|
87
|
+
connection = await this.node.dial(address, {
|
|
88
|
+
signal: ccc.abortSignalAny([
|
|
89
|
+
this.controller.signal,
|
|
90
|
+
AbortSignal.timeout(DIAL_TIMEOUT_MS),
|
|
91
|
+
]),
|
|
92
|
+
});
|
|
93
|
+
this.controller.signal.throwIfAborted();
|
|
94
|
+
this.connection = connection;
|
|
95
|
+
if (connection.status !== "open") {
|
|
96
|
+
this.connection = undefined;
|
|
97
|
+
throw new Error("Relay connection closed while dialing");
|
|
98
|
+
}
|
|
99
|
+
this.selectedAddress = address;
|
|
100
|
+
this.options.onConnectionChange?.(connection);
|
|
101
|
+
return connection;
|
|
102
|
+
} catch (cause) {
|
|
103
|
+
lastError = cause;
|
|
104
|
+
await connection?.close();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw lastError;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private readonly handleConnectionClose = (event: CustomEvent<Connection>) => {
|
|
112
|
+
if (event.detail.id !== this.connection?.id) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.connection = undefined;
|
|
117
|
+
this.options.onConnectionChange?.();
|
|
118
|
+
void this.connect().catch(() => {});
|
|
119
|
+
};
|
|
120
|
+
}
|