first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user