The Directed Acyclic Graph State Engine for JavaScript.
Conventional state managers treat history as a flat stack. When you undo and mutate, future steps are permanently destroyed. HomuraJS forks non-destructive timeline branches — Git for application state.
npm install @biagioscaglia/homurajs
The Fundamental Flaw of Linear History
Every standard undo/redo implementation (from Redux-Undo to naive history arrays) uses a 1D linear stack. This model creates silent, permanent data loss during real-world user interaction:
Because linear arrays have only one future pointer, applying any change from a previous state truncates everything ahead.
Every mutation creates a new node in the Directed Acyclic Graph. Branch divergence preserves 100% of historical states forever.
Directed Acyclic Graph (DAG) Topology
Each state in HomuraJS is an immutable node containing state snapshots, parent-child links, timestamps, and semantic metadata:
export interface HistoryEntry<T> {
id: string; // UUIDv4 node identifier
parentId: string | null; // Direct ancestor pointer
childrenIds: string[]; // Successor branches (forks)
branchId: string; // Active timeline branch
timestamp: number; // Unix epoch ms
label: string; // Semantic operation descriptor
state: T; // Frozen immutable snapshot
metadata?: Record<string, unknown>;
}
Package Installation
Install the unified meta-package or individual modular packages according to your project requirements:
npm install @biagioscaglia/homurajs
Quick Start Specification
Initialize the engine, perform mutations via Copy-On-Write draft proxies, and execute non-destructive time travel:
import { createHomura } from '@biagioscaglia/homurajs';
interface AppState {
counter: number;
user: { name: string; role: string };
}
const homura = createHomura<AppState>({
initialState: {
counter: 0,
user: { name: 'Homura', role: 'Architect' }
}
});
// Mutate via Copy-On-Write Proxy Draft
homura.update(draft => {
draft.counter += 10;
draft.user.role = 'Lead';
}, { label: 'Promote user and increment' });
// Non-destructive Time Travel
homura.undo(); // State reverts to counter: 0
homura.redo(); // State returns to counter: 10
// Branching
homura.createBranch('experimental');
homura.update(draft => { draft.counter = 999; });
Monorepo Package Architecture
| Package | Bundle Size | Role & Capabilities |
|---|---|---|
| @homura-js/core | < 5.2 kB |
Core DAG graph engine, Proxy draft immutability, structural diffing, snapshot registry, persistence. |
| @homura-js/devtools | < 9.4 kB |
Standalone diagnostic GUI: visual timeline tree, JSON state tree inspector, side-by-side diff viewer. |
| @homura-js/vanilla | < 2.8 kB |
Reactive DOM binding (bindState) and Zero-JS form auto-binding crash recovery (bindForm). |
| @homura-js/react | < 2.1 kB |
React 18+ bindings with useSyncExternalStore and selector optimization. |
| @homura-js/vue | < 2.3 kB |
Vue 3 Composition API hook (useHomura) and plugin wrapper. |
| @biagioscaglia/homurajs | Meta |
Unified meta-package + standalone browser CDN bundle (dist/index.global.js). |
Copy-On-Write Proxy Immutability
HomuraJS intercepts state writes using JavaScript Proxies. Mutations are isolated in a temporary draft and applied with structural sharing:
Published snapshots are deep-frozen: mutating getState() throws. Use update(..., { silent: true }) for in-place edits without a history node, and setStateAsync when middleware returns a Promise.
// Atomic Transactions: Multiple operations collapsed into a single DAG node
homura.transaction(draft => {
draft.counter += 50;
draft.user.name = 'Madoka';
draft.user.role = 'Guardian';
}, { label: 'Atomic Profile Update' });
Structural Diff Engine & Replay
Calculate recursive JSON difference sets between any two nodes in the DAG with exact dot-paths and types:
const diffs = homura.diff('entry-node-a', 'entry-node-b');
// Returns:
// [
// { type: 'modified', path: 'user.role', oldValue: 'Architect', newValue: 'Lead' },
// { type: 'added', path: 'flags.experimental', newValue: true }
// ]
Vanilla JS & Zero-JS Static Site Form Engine
Turn any static HTML form (Webflow, Shopify, Squarespace, Static HTML) into a time-travel crash-recovery machine without writing JavaScript:
<!-- CDN Standalone Script -->
<script src="https://unpkg.com/@biagioscaglia/homurajs/dist/index.global.js"></script>
<!-- Declarative Auto-Bound Form -->
<form data-homura-form="lead_capture" data-homura-persist="localstorage">
<span data-homura-status></span>
<div data-homura-breadcrumbs></div>
<input type="text" name="name" placeholder="Full Name" />
<input type="email" name="email" placeholder="Email Address" />
<textarea name="specifications"></textarea>
<button type="button" data-homura-undo>Undo</button>
<button type="button" data-homura-redo>Redo</button>
<button type="submit">Submit Form</button>
</form>
Multi-Step Form Wizard (data-homura-wizard)
<form data-homura-wizard="quote_wizard" data-homura-persist="localstorage">
<div data-homura-step="1">
<label>Step 1: Budget</label>
<input type="number" name="budget" />
<button type="button" data-homura-next>Next Step</button>
</div>
<div data-homura-step="2">
<label>Step 2: Contact</label>
<input type="email" name="email" />
<button type="button" data-homura-prev>Back</button>
<button type="submit" data-homura-next data-submit-label="Finish">Submit</button>
</div>
</form>
WordPress & WooCommerce Integration
Install directly from WordPress Admin via Plugins → Add New → Search "Homura Time Travel", or download from WordPress.org:
The official HomuraJS WordPress plugin brings DAG state history, checkout recovery, and form crash protection to WordPress & WooCommerce forms with zero configuration:
[homura_form id="lead_quote" persist="localstorage" crypto="aes-gcm"]
[homura_status form="lead_quote"]
[homura_breadcrumbs form="lead_quote"]
[homura_ghost_assist form="lead_quote"]
<input type="text" name="company" placeholder="Company Name" />
<textarea name="project_scope" placeholder="Describe scope..."></textarea>
[homura_undo form="lead_quote" label="Undo"]
[homura_redo form="lead_quote" label="Redo"]
[homura_handoff form="lead_quote" label="📱 Continue on Mobile"]
[homura_visual_diff form="lead_quote" field="project_scope" label="📝 Visual Diff"]
[homura_clear form="lead_quote" label="Clear Draft"]
[/homura_form]
Automatic Hooks for Top Form Engines
The plugin auto-detects and attaches crash recovery to:
- WooCommerce Checkout (
.woocommerce-checkout) - Contact Form 7 (
.wpcf7 form) - WPForms & Gravity Forms (
.wpforms-form,.gform_wrapper) - Elementor Forms, Fluent Forms & Ninja Forms
React 18+ Integration (@homura-js/react)
import React from 'react';
import { useHomura } from '@homura-js/react';
import { homura } from './store';
export function UserDashboard() {
const { state: user, update, undo, redo, canUndo, canRedo } = useHomura(
homura,
s => s.user
);
return (
<div>
<h3>User: {user.name}</h3>
<button onClick={() => update(d => { d.user.name = 'New Name'; })}>Update</button>
<button disabled={!canUndo} onClick={() => undo()}>Undo</button>
<button disabled={!canRedo} onClick={() => redo()}>Redo</button>
</div>
);
}
Vue 3 Composition API (@homura-js/vue)
<template>
<div>
<h3>Counter: {{ state.counter }}</h3>
<button @click="increment">+1</button>
<button :disabled="!canUndo" @click="undo">Undo</button>
<button :disabled="!canRedo" @click="redo">Redo</button>
</div>
</template>
<script setup lang="ts">
import { useHomura } from '@homura-js/vue';
import { homura } from './store';
const { state, update, undo, redo, canUndo, canRedo } = useHomura(homura);
function increment() {
update(d => { d.counter++; }, { label: 'Increment' });
}
</script>
Embedded Diagnostic DevTools & Visual Time Machine
Zero-dependency diagnostic panel with interactive DAG visual tree, keyboard navigation (←/→/Space), speed controls, drag-and-drop .homura session replay, and diff scrubber:
import { mountDevTools } from '@homura-js/devtools';
// Mount interactive Visual Time Machine
mountDevTools(homura, {
position: 'floating', // 'floating' | 'embedded'
theme: 'dark',
defaultOpen: true
});
// Keyboard Navigation Active:
// [← / Undo] [→ / Redo] [Space / Play-Pause] [Home / Rewind] [End / Latest]
Web Worker Off-Thread Async Diffing Engine
Compute massive state differences without blocking the UI thread (120 FPS guaranteed) using Web Worker async diffing with zero separate assets and automatic Node/SSR fallback:
import { diffStatesAsync, createAsyncDiffer } from '@biagioscaglia/homurajs';
// Offload heavy structural diffing to background Web Worker thread
const changes = await diffStatesAsync(largeDataSetA, largeDataSetB, {
timeoutMs: 3000
});
console.log('Detected diffs off-thread:', changes);
Forensic Bug Reporting & Session Playback (.homura)
Debug application state like you debug source code. Export entire DAG timelines from production or QA sessions and replay them step-by-step on any developer machine:
// 1. Client / User encounters an unexpected bug in production
window.addEventListener('error', () => {
// Capture 100% of the DAG state evolution up to the moment of failure
const sessionData = homura.export();
sendErrorReport('crash-session.homura', sessionData);
});
// 2. Developer imports the session in DevTools or local environment
homura.import(crashSessionData);
// 3. Replay the exact sequence of user actions that corrupted the state
await homura.replay({
speed: 2,
onStep: (entry, step, total) => {
console.log(`[Step ${step}/${total}] Transition: ${entry.label}`, entry.state);
}
});
// 4. Jump directly to the failing node and compute the recursive structural diff
homura.jumpTo('corrupted-node-id');
const diffs = homura.diff('previous-valid-node', 'corrupted-node-id');
HomuraDB & Full-Stack State Correlation (@homura-js/db)
Version your database state
Record table inserts, updates, and deletes as an immutable Directed Acyclic Graph (DAG) on top of embedded SQLite or memory stores with instant time-travel and branch merging.
Full-Stack State Correlation
Connect UI state (#184), network requests, and database transactions (#52) into one reproducible causal history exported as a unified .homura forensic session:
import { createHomuraDB, createForensicRecorder } from '@homura-js/db';
// 1. Initialize Versioned Database Engine (Embedded SQLite / Memory)
const db = createHomuraDB({ name: 'production_store' });
db.createTable('orders');
db.createTable('inventory');
// 2. Attach Full-Stack Forensic Recorder
const recorder = createForensicRecorder({
clientHomura, // Frontend UI Homura State (#184)
db // Backend Database HomuraDB State (#52)
});
// 3. Correlate HTTP Network Requests with Database Mutations
recorder.recordNetworkTrace({
url: '/api/checkout',
method: 'POST',
statusCode: 200,
requestBody: { cart: ['cyber_shield_01'] }
});
db.transaction(tx => {
tx.insert('orders', { id: 'ord_99', total: 250, status: 'confirmed' });
tx.update('inventory', 'SHIELD-01', { stock: 48 });
}, {
label: 'Atomic Checkout Transaction',
clientStateId: clientHomura.getCurrentEntry().id
});
// 4. Export Unified Forensic Session (.homura)
const sessionJSON = recorder.exportJSON();
// sessionJSON contains: Client UI DAG + Network Trace Timeline + Database Mutation DAG
Architectural Scope & Paradigm Comparison
Understanding how HomuraJS state history infrastructure compares to focused single-purpose libraries across different layers of the JavaScript stack:
| Paradigm / Scope | HomuraJS | Redux-Undo | Zustand History | XState | Immer |
|---|---|---|---|---|---|
| Primary Design Target | State History Infrastructure |
Redux Store Enhancer | Zustand Middleware | State Machine Orchestration | Immutability Utility |
| History Model | Directed Acyclic Graph (DAG) |
1D Linear Stack (Past/Future) | 1D Linear Stack (Temporal) | Transition Graph / Statechart | N/A (Copy-on-Write Proxy) |
| Branching & 3-Way Merge | Built-in (Non-destructive) |
Destructive on new action | Destructive on new action | Parallel State Nodes | N/A |
| Full-Stack Forensics (.homura) | UI ↔ Network ↔ DB Correlation |
N/A (Client Redux only) | N/A (Client Store only) | Event Inspection Protocol | N/A |
| Zero-JS Static & WordPress Forms | Auto-Recovery & WP Plugin |
N/A | N/A | N/A | N/A |
| Structural Diffing Engine | Recursive Dot-Path Engine |
N/A | N/A | N/A | JSON Patches (RFC 6902) |
| Diagnostic GUI & Replay | Embedded Visual DevTools |
Browser Extension / Redux DevTools | N/A | XState Stately Visualizer | N/A |
| Runtime Dependencies | Zero Dependencies |
Requires Redux | Requires Zustand | Zero Dependencies | Zero Dependencies |
Complete Homura<T> API Matrix
| Method Signature | Return Type | Description |
|---|---|---|
getState() |
T |
Returns the current deep-frozen immutable snapshot (mutations throw). |
update(updater, options?) |
HistoryEntry<T> |
Mutates via Copy-On-Write draft. Supports silent: true (in-place, no history node) and skips no-op drafts. |
setState(state, options?) |
HistoryEntry<T> |
Replaces state synchronously through the middleware pipeline. |
setStateAsync(state, options?) |
Promise<HistoryEntry<T>> |
Await async middleware before committing a replacement state. |
undo() |
HistoryEntry<T> | null |
Steps back to parent entry in DAG. |
redo() |
HistoryEntry<T> | null |
Steps forward to next child entry on branch. |
canUndo() / canRedo() |
boolean |
Returns boolean availability of undo/redo actions. |
transaction(fn, options?) |
HistoryEntry<T> |
Batches multiple operations into a single atomic history node. |
createBranch(name, fromEntryId?) |
Branch |
Forks an alternative timeline branch from any node. |
merge(sourceBranchId, options?) |
HistoryEntry<T> |
Merges a branch into the active branch (3-way, fast-forward, or custom resolver). |
diff(entryA, entryB?) |
DiffChange[] |
Deep structural dot-path diffing between two states. |
snapshot(name?, metadata?) |
Snapshot<T> |
Creates a named milestone bookmark at active node. |
replay(options?) |
Promise<void> |
Automated step-by-step timeline playback. |
Engine Performance Specs
| Operation | Throughput | Latency (10,000 Nodes) |
|---|---|---|
| Node Mutation (Proxy Commit) | ~185,000 ops/sec |
0.005 ms |
| DAG Timeline Jump (Rewind/Fast-Forward) | ~420,000 ops/sec |
0.002 ms |
| Deep Structural Diff (Nested 500 keys) | ~95,000 ops/sec |
0.010 ms |
| State Serialization (export/import) | ~45,000 ops/sec |
0.022 ms |
- O(1) Hash Map Timeline Jump (
~0.002 ms): Homura indexes all DAG entries in a flat dictionary by UUID. Jumping to any node resolves in O(1) time directly without replaying intermediate node deltas. - Proxy Mutation (
~0.005 ms): Copy-On-Write drafts clone only modified dictionary branches, sharing memory references for unmodified subtrees. - Dataset Size: Tested across 10,000 active nodes and 500 deep nested object properties per state.
All performance metrics are fully reproducible. Run the benchmark suite on your local machine:
git clone https://github.com/biagio-scaglia/homura-js.git
cd homura-js && pnpm install
pnpm run bench
Frequently Asked Questions
How does HomuraJS prevent memory leaks with large DAG histories?
HomuraJS uses Copy-On-Write structural sharing: unmodified parts of the state tree share memory references across all nodes. Additionally, compact() and pruneHistory() allow pruning redundant linear steps while preserving named snapshots and branch junctions.
Can HomuraJS be used without React or Vue?
Yes. @homura-js/core has zero dependencies. You can use it in Vanilla JS, Node.js, Svelte, Solid, Angular, or in static HTML/WordPress sites via @homura-js/vanilla or shortcodes.
How does HomuraJS protect WooCommerce checkout & WordPress forms?
The official WordPress plugin auto-hooks into .woocommerce-checkout, .wpcf7, .wpforms-form, .gform_wrapper, and .elementor-form, saving input to LocalStorage or SessionStorage in real-time. Real QR handoff, Ghost Assist, and WooCommerce AJAX recovery restore drafts after crash, refresh, or cart recalculation.
Can HomuraJS be used on static sites without npm (CDN)?
Yes. Include unpkg.com/@biagioscaglia/homurajs/dist/index.global.js via script tag and declare data-homura-form="form_id" data-homura-persist="localstorage" or "sessionstorage" on your HTML forms.
Is published state frozen, and how do I use async middleware?
Yes. getState() returns a deep-frozen snapshot. Use update(..., { silent: true }) for in-place changes without a history node, and setStateAsync when middleware returns a Promise.