first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
# `RollbackJournal`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`RollbackJournal` is the per-plugin bounded ring buffer the API plugin host
uses to retain the last N known-good module activations. When a hot reload
fails its post-start health probe, the host re-activates the previous
specifier from the journal — making rollback atomic and almost free at
steady state. The contract lives here (in `@suaveplan/api-loader`); the
host-side concrete implementations (in-memory + durable ledger-backed)
land in `@suaveplan/api-host` Stage 2. Design intent: design doc §8.1a.
This module ships both the abstract interface (`RollbackJournal` type) and
a small in-memory reference implementation (`createRollbackJournal`). The
reference implementation is enough for downstream packages that need a
local journal without pulling in the full host (e.g. unit tests, sandboxed
plugin runners, the loader-conformance harness). Production hosts replace
it with their own implementation that persists the durable fields to the
plugin ledger so a restart can resume.
## Features
- Bounded FIFO ring buffer with configurable depth (default `3`).
- `push` evicts the oldest entry once depth is reached — never grows
unbounded.
- `peek(n)` returns up to `n` entries newest-first; defensive copy.
- `latest()` returns the most recent entry without allocating an array.
- `clear()` empties the journal; subsequent pushes resume normally.
- Strict argument validation: non-positive or non-integer depth throws
`RangeError`; non-integer or negative `peek(n)` throws `RangeError`.
## Usage: Basic
```ts
import { createRollbackJournal } from "@suaveplan/api-loader/rollback-journal";
const journal = createRollbackJournal();
journal.push({
specifier: "file:///opt/plugin/index.js?v=1",
manifestHash: "sha256:abc...",
moduleRef: null,
migrationCursor: 4,
capturedAt: "2026-05-25T08:14:22.014Z",
version: "1.4.2",
});
const previous = journal.latest();
if (previous !== null) {
await import(previous.specifier);
}
```
## Usage: Advanced
Custom depth + reading history newest-first to display a rollback menu:
```ts
import { createRollbackJournal } from "@suaveplan/api-loader/rollback-journal";
const journal = createRollbackJournal(10);
// ... many pushes happen ...
const history = journal.peek(5);
for (const entry of history) {
console.log(`${entry.capturedAt} ${entry.version} ${entry.manifestHash}`);
}
```
## API Reference
```ts
export interface RollbackJournal {
readonly length: number;
readonly depth: number;
push(entry: RollbackJournalEntry): void;
peek(n: number): readonly RollbackJournalEntry[];
latest(): RollbackJournalEntry | null;
clear(): void;
}
export const DEFAULT_ROLLBACK_JOURNAL_DEPTH: 3;
export function createRollbackJournal(depth?: number): RollbackJournal;
export type RollbackJournalFactory = (depth: number) => RollbackJournal;
```
The `RollbackJournalEntry` shape is owned by `@suaveplan/api-types` and
re-exported here for ergonomics (see `journal-entry/`).
## Implementation Notes
The reference implementation uses a plain `Array<RollbackJournalEntry>` with
`shift()` eviction — the journal is small (default depth 3) so the O(n)
shift is dominated by the ESM module-import cost the host pays on every
flip. A circular-buffer-with-head implementation would be marginally faster
but harder to audit; we accept the constant-factor cost.
`peek` returns a freshly-allocated array on every call; we explicitly do
**not** memo because the host calls `peek` once per reload, and the entries
themselves are immutable. The defensive copy guarantees that callers who
hold the returned array cannot corrupt the journal's internal state by
mutating it (a regression test pins this invariant).
Argument validation uses synchronous `RangeError` throws rather than
`SuaveplanError` because mis-construction of the journal indicates a
host-internal bug, not a recoverable runtime condition. The `LOADER.*` code
namespace (`error-codes`) is reserved for fault paths that the host's
operator UX needs to discriminate.