Skip to content

Time Travel

StateMesh can record all state changes and replay to any point in time. Useful for debugging, testing, and audit trails.

Enable Time Travel

ts
const mesh = createMesh({
  state: { /* ... */ },
  timeTravel: { maxEntries: 1000 }  // Ring buffer size (default: 1000)
});
OptionDefaultDescription
maxEntries1000Ring buffer size. Oldest entries are evicted when exceeded.

API

ts
mesh.enableTimeTravel();     // Start recording
mesh.disableTimeTravel();    // Stop recording (preserves log)
mesh.isTimeTravelEnabled;    // true when recording

mesh.getTimeTravelLog();     // Return a copy of the recorded log
mesh.replayTo(index);        // Restore state at log entry index
mesh.replayToTimestamp(ts);  // Restore state nearest to timestamp (O(log n))
mesh.clearTimeTravelLog();   // Clear the log and free memory

Usage

ts
mesh.enableTimeTravel();

mesh.setPath("count", 1);
mesh.setPath("count", 2);
mesh.setPath("count", 3);

const log = mesh.getTimeTravelLog();
// [
//   { index: 0, event: ..., stateBefore: {count:0}, stateAfter: {count:1}, timestamp: ... },
//   { index: 1, event: ..., stateBefore: {count:1}, stateAfter: {count:2}, timestamp: ... },
//   { index: 2, event: ..., stateBefore: {count:2}, stateAfter: {count:3}, timestamp: ... }
// ]

mesh.replayTo(0);              // Restores state to { count: 1 }
mesh.replayToTimestamp(Date.now() - 5000);  // Restores state from 5 seconds ago

Log Entries

Each log entry contains:

FieldTypeDescription
indexnumberSequential index in the log
eventMeshEventThe event that caused the state change
stateBeforeTStateDeep clone of state before the change
stateAfterTStateDeep clone of state after the change
timestampnumberDate.now() when the change occurred

Replay Safety

  • Replay does not trigger undo bookkeeping
  • Replay does not re-record the replay as a new time travel entry
  • Replay emits a state.changed event with metadata.phase set to "replay"

Timestamp Lookup

replayToTimestamp uses binary search for O(log n) lookup:

ts
// Restore state from 10 seconds ago
const tenSecondsAgo = Date.now() - 10000;
mesh.replayToTimestamp(tenSecondsAgo);

// Restore state from a specific moment
mesh.replayToTimestamp(specificTimestamp);

Ring Buffer

The time travel log uses a ring buffer. When the log exceeds maxEntries, the oldest entry is evicted:

ts
const mesh = createMesh({
  state: { /* ... */ },
  timeTravel: { maxEntries: 100 }  // Keep only the last 100 changes
});

// After 101 changes, entry 0 is evicted
// mesh.replayTo(0) would fail — the entry no longer exists

WARNING

Each log entry stores deep clones of state before and after the change. With large state trees and high maxEntries, this can use significant memory. Set maxEntries to a reasonable value for your use case.

Disable and Preserve

ts
mesh.disableTimeTravel();  // Stop recording

// Existing log is preserved
const log = mesh.getTimeTravelLog();  // Still available

// Can still replay
mesh.replayTo(0);

Important Notes

TIP

Time travel is opt-in. No memory or CPU cost unless you enable it.

TIP

Use clearTimeTravelLog() to free memory when the log is no longer needed.

WARNING

Time travel and undo/redo are independent features. Time travel records all changes; undo/redo tracks snapshots for reverting. You can use either or both.

Next Steps

Released under the MIT License.