P1: typed RPC bridge + FileSystemProvider + dual-panel local browsing
- shared/types + shared/rpc: full wire contract (Result envelope, no exceptions cross the bridge) - FileSystemProvider interface, LocalProvider, SessionRegistry with built-in 'local' - walk.ts: provider-generic recursive walk + removeRecursive - rpc-handlers wired into BrowserWindow; 10min request timeout for slow SFTP ops - Angular: RpcService (Electroview bridge + plain-browser guard), PanelsService with signals-based PanelState (sort/cursor/selection), FilePanel component with keyboard nav (arrows/Space/Enter/Backspace/Tab), path bar, toasts - verified: both panels listDir home over the bridge in the running app Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { BrowserView } from "electrobun/bun";
|
||||
import type { AppRPC } from "../shared/rpc";
|
||||
import type { Result } from "../shared/types";
|
||||
import type { SessionRegistry } from "./fs/registry";
|
||||
import { removeRecursive } from "./transfers/walk";
|
||||
|
||||
// SFTP ops can be slow (big listings, recursive deletes) — the 1s electrobun
|
||||
// default would cut them off.
|
||||
const MAX_REQUEST_TIME_MS = 10 * 60_000;
|
||||
|
||||
async function wrap<T extends object>(fn: () => Promise<T> | T): Promise<Result<T>> {
|
||||
try {
|
||||
return { ok: true, ...(await fn()) };
|
||||
} catch (error) {
|
||||
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
const NOT_YET = { ok: false as const, error: "Not implemented yet" };
|
||||
|
||||
export function createAppRPC(registry: SessionRegistry) {
|
||||
return BrowserView.defineRPC<AppRPC>({
|
||||
maxRequestTime: MAX_REQUEST_TIME_MS,
|
||||
handlers: {
|
||||
requests: {
|
||||
listDir: ({ sessionId, path }) =>
|
||||
wrap(async () => {
|
||||
console.log(`[rpc] listDir ${sessionId} ${path ?? "(home)"}`);
|
||||
const fs = registry.get(sessionId);
|
||||
const dir = fs.normalize(path ?? (await fs.homeDir()));
|
||||
return { path: dir, entries: await fs.list(dir) };
|
||||
}),
|
||||
|
||||
mkdir: ({ sessionId, path }) =>
|
||||
wrap(async () => {
|
||||
await registry.get(sessionId).mkdir(path);
|
||||
return {};
|
||||
}),
|
||||
|
||||
createFile: ({ sessionId, path }) =>
|
||||
wrap(async () => {
|
||||
await registry.get(sessionId).createEmptyFile(path);
|
||||
return {};
|
||||
}),
|
||||
|
||||
rename: ({ sessionId, oldPath, newPath }) =>
|
||||
wrap(async () => {
|
||||
await registry.get(sessionId).rename(oldPath, newPath);
|
||||
return {};
|
||||
}),
|
||||
|
||||
deleteEntries: ({ sessionId, entries }) =>
|
||||
wrap(async () => {
|
||||
const fs = registry.get(sessionId);
|
||||
for (const { path, type } of entries) {
|
||||
await removeRecursive(fs, path, type);
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
|
||||
chmod: ({ sessionId, paths, mode }) =>
|
||||
wrap(async () => {
|
||||
const fs = registry.get(sessionId);
|
||||
for (const path of paths) {
|
||||
await fs.chmod(path, mode);
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
|
||||
// P4: SFTP sessions
|
||||
connect: async () => NOT_YET,
|
||||
disconnect: async () => NOT_YET,
|
||||
// P3: transfer engine
|
||||
enqueueTransfers: async () => NOT_YET,
|
||||
cancelTransfer: async () => NOT_YET,
|
||||
dismissTransfer: async () => NOT_YET,
|
||||
// P4: connection store
|
||||
getConnections: async () => NOT_YET,
|
||||
upsertConnection: async () => NOT_YET,
|
||||
deleteConnection: async () => NOT_YET,
|
||||
upsertFolder: async () => NOT_YET,
|
||||
deleteFolder: async () => NOT_YET,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user