import type { VmSandboxHandle } from "@suaveplan/api-sandbox-vm"; import { SAND_VM_BRIDGE_CODES, SandboxVmBridgeError, } from "../error-codes/error-codes.js"; /** * Idempotent lifecycle forwarder around a `VmSandboxHandle`. Wraps * `start` so a second call is a silent no-op (the underlying sandbox * has already evaluated its module) and wraps `dispose` so a second * call is also a no-op (the sandbox's own `isDisposed()` would short- * circuit, but having the guard at the bridge layer means the host * never sees a stray throw if a future sandbox impl tightens its * behaviour). * * Underlying-handle failures are re-wrapped as * `SAND_VM_BRIDGE.LIFECYCLE_FAILED` with the original error preserved * as `cause`. */ export interface SandboxLifecycleForwarder { readonly start: () => Promise; readonly dispose: () => void; readonly hasStarted: () => boolean; readonly isDisposed: () => boolean; } /** * Build the idempotent lifecycle forwarder around a `VmSandboxHandle`. */ export function createSandboxLifecycleForwarder( sandbox: VmSandboxHandle ): SandboxLifecycleForwarder { let started = false; let disposed = false; return Object.freeze({ async start(): Promise { if (started) return; if (disposed) { throw new SandboxVmBridgeError( SAND_VM_BRIDGE_CODES.LIFECYCLE_FAILED, "cannot start a disposed sandbox bridge" ); } try { await sandbox.start(); } catch (err) { throw new SandboxVmBridgeError( SAND_VM_BRIDGE_CODES.LIFECYCLE_FAILED, "sandbox start failed", { cause: err } ); } started = true; }, dispose(): void { if (disposed) return; disposed = true; try { sandbox.dispose(); } catch (err) { throw new SandboxVmBridgeError( SAND_VM_BRIDGE_CODES.LIFECYCLE_FAILED, "sandbox dispose failed", { cause: err } ); } }, hasStarted(): boolean { return started; }, isDisposed(): boolean { return disposed; }, }); }