# `wasi-stdio-lifecycle` ## Purpose Idempotent `start` / `dispose` forwarder for the WASI-stdio variant. Each call sends one lifecycle envelope (`{kind:"start"}` or `{kind:"dispose"}`) to the guest and verifies the reply is `{kind:"ok"}`. Wraps the host's `IdempotentLifecycle` shape so a caller swapping between variants sees no API divergence. The two methods are guarded by once-flags so the host's "reload twice" or "dispose-then-redispose" patterns never double-invoke the guest's lifecycle hooks. `dispose()` ALWAYS chains through to the runner's dispose, even when the guest's reply was unexpected — the host must never leak the underlying instance. ## Features - `start()` / `dispose()` — idempotent forwarders matching the direct variant's `IdempotentLifecycle` interface, so the bridge factory produces a single shape across variants. - `started()` / `disposed()` — getters for the once-flags, used by the conformance harness invariant. - Plugin-originated `{kind:"error"}` replies surface as `SAND_WASM_BRIDGE.LIFECYCLE_FAILED` carrying the plugin's code. - Unexpected reply kinds (e.g. `{kind:"response"}` to a `start`) surface as `LIFECYCLE_FAILED` with `context.expected = "ok"` and `context.received = ""`. ## Usage: Basic ```ts import { createWasiStdioLifecycle, createWasiStdioRunner, } from "@suaveplan/api-sandbox-wasm-host-bridge/wasi-stdio"; declare const module: WebAssembly.Module; const runner = createWasiStdioRunner({ pluginName: "ex", wasmModule: module }); const lifecycle = createWasiStdioLifecycle({ runner, pluginName: "ex" }); lifecycle.start(); lifecycle.dispose(); ``` ## Usage: Advanced ```ts import { createWasiStdioLifecycle } from "@suaveplan/api-sandbox-wasm-host-bridge/wasi-stdio"; import { SAND_WASM_BRIDGE_CODES, SandboxWasmBridgeError, } from "@suaveplan/api-sandbox-wasm-host-bridge/error-codes"; declare const runner: import("./wasi-stdio-runner.js").WasiStdioRunner; const lifecycle = createWasiStdioLifecycle({ runner, pluginName: "billing", }); try { lifecycle.start(); // Repeated start is a no-op — host's reload boundary safely re-calls it. lifecycle.start(); } catch (err) { if ( err instanceof SandboxWasmBridgeError && err.code === SAND_WASM_BRIDGE_CODES.LIFECYCLE_FAILED ) { // Guest reported a fatal init failure — dispose the runner. } throw err; } finally { lifecycle.dispose(); } ``` ## API Reference ### `createWasiStdioLifecycle(deps): WasiStdioLifecycle` - `deps.runner: WasiStdioRunner` — runner the lifecycle drives. - `deps.pluginName: string` — error-context label. Returns `WasiStdioLifecycle`: - `start(): void` — first call sends `{kind:"start"}` and verifies `{kind:"ok"}`. Subsequent calls are no-ops. - `dispose(): void` — first call sends `{kind:"dispose"}` then chains to `runner.dispose()`. Subsequent calls are no-ops. The chain runs even if the dispose envelope reply was unexpected. - `started(): boolean` / `disposed(): boolean` — once-flag readers. ## Implementation Notes - The once-flag for each method is set BEFORE the envelope is sent. This guarantees that a trap during the envelope round-trip does not leave the flag flipped back to `false`, which would invite a second `_start` invocation on the guest. - `dispose()` always calls `runner.dispose()` in its `finally` block. An unexpected reply during dispose is reported via the thrown `SandboxWasmBridgeError` AND the underlying instance is still torn down. This matches the direct variant's "never leak the handle" guarantee. - The plugin's own `errorCode` from a `{kind:"error"}` reply becomes the thrown error's `code`. Operators see the actual root cause rather than a generic bridge-namespaced label.