first commit
This commit is contained in:
121
docs/api-stack/api/api-host/src/plugin-ledger/plugin-ledger.md
Normal file
121
docs/api-stack/api/api-host/src/plugin-ledger/plugin-ledger.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# `plugin-ledger.ts`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`PluginLedger` is the host-owned metadata store backing every plugin
|
||||
record the API host orchestrates. It persists the §4 plugin row
|
||||
(state, version, manifest hash, isolation tier, crash + install-failure
|
||||
counters, capability set) and the §8.1a rollback-journal subset
|
||||
(ring buffer of `(specifier, manifestHash, migrationCursor, capturedAt,
|
||||
version)` tuples). The store is the source of truth that survives host
|
||||
restart — the in-memory `WeakRef<Module>` from `@suaveplan/api-loader`
|
||||
stays in memory, but the persisted subset round-trips through this
|
||||
ledger so a post-restart rollback only costs one extra `import()`.
|
||||
|
||||
The ledger is intentionally driver-agnostic: it consumes a structural
|
||||
`LedgerDb` (a SQLite-shaped `prepare(sql).run(...) / .all(...) / .get(...)`)
|
||||
seam so `bun:sqlite`, `better-sqlite3`, or any thin wrapper can wire in
|
||||
without recompilation. Forward-only migrations install the host-owned
|
||||
tables idempotently — re-instantiating a `PluginLedger` against an
|
||||
already-migrated DB is a no-op.
|
||||
|
||||
## Features
|
||||
|
||||
- Forward-only idempotent migration runner (`HostMigrationRunner`).
|
||||
- Schema covers the five §4 host-owned tables — the active plugin
|
||||
ledger + rollback journal, plus reserved schemas for Stage-6 saga +
|
||||
2PC.
|
||||
- Ring-buffer trim with `(plugin_name, position)` composite PK so the
|
||||
oldest entry is overwritten when depth (default 3) is exceeded.
|
||||
- Adapter views (`asLifecycleLedger`, `asRollbackPersistence`) hand the
|
||||
lifecycle + reload drivers their narrow ports.
|
||||
- Zod-validated journal rows — corrupt entries raise
|
||||
`LEDGER.INVALID_ROW`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { Database } from "bun:sqlite";
|
||||
import { PluginLedger } from "@suaveplan/api-host/plugin-ledger";
|
||||
|
||||
const db = new Database(":memory:");
|
||||
const ledger = new PluginLedger({ db });
|
||||
|
||||
ledger.upsertPlugin({
|
||||
name: "@acme/billing",
|
||||
version: "1.4.2",
|
||||
manifestHash: "sha256:abc",
|
||||
isolation: "trusted",
|
||||
state: "started",
|
||||
lastTransitionAt: null,
|
||||
installFailures: 0,
|
||||
crashCount: 0,
|
||||
crashBudgetResetsAt: null,
|
||||
capabilities: [{ id: "audit-logger", version: "1.0.0" }],
|
||||
});
|
||||
|
||||
const all = ledger.listPlugins();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
// Hand the rollback driver its persisted journal seam.
|
||||
import { ReloadDriver } from "@suaveplan/api-host/hot-reload";
|
||||
|
||||
const driver = new ReloadDriver({
|
||||
// ...,
|
||||
adapters: {
|
||||
// ...,
|
||||
persistence: ledger.asRollbackPersistence(),
|
||||
},
|
||||
});
|
||||
|
||||
// Push a journal entry directly (e.g. on initial activation).
|
||||
ledger.pushJournalEntry("@acme/billing", {
|
||||
specifier: "file:///opt/plugins/billing/dist/index.js?v=1",
|
||||
manifestHash: "sha256:abc",
|
||||
migrationCursor: 7,
|
||||
capturedAt: new Date().toISOString(),
|
||||
version: "1.4.2",
|
||||
});
|
||||
|
||||
const latest = ledger.latestJournal("@acme/billing");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new PluginLedger({ db, journalDepth?, now?, autoMigrate? })` — opens
|
||||
the ledger and runs migrations unless `autoMigrate === false`.
|
||||
- `upsertPlugin(record): PluginLedgerRow` — insert-or-replace.
|
||||
- `getPlugin(name): PluginLedgerRow | null`
|
||||
- `listPlugins(): readonly PluginLedgerRow[]`
|
||||
- `removePlugin(name): boolean`
|
||||
- `pushJournalEntry(plugin, entry)` — appends with ring-buffer trim.
|
||||
- `peekJournal(plugin, n): readonly PersistedRollbackEntry[]`
|
||||
- `latestJournal(plugin): PersistedRollbackEntry | null`
|
||||
- `journalLength(plugin): number`
|
||||
- `asLifecycleLedger(): LedgerAdapter`
|
||||
- `asRollbackPersistence(): RollbackPersistence`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The ring-buffer trim chooses the oldest entry by `(captured_at,
|
||||
position)` so ties (same timestamp on two pushes) deterministically
|
||||
pick the lower position. The composite primary key `(plugin_name,
|
||||
position)` lets the upsert `ON CONFLICT` clause overwrite the chosen
|
||||
slot atomically.
|
||||
|
||||
`asLifecycleLedger().write` preserves the existing isolation tier and
|
||||
counter values when the lifecycle driver writes a partial row. The
|
||||
caller is expected to call `upsertPlugin` with the full record once
|
||||
the manifest is parsed.
|
||||
|
||||
`materialiseRow` re-parses the JSON-serialised capability set on every
|
||||
read so the in-memory shape is always validated. A malformed
|
||||
`capabilities_json` raises `LEDGER.INVALID_ROW` rather than passing
|
||||
through a half-typed array — operators see the corrupt row early.
|
||||
Reference in New Issue
Block a user