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>
This commit is contained in:
@@ -17,6 +17,7 @@ export class RpcService {
|
||||
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;
|
||||
@@ -31,12 +32,36 @@ export class RpcService {
|
||||
},
|
||||
},
|
||||
});
|
||||
new Electroview({ rpc: this.rpc });
|
||||
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({...})`. */
|
||||
/** Typed request proxy: `await rpc.request.listDir({...})`. Waits for the bridge. */
|
||||
get request() {
|
||||
if (!this.rpc) throw new Error('RPC is only available inside the desktop app');
|
||||
return this.rpc.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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user