@yefengr/remote-pi 0.7.10 → 0.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/daemon/commands.d.ts +5 -3
- package/dist/daemon/commands.js +22 -9
- package/dist/daemon/commands.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +67 -155
- package/dist/index.js.map +1 -1
- package/dist/pairing/qr.d.ts +2 -2
- package/dist/pairing/qr.js +11 -3
- package/dist/pairing/qr.js.map +1 -1
- package/dist/runtime/relay_lifecycle.d.ts +58 -0
- package/dist/runtime/relay_lifecycle.js +241 -0
- package/dist/runtime/relay_lifecycle.js.map +1 -0
- package/dist/transport/relay_client.d.ts +13 -0
- package/dist/transport/relay_client.js +163 -38
- package/dist/transport/relay_client.js.map +1 -1
- package/package.json +1 -1
package/dist/pairing/qr.js
CHANGED
|
@@ -35,7 +35,7 @@ export class QRSession {
|
|
|
35
35
|
}
|
|
36
36
|
export const qrSession = new QRSession();
|
|
37
37
|
/** Builds the endpoint-aware pairing URI. */
|
|
38
|
-
export function buildQRUri(token, longtermEdPk, sessionName, endpointId, runtimeInstanceId) {
|
|
38
|
+
export function buildQRUri(token, longtermEdPk, sessionName, endpointId, runtimeInstanceId, relayUrl) {
|
|
39
39
|
const params = new URLSearchParams({
|
|
40
40
|
t: token,
|
|
41
41
|
epk: Buffer.from(longtermEdPk).toString("base64url"),
|
|
@@ -43,6 +43,14 @@ export function buildQRUri(token, longtermEdPk, sessionName, endpointId, runtime
|
|
|
43
43
|
ep: endpointId,
|
|
44
44
|
rt: runtimeInstanceId,
|
|
45
45
|
});
|
|
46
|
+
if (relayUrl) {
|
|
47
|
+
try {
|
|
48
|
+
const protocol = new URL(relayUrl).protocol;
|
|
49
|
+
if (protocol === "http:" || protocol === "https:")
|
|
50
|
+
params.set("r", relayUrl);
|
|
51
|
+
}
|
|
52
|
+
catch { /* invalid optional hint */ }
|
|
53
|
+
}
|
|
46
54
|
return `remotepi://pair?${params.toString()}`;
|
|
47
55
|
}
|
|
48
56
|
export function renderQRAscii(uri) {
|
|
@@ -53,14 +61,14 @@ export function renderQRAscii(uri) {
|
|
|
53
61
|
export function displayQR(uri) {
|
|
54
62
|
process.stderr.write(`\nScan to pair:\n\n${renderQRAscii(uri)}\n`);
|
|
55
63
|
}
|
|
56
|
-
export function startQRRotation(longtermEdPk, sessionName, endpointId, runtimeInstanceId) {
|
|
64
|
+
export function startQRRotation(longtermEdPk, sessionName, endpointId, runtimeInstanceId, relayUrl) {
|
|
57
65
|
let timer = null;
|
|
58
66
|
let stopped = false;
|
|
59
67
|
const rotate = () => {
|
|
60
68
|
if (stopped)
|
|
61
69
|
return;
|
|
62
70
|
const { token, expiresAt } = qrSession.issueToken();
|
|
63
|
-
displayQR(buildQRUri(token, longtermEdPk, sessionName, endpointId, runtimeInstanceId));
|
|
71
|
+
displayQR(buildQRUri(token, longtermEdPk, sessionName, endpointId, runtimeInstanceId, relayUrl));
|
|
64
72
|
console.log(`Renews at ${new Date(expiresAt).toLocaleTimeString()} — waiting for scan…`);
|
|
65
73
|
timer = setTimeout(rotate, TOKEN_TTL_MS);
|
|
66
74
|
};
|
package/dist/pairing/qr.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qr.js","sourceRoot":"","sources":["../../src/pairing/qr.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,UAAU,MAAM,iBAAiB,CAAC;AAEzC,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AACnC,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC;AACtC,MAAM,CAAC,MAAM,eAAe,GAAG,OAAO,CAAC;AAEvC,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,YAAY,CAAC;IACjD,OAAO,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACjF,CAAC;AAQD,MAAM,OAAO,SAAS;IACZ,MAAM,GAAuB,IAAI,CAAC;IAE1C,aAAa;QACX,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC/C,CAAC;IAED,UAAU,CAAC,QAAgB,YAAY;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;QACrC,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QACpD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAC9B,CAAC;IAED,YAAY,CAAC,KAAa;QACxB,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,KAAK;YAAE,OAAO,SAAS,CAAC;QAClE,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ;YAAE,OAAO,UAAU,CAAC;QAC5C,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS;YAAE,OAAO,SAAS,CAAC;QACzD,IAAI,CAAC,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK;QACH,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;IACrB,CAAC;CACF;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC;AAEzC,6CAA6C;AAC7C,MAAM,UAAU,UAAU,CACxB,KAAa,EACb,YAAwB,EACxB,WAAmB,EACnB,UAAkB,EAClB,iBAAyB;
|
|
1
|
+
{"version":3,"file":"qr.js","sourceRoot":"","sources":["../../src/pairing/qr.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,UAAU,MAAM,iBAAiB,CAAC;AAEzC,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AACnC,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC;AACtC,MAAM,CAAC,MAAM,eAAe,GAAG,OAAO,CAAC;AAEvC,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,YAAY,CAAC;IACjD,OAAO,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACjF,CAAC;AAQD,MAAM,OAAO,SAAS;IACZ,MAAM,GAAuB,IAAI,CAAC;IAE1C,aAAa;QACX,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC/C,CAAC;IAED,UAAU,CAAC,QAAgB,YAAY;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;QACrC,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QACpD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAC9B,CAAC;IAED,YAAY,CAAC,KAAa;QACxB,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,KAAK;YAAE,OAAO,SAAS,CAAC;QAClE,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ;YAAE,OAAO,UAAU,CAAC;QAC5C,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS;YAAE,OAAO,SAAS,CAAC;QACzD,IAAI,CAAC,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK;QACH,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;IACrB,CAAC;CACF;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC;AAEzC,6CAA6C;AAC7C,MAAM,UAAU,UAAU,CACxB,KAAa,EACb,YAAwB,EACxB,WAAmB,EACnB,UAAkB,EAClB,iBAAyB,EACzB,QAAiB;IAEjB,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,CAAC,EAAE,KAAK;QACR,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;QACpD,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;QAC3B,EAAE,EAAE,UAAU;QACd,EAAE,EAAE,iBAAiB;KACtB,CAAC,CAAC;IACH,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;YAC5C,IAAI,QAAQ,KAAK,OAAO,IAAI,QAAQ,KAAK,QAAQ;gBAAE,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC/E,CAAC;QAAC,MAAM,CAAC,CAAC,2BAA2B,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,mBAAmB,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,UAAU,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,GAAG,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACzE,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sBAAsB,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,YAAwB,EACxB,WAAmB,EACnB,UAAkB,EAClB,iBAAyB,EACzB,QAAgB;IAEhB,IAAI,KAAK,GAAyC,IAAI,CAAC;IACvD,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,MAAM,GAAG,GAAG,EAAE;QAClB,IAAI,OAAO;YAAE,OAAO;QACpB,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,SAAS,CAAC,UAAU,EAAE,CAAC;QACpD,SAAS,CAAC,UAAU,CAAC,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,iBAAiB,EAAE,QAAQ,CAAC,CAAC,CAAC;QACjG,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,kBAAkB,EAAE,sBAAsB,CAAC,CAAC;QACzF,KAAK,GAAG,UAAU,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC3C,CAAC,CAAC;IACF,MAAM,EAAE,CAAC;IACT,OAAO,GAAG,EAAE;QACV,OAAO,GAAG,IAAI,CAAC;QACf,IAAI,KAAK,KAAK,IAAI;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QACxC,SAAS,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { RelayResolution } from "../config.js";
|
|
3
|
+
import type { Ed25519Keypair } from "../pairing/crypto.js";
|
|
4
|
+
import type { HostConnectOptions, RelayClient } from "../transport/relay_client.js";
|
|
5
|
+
export type RemoteState = "idle" | "started";
|
|
6
|
+
export type RelayConnectivity = "connected" | "reconnecting" | "disconnected";
|
|
7
|
+
export type InitialRelayResult = "completed" | "cancelled";
|
|
8
|
+
export type RelayStartContext = Pick<ExtensionContext, "ui" | "cwd">;
|
|
9
|
+
export interface RelayLifecycleDependencies {
|
|
10
|
+
loadIdentity(): Promise<Ed25519Keypair>;
|
|
11
|
+
resolveRelayUrl(): RelayResolution;
|
|
12
|
+
createClient(relayUrl: string, identity: Ed25519Keypair): RelayClient;
|
|
13
|
+
buildConnectOptions(cwd?: string): Promise<HostConnectOptions>;
|
|
14
|
+
handleIdentityError(error: unknown, ctx: RelayStartContext): boolean;
|
|
15
|
+
describeConnection(resolution: RelayResolution): string;
|
|
16
|
+
onStateChange(): void;
|
|
17
|
+
onConnected(client: RelayClient, ctx?: RelayStartContext): void;
|
|
18
|
+
onDisconnected(): void;
|
|
19
|
+
}
|
|
20
|
+
export declare class RelayLifecycle {
|
|
21
|
+
private readonly deps;
|
|
22
|
+
private activeRelay;
|
|
23
|
+
private pendingCandidate;
|
|
24
|
+
private currentRelayUrl;
|
|
25
|
+
private identity;
|
|
26
|
+
private initialAttempt;
|
|
27
|
+
private cancelInitialAttempt;
|
|
28
|
+
private reconnectTimer;
|
|
29
|
+
private reconnectAttempt;
|
|
30
|
+
private generation;
|
|
31
|
+
private isDisposed;
|
|
32
|
+
private currentState;
|
|
33
|
+
private readonly closedCandidates;
|
|
34
|
+
constructor(deps: RelayLifecycleDependencies);
|
|
35
|
+
get state(): RemoteState;
|
|
36
|
+
get relay(): RelayClient | null;
|
|
37
|
+
get relayUrl(): string | null;
|
|
38
|
+
get keypair(): Ed25519Keypair | null;
|
|
39
|
+
get disposed(): boolean;
|
|
40
|
+
get hasPendingReconnect(): boolean;
|
|
41
|
+
get status(): RelayConnectivity;
|
|
42
|
+
isCurrent(candidate: RelayClient): boolean;
|
|
43
|
+
setDisposed(value: boolean): void;
|
|
44
|
+
start(ctx: RelayStartContext): Promise<InitialRelayResult>;
|
|
45
|
+
waitForInitial(): Promise<InitialRelayResult>;
|
|
46
|
+
stop(): void;
|
|
47
|
+
private startInitial;
|
|
48
|
+
private scheduleReconnect;
|
|
49
|
+
private reconnect;
|
|
50
|
+
private onRelayClose;
|
|
51
|
+
private handleConnectionFailure;
|
|
52
|
+
private clearCandidate;
|
|
53
|
+
private closeCandidateOnce;
|
|
54
|
+
private clearInitialAttempt;
|
|
55
|
+
private isOperationCurrent;
|
|
56
|
+
private isReconnectCurrent;
|
|
57
|
+
private notify;
|
|
58
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
const RECONNECT_BACKOFFS_MS = [1_000, 2_000, 5_000, 10_000, 30_000];
|
|
2
|
+
export class RelayLifecycle {
|
|
3
|
+
deps;
|
|
4
|
+
activeRelay = null;
|
|
5
|
+
pendingCandidate = null;
|
|
6
|
+
currentRelayUrl = null;
|
|
7
|
+
identity = null;
|
|
8
|
+
initialAttempt = null;
|
|
9
|
+
cancelInitialAttempt = null;
|
|
10
|
+
reconnectTimer = null;
|
|
11
|
+
reconnectAttempt = 0;
|
|
12
|
+
generation = 0;
|
|
13
|
+
isDisposed = false;
|
|
14
|
+
currentState = "idle";
|
|
15
|
+
closedCandidates = new WeakSet();
|
|
16
|
+
constructor(deps) {
|
|
17
|
+
this.deps = deps;
|
|
18
|
+
}
|
|
19
|
+
get state() { return this.currentState; }
|
|
20
|
+
get relay() { return this.activeRelay; }
|
|
21
|
+
get relayUrl() { return this.currentRelayUrl; }
|
|
22
|
+
get keypair() { return this.identity; }
|
|
23
|
+
get disposed() { return this.isDisposed; }
|
|
24
|
+
get hasPendingReconnect() { return this.reconnectTimer !== null; }
|
|
25
|
+
get status() {
|
|
26
|
+
if (this.currentState === "idle")
|
|
27
|
+
return "disconnected";
|
|
28
|
+
return this.activeRelay?.isOpen() ? "connected" : "reconnecting";
|
|
29
|
+
}
|
|
30
|
+
isCurrent(candidate) {
|
|
31
|
+
return !this.isDisposed && this.currentState === "started" && this.activeRelay === candidate;
|
|
32
|
+
}
|
|
33
|
+
setDisposed(value) {
|
|
34
|
+
this.isDisposed = value;
|
|
35
|
+
}
|
|
36
|
+
start(ctx) {
|
|
37
|
+
const existing = this.initialAttempt;
|
|
38
|
+
if (existing)
|
|
39
|
+
return existing;
|
|
40
|
+
if (this.currentState !== "idle") {
|
|
41
|
+
const message = this.status === "reconnecting"
|
|
42
|
+
? "[remote-pi] Relay is reconnecting in background."
|
|
43
|
+
: "[remote-pi] Already connected.";
|
|
44
|
+
this.notify(ctx, message, "warning");
|
|
45
|
+
return Promise.resolve("completed");
|
|
46
|
+
}
|
|
47
|
+
const operation = this.startInitial(ctx);
|
|
48
|
+
let cancel;
|
|
49
|
+
const cancellation = new Promise((resolve) => {
|
|
50
|
+
cancel = () => resolve("cancelled");
|
|
51
|
+
});
|
|
52
|
+
const attempt = Promise.race([operation.then(() => "completed"), cancellation]);
|
|
53
|
+
this.initialAttempt = attempt;
|
|
54
|
+
this.cancelInitialAttempt = cancel;
|
|
55
|
+
void operation.catch(() => undefined);
|
|
56
|
+
void attempt.then(() => this.clearInitialAttempt(attempt, cancel), () => this.clearInitialAttempt(attempt, cancel));
|
|
57
|
+
return attempt;
|
|
58
|
+
}
|
|
59
|
+
waitForInitial() {
|
|
60
|
+
return this.initialAttempt ?? Promise.resolve("completed");
|
|
61
|
+
}
|
|
62
|
+
stop() {
|
|
63
|
+
this.generation += 1;
|
|
64
|
+
if (this.reconnectTimer) {
|
|
65
|
+
clearTimeout(this.reconnectTimer);
|
|
66
|
+
this.reconnectTimer = null;
|
|
67
|
+
}
|
|
68
|
+
this.reconnectAttempt = 0;
|
|
69
|
+
const active = this.activeRelay;
|
|
70
|
+
const pending = this.pendingCandidate;
|
|
71
|
+
const cancel = this.cancelInitialAttempt;
|
|
72
|
+
this.activeRelay = null;
|
|
73
|
+
this.pendingCandidate = null;
|
|
74
|
+
this.currentRelayUrl = null;
|
|
75
|
+
this.initialAttempt = null;
|
|
76
|
+
this.cancelInitialAttempt = null;
|
|
77
|
+
this.currentState = "idle";
|
|
78
|
+
cancel?.();
|
|
79
|
+
if (active)
|
|
80
|
+
this.closeCandidateOnce(active);
|
|
81
|
+
if (pending && pending !== active)
|
|
82
|
+
this.closeCandidateOnce(pending);
|
|
83
|
+
this.deps.onDisconnected();
|
|
84
|
+
this.deps.onStateChange();
|
|
85
|
+
}
|
|
86
|
+
async startInitial(ctx) {
|
|
87
|
+
const expectedGeneration = ++this.generation;
|
|
88
|
+
let identity;
|
|
89
|
+
try {
|
|
90
|
+
identity = await this.deps.loadIdentity();
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (!this.isOperationCurrent(expectedGeneration))
|
|
94
|
+
return;
|
|
95
|
+
if (this.deps.handleIdentityError(error, ctx))
|
|
96
|
+
return;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
if (!this.isOperationCurrent(expectedGeneration))
|
|
100
|
+
return;
|
|
101
|
+
this.identity = identity;
|
|
102
|
+
const resolution = this.deps.resolveRelayUrl();
|
|
103
|
+
this.currentRelayUrl = resolution.url;
|
|
104
|
+
this.currentState = "started";
|
|
105
|
+
this.deps.onStateChange();
|
|
106
|
+
this.notify(ctx, this.deps.describeConnection(resolution));
|
|
107
|
+
let options;
|
|
108
|
+
try {
|
|
109
|
+
options = await this.deps.buildConnectOptions(ctx.cwd);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
this.handleConnectionFailure(expectedGeneration, resolution.url, ctx, error);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!this.isReconnectCurrent(expectedGeneration))
|
|
116
|
+
return;
|
|
117
|
+
const candidate = this.deps.createClient(resolution.url, identity);
|
|
118
|
+
this.pendingCandidate = candidate;
|
|
119
|
+
try {
|
|
120
|
+
await candidate.connect(options);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
this.clearCandidate(candidate, true);
|
|
124
|
+
this.handleConnectionFailure(expectedGeneration, resolution.url, ctx, error);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!this.isReconnectCurrent(expectedGeneration) || this.pendingCandidate !== candidate) {
|
|
128
|
+
this.closeCandidateOnce(candidate);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.pendingCandidate = null;
|
|
132
|
+
this.activeRelay = candidate;
|
|
133
|
+
this.reconnectAttempt = 0;
|
|
134
|
+
candidate.on("close", () => this.onRelayClose(candidate));
|
|
135
|
+
this.deps.onConnected(candidate, ctx);
|
|
136
|
+
this.deps.onStateChange();
|
|
137
|
+
}
|
|
138
|
+
scheduleReconnect(url) {
|
|
139
|
+
if (!this.identity || this.isDisposed || this.currentState !== "started")
|
|
140
|
+
return;
|
|
141
|
+
if (this.reconnectTimer)
|
|
142
|
+
clearTimeout(this.reconnectTimer);
|
|
143
|
+
const expectedGeneration = ++this.generation;
|
|
144
|
+
const delay = RECONNECT_BACKOFFS_MS[Math.min(this.reconnectAttempt++, RECONNECT_BACKOFFS_MS.length - 1)];
|
|
145
|
+
this.reconnectTimer = setTimeout(() => {
|
|
146
|
+
this.reconnectTimer = null;
|
|
147
|
+
if (!this.isReconnectCurrent(expectedGeneration))
|
|
148
|
+
return;
|
|
149
|
+
void this.reconnect(url, expectedGeneration);
|
|
150
|
+
}, delay);
|
|
151
|
+
}
|
|
152
|
+
async reconnect(url, expectedGeneration) {
|
|
153
|
+
const identity = this.identity;
|
|
154
|
+
if (!identity || !this.isReconnectCurrent(expectedGeneration))
|
|
155
|
+
return;
|
|
156
|
+
let options;
|
|
157
|
+
try {
|
|
158
|
+
options = await this.deps.buildConnectOptions();
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
if (this.isReconnectCurrent(expectedGeneration)) {
|
|
162
|
+
this.deps.onStateChange();
|
|
163
|
+
this.scheduleReconnect(url);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!this.isReconnectCurrent(expectedGeneration) || this.identity !== identity)
|
|
168
|
+
return;
|
|
169
|
+
const candidate = this.deps.createClient(url, identity);
|
|
170
|
+
this.pendingCandidate = candidate;
|
|
171
|
+
try {
|
|
172
|
+
await candidate.connect(options);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
this.clearCandidate(candidate, true);
|
|
176
|
+
if (this.isReconnectCurrent(expectedGeneration)) {
|
|
177
|
+
this.deps.onStateChange();
|
|
178
|
+
this.scheduleReconnect(url);
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (!this.isReconnectCurrent(expectedGeneration) || this.pendingCandidate !== candidate) {
|
|
183
|
+
this.closeCandidateOnce(candidate);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.pendingCandidate = null;
|
|
187
|
+
this.activeRelay = candidate;
|
|
188
|
+
this.reconnectAttempt = 0;
|
|
189
|
+
candidate.on("close", () => this.onRelayClose(candidate));
|
|
190
|
+
this.deps.onConnected(candidate);
|
|
191
|
+
this.deps.onStateChange();
|
|
192
|
+
}
|
|
193
|
+
onRelayClose(closed) {
|
|
194
|
+
if (closed !== this.activeRelay || this.currentState === "idle")
|
|
195
|
+
return;
|
|
196
|
+
this.activeRelay = null;
|
|
197
|
+
this.deps.onDisconnected();
|
|
198
|
+
this.deps.onStateChange();
|
|
199
|
+
if (this.currentRelayUrl)
|
|
200
|
+
this.scheduleReconnect(this.currentRelayUrl);
|
|
201
|
+
}
|
|
202
|
+
handleConnectionFailure(expectedGeneration, url, ctx, error) {
|
|
203
|
+
if (!this.isReconnectCurrent(expectedGeneration))
|
|
204
|
+
return;
|
|
205
|
+
this.notify(ctx, `[remote-pi] Relay unavailable; reconnecting in background: ${String(error)}`, "warning");
|
|
206
|
+
this.deps.onStateChange();
|
|
207
|
+
this.scheduleReconnect(url);
|
|
208
|
+
}
|
|
209
|
+
clearCandidate(candidate, close) {
|
|
210
|
+
if (this.pendingCandidate !== candidate)
|
|
211
|
+
return;
|
|
212
|
+
this.pendingCandidate = null;
|
|
213
|
+
if (close)
|
|
214
|
+
this.closeCandidateOnce(candidate);
|
|
215
|
+
}
|
|
216
|
+
closeCandidateOnce(candidate) {
|
|
217
|
+
if (this.closedCandidates.has(candidate))
|
|
218
|
+
return;
|
|
219
|
+
this.closedCandidates.add(candidate);
|
|
220
|
+
candidate.close();
|
|
221
|
+
}
|
|
222
|
+
clearInitialAttempt(attempt, cancel) {
|
|
223
|
+
if (this.initialAttempt === attempt)
|
|
224
|
+
this.initialAttempt = null;
|
|
225
|
+
if (this.cancelInitialAttempt === cancel)
|
|
226
|
+
this.cancelInitialAttempt = null;
|
|
227
|
+
}
|
|
228
|
+
isOperationCurrent(expected) {
|
|
229
|
+
return !this.isDisposed && this.generation === expected;
|
|
230
|
+
}
|
|
231
|
+
isReconnectCurrent(expected) {
|
|
232
|
+
return this.currentState === "started" && this.isOperationCurrent(expected);
|
|
233
|
+
}
|
|
234
|
+
notify(ctx, message, kind = "info") {
|
|
235
|
+
try {
|
|
236
|
+
ctx.ui.notify(message, kind);
|
|
237
|
+
}
|
|
238
|
+
catch { /* stale context */ }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=relay_lifecycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay_lifecycle.js","sourceRoot":"","sources":["../../src/runtime/relay_lifecycle.ts"],"names":[],"mappings":"AAsBA,MAAM,qBAAqB,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAU,CAAC;AAE7E,MAAM,OAAO,cAAc;IAcI;IAbrB,WAAW,GAAuB,IAAI,CAAC;IACvC,gBAAgB,GAAuB,IAAI,CAAC;IAC5C,eAAe,GAAkB,IAAI,CAAC;IACtC,QAAQ,GAA0B,IAAI,CAAC;IACvC,cAAc,GAAuC,IAAI,CAAC;IAC1D,oBAAoB,GAAwB,IAAI,CAAC;IACjD,cAAc,GAAyC,IAAI,CAAC;IAC5D,gBAAgB,GAAG,CAAC,CAAC;IACrB,UAAU,GAAG,CAAC,CAAC;IACf,UAAU,GAAG,KAAK,CAAC;IACnB,YAAY,GAAgB,MAAM,CAAC;IAC1B,gBAAgB,GAAG,IAAI,OAAO,EAAe,CAAC;IAE/D,YAA6B,IAAgC;QAAhC,SAAI,GAAJ,IAAI,CAA4B;IAAG,CAAC;IAEjE,IAAI,KAAK,KAAkB,OAAO,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IACtD,IAAI,KAAK,KAAyB,OAAO,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IAC5D,IAAI,QAAQ,KAAoB,OAAO,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC;IAC9D,IAAI,OAAO,KAA4B,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9D,IAAI,QAAQ,KAAc,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;IACnD,IAAI,mBAAmB,KAAc,OAAO,IAAI,CAAC,cAAc,KAAK,IAAI,CAAC,CAAC,CAAC;IAC3E,IAAI,MAAM;QACR,IAAI,IAAI,CAAC,YAAY,KAAK,MAAM;YAAE,OAAO,cAAc,CAAC;QACxD,OAAO,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC;IACnE,CAAC;IAED,SAAS,CAAC,SAAsB;QAC9B,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS,CAAC;IAC/F,CAAC;IAED,WAAW,CAAC,KAAc;QACxB,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,GAAsB;QAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC;QACrC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,IAAI,IAAI,CAAC,YAAY,KAAK,MAAM,EAAE,CAAC;YACjC,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,KAAK,cAAc;gBAC5C,CAAC,CAAC,kDAAkD;gBACpD,CAAC,CAAC,gCAAgC,CAAC;YACrC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;YACrC,OAAO,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QACzC,IAAI,MAAmB,CAAC;QACxB,MAAM,YAAY,GAAG,IAAI,OAAO,CAAqB,CAAC,OAAO,EAAE,EAAE;YAC/D,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,WAAoB,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC;QACzF,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC;QAC9B,IAAI,CAAC,oBAAoB,GAAG,MAAM,CAAC;QACnC,KAAK,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACtC,KAAK,OAAO,CAAC,IAAI,CACf,GAAG,EAAE,CAAC,IAAI,CAAC,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,EAC/C,GAAG,EAAE,CAAC,IAAI,CAAC,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,CAChD,CAAC;QACF,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,cAAc;QACZ,OAAO,IAAI,CAAC,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7D,CAAC;IAED,IAAI;QACF,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;QACrB,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YAClC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC7B,CAAC;QACD,IAAI,CAAC,gBAAgB,GAAG,CAAC,CAAC;QAE1B,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC;QAChC,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,CAAC;QACtC,MAAM,MAAM,GAAG,IAAI,CAAC,oBAAoB,CAAC;QACzC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC5B,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC3B,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACjC,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC;QAE3B,MAAM,EAAE,EAAE,CAAC;QACX,IAAI,MAAM;YAAE,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAC5C,IAAI,OAAO,IAAI,OAAO,KAAK,MAAM;YAAE,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;QACpE,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;IAC5B,CAAC;IAEO,KAAK,CAAC,YAAY,CAAC,GAAsB;QAC/C,MAAM,kBAAkB,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC;QAC7C,IAAI,QAAwB,CAAC;QAC7B,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QAC5C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC;gBAAE,OAAO;YACzD,IAAI,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,KAAK,EAAE,GAAG,CAAC;gBAAE,OAAO;YACtD,MAAM,KAAK,CAAC;QACd,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC;YAAE,OAAO;QAEzD,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;QAC/C,IAAI,CAAC,eAAe,GAAG,UAAU,CAAC,GAAG,CAAC;QACtC,IAAI,CAAC,YAAY,GAAG,SAAS,CAAC;QAC9B,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;QAC1B,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC,CAAC;QAE3D,IAAI,OAA2B,CAAC;QAChC,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACzD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,uBAAuB,CAAC,kBAAkB,EAAE,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;YAC7E,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC;YAAE,OAAO;QAEzD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACnE,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAC;QAClC,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACnC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YACrC,IAAI,CAAC,uBAAuB,CAAC,kBAAkB,EAAE,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;YAC7E,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,IAAI,IAAI,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACxF,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC;YACnC,OAAO;QACT,CAAC;QAED,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,SAAS,CAAC;QAC7B,IAAI,CAAC,gBAAgB,GAAG,CAAC,CAAC;QAC1B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;IAC5B,CAAC;IAEO,iBAAiB,CAAC,GAAW;QACnC,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;YAAE,OAAO;QACjF,IAAI,IAAI,CAAC,cAAc;YAAE,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAC3D,MAAM,kBAAkB,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC;QAC7C,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,qBAAqB,CAAC,MAAM,GAAG,CAAC,CAAC,CAAE,CAAC;QAC1G,IAAI,CAAC,cAAc,GAAG,UAAU,CAAC,GAAG,EAAE;YACpC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC;gBAAE,OAAO;YACzD,KAAK,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;QAC/C,CAAC,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC;IAEO,KAAK,CAAC,SAAS,CAAC,GAAW,EAAE,kBAA0B;QAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC/B,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC;YAAE,OAAO;QAEtE,IAAI,OAA2B,CAAC;QAChC,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,EAAE,CAAC;gBAChD,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;gBAC1B,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;YAC9B,CAAC;YACD,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ;YAAE,OAAO;QAEvF,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACxD,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAC;QAClC,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YACrC,IAAI,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,EAAE,CAAC;gBAChD,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;gBAC1B,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;YAC9B,CAAC;YACD,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,IAAI,IAAI,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACxF,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC;YACnC,OAAO;QACT,CAAC;QAED,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,SAAS,CAAC;QAC7B,IAAI,CAAC,gBAAgB,GAAG,CAAC,CAAC;QAC1B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QACjC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;IAC5B,CAAC;IAEO,YAAY,CAAC,MAAmB;QACtC,IAAI,MAAM,KAAK,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,YAAY,KAAK,MAAM;YAAE,OAAO;QACxE,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;QAC1B,IAAI,IAAI,CAAC,eAAe;YAAE,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IACzE,CAAC;IAEO,uBAAuB,CAAC,kBAA0B,EAAE,GAAW,EAAE,GAAsB,EAAE,KAAc;QAC7G,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC;YAAE,OAAO;QACzD,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,8DAA8D,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;QAC3G,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;QAC1B,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;IAEO,cAAc,CAAC,SAAsB,EAAE,KAAc;QAC3D,IAAI,IAAI,CAAC,gBAAgB,KAAK,SAAS;YAAE,OAAO;QAChD,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,IAAI,KAAK;YAAE,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC;IAChD,CAAC;IAEO,kBAAkB,CAAC,SAAsB;QAC/C,IAAI,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,OAAO;QACjD,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrC,SAAS,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;IAEO,mBAAmB,CAAC,OAAoC,EAAE,MAAkB;QAClF,IAAI,IAAI,CAAC,cAAc,KAAK,OAAO;YAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAChE,IAAI,IAAI,CAAC,oBAAoB,KAAK,MAAM;YAAE,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAC7E,CAAC;IAEO,kBAAkB,CAAC,QAAgB;QACzC,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,KAAK,QAAQ,CAAC;IAC1D,CAAC;IAEO,kBAAkB,CAAC,QAAgB;QACzC,OAAO,IAAI,CAAC,YAAY,KAAK,SAAS,IAAI,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAC9E,CAAC;IAEO,MAAM,CAAC,GAAsB,EAAE,OAAe,EAAE,OAA2B,MAAM;QACvF,IAAI,CAAC;YAAC,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,mBAAmB,CAAC,CAAC;IACrE,CAAC;CACF"}
|
|
@@ -33,15 +33,28 @@ export declare class RelayClient extends EventEmitter {
|
|
|
33
33
|
private ws;
|
|
34
34
|
private lastActivityAt;
|
|
35
35
|
private livenessTimer;
|
|
36
|
+
private livenessSocket;
|
|
37
|
+
private pendingConnection;
|
|
38
|
+
private pendingMessage;
|
|
36
39
|
constructor(url: string, keypair: Ed25519Keypair);
|
|
37
40
|
connect(options?: ConnectOptions): Promise<void>;
|
|
38
41
|
isOpen(): boolean;
|
|
39
42
|
send(line: string): void;
|
|
40
43
|
sendControl(frame: object): void;
|
|
41
44
|
close(): void;
|
|
45
|
+
private finishConnection;
|
|
42
46
|
private startLiveness;
|
|
43
47
|
private stopLiveness;
|
|
44
48
|
private authenticate;
|
|
45
49
|
private nextMessage;
|
|
50
|
+
private isPendingConnection;
|
|
51
|
+
private assertPendingConnection;
|
|
52
|
+
private resolvePendingConnection;
|
|
53
|
+
private abortPendingConnection;
|
|
54
|
+
private clearConnectTimer;
|
|
55
|
+
private clearPendingConnectionListeners;
|
|
56
|
+
private cancelPendingMessage;
|
|
57
|
+
private reportError;
|
|
58
|
+
private asError;
|
|
46
59
|
private rawSend;
|
|
47
60
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import WebSocket from "ws";
|
|
3
3
|
import { ed25519Sign } from "../pairing/crypto.js";
|
|
4
|
+
const CONNECT_TIMEOUT_MS = 10_000;
|
|
4
5
|
const AUTH_TIMEOUT_MS = 5_000;
|
|
5
6
|
const LIVENESS_TIMEOUT_MS = 70_000;
|
|
6
7
|
const LIVENESS_CHECK_MS = 20_000;
|
|
@@ -11,43 +12,36 @@ export class RelayClient extends EventEmitter {
|
|
|
11
12
|
ws = null;
|
|
12
13
|
lastActivityAt = 0;
|
|
13
14
|
livenessTimer = null;
|
|
15
|
+
livenessSocket = null;
|
|
16
|
+
pendingConnection = null;
|
|
17
|
+
pendingMessage = null;
|
|
14
18
|
constructor(url, keypair) {
|
|
15
19
|
super();
|
|
16
20
|
this.url = url;
|
|
17
21
|
this.keypair = keypair;
|
|
18
22
|
}
|
|
19
|
-
|
|
23
|
+
connect(options = { role: "owner" }) {
|
|
24
|
+
this.close();
|
|
20
25
|
return new Promise((resolve, reject) => {
|
|
21
26
|
const ws = new WebSocket(this.url);
|
|
27
|
+
const onEarlyClose = () => this.abortPendingConnection(ws, new Error("relay connection closed"), false);
|
|
28
|
+
const onOpen = () => {
|
|
29
|
+
this.clearConnectTimer(ws);
|
|
30
|
+
void this.finishConnection(ws, options);
|
|
31
|
+
};
|
|
22
32
|
this.ws = ws;
|
|
23
|
-
ws
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const trimmed = line.trim();
|
|
33
|
-
if (trimmed)
|
|
34
|
-
this.emit("message", trimmed);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
ws.on("ping", () => { this.lastActivityAt = Date.now(); });
|
|
38
|
-
ws.on("pong", () => { this.lastActivityAt = Date.now(); });
|
|
39
|
-
ws.on("close", () => {
|
|
40
|
-
this.stopLiveness();
|
|
41
|
-
this.emit("close");
|
|
42
|
-
});
|
|
43
|
-
this.startLiveness(ws);
|
|
44
|
-
resolve();
|
|
45
|
-
}
|
|
46
|
-
catch (err) {
|
|
47
|
-
ws.terminate();
|
|
48
|
-
reject(err);
|
|
49
|
-
}
|
|
33
|
+
const pending = { ws, resolve, reject, onEarlyClose, onOpen, connectTimer: null };
|
|
34
|
+
this.pendingConnection = pending;
|
|
35
|
+
pending.connectTimer = setTimeout(() => this.abortPendingConnection(ws, new Error("relay connection timeout"), true), CONNECT_TIMEOUT_MS);
|
|
36
|
+
ws.on("error", (error) => {
|
|
37
|
+
if (this.ws !== ws)
|
|
38
|
+
return;
|
|
39
|
+
this.reportError(error);
|
|
40
|
+
if (this.isPendingConnection(ws))
|
|
41
|
+
this.abortPendingConnection(ws, error, true);
|
|
50
42
|
});
|
|
43
|
+
ws.once("close", onEarlyClose);
|
|
44
|
+
ws.once("open", onOpen);
|
|
51
45
|
});
|
|
52
46
|
}
|
|
53
47
|
isOpen() {
|
|
@@ -65,26 +59,77 @@ export class RelayClient extends EventEmitter {
|
|
|
65
59
|
this.ws.send(JSON.stringify(frame));
|
|
66
60
|
}
|
|
67
61
|
close() {
|
|
68
|
-
this.
|
|
69
|
-
|
|
62
|
+
const ws = this.ws;
|
|
63
|
+
if (!ws)
|
|
64
|
+
return;
|
|
65
|
+
this.stopLiveness(ws);
|
|
66
|
+
if (this.isPendingConnection(ws)) {
|
|
67
|
+
this.abortPendingConnection(ws, new Error("relay connection closed"), true);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
70
|
this.ws = null;
|
|
71
|
+
ws.close();
|
|
72
|
+
}
|
|
73
|
+
async finishConnection(ws, options) {
|
|
74
|
+
if (!this.isPendingConnection(ws))
|
|
75
|
+
return;
|
|
76
|
+
try {
|
|
77
|
+
await this.authenticate(ws, options);
|
|
78
|
+
if (!this.isPendingConnection(ws))
|
|
79
|
+
return;
|
|
80
|
+
this.lastActivityAt = Date.now();
|
|
81
|
+
const pending = this.pendingConnection;
|
|
82
|
+
if (!pending || pending.ws !== ws)
|
|
83
|
+
return;
|
|
84
|
+
ws.removeListener("close", pending.onEarlyClose);
|
|
85
|
+
ws.on("message", (raw) => {
|
|
86
|
+
if (this.ws !== ws)
|
|
87
|
+
return;
|
|
88
|
+
this.lastActivityAt = Date.now();
|
|
89
|
+
const text = Buffer.isBuffer(raw) ? raw.toString() : String(raw);
|
|
90
|
+
for (const line of text.split("\n")) {
|
|
91
|
+
const trimmed = line.trim();
|
|
92
|
+
if (trimmed)
|
|
93
|
+
this.emit("message", trimmed);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
ws.on("ping", () => { if (this.ws === ws)
|
|
97
|
+
this.lastActivityAt = Date.now(); });
|
|
98
|
+
ws.on("pong", () => { if (this.ws === ws)
|
|
99
|
+
this.lastActivityAt = Date.now(); });
|
|
100
|
+
ws.on("close", () => {
|
|
101
|
+
if (this.ws !== ws)
|
|
102
|
+
return;
|
|
103
|
+
this.stopLiveness(ws);
|
|
104
|
+
this.ws = null;
|
|
105
|
+
this.emit("close");
|
|
106
|
+
});
|
|
107
|
+
this.startLiveness(ws);
|
|
108
|
+
this.resolvePendingConnection(ws);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
this.abortPendingConnection(ws, this.asError(error), true);
|
|
112
|
+
}
|
|
71
113
|
}
|
|
72
114
|
startLiveness(ws) {
|
|
73
115
|
this.stopLiveness();
|
|
116
|
+
this.livenessSocket = ws;
|
|
74
117
|
this.livenessTimer = setInterval(() => {
|
|
75
118
|
if (Date.now() - this.lastActivityAt > LIVENESS_TIMEOUT_MS) {
|
|
76
|
-
this.stopLiveness();
|
|
119
|
+
this.stopLiveness(ws);
|
|
77
120
|
ws.terminate();
|
|
78
121
|
}
|
|
79
122
|
}, LIVENESS_CHECK_MS);
|
|
80
123
|
}
|
|
81
|
-
stopLiveness() {
|
|
82
|
-
if (!this.livenessTimer)
|
|
124
|
+
stopLiveness(ws) {
|
|
125
|
+
if (!this.livenessTimer || (ws && this.livenessSocket !== ws))
|
|
83
126
|
return;
|
|
84
127
|
clearInterval(this.livenessTimer);
|
|
85
128
|
this.livenessTimer = null;
|
|
129
|
+
this.livenessSocket = null;
|
|
86
130
|
}
|
|
87
131
|
async authenticate(ws, options) {
|
|
132
|
+
this.assertPendingConnection(ws);
|
|
88
133
|
const pubkey = Buffer.from(this.keypair.publicKey).toString("base64");
|
|
89
134
|
const hello = options.role === "host"
|
|
90
135
|
? {
|
|
@@ -100,6 +145,7 @@ export class RelayClient extends EventEmitter {
|
|
|
100
145
|
: { type: "hello", protocol_version: 2, role: "owner", pubkey };
|
|
101
146
|
this.rawSend(ws, JSON.stringify(hello));
|
|
102
147
|
const challengeRaw = await this.nextMessage(ws);
|
|
148
|
+
this.assertPendingConnection(ws);
|
|
103
149
|
let challenge;
|
|
104
150
|
try {
|
|
105
151
|
challenge = JSON.parse(challengeRaw);
|
|
@@ -119,17 +165,96 @@ export class RelayClient extends EventEmitter {
|
|
|
119
165
|
type: "auth",
|
|
120
166
|
sig: Buffer.from(ed25519Sign(this.keypair.secretKey, nonce)).toString("base64"),
|
|
121
167
|
};
|
|
168
|
+
this.assertPendingConnection(ws);
|
|
122
169
|
this.rawSend(ws, JSON.stringify(auth));
|
|
123
170
|
}
|
|
124
171
|
nextMessage(ws) {
|
|
125
172
|
return new Promise((resolve, reject) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
173
|
+
let timer = null;
|
|
174
|
+
let settled = false;
|
|
175
|
+
const cleanup = () => {
|
|
176
|
+
if (timer)
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
ws.removeListener("message", onMessage);
|
|
179
|
+
ws.removeListener("error", onError);
|
|
180
|
+
ws.removeListener("close", onClose);
|
|
181
|
+
if (this.pendingMessage?.ws === ws)
|
|
182
|
+
this.pendingMessage = null;
|
|
183
|
+
};
|
|
184
|
+
const finish = (settle) => {
|
|
185
|
+
if (settled)
|
|
186
|
+
return;
|
|
187
|
+
settled = true;
|
|
188
|
+
cleanup();
|
|
189
|
+
settle();
|
|
190
|
+
};
|
|
191
|
+
const onMessage = (raw) => finish(() => resolve(Buffer.isBuffer(raw) ? raw.toString() : String(raw)));
|
|
192
|
+
const onError = (error) => finish(() => reject(error));
|
|
193
|
+
const onClose = () => finish(() => reject(new Error("relay connection closed")));
|
|
194
|
+
timer = setTimeout(() => finish(() => reject(new Error("relay auth timeout"))), AUTH_TIMEOUT_MS);
|
|
195
|
+
ws.once("message", onMessage);
|
|
196
|
+
ws.once("error", onError);
|
|
197
|
+
ws.once("close", onClose);
|
|
198
|
+
this.pendingMessage = { ws, cancel: (error) => finish(() => reject(error)) };
|
|
131
199
|
});
|
|
132
200
|
}
|
|
201
|
+
isPendingConnection(ws) {
|
|
202
|
+
return this.ws === ws && this.pendingConnection?.ws === ws;
|
|
203
|
+
}
|
|
204
|
+
assertPendingConnection(ws) {
|
|
205
|
+
if (!this.isPendingConnection(ws))
|
|
206
|
+
throw new Error("relay connection closed");
|
|
207
|
+
}
|
|
208
|
+
resolvePendingConnection(ws) {
|
|
209
|
+
const pending = this.pendingConnection;
|
|
210
|
+
if (!pending || pending.ws !== ws)
|
|
211
|
+
return;
|
|
212
|
+
this.pendingConnection = null;
|
|
213
|
+
this.clearPendingConnectionListeners(pending);
|
|
214
|
+
pending.resolve();
|
|
215
|
+
}
|
|
216
|
+
abortPendingConnection(ws, error, terminate) {
|
|
217
|
+
const pending = this.pendingConnection;
|
|
218
|
+
if (!pending || pending.ws !== ws)
|
|
219
|
+
return;
|
|
220
|
+
this.pendingConnection = null;
|
|
221
|
+
this.clearPendingConnectionListeners(pending);
|
|
222
|
+
this.cancelPendingMessage(ws, error);
|
|
223
|
+
this.stopLiveness(ws);
|
|
224
|
+
if (this.ws === ws)
|
|
225
|
+
this.ws = null;
|
|
226
|
+
pending.reject(error);
|
|
227
|
+
if (terminate)
|
|
228
|
+
ws.terminate();
|
|
229
|
+
}
|
|
230
|
+
clearConnectTimer(ws) {
|
|
231
|
+
const pending = this.pendingConnection;
|
|
232
|
+
if (!pending || pending.ws !== ws || !pending.connectTimer)
|
|
233
|
+
return;
|
|
234
|
+
clearTimeout(pending.connectTimer);
|
|
235
|
+
pending.connectTimer = null;
|
|
236
|
+
}
|
|
237
|
+
clearPendingConnectionListeners(pending) {
|
|
238
|
+
if (pending.connectTimer)
|
|
239
|
+
clearTimeout(pending.connectTimer);
|
|
240
|
+
pending.connectTimer = null;
|
|
241
|
+
pending.ws.removeListener("close", pending.onEarlyClose);
|
|
242
|
+
pending.ws.removeListener("open", pending.onOpen);
|
|
243
|
+
}
|
|
244
|
+
cancelPendingMessage(ws, error) {
|
|
245
|
+
const pending = this.pendingMessage;
|
|
246
|
+
if (!pending || pending.ws !== ws)
|
|
247
|
+
return;
|
|
248
|
+
this.pendingMessage = null;
|
|
249
|
+
pending.cancel(error);
|
|
250
|
+
}
|
|
251
|
+
reportError(error) {
|
|
252
|
+
if (this.listenerCount("error") > 0)
|
|
253
|
+
this.emit("error", error);
|
|
254
|
+
}
|
|
255
|
+
asError(error) {
|
|
256
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
257
|
+
}
|
|
133
258
|
rawSend(ws, data) {
|
|
134
259
|
ws.send(data);
|
|
135
260
|
}
|