Ledger 3.0
Ledger 3.0 removes its Ripple dependency and runtime injection. Command, queue, history, cancellation, composition, and command-error behavior remain unchanged. State subscriptions become framework-neutral invalidation callbacks with isolated listener failures.
Remove the runtime option
// Before
import * as ripple from '@vielzeug/ripple';
const ledger = createLedger({ maxHistory: 50, runtime: ripple });
// After
const ledger = createLedger({ maxHistory: 50 });LedgerOptions now contains only maxHistory.
Adapt the structural state readable
ledger.state is no longer a Ripple signal. It implements the exported LedgerReadable contract with readonly value, peek(), and subscribe(listener). Subscriptions are invalidation callbacks: they are not immediate, receive no value argument, and run from a listener snapshot. Failures are rethrown in a microtask without interrupting Ledger bookkeeping or later subscribers.
// Before — Ripple dependency tracking
const stop = effect(() => renderHistory(ledger.state.value));
// After — framework-neutral subscription
const render = () => renderHistory(ledger.state.value);
render();
const stop = ledger.state.subscribe(render);Project it into Ripple explicitly when the application needs Ripple derivations:
import { signal } from '@vielzeug/ripple';
const state = signal(ledger.state.value);
const stop = ledger.state.subscribe(() => {
state.value = ledger.state.value;
});State replacements, history arrays, and history entries remain frozen. Metadata remains a caller-owned reference.
Ledger 2.0
Ledger 2.0 accepts only reversible commands, publishes one atomic state snapshot, and separates queued cancellation from active cooperative cancellation.
Rename commands
Replace optional execute and rollback with required apply and revert.
// Before
await ledger.do({
execute: saveNext,
rollback: restorePrevious,
});
// After
await ledger.do({
apply: saveNext,
revert: restorePrevious,
});Move irreversible work outside Ledger commands.
Replace individual readables
// Before
if (ledger.canUndo.value) void ledger.undo();
console.log(ledger.historySnapshot.value);
// After
if (ledger.state.value.undo.length > 0) void ledger.undo();
console.log(ledger.state.value.undo);Removed names include Command, CommandMeta, canUndo, canRedo, historySize, historySnapshot, isProcessing, and pendingCount.
Handle cancellation
Queued signal cancellation rejects with LedgerCancelledError before user code starts. Active work receives context.signal and must stop cooperatively.
const controller = new AbortController();
const operation = ledger.do(command, { signal: controller.signal });
controller.abort();
await operation.catch((error) => {
if (!(error instanceof LedgerCancelledError)) throw error;
});If active code ignores cancellation and finishes, its history transition is not committed. Ledger does not automatically compensate partial effects.
Compose only reversible steps
compose() accepts readonly reversible commands, applies them in order, and reverts or compensates in reverse order.
await ledger.do(
compose([
{ apply: updateX, revert: restoreX },
{ apply: updateY, revert: restoreY },
], 'Move item'),
);If application and compensation both fail, inspect LedgerExecutionError.cause as an AggregateError.
Upgrade Checklist
- Remove
LedgerOptions.runtime. - Replace automatic Ripple dependency tracking with
ledger.state.subscribe()or an explicit adapter signal. - Import
LedgerReadableandUnsubscribewhen framework adapters need explicit annotations. - Replace
CommandwithReversibleCommand. - Rename
execute/rollbacktoapply/revert. - Replace individual history readables with one
statesnapshot. - Catch
LedgerCancelledErrorseparately from execution and rollback failures. - Keep irreversible work outside commands.
- Await
whenIdle()at lifecycle drain boundaries.
Review the Usage Guide and API Reference for current contracts.