WizardConnect/packages/core/src/relay-handler.ts

168 lines
4.5 KiB
TypeScript
Raw Normal View History

2026-02-26 11:19:47 +01:00
// Copyright (C) 2026 Whiterun LLC,
// This software is licensed under the GNU Lesser General Public License (LGPL), version 3.0 or later.
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/lgpl-3.0.html
import { RelayClient } from "./relay-client.js";
import {
createConnectionManager,
VisibilityChangeContext,
} from "./connection-manager.js";
import { debug, error as logError, Scope } from "./log.js";
type RelayStatusCode =
| "connected"
| "reconnecting"
| "disconnected"
| "session_deleted";
export class RelayStatus {
status: RelayStatusCode;
error: string | null;
sessionId: string | null;
private constructor(
status: RelayStatusCode,
error: string | null,
sessionId: string | null = null,
) {
this.status = status;
this.error = error;
this.sessionId = sessionId;
}
static connected(sessionId?: string): RelayStatus {
return new RelayStatus("connected", null, sessionId || null);
}
static reconnecting(reason: string | null, sessionId?: string): RelayStatus {
return new RelayStatus("reconnecting", reason, sessionId || null);
}
static disconnected(): RelayStatus {
return new RelayStatus("disconnected", null, null);
}
static sessionDeleted(): RelayStatus {
return new RelayStatus("session_deleted", null, null);
}
}
export interface RelayUpdatePayload {
client: RelayClient;
status: RelayStatus;
}
export type RelayStatusCallback = (_payload: RelayUpdatePayload) => void;
export const initiateRelay = (
dispatchCallback: RelayStatusCallback,
signerPrivateKey: Uint8Array,
pairPublicKey: Uint8Array,
options?: {
explicitRelayUrls: string[];
reconnectInterval?: number;
maxReconnectAttempts?: number;
enableVisibilityHandling?: boolean;
},
) => {
const client = new RelayClient({
explicitRelayUrls: options?.explicitRelayUrls ?? [],
signerPrivateKey: signerPrivateKey,
pairedPublicKey: pairPublicKey,
});
let lastProcessedTimestamp: number = 0;
const connectionManager = createConnectionManager(
client,
{
onConnected: () => {
if (lastProcessedTimestamp > 0) {
client.setLastProcessedTimestamp(lastProcessedTimestamp);
} else {
lastProcessedTimestamp = client.getLastProcessedTimestamp();
}
dispatchCallback({
client,
status: RelayStatus.connected(),
});
},
onReconnecting: (_client: RelayClient, reason: string | null) => {
dispatchCallback({
client,
status: RelayStatus.reconnecting(reason),
});
},
onDisconnected: () => {
dispatchCallback({
client,
status: RelayStatus.disconnected(),
});
},
onError: (_client: RelayClient, _error: any) => {
// Error handling is already done in onReconnecting
},
},
{
connected: "connection",
disconnected: "disconnect",
error: "error",
},
{
reconnectInterval: options?.reconnectInterval,
maxReconnectAttempts: options?.maxReconnectAttempts,
enableVisibilityHandling: options?.enableVisibilityHandling ?? true,
scope: Scope.Relay,
onVisibilityChange: async (
context: VisibilityChangeContext<RelayClient>,
) => {
if (context.state === "hidden") {
debug(Scope.Relay, "Page hidden, disconnecting relay");
context.setPaused(true);
try {
await client.disconnect();
lastProcessedTimestamp = client.getLastProcessedTimestamp();
debug(
Scope.Relay,
`Disconnected, last processed timestamp: ${lastProcessedTimestamp}`,
);
dispatchCallback({
client,
status: RelayStatus.disconnected(),
});
} catch (error) {
logError(
Scope.Relay,
"Error disconnecting on visibility change:",
error,
);
}
} else if (context.state === "visible") {
if (context.isPaused()) {
debug(Scope.Relay, "Page visible, reconnecting relay");
context.setPaused(false);
context.startConnectionLoop();
}
}
},
},
);
connectionManager.startConnectionLoop();
return () => {
dispatchCallback({
client,
status: RelayStatus.disconnected(),
});
(async () => {
try {
await connectionManager.cleanup();
} catch {
// Ignore disconnect errors during cleanup
}
})();
};
};