first commit
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# `capability-registry` — runtime registry of host-injected capabilities
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `CapabilityRegistry` is the live, single-instance store of every capability adapter wired into a host process. Stage-4 adapter packages (`api-router-hono`, `api-auth-jwt`, `api-db-adapter`, etc.) register against this registry at host boot. The host's lifecycle driver consults it on every plugin install to (a) verify every entry in `manifest.requiredCapabilities` resolves to a registered adapter and (b) verify the adapter's version satisfies the plugin's `apiHost` semver range.
|
||||
|
||||
The registry is also the disposal coordinator — `stop()` on the host invokes `dispose()` here, and disposers run in **reverse-registration** order so deeper consumers (e.g. `db`) shut down before their dependencies (e.g. `logger`).
|
||||
|
||||
## Features
|
||||
|
||||
- `register(...)` with semver-shape validation (`INVALID_VERSION`), scope-enum validation (`INVALID_SCOPE`), and duplicate detection (`DUPLICATE_REGISTRATION`).
|
||||
- `lookup(id)` / `require(id)` (strict, throws `NOT_REGISTERED`).
|
||||
- `assertSatisfies(ranges)` — per-plugin compat check used by the lifecycle driver; throws `NOT_REGISTERED` or `VERSION_MISMATCH` with the failing `(id, range, actual)` context.
|
||||
- Reverse-registration-order `dispose()` — collects every disposer failure and surfaces them via one aggregated `DISPOSER_FAILED` error so a single bad disposer does not block teardown.
|
||||
- `defineCapability(...)` — author-facing helper (identity at runtime) used by adapter packages.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
CapabilityRegistry,
|
||||
defineCapability,
|
||||
} from "@suaveplan/api-host/capability-registry";
|
||||
|
||||
const registry = new CapabilityRegistry();
|
||||
registry.register(
|
||||
defineCapability({
|
||||
id: "logger",
|
||||
version: "1.0.0",
|
||||
scope: "host",
|
||||
provider: logger,
|
||||
})
|
||||
);
|
||||
|
||||
const entry = registry.require("logger"); // throws CAPABILITY.NOT_REGISTERED if missing.
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Plug into a manifest's `requiredCapabilities` resolution:
|
||||
|
||||
```ts
|
||||
import { CapabilityRegistry } from "@suaveplan/api-host/capability-registry";
|
||||
|
||||
const registry = new CapabilityRegistry();
|
||||
// ... register every adapter ...
|
||||
|
||||
function installPlugin(manifest: PluginManifest): void {
|
||||
// The manifest pins ranges via `requiredCapabilities` + per-id range table.
|
||||
registry.assertSatisfies({
|
||||
db: "^1.0.0",
|
||||
"auth.session": "~1.4",
|
||||
});
|
||||
// ... proceed with lifecycle install ...
|
||||
}
|
||||
|
||||
// Host teardown — disposers run in REVERSE-registration order.
|
||||
await registry.dispose();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class CapabilityRegistry`
|
||||
- `register(reg)`: void
|
||||
- `lookup<Id>(id): CapabilityRegistration<Id> | undefined`
|
||||
- `require<Id>(id): CapabilityRegistration<Id>`
|
||||
- `has(id): boolean`
|
||||
- `ids(): readonly CapabilityId[]`
|
||||
- `size(): number`
|
||||
- `assertSatisfies(ranges: Record<string, string>): void`
|
||||
- `dispose(): Promise<void>`
|
||||
- `defineCapability<const Id>(reg): CapabilityRegistration<Id>`
|
||||
- `CapabilityScope`: `"host" | "plugin" | "request"`
|
||||
- `CapabilityRegistration<Id>`: shape consumed by `register`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Version compatibility uses `@suaveplan/api-manifest/dep-graph`'s `satisfies(version, range)` — the same narrow semver dialect the manifest validator accepts (pinned, caret, tilde, major-wildcard, `*`). Unparseable ranges throw at `register` time inside `satisfies`; the registry catches and re-raises as `CAPABILITY.VERSION_MISMATCH` so the lifecycle driver only ever has to match on one code. Disposers are awaited sequentially in reverse-registration order — concurrent dispose would risk teardown races between adapters that share resources (e.g. `events` dispatching to `logger`). Idempotency: a second `dispose()` call is a no-op, so the host's stop path can call it freely even after a partial failure.
|
||||
Reference in New Issue
Block a user