///
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;
}