@ledgex/core
v0.5.0
Published
Key-value state management with efficient undo/redo
Maintainers
Readme
Ledgex
"Smart, memory-efficient state management for JS apps with built-in undo/redo. Inspired by Git and Photoshop"

@ledgex/react 
Table of Contents
Features
Simple Global Key-Value Store A lightweight, globally shared state container with full type safety (when used with TypeScript).
Built-in Time Travel Native undo / redo support with minimal runtime and memory overhead.
Layered State Management Organize and isolate state changes using layers—similar to Photoshop layers—making complex state flows easier to manage.
Smart & Efficient History Storage
- Only changed properties are recorded—never entire objects
- Repeated updates that don’t alter state are automatically ignored
- Large objects (30+ properties) remain memory-efficient because unchanged values reference existing state
No Redundant History Entries Only meaningful changes create history records. Setting the same value twice produces no extra entries.
Tiny Footprint Under 8KB gzipped, with no unnecessary dependencies.
Framework-Agnostic Designed for plain JavaScript and works equally well in any environment (React, Vue, Svelte, or no framework at all).
Installation
npm install @ledgex/coreLedgex
Basic Usage
1. Create a Ledgex instance
import { Ledgex } from '@ledgex/core';
// Create a global or scoped instance
const store = new Ledgex({
bufferSize: 100 // optional, default is unlimited
});You can create:
- one global instance for the entire app
- or multiple instances for isolated state domains (e.g. editor, history, settings)
2. Use anywhere
const layer = store.get('background');
store.set('background', { color: '#202020' });
store.undo();
store.redo();Real-World Example: Photo Editor Layer Backups (Undo / Redo)
This example shows how Ledgex can be used to efficiently back up complex UI state in applications like photo editors, diagram tools, or design software.
Concept
- Use your own live state for immediate rendering and user feedback
- Use Ledgex only for history snapshots
- Apply throttling to avoid recording excessive intermediate states
Ledgex becomes a history engine, not your rendering bottleneck.
Example Code
import throttle from 'lodash/throttle';
import { Ledgex } from '@ledgex/core';
import { layers, setLayers } from './layersStore'; // your live state
const ledgex = new Ledgex({ bufferSize: 200 });
// Throttle backups to once every 300ms
const throttledSet = throttle(ledgex.set.bind(ledgex), 300);
function handlePropsChange(layerId, newProps) {
setLayers(prevLayers =>
prevLayers.map(layer => {
if (layer.id === layerId) {
const updatedLayer = layer.clone();
updatedLayer.updateProps(newProps);
// Backup only meaningful state changes
throttledSet(layerId, updatedLayer.toObject());
return updatedLayer;
}
return layer;
})
);
}Why This Works So Well
Efficient Memory Usage Only meaningful diffs are stored — no full snapshots during rapid updates.
Smooth User Experience Live updates remain instant, while undo/redo history stays compact.
Throttle-Friendly Dragging, sliders, and key presses don’t flood history with noise.
Advanced Features
Batch Updates (Atomic State Changes)
Ledgex allows multiple updates to be grouped into one undo/redo step.
This is ideal when several changes should be treated as one user action.
ledgex.set({
layer1: { x: 100, y: 200 },
layer2: { x: 100, y: 200 },
layer3: { x: 100, y: 200 }
}); // Single undo/redo entryUse cases:
- Aligning multiple layers
- Applying a preset
- Group transformations
Deeply Nested Object Support
Ledgex efficiently handles nested updates, storing only the deepest changes.
ledgex.set({
layer1: {
filters: {
brightness: 1.2,
contrast: 0.8
}
}
});Why it matters:
- Safe for complex structured data
- No redundant history entries
- No memory blow-up with deep objects
Efficient History Management
Ledgex gives you full control over history size using a configurable buffer.
const ledgex = new Ledgex({
bufferSize: 100 // keep last 100 meaningful changes
});How it works
- Limits the number of undo/redo steps kept in memory
- Oldest entries are automatically discarded
- Works together with diff-based storage
Why This Matters
- Memory Safety → history never grows unbounded
- Performance → undo/redo remains fast
- Flexibility → tune buffer size per app or feature
Ledgex API Reference
The Ledgex class is a time-travel–enabled, layered state manager with efficient history tracking, undo/redo support, and subscription-based updates.
It is framework-agnostic and designed for applications that require precise state history, such as editors, design tools, and complex UIs.
Import
import { Ledgex } from '@ledgex/core';Constructor
new Ledgex(options?)Parameters
| Name | Type | Default | Description |
| ------------------------- | -------- | ------- | -------------------------------------------------- |
| options | Object | {} | Optional configuration |
| options.bufferSize | number | 100 | Maximum number of meaningful history steps to keep |
| options.toleranceWindow | number | 20 | Time window for collapsing intermediate updates |
Example
const ledger = new Ledgex({
bufferSize: 200,
toleranceWindow: 50
});Core Concepts
Layers
- Each layer is an independent key-value state container.
- Layers can be activated, deactivated, and updated independently.
- History is tracked per layer, but undo/redo operates globally.
Time Travel
- Every meaningful change advances time.
- Undo and redo move backward or forward through meaningful states only.
- Repeated updates that do not change state are ignored.
Methods
set(updates)
Applies state updates at the current time.
- Automatically ignores non-meaningful changes
- Supports batch updates
- Creates a single undo/redo step
ledger.set({
background: { color: '#202020' },
layer1: { x: 100, y: 200 }
});Parameters
| Name | Type | Description |
| --------- | ------------------------ | -------------------------------- |
| updates | Object<string, Object> | Map of layerId → partial state |
Notes
- Nested objects are merged deeply.
- Only changed properties are recorded.
- If nothing meaningful changes, no history entry is created.
get(layerIds?)
Returns the current state of one or more layers.
ledger.get(); // all active layers
ledger.get(['background', 'layer1']);Parameters
| Name | Type | Description |
| ---------- | --------------------- | ----------------------- |
| layerIds | string[] (optional) | Specific layers to read |
Returns
Object<string, Object>Only active layers are included.
undo()
Moves to the previous meaningful state.
ledger.undo();Returns
Object<string, Object> | undefinedThe current state after undo, or undefined if undo is not possible.
redo()
Moves to the next meaningful state.
ledger.redo();Returns
Object<string, Object> | undefinedThe current state after redo, or undefined if redo is not possible.
remove(layerId)
Deactivates a layer at the current time.
ledger.remove('background');Parameters
| Name | Type | Description |
| --------- | -------- | ------------------- |
| layerId | string | Layer to deactivate |
Notes
- Deactivation is recorded in history
- Undo will restore the layer
prune(minTime)
Removes history entries older than minTime.
ledger.prune(50);Parameters
| Name | Type | Description |
| --------- | -------- | ----------------------- |
| minTime | number | Earliest time to retain |
Notes
- Layers with no remaining state are removed
- Useful for manual memory management
flush()
Automatically prunes history based on bufferSize.
ledger.flush();Behavior
- Keeps only the last
bufferSizemeaningful steps - Updates internal flush time
- Invoked automatically when history grows too large
subscribe(callback)
Subscribes to state changes.
const unsubscribe = ledger.subscribe(() => {
console.log('State changed:', ledger.get());
});Parameters
| Name | Type | Description |
| ---------- | ---------- | ----------------------------- |
| callback | Function | Called after any state change |
Returns
() => voidUnsubscribe function.
Notes
- Callbacks are triggered asynchronously
- Safe to read state inside callback
Automatic Behavior
Auto-Flushing
Ledgex automatically flushes history when:
currentTime - lastFlushTime > bufferSize + toleranceWindowThis prevents unbounded memory growth during rapid updates.
Usage Example
const ledger = new Ledgex({ bufferSize: 100 });
ledger.set({
layer1: { x: 10, y: 20 }
});
ledger.set({
layer1: { x: 15 }
});
ledger.undo(); // reverts x to 10
ledger.redo(); // reapplies x = 15Guarantees
- Only meaningful changes are stored
- Undo/redo is deterministic
- Memory usage scales with change size, not object size
- No duplicate or empty history entries
Intended Use Cases
- Photo / video editors
- Diagram & design tools
- Complex form editors
- Any application requiring efficient undo/redo
Support
⭐ Star the repo if you find it useful! 🐞 Report issues on GitHub
