Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121/// <reference types="vite/client" />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<Record<string, string>> = { 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<SandboxKernel>; /** 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<SandboxKernel> = () => connectTo(draftSandbox())): Sandbox { let kernel: Promise<SandboxKernel> | undefined; let ready = false; const booted = () => (kernel ??= boot().then((k) => ((ready = true), k))); let files: Files = memoryFiles({}); let pending = new Map<string, string | null>(); let flushing = false; let mirrored = new Set<string>(); let unwatch = () => {}; const terminals = new Map<string, TerminalSession>();
// 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;}