// 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, ) => { 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 } })(); }; };