Files
miniSCP/ui/src/app/rpc.service.ts
T
JokinandClaude Fable 5 0d0e4cfde4 P4: SFTP provider, connection store, connection manager UI
- SftpProvider (ssh2): password + key auth, keepalive, clean-dispose vs
  unexpected-close distinction, POSIX path helpers, s->ms attr normalization
- ConnectionStore: plain JSON at appDataDir, 0600, atomic writes, folders
- connect/disconnect + store CRUD handlers; sessionClosed push -> panels fall
  back to local and in-flight jobs fail
- UI: session dropdown per panel (local/saved connections/manage), disconnect
  button, connection manager modal (folders, edit form, save/connect)
- electrobun bundler: external cpu-features/*.node (ssh2 optional natives)
- RpcService: gate requests on RPC socket open; defer initial panel loads past
  first render
- verified against 192.168.10.122: full op matrix, upload/download/remote-remote
  recursive transfers, roundtrip integrity, recursive remote delete, store CRUD

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 14:12:00 +02:00

68 lines
2.3 KiB
TypeScript

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<TransferSnapshot | null>(null);
readonly sessionClosed = signal<SessionClosedEvent | null>(null);
private rpc: ReturnType<typeof Electroview.defineRPC<AppRPC>> | undefined;
private ready: Promise<void> = Promise.resolve();
constructor() {
this.available = typeof window !== 'undefined' && '__electrobun' in window;
if (!this.available) return;
this.rpc = Electroview.defineRPC<AppRPC>({
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<unknown>
>;
return methods[method](params);
},
});
}
}