first commit
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
# `side-effects`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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
|
||||
|
||||
```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<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.
|
||||
Reference in New Issue
Block a user