diff --git a/tools/ui/src/lib/enums/index.ts b/tools/ui/src/lib/enums/index.ts index 98e0d17b6..fd6e83e09 100644 --- a/tools/ui/src/lib/enums/index.ts +++ b/tools/ui/src/lib/enums/index.ts @@ -95,4 +95,4 @@ export { export { SplashOrientation } from './splash.enums'; -export { RemoteAccessMode, TunnelStatus } from './webrtc.enums'; +export { RemoteAccessMode, ServerType, TunnelStatus } from './webrtc.enums'; diff --git a/tools/ui/src/lib/enums/webrtc.enums.ts b/tools/ui/src/lib/enums/webrtc.enums.ts index 4623c760c..e9470064e 100644 --- a/tools/ui/src/lib/enums/webrtc.enums.ts +++ b/tools/ui/src/lib/enums/webrtc.enums.ts @@ -1,3 +1,11 @@ +/** + * Stored server type enum - discriminates the entries of the server registry + */ +export enum ServerType { + /** Reached through a WebRTC tunnel, addressed by its share code */ + TUNNEL = 'tunnel' +} + /** * Remote access mode enum - whether requests go through the tunnel */ diff --git a/tools/ui/src/lib/stores/webrtc.svelte.ts b/tools/ui/src/lib/stores/webrtc.svelte.ts index bb0f327fb..6c37af049 100644 --- a/tools/ui/src/lib/stores/webrtc.svelte.ts +++ b/tools/ui/src/lib/stores/webrtc.svelte.ts @@ -1,13 +1,12 @@ import { browser } from '$app/environment'; import { CODE_LENGTHS } from '$lib/constants'; import { IS_WEB_ONLY } from '$lib/constants/web-only.constants'; -import { RemoteAccessMode, TunnelStatus } from '$lib/enums'; +import { RemoteAccessMode, ServerType, TunnelStatus } from '$lib/enums'; +import type { StoredServer } from '$lib/types'; import { ClientTunnel } from '$lib/utils/webrtc-tunnel'; -// Stores the active session for auto-reconnect on reload. -const SESSION_KEY = 'llama_webrtc_session'; - -type SessionData = { roomCode: string; passCode: string }; +// Registry of the servers the app can talk to, reconnected to on reload. +const SERVERS_KEY = 'llama_servers'; class WebRTCStore { mode = $state(RemoteAccessMode.OFF); @@ -22,7 +21,7 @@ class WebRTCStore { constructor() { if (browser) { - this.restoreSession(); + this.restoreServers(); } } @@ -84,7 +83,7 @@ class WebRTCStore { try { await tunnel.connect(); this.clientTunnel = tunnel; - this.writeSession({ passCode, roomCode }); + this.writeServers([{ code: roomCode + passCode, type: ServerType.TUNNEL }]); // Release any requests that were queued while connecting. const waiters = this.connectionWaiters.splice(0); @@ -111,18 +110,20 @@ class WebRTCStore { } } - /** Retry the saved session after a failed or dropped connection. */ + /** Retry the saved server after a failed or dropped connection. */ async reconnect(): Promise { - const raw = localStorage.getItem(SESSION_KEY); - - if (!raw) return; + const server = this.readTunnelServer(); - const session = JSON.parse(raw) as SessionData; + if (!server) return; this.clientTunnel?.disconnect(); this.clientTunnel = null; - await this.activateClient(session.roomCode, session.passCode, true); + await this.activateClient( + server.code.slice(0, CODE_LENGTHS.ROOM), + server.code.slice(CODE_LENGTHS.ROOM), + true + ); } leaveAsClient(): void { @@ -131,7 +132,7 @@ class WebRTCStore { this.clientTunnel = null; this.mode = RemoteAccessMode.OFF; this.status = TunnelStatus.IDLE; - this.clearSession(); + this.clearServers(); } // ------------------------------------------------------------------------- @@ -194,28 +195,42 @@ class WebRTCStore { // Persistence helpers // ------------------------------------------------------------------------- - private restoreSession(): void { + private restoreServers(): void { + const server = this.readTunnelServer(); + + if (!server) return; + + void this.activateClient( + server.code.slice(0, CODE_LENGTHS.ROOM), + server.code.slice(CODE_LENGTHS.ROOM), + true + ).catch(() => { + // state is already reflected in status and errorMessage + }); + } + + /** First tunnel entry of the registry, the only one reachable for now. */ + private readTunnelServer(): StoredServer | null { try { - const raw = localStorage.getItem(SESSION_KEY); + const raw = localStorage.getItem(SERVERS_KEY); - if (!raw) return; + if (!raw) return null; - const session = JSON.parse(raw) as SessionData; + const servers = JSON.parse(raw) as StoredServer[]; - void this.activateClient(session.roomCode, session.passCode, true).catch(() => { - // state is already reflected in status and errorMessage - }); + return servers.find((server) => server.type === ServerType.TUNNEL) ?? null; } catch { // ignore corrupt storage + return null; } } - private writeSession(data: SessionData): void { - localStorage.setItem(SESSION_KEY, JSON.stringify(data)); + private writeServers(servers: StoredServer[]): void { + localStorage.setItem(SERVERS_KEY, JSON.stringify(servers)); } - private clearSession(): void { - localStorage.removeItem(SESSION_KEY); + private clearServers(): void { + localStorage.removeItem(SERVERS_KEY); } } diff --git a/tools/ui/src/lib/types/index.ts b/tools/ui/src/lib/types/index.ts index 061d614a5..359ff042c 100644 --- a/tools/ui/src/lib/types/index.ts +++ b/tools/ui/src/lib/types/index.ts @@ -217,3 +217,6 @@ export type { ReasoningEffortLevel } from './reasoning'; // Splash export type { SplashDimensions } from './splash'; + +// Server registry +export type { StoredServer, StoredTunnelServer } from './server-registry'; diff --git a/tools/ui/src/lib/types/server-registry.d.ts b/tools/ui/src/lib/types/server-registry.d.ts new file mode 100644 index 000000000..72cd0d7b4 --- /dev/null +++ b/tools/ui/src/lib/types/server-registry.d.ts @@ -0,0 +1,18 @@ +import type { ServerType } from '$lib/enums'; + +/** + * Server registry types + */ + +/** + * A server the app can talk to. Entries are discriminated by `type` so the + * registry accepts new kinds of servers without changing the stored shape. + */ +export interface StoredTunnelServer { + type: ServerType.TUNNEL; + /** Share code of the llama-connect host, room code followed by pass code */ + code: string; +} + +/** Entry of the persisted server registry. */ +export type StoredServer = StoredTunnelServer;