import type { Fd } from "@bjorn3/browser_wasi_shim"; import type { WASIFarmPark } from "./park.ts"; import type { WASIFarmRefObject } from "./ref.ts"; import { WASIFarmParkUseArrayBuffer } from "./shared_array_buffer/index.ts"; /** * WASIFarm is the central manager for a virtualized WASI environment. * * It coordinates file descriptors and provides a "park" backend that handles * system calls via shared memory across multiple worker threads. */ export class WASIFarm { private fds: Array; private park: WASIFarmPark | null; private can_array_buffer: boolean; /** * Initializes a new WASIFarm. * * @param stdin The standard input file descriptor. * @param stdout The standard output file descriptor. * @param stderr The standard error file descriptor. * @param fds Additional file descriptors to include in the farm. * @param options Configuration for shared allocation, descriptor capacity, * base-call capacity, and the host callback. `max_base_calls_limit` defaults * to 128 and must be an integer from 1 through 1024. Construction starts the * non-blocking Park listener; Animals and external Workers are not created. */ constructor( stdin?: Fd, stdout?: Fd, stderr?: Fd, fds: Array = [], options: { allocator_size?: number; /** * Shared payload allocator size for multiplexed base calls, in bytes. * * Defaults to 10 MiB. This is independent from `allocator_size`; requests * and responses fail with `OutOfMemory` when the configured free capacity is * insufficient. */ base_call_allocator_size?: number; max_fds_limit?: number; /** Maximum concurrent user base calls, excluding one reserved system slot. */ max_base_calls_limit?: number; unknown_fn?: ((arg: unknown) => Promise | unknown) | null; } = {}, ) { const new_fds = []; let stdin_: number | undefined; let stdout_: number | undefined; let stderr_: number | undefined; if (stdin) { new_fds.push(stdin); stdin_ = new_fds.length - 1; } if (stdout) { new_fds.push(stdout); stdout_ = new_fds.length - 1; } if (stderr) { new_fds.push(stderr); stderr_ = new_fds.length - 1; } new_fds.push(...fds); const default_allow_fds = []; for (let i = 0; i < new_fds.length; i++) { default_allow_fds.push(i); } this.fds = new_fds; // WebAssembly.Memory can be used to create a SharedArrayBuffer, but it cannot be transferred by postMessage. // Uncaught (in promise) DataCloneError: // Failed to execute 'postMessage' on 'Worker': // SharedArrayBuffer transfer requires self.crossOriginIsolated. try { new SharedArrayBuffer(4); this.can_array_buffer = true; } catch (e) { this.can_array_buffer = false; console.warn("SharedArrayBuffer is not supported:", e); if ( !(globalThis as unknown as { crossOriginIsolated: unknown }) .crossOriginIsolated ) { console.warn( "SharedArrayBuffer is not supported because crossOriginIsolated is not enabled.", ); } } if (this.can_array_buffer) { this.park = new WASIFarmParkUseArrayBuffer( this.fds_ref(), stdin_, stdout_, stderr_, default_allow_fds, options?.allocator_size, options?.max_fds_limit, options?.unknown_fn, options?.max_base_calls_limit, options?.base_call_allocator_size, ); } else { throw new Error("Non SharedArrayBuffer is not supported yet"); } this.park.listen(); } private fds_ref(): Array { const fds = new Proxy([] as Array, { get: (_, prop) => { if (prop === "push") { return (fd: Fd) => { const len = this.fds.push(fd); return len; }; } // @ts-expect-error return this.fds[prop]; }, set: (_, prop, value) => { // @ts-expect-error this.fds[prop] = value; return true; }, }); return fds; } /** * Generates a reference object that can be transferred to a worker thread. * * @returns A serialized reference to the WASI farm. */ get_ref(): WASIFarmRefObject { if (this.park === null) { throw new Error("WASIFarm is already destroyed"); } return this.park.get_ref(); } /** * Destroys this farm and its exclusively owned Park. * * The operation is synchronous and idempotent. It rejects new base calls, * wakes blocked callers, and does not wait for unresolved user callbacks. It * does not destroy Animals or terminate external Workers holding a reference. */ destroy(): void { this.park?.destroy(); this.park = null; } }