Files
skills/docs/api-stack/api/api-lifecycle/src/side-effects/side-effects.md
2026-05-29 08:20:56 +01:00

4.0 KiB

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 "<from>|<to>" 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

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:

import { type SideEffectOp } from "@suaveplan/api-lifecycle/side-effects";

async function dispatch(op: SideEffectOp, ctx: HostContext): Promise<void> {
    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<"<from>|<to>", 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.