/** * RPC singleton — breaks the circular import chain. * * main.ts creates the Electroview and RPC, then sets it here. * All other modules import from this file instead of main.ts. * * Fix #17: Typed RPC interface instead of `any`. */ import type { PtyRPCSchema, PtyRPCRequests, PtyRPCMessages } from '../shared/pty-rpc-schema.ts'; // ── Typed RPC interface ────────────────────────────────────────────────────── type RequestFn = (params: PtyRPCRequests[K]['params']) => Promise; type MessagePayload = PtyRPCMessages[K]; type MessageListener = (payload: MessagePayload) => void; export interface AppRpcHandle { request: { [K in keyof PtyRPCRequests]: RequestFn }; addMessageListener: (event: K, handler: MessageListener) => void; removeMessageListener?: (event: K, handler: MessageListener) => void; } // ── Internal holder ────────────────────────────────────────────────────────── let _rpc: AppRpcHandle | null = null; /** Called once from main.ts after Electroview.defineRPC(). */ export function setAppRpc(rpc: AppRpcHandle): void { _rpc = rpc; } /** * The app-wide RPC handle. * Safe to call after main.ts has executed (Svelte components mount after). */ export const appRpc: AppRpcHandle = new Proxy({} as AppRpcHandle, { get(_target, prop) { if (!_rpc) { // Graceful degradation: return no-ops instead of throwing. // This allows the app to mount even when RPC isn't available // (e.g., E2E tests loading via http:// instead of views://). if (prop === 'request') return new Proxy({}, { get: () => async () => null }); if (prop === 'addMessageListener') return () => {}; if (prop === 'removeMessageListener') return () => {}; console.warn(`[rpc] accessed before init — property "${String(prop)}" (returning no-op)`); return () => {}; } return (_rpc as Record)[prop]; }, });