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
SideEffectOpcovering 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_EFFECTSmap keyed"<from>|<to>"matching the lookup form used by the FSM factory.getSideEffects(from, to)query helper;undefinedfor unknown pairs.DEFAULT_GRACEFUL_MS = 30_000constant pinning the design-doc default for thestophook's graceful budget.disposalOrder: "reverse-registration"field surfaced onstopping → stoppedfor 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.