Files
suaveplan/packages/api-stack/api/api-sandbox-wasm-host-bridge/src/wasi-stdio/wasi-stdio-lifecycle.md
2026-05-27 23:29:53 +01:00

3.7 KiB

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 = "<kind>".

Usage: Basic

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

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.