/** * Public API surface for `@absolutejs/isolated-jsc`. Types only — no runtime * yet. Review this first, then we wire the Worker-backed v1 implementation * behind it. * * Shape mirrors `isolated-vm` so anyone porting from Node + isolated-vm gets * familiar ergonomics. Backends are swappable: * v1 (now): Bun `Worker` per isolate, soft resource limits via * setTimeout + bun:jsc.heapSize polling * v2 (later): bun:ffi to a standalone libJSC build with hard limits + * interrupt-driven CPU + microtask budgets * * Anything in this file is the durable contract. Anything in `./worker.ts` * (the v1 backend) is implementation detail that v2 will replace without * users noticing. */ import type { IsolatePolicyName, ResolvedIsolatePolicy } from "./policy"; /** Backend implementation selected for an {@link Isolate}. */ export type IsolateBackend = "ffi" | "worker"; /** Construction options for an {@link Isolate}. */ export type IsolateOptions = { /** * Product posture preset to apply before explicit options. Pass a preset * name for the built-in defaults, or a {@link ResolvedIsolatePolicy} from * {@link resolveIsolatePolicy} when you need overrides. * * Explicit `IsolateOptions` fields win over the preset, so * `createIsolate({ policy: "ai-tool", memoryLimit: 256 })` keeps the * AI-tool timeout/hardening defaults but raises the heap cap. */ policy?: IsolatePolicyName | ResolvedIsolatePolicy; /** * Per-isolate defaults for {@link Script.run}, {@link Script.runWithMetrics}, * {@link Callable.call}, and {@link Callable.callWithMetrics}. Call-level * options still win. Policy recipes populate this with their runtime * timeout and result-size limit. */ defaultRunOptions?: Pick; /** * Hard cap on heap memory (MB). When the isolate's heap exceeds this, the * isolate is terminated and any in-flight `script.run` rejects with * {@link MemoryLimitError}. v1 enforces via polled * `bun:jsc.heapSize` (soft + millisecond-grained); v2 will enforce via * libJSC's heap settings (synchronous, on-allocate). * * Defaults to 256 MB. The worker backend samples only its own JSC heap; * host and peer-worker allocation does not count against this limit. */ memoryLimit?: number; /** * Bootstrap script run once when the isolate spawns, before any user code. * Use this to expose host-provided globals via {@link Reference}s or to * install polyfills. */ bootstrap?: string; /** * `onConsole` hook — if user code calls `console.log` etc inside the * isolate, where do those messages go? Defaults to dropping them silently * (untrusted code shouldn't pollute host logs). Pass a function to capture. */ onConsole?: (level: "log" | "warn" | "error", args: unknown[]) => void; /** * Maximum number of console events to forward through `onConsole` for this * isolate. Extra entries are dropped and reflected in execution receipts. */ maxConsoleEntries?: number; /** * Maximum JSON-encoded console payload bytes to forward through `onConsole` * for this isolate. Extra entries are dropped and reflected in receipts. */ maxConsoleBytes?: number; /** * Strip host-capability globals from the sandbox. Default `true`. When on, * the sandbox cannot reach `fetch`, `Bun`, `process`, `Worker`, * `WebSocket`, `navigator`, host `postMessage`/`addEventListener`, etc. * via bare lookup, `this.X`, `globalThis.X`, or direct `eval(X)`. * * Documented residual: `(0, eval)('Bun')` and `new Function('return Bun')()` * still escape because indirect-eval and the Function constructor run in * the worker's real global scope. Removing them would break async * functions, class generators, and most async libraries. This will close * in the FFI rewrite (v2) where the sandbox has its own global object. * * Pure JS built-ins (Math, JSON, Date, Promise, Map, Set, …) and safe * Web primitives (URL, TextEncoder, crypto.{getRandomValues,randomUUID,subtle}, * setTimeout, console) stay reachable. * * Set `false` to keep the v0.0.1 behaviour where the worker's full * globalThis is exposed (useful only for trusted code). */ harden?: boolean; /** * When {@link harden} is on, names from the hardened list to keep * reachable anyway. Use sparingly — every entry is an unguarded * capability. Typical use: `['fetch']` for a sandbox that needs to make * HTTP calls but should otherwise be locked down. */ unsafelyExposeGlobals?: string[]; /** * Pick the backend explicitly. * * - `"auto"` (default): try FFI first (direct libJavaScriptCore via * `bun:ffi`); fall back to the Worker backend if libJSC isn't reachable * (Windows, Linux without `libjavascriptcoregtk` installed, etc). * - `"ffi"`: require FFI; throw {@link JscLibraryNotFoundError} if libJSC * isn't available. * - `"worker"`: always use the Worker backend, even when FFI would work. * Useful as an escape hatch. * * The FFI backend is strictly better when available: cold heap is ~300 KB * vs ~46 MB, the two T2 documented residuals (`(0, eval)('Bun')` and * `new Function('return Bun')()`) are closed via * `JSGlobalContextSetEvalEnabled`, timeouts use JSC's interrupt-driven * watchdog (the isolate keeps running after a TimeoutError), and value * marshalling skips the Worker postMessage clone path. */ backend?: "auto" | "ffi" | "worker"; }; /** * A V8-Isolate-equivalent: separate JavaScriptCore VM with its own heap. No * memory or value sharing with the host or with other isolates — values cross * the boundary via structured clone or {@link Reference} call-through. * * Disposing the isolate terminates its worker, frees its heap, and rejects any * pending operations. Holding a reference to a disposed isolate is safe but * every method on it will throw. */ export type Isolate = { /** Construction options the isolate was built with. */ readonly options: Readonly>>; /** Runtime defaults applied when a run/call omits the corresponding option. */ readonly defaultRunOptions: Readonly> & Pick>; /** Resolved policy used to construct this isolate, when one was supplied. */ readonly policy?: ResolvedIsolatePolicy; /** Backend selected for this isolate after `auto` resolution. */ readonly backend: IsolateBackend; /** `true` once {@link dispose} has been called or the isolate self-died. */ readonly isDisposed: boolean; /** * Compile a JS source string in the isolate's VM. Returns a {@link Script} * you can run any number of times against any {@link Context} from this * isolate. The compile happens on the isolate side — syntax errors throw * here as {@link CompileError}. */ compileScript: (source: string) => Promise