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,188 @@
# `LifecycleDriver`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`LifecycleDriver` is the runtime executor for the per-plugin lifecycle FSM
defined in `@suaveplan/api-lifecycle`. It owns one record per registered
plugin (manifest hash, plugin module from `definePlugin`, schemaVersion,
current FSM state, retry trackers, hook log) and translates the
author-facing verbs `install` / `start` / `stop` / `uninstall` / `crash` /
`retryInstall` / `retryStart` / `clearRollback` / `forceUninstall` /
`repair` into the trigger sequences that the `createPluginFsm` interpreter
applies against the legal-transition table.
Each high-level operation dispatches the entry trigger, executes the
side-effect descriptors captured on the FSM (the discriminated-union
`SideEffectOp` from `@suaveplan/api-lifecycle/side-effects`), then
dispatches the completion trigger. Failures along the way auto-dispatch
the matching `*Failed` trigger and the FSM ends in the documented parked
state (`install_failed`, `start_failed`, `uninstall_failed`). Auto-retry of
`start_failed` follows the `@suaveplan/retry` exponential-backoff policy
defined in `RETRY_POLICIES.startFailed`; crashed plugins auto-restart up
to `RETRY_POLICIES.crashed.crashRestartMax` (default 3) with cooldown
reset through `CrashBudgetTracker`.
## Features
- Full coverage of every `(state, trigger)` edge in
`@suaveplan/api-lifecycle`'s `PLUGIN_TRANSITIONS` table.
- Per-plugin mutex enforcement inherited from `createPluginFsm` — concurrent
triggers queue FIFO; tests assert this dedup.
- `SideEffectOp` execution against pluggable adapter surfaces
(`RouterAdapter`, `JobSchedulerAdapter`, `TopicSubscriberAdapter`,
`HealthMarkerAdapter`, `LedgerAdapter`, `MigrationRunner`, `RbacAdapter`,
`CapabilityInjector`, `InflightTracker`).
- Emits `plugin.lifecycle_transitions_total{plugin, from, to, trigger,
outcome}` on every transition via Chunk-A's `HostInstruments`.
- Emits typed events onto the host `EventBus<LifecycleEventMap>` —
`plugin.installed`, `plugin.started`, `plugin.stopped`, `plugin.uninstalled`,
`plugin.rolled_back`, `plugin.crashed`.
- `snapshot(plugin)` renders an admin-friendly view with the FSM state, the
most recent hook outcomes, and any captured `lastError`.
- `rollback(plugin, cause)` is invoked by the `ReloadDriver` (see
`../hot-reload/`) on a failed reload — drives `starting → rolled_back`
without operator intervention.
## Usage: Basic
```ts
import { EventBus } from "@suaveplan/events";
import {
createHostInstruments,
LifecycleDriver,
} from "@suaveplan/api-host";
const events = new EventBus();
const instruments = createHostInstruments({
hostId: "h1",
hostVersion: "0.1.0",
});
const driver = new LifecycleDriver({
adapters: {
router: { mount() {}, unmount() {} },
jobs: { register() {}, pause() {}, unpause() {} },
topics: { subscribe() {}, unsubscribe() {} },
health: { mark() {} },
ledger: {
async write() {},
async drop() {},
async read() { return null; },
},
migrations: { async runUpTo() {} },
rbac: { registerScopes() {}, dropScopes() {} },
capabilities: {
inject: () => ({}) as never,
dispose: async () => {},
},
inflight: {
async drain() { return 0; },
forceCancel: () => 0,
},
},
events,
instruments,
});
driver.register({
module: pluginModule,
manifest,
manifestHash: "sha256:...",
specifier: "file:///opt/plugin/dist/index.js?v=1",
schemaVersion: 1,
});
await driver.install(pluginModule.name);
await driver.start(pluginModule.name);
```
## Usage: Advanced
Customise the per-plugin mutex (e.g. backed by a distributed lock) and the
crash-restart cap:
```ts
import { LifecycleDriver } from "@suaveplan/api-host/lifecycle";
import type { PluginMutex } from "@suaveplan/api-lifecycle";
class RedisPluginMutex implements PluginMutex {
async acquire(plugin: string) { /* … */ }
tryAcquire(plugin: string) { /* … */ }
}
const driver = new LifecycleDriver({
adapters,
events,
instruments,
mutex: new RedisPluginMutex(),
crashRestartMax: 5,
sleep: () => Promise.resolve(),
});
driver.events.on("plugin.rolled_back", ({ plugin, from, to }) => {
operator.notify(`${plugin} rolled back ${from} → ${to}`);
});
await driver.crash(plugin.name, new Error("worker exit code 137"));
const snap = driver.snapshot(plugin.name);
if (snap.lastError !== null) operator.escalate(snap);
```
## API Reference
- `class LifecycleDriver`
- `constructor(options: LifecycleDriverOptions)`
- `register(record: RegisteredPlugin): void`
- `update(record: RegisteredPlugin): void`
- `install(plugin: string): Promise<PluginState>`
- `start(plugin: string): Promise<PluginState>`
- `stop(plugin: string): Promise<PluginState>`
- `uninstall(plugin: string): Promise<PluginState>`
- `crash(plugin: string, cause: unknown): Promise<PluginState>`
- `retryInstall(plugin: string): Promise<PluginState>`
- `retryStart(plugin: string): Promise<PluginState>`
- `clearRollback(plugin: string): Promise<PluginState>`
- `forceUninstall(plugin: string): Promise<PluginState>`
- `repair(plugin: string): Promise<PluginState>`
- `rollback(plugin: string, cause: unknown): Promise<PluginState>`
- `snapshot(plugin: string): LifecycleSnapshot`
- `names(): readonly string[]`
- `interface LifecycleDriverOptions` — `adapters`, `events`, `instruments`,
`mutex?`, `crashRestartMax?`, `sleep?`
- `interface LifecycleEventMap` — `plugin.installed`, `plugin.started`,
`plugin.stopped`, `plugin.uninstalled`, `plugin.rolled_back`,
`plugin.crashed`.
- Adapter surfaces from `lifecycle.types.ts` — `RouterAdapter`,
`JobSchedulerAdapter`, `TopicSubscriberAdapter`, `HealthMarkerAdapter`,
`LedgerAdapter`, `MigrationRunner`, `RbacAdapter`, `CapabilityInjector`,
`InflightTracker`, `LifecycleAdapters`.
## Implementation Notes
The driver intentionally delegates illegal-transition detection to the
underlying `PluginFsm` — every operator verb maps to one entry trigger;
attempting `start()` from `crashed` therefore raises a
`LifecycleError(ILLEGAL_TRANSITION)` from the FSM rather than a duplicate
guard in the driver. This keeps the source-of-truth for legal transitions
in The contract package.
Side-effect ops `validate-manifest`, `resolve-deps`, `acquire-db-connection`,
`complete-otel-span`, `apply-data-policy`, and `rollback-to-prior-module`
are documented but execute as no-ops inside the driver: manifest validation
ran at registration time, dep-graph resolution is computed by
`api-manifest/dep-graph` before registration, the ledger adapter holds
the DB connection, OTel span completion is captured via the lifecycle
counter, the data policy is applied by the next `drop-ledger` op, and the
rollback module switch is performed by the `ReloadDriver` before the
`rolled_back` transition fires. Keeping the no-op branches lets Stage-3
wire concrete behaviour without revising the FSM definition.
Auto-retry of `start_failed` uses `backoffDelay(base, factor, attempt, max)`
locally rather than `@suaveplan/retry`'s `retry()` engine because the
driver needs to inter-leave the FSM transitions between attempts —
`@suaveplan/retry`'s engine is single-call-with-retries semantics. The
underlying delay strategy is identical to `RETRY_POLICIES.startFailed.delay`.