Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
# `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
```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.