/// import { connectSandbox, type SandboxKernel } from "@nibling/sandbox/client"; import { memoryFiles, type Files } from "./files.ts"; import { draftSandbox, sandboxOrigin } from "./sandbox-name.ts"; import { TerminalSession } from "./terminal-session.ts"; /** Where a project's files live in the kernel's filesystem, and where its processes start. */ export const PROJECT_ROOT = "/app"; /** The environment the app starts processes with. */ export const PROCESS_ENV: Readonly> = { HOME: PROJECT_ROOT, PATH: `${PROJECT_ROOT}/node_modules/.bin:/usr/local/bin:/usr/bin:/bin`, USER: "user", LANG: "C.UTF-8", }; /** Where sandboxes are served: an origin template holding `{id}`, set at build time. Node tests have no `import.meta.env`. */ const SANDBOX_TEMPLATE: string = import.meta.env?.VITE_NIBLING_SANDBOX || "http://{id}.localhost:5174"; /** Boots the sandbox `name` on its own origin. */ export const connectTo = (name: string) => connectSandbox({ origin: sandboxOrigin(name, SANDBOX_TEMPLATE) }); /** A path in the project's namespace ("/" is its root), as the kernel names it. */ export const kernelPath = (path: string) => `${PROJECT_ROOT}${path.replace(/\/+$/, "")}`; /** * The kernel the editor runs a project in, on the sandbox's own origin (D27), the project's files * mirrored into it, and its terminals. The mirror is one way for now: `Files` writes reach the kernel, * and what processes write stays there. */ export interface Sandbox { /** Resolves once the kernel has booted and holds the files mirrored so far. Views render before it does. */ readonly kernel: Promise; /** Whether `kernel` has resolved. */ readonly ready: boolean; /** The files last mirrored. */ readonly files: Files; /** Mirrors `files` into the kernel from now on, deleting what an earlier mirror wrote that `files` lacks. */ mirror(files: Files): void; /** The terminal session `id`, created in `cwd` on first use. It outlives the views that show it. */ terminal(id: string, cwd: string): TerminalSession; /** Stops following the files and ends the kernel, if it booted. */ close(): void; } export function sandbox(boot: () => Promise = () => connectTo(draftSandbox())): Sandbox { let kernel: Promise | undefined; let ready = false; const booted = () => (kernel ??= boot().then((k) => ((ready = true), k))); let files: Files = memoryFiles({}); let pending = new Map(); let flushing = false; let mirrored = new Set(); let unwatch = () => {}; const terminals = new Map(); // Writes that land while a flush is in flight wait for it and go in the next batch, so a burst of // keystrokes costs one applyFiles and each path's writes reach the kernel in order. const flush = async () => { flushing = true; try { // A kernel that fails to boot is reported by the views that use it. const k = await booted().catch(() => undefined); if (!k) return; while (pending.size) { const batch = Object.fromEntries([...pending].map(([path, text]) => [`${PROJECT_ROOT}/${path}`, text])); pending = new Map(); await k.applyFiles(batch).catch((error: unknown) => console.error("mirroring files into the sandbox failed:", error)); } } finally { flushing = false; } }; let flushed = Promise.resolve(); const schedule = () => { if (!flushing) flushed = flush(); }; const box: Sandbox = { get kernel() { return booted().then(async (k) => { await flushed; return k; }); }, get ready() { return ready; }, get files() { return files; }, mirror(next) { unwatch(); files = next; const paths = new Set(next.list()); for (const path of mirrored) if (!paths.has(path)) pending.set(path, null); for (const path of paths) pending.set(path, next.read(path) ?? ""); mirrored = paths; unwatch = next.watch((path) => { mirrored.add(path); pending.set(path, next.read(path) ?? ""); schedule(); }); schedule(); }, terminal(id, cwd) { let session = terminals.get(id); if (!session) terminals.set(id, (session = new TerminalSession(box, cwd))); return session; }, close() { unwatch(); pending.clear(); if (kernel) void kernel.then((k) => k.close(), () => {}); else (kernel = Promise.reject(new Error("The sandbox closed."))).catch(() => {}); }, }; return box; }