import type { ExecOptions, GuestWrite, HostFs, ProcessInfo, PtyExecOptions, Signal } from "@nibling/kernel"; /** What the coordinator posts to the sandbox page's window, with the port the rest goes over. */ export interface Connect { t: "connect"; } /** An error as it crosses the port. */ export interface ErrorRecord { name: string; message: string; code?: string; } /** A `HostFs` method, called by name. */ export type FsOp = keyof HostFs; /** A call of one `HostFs` method, with the arguments it takes. */ export type FsCall = { [K in FsOp]: { t: "fs"; id: number; op: K; args: Parameters } }[FsOp]; /** What a `HostFs` method resolves with. */ export type FsValue = { [K in FsOp]: Awaited> }[FsOp]; /** What a subscription follows. Each subscription is one listener on the sandbox's kernel. */ export type Topic = "listening" | "unlistening" | "processes" | "writes"; /** A subscription's topic, with the guest writes' path prefixes for `writes`. */ export type Subscription = { topic: Exclude } | { topic: "writes"; prefixes: string[] }; /** * Coordinator to sandbox. A request carries an `id` its answer echoes. A process is named by the * `proc` handle the coordinator picks when it calls exec, so stdin and signals can follow at once. * A process started with `pty` takes `ptyWrite` and `ptyResize` instead of stdin. */ export type ToSandbox = | { t: "applyFiles"; id: number; files: Record } | { t: "putFile"; id: number; path: string; data: string | Uint8Array } | FsCall | { t: "exec"; proc: number; entry: string; options: ExecOptions | PtyExecOptions } | { t: "stdin"; proc: number; bytes: Uint8Array } | { t: "ptyWrite"; proc: number; data: string | Uint8Array } | { t: "ptyResize"; proc: number; cols: number; rows: number } | { t: "stdinEnd"; proc: number } | { t: "signal"; proc: number; signal?: Signal } | { t: "kill"; pid: number; signal?: Signal } | ({ t: "subscribe"; sub: number } & Subscription) | { t: "unsubscribe"; sub: number }; /** Sandbox to coordinator. */ export type FromSandbox = /** The handshake was accepted; `ready` or `bootFailed` follows once the kernel boots. */ | { t: "accepted" } | { t: "ready" } | { t: "bootFailed"; error: ErrorRecord } | { t: "done"; id: number } /** putFile's answer: the generation of the state it wrote. */ | { t: "written"; id: number; gen: number } /** An fs call's answer, with a `readFile`'s bytes transferred. */ | { t: "fsDone"; id: number; value: FsValue } | { t: "failed"; id: number; error: ErrorRecord } | { t: "pid"; proc: number; pid: number } /** A pty process's output, echo included, comes as its stdout. */ | { t: "output"; proc: number; stream: "stdout" | "stderr"; bytes: Uint8Array } | { t: "end"; proc: number; stream: "stdout" | "stderr" } | { t: "exit"; proc: number; code: number; signal: Signal | null } /** exec threw: the process never started. */ | { t: "spawnFailed"; proc: number; error: ErrorRecord } /** `url` is absolute, on the sandbox's origin. */ | { t: "listening"; sub: number; port: number; url: string } | { t: "unlistening"; sub: number; port: number } | { t: "processes"; sub: number; table: readonly ProcessInfo[] } /** A GuestWrite, with its `data` buffer transferred. */ | ({ t: "write"; sub: number } & GuestWrite); export function errorRecord(error: unknown): ErrorRecord { if (!(error instanceof Error)) return { name: "Error", message: String(error) }; const code = (error as { code?: unknown }).code; return { name: error.name, message: error.message, ...(typeof code === "string" ? { code } : {}) }; } /** A Node error code, such as `ENOENT` or `ERR_INVALID_ARG_TYPE`. */ const ERROR_CODE = /^[A-Z][A-Z0-9_]{0,63}$/; /** The error a record from the sandbox describes. A field of the wrong shape is dropped, as is a `code` unlike Node's. */ export function fromRecord(record: unknown): Error { const { name, message, code } = (typeof record === "object" && record !== null ? record : {}) as Record; const error = new Error(typeof message === "string" ? message : "The sandbox failed."); if (typeof name === "string") error.name = name; return typeof code === "string" && ERROR_CODE.test(code) ? Object.assign(error, { code }) : error; } /** A copy of `bytes` that owns its whole buffer, so the buffer can be transferred without detaching the caller's. */ export function owned(bytes: Uint8Array): Uint8Array { return new Uint8Array(bytes); }