# `side-effects` _(no submodules)_ ## Purpose Encodes every side-effect the host runtime SHALL execute on each documented transition — `mount-routes`, `unmount-routes` with 503 + Retry-After, `call-user-hook` with the user-facing install/start/stop/uninstall hooks, `dispose-capabilities` in reverse-registration order, `emit-event` for the public `plugin.*` topics, OTel-span completion, and the migration runner's ledger writes. The table is data; Stage 2's `api-host` is the executor. ## Features - Discriminated-union `SideEffectOp` covering all 24 distinct effect kinds named in design §4's table — every operation is statically typed so the Stage-2 dispatcher can exhaustively switch. - `SIDE_EFFECTS` map keyed `"|"` matching the lookup form used by the FSM factory. - `getSideEffects(from, to)` query helper; `undefined` for unknown pairs. - `DEFAULT_GRACEFUL_MS = 30_000` constant pinning the design-doc default for the `stop` hook's graceful budget. - `disposalOrder: "reverse-registration"` field surfaced on `stopping → stopped` for direct inspection. ## Usage: Basic ```ts import { getSideEffects } from "@suaveplan/api-lifecycle/side-effects"; const entry = getSideEffects("installing", "installed"); for (const op of entry?.ops ?? []) { // dispatch op … } ``` ## Usage: Advanced Dispatch table for The host driver — exhaustively handles every `SideEffectOp` kind. The compiler reports an error if a new effect is added to the union without a corresponding handler: ```ts import { type SideEffectOp } from "@suaveplan/api-lifecycle/side-effects"; async function dispatch(op: SideEffectOp, ctx: HostContext): Promise { switch (op.kind) { case "validate-manifest": return ctx.validateManifest(); case "resolve-deps": return ctx.resolveDeps(); case "acquire-db-connection": return ctx.acquireDb(); case "run-migrations": return ctx.runMigrations(op.upTo); case "write-ledger": return ctx.writeLedger(); case "register-scopes": return ctx.registerScopes(); case "call-user-hook": return ctx.callHook(op.hook, op.gracefulMs); case "emit-event": return ctx.emit(op.topic); case "complete-otel-span": return ctx.endSpan(); case "mount-routes": return ctx.mountRoutes(); case "unmount-routes": return ctx.unmountRoutes(op.status, op.retryAfter); case "subscribe-events": return ctx.subscribe(); case "unsubscribe-events": return ctx.unsubscribe(); case "register-jobs": return ctx.registerJobs({ paused: op.paused }); case "pause-jobs": return ctx.pauseJobs(); case "unpause-jobs": return ctx.unpauseJobs(); case "mark-health": return ctx.markHealth(op.state); case "inject-capabilities": return ctx.injectCaps(); case "dispose-capabilities": return ctx.disposeCaps(op.order); case "force-cancel-stragglers": return ctx.forceCancel(); case "apply-data-policy": return ctx.applyDataPolicy(); case "drop-scopes": return ctx.dropScopes(); case "drop-ledger": return ctx.dropLedger(); case "rollback-to-prior-module": return ctx.rollback(); } } ``` ## API Reference - `type SideEffectOp` — 24-arm discriminated union. - `interface SideEffectEntry` — `{ from, to, ops, disposalOrder? }`. - `SIDE_EFFECTS` — `ReadonlyMap<"|", SideEffectEntry>`. - `getSideEffects(from, to)` — query helper. - `DEFAULT_GRACEFUL_MS` — `30_000`. ## Implementation Notes Every entry is `Object.freeze`-d at module load so accidental mutation throws in strict mode. The crash-recovery (`crashed → starting`) and hot-reload (`stopped → starting`) entries reuse the same ops as the `installed → starting` path — the design doc specifies they MUST be indistinguishable from the operator's perspective. `started → stopping` and `rolled_back → stopping` are similarly aliased; the test suite asserts that the two ops sequences are equal so a future divergence is caught.