@monkin/history
v0.1.4
Published
An immutable library for document history management with first-class undo/redo support
Downloads
448
Readme
@monkin/history
An immutable library for document history management with first-class undo/redo support.
While any document state can be reconstructed by replaying operations from the beginning, @monkin/history optimizes this process by combining an operation timeline with state snapshots.
Core Concept
Current State = Latest Snapshot + Subsequent Operations
[!TIP] Built for Synchronization
Since history is immutable, you can save a reference at any moment and later
diffit with the newest version. This makes it easy to implement background persistence or synchronization logic.
Features
- Immutable by Design: Every operation returns a new instance of
OperationListorSnapshotList. - Linked-List Timeline: Operations are modeled as a sequence where each entry points to its predecessor, allowing efficient undo/redo traversal.
- Flexible ID Generation: Use numeric IDs, UUIDs, bigints, or any custom identifier.
- Decoupled Snapshots: Manage state snapshots independently for maximum architectural flexibility.
Installation
npm install @monkin/historyCore Components
OperationList
Tracks document operations and manages the undo/redo pointer.
[!IMPORTANT] The
IdGeneratormust always return an identifier strictly greater than the providedmaxId. This is required for correct history traversal and ordering.
Basic Usage
import { OperationList } from "@monkin/history";
// Initialize with an ID generator
const generateId: OperationList.IdGenerator<number | bigint> = (maxId) => (maxId ?? 0n) + 1n;
let history = OperationList.empty<number | bigint, string>(generateId);
// Record operations
history = history.add("Add Header");
history = history.add("Change Background");
// Undo and Redo
if (history.canUndo) history = history.undo(); // Moves pointer back
if (history.canRedo) history = history.redo(); // Moves pointer forward
// Reconstruct state from the pointer branch
for (const entry of history) {
console.log(entry.operation);
}API Highlights
history.pointer: The ID of the current operation.history.setPointer(id): Moves the history pointer to a specific operation id.history.isUndone(id): Checks if an operation exists but is currently undone.history.ageOf(id): Distance between current state and a given entry.history.get(id): Retrieves an entry reachable from the current state.history.entry(id): Retrieves an entry by ID, including undone ones.history.entries(): Generator yielding all recorded entries.[Symbol.iterator]: Iterates over the pointer branch (skipping undone operations).history.upload(items): Bulk-upload entries, useful for partial history loading.OperationList.diff(before, after): Static method comparing two history lists to find added, removed, or changed operations.
SnapshotList
An immutable collection for managing state snapshots indexed by identifiers.
Basic Usage
import { SnapshotList } from "@monkin/history";
let snapshots = SnapshotList.empty<number | bigint, string>();
// Add and retrieve snapshots
snapshots = snapshots.add(1, "Initial Content");
const item = snapshots.get(1);
console.log(item?.snapshot); // "Initial Content"
// Cleanup
snapshots = snapshots.remove(1);API Highlights
snapshots.add(id, snapshot): Adds a new snapshot.snapshots.addAll(items): Bulk-add snapshots from an iterable.snapshots.remove(id): Removes a snapshot by its ID.snapshots.get(id): Retrieves a snapshot item by ID.snapshots.filter(predicate): Filter snapshots into a new list.[Symbol.iterator]: Iterates over all items in sorted order.SnapshotList.diff(before, after): Static method comparing two snapshot lists to find added, removed, or changed items.
Design Philosophy
Why Decoupled?
OperationList and SnapshotList are intentionally separate primitives. This approach avoids imposing a specific state structure and enables complex compositions:
- Modular Architecture: Most apps use one list of each, but some require more. An animation editor might use one
OperationListfor global actions but 30 separateSnapshotListinstances—one per frame. - Lightweight: The library provides the mathematical primitives; you can easily wrap them in a higher-level "History Manager" tailored to your application's needs.
Historical Note
OperationList was originally named History. It was renamed to keep the History name available for consumers who want to implement their own domain-specific manager by combining these primitives.
Performance
The library is optimized for the latest history items. While accessing very deep history might be slower, it typically remains efficient enough for standard use cases. Use snapshots strategically to keep the number of operations to replay minimal.
Development
npm run dev: Start Vite development server.npm run build: Build the project (TypeScript & Vite).npm run test: Run tests using Vitest.npm run lint: Lint the project.npm run format: Format the project.npm run prepublishOnly: Build, lint, and test before publish.
License
MIT
