import { Injectable, signal } from '@angular/core'; import { Electroview } from 'electrobun/view'; import type { AppRPC } from '@shared/rpc'; import type { TransferSnapshot } from '@shared/types'; // Matches MAX_REQUEST_TIME_MS on the bun side — SFTP ops can be slow. const MAX_REQUEST_TIME_MS = 10 * 60_000; export type SessionClosedEvent = { sessionId: string; reason: string }; @Injectable({ providedIn: 'root' }) export class RpcService { /** False when the page is opened in a plain browser instead of the app. */ readonly available: boolean; readonly transferUpdate = signal(null); readonly sessionClosed = signal(null); private rpc: ReturnType> | undefined; private ready: Promise = Promise.resolve(); constructor() { this.available = typeof window !== 'undefined' && '__electrobun' in window; if (!this.available) return; this.rpc = Electroview.defineRPC({ maxRequestTime: MAX_REQUEST_TIME_MS, handlers: { messages: { transferUpdate: (snapshot) => this.transferUpdate.set(snapshot), sessionClosed: (event) => this.sessionClosed.set(event), }, }, }); const electroview = new Electroview({ rpc: this.rpc }); // Requests fired before the RPC WebSocket finishes connecting are lost, so // everything below gates on it being open (services fire calls at startup). const socket = electroview.bunSocket; if (socket && socket.readyState !== WebSocket.OPEN) { this.ready = new Promise((resolve) => { socket.addEventListener('open', () => resolve(), { once: true }); socket.addEventListener('error', () => resolve(), { once: true }); }); } } /** Typed request proxy: `await rpc.request.listDir({...})`. Waits for the bridge. */ get request() { const rpc = this.rpc; if (!rpc) throw new Error('RPC is only available inside the desktop app'); const ready = this.ready; return new Proxy({} as (typeof rpc)['request'], { get: (_target, method: string) => async (params?: unknown) => { await ready; const methods = rpc.request as unknown as Record< string, (p?: unknown) => Promise >; return methods[method](params); }, }); } }