The USE system includes a sophisticated snapshot system for capturing and replaying state changes with flexible trigger logic, dual storage backends, and powerful time-travel capabilities. Snapshots enable undo/redo systems, session restoration, automated saving, and debugging workflows with precise control over when and what state gets captured.
This guide covers:
onSnapshot
and onReplay
callbacks for reliable coordinationThe snapshot system is fully asynchronous from top to bottom, ensuring reliable operation without blocking the main thread. Understanding this architecture is critical for proper usage, especially in tests and coordination scenarios.
Complete Async Flow:
State Change
↓
Observer Triggered (microtask)
↓
Snapshot Queued
↓
Snapshot Captured (async)
↓
Timeline Storage (async, queued per snapshot name)
↓
Custom callback() executed (can be async)
↓
onSnapshot() notification fired (completion signal)
Key Async Behaviors:
Initial Snapshots: When a snapshot is registered with wildcard ('*'
) or matching triggers, an initial snapshot is captured immediately if current state matches conditions
const state = Doh.use_state({ document: { content: 'Hello' } });
// This will capture an INITIAL snapshot because content exists and matches '*'
await Doh.use_snapshot('doc', {
triggers: { 'document.content': '*' }, // Wildcard matches existing 'Hello'
states: ['document.content'],
in_timeline: true
});
// Timeline now has 1 entry (initial snapshot)
state.document.content = 'World'; // Creates 2nd snapshot
await Doh.microtask();
// Timeline now has 2 entries (initial + this change)
Microtask Batching: Multiple rapid changes to the same path create only one snapshot due to USE system's microtask batching
state.document.content = 'A';
state.document.content = 'B';
state.document.content = 'C';
// Result: ONE snapshot with final value 'C'
// To create separate snapshots:
state.document.content = 'A';
await Doh.microtask(); // Let it process
state.document.content = 'B';
await Doh.microtask(); // Let it process
state.document.content = 'C';
// Result: THREE separate snapshots
Queue-Based Timeline Operations: Each snapshot name has its own operation queue, ensuring:
onSnapshot
fires only after ALL async operations complete// Rapid changes are queued and processed in order
state.doc.content = 'Change 1'; // Queued
state.doc.content = 'Change 2'; // Queued
state.doc.content = 'Change 3'; // Queued
// Meanwhile, different snapshot can operate in parallel
state.user.name = 'Alice'; // Different queue, processes concurrently
Completion Notifications: The onSnapshot
and onReplay
callbacks signal when ALL async operations finish
let completed = false;
await Doh.use_snapshot('test', {
triggers: { 'data': '*' },
states: ['data'],
in_timeline: true,
callback: function(snap) {
// Fires DURING capture (before storage completes)
console.log('Captured');
},
onSnapshot: function(snap) {
// Fires AFTER storage completes (including IndexedDB writes)
completed = true;
}
});
state.data = 'value';
await Doh.microtask();
// Now safe to wait for completion
while (!completed) await Doh.microtask();
// Timeline is guaranteed to be updated
const info = await Doh.use_timeline_info('test');
console.log(info.length); // Accurate count
Test Pattern: Await Completion Instead of Arbitrary Timeouts
// ❌ BAD: Random waits
state.document.content = 'Hello';
await new Promise(resolve => setTimeout(resolve, 100)); // Hope it's enough?
// ✅ GOOD: Use completion notifications
let snapshotDone = false;
await Doh.use_snapshot('doc', {
triggers: { 'document.content': '*' },
states: ['document.content'],
onSnapshot: () => { snapshotDone = true; }
});
state.document.content = 'Hello';
while (!snapshotDone) await Doh.microtask();
// Guaranteed complete
Every snapshot definition includes:
name
: Unique identifier for the snapshot collectiontriggers
: Conditions that determine when snapshots are capturedstates
: Which state paths to capture in the snapshotawait Doh.use_snapshot('snapshot_name', {
triggers: { /* trigger conditions */ },
states: [ /* state paths to capture */ ],
// Storage options (choose one or both)
as_history: true, // Browser history integration
in_timeline: 50, // IndexedDB timeline with rotation
// Advanced options
save_as: 'shared_name', // Optional: Save to shared timeline(s)
trigger_mode_or: false, // AND logic (default) vs OR logic
callback: async function(data) {}, // Custom processing (async supported)
// Completion notifications (async supported)
onSnapshot: async function(data) {}, // Called when snapshot capture completes
onReplay: async function(data) {} // Called when replay operation completes
});
Backend | Persistence | Capacity | Navigation | Performance | Use Case |
---|---|---|---|---|---|
Browser History | Session only | ~50 entries | Browser back/forward | Synchronous | Navigation states |
IndexedDB Timeline | Cross-session | Unlimited | Built-in undo/redo | Asynchronous | Document editing, complex workflows |
Critical Distinction: Snapshot triggers differentiate between watching for specific values versus path bubbling behavior:
// String format - watches path and all its properties via bubbling
await Doh.use_snapshot('string_triggers', {
triggers: 'user.profile', // Triggers on user.profile.name, user.profile.email, etc.
states: ['user']
});
// Array format - multiple paths with bubbling
await Doh.use_snapshot('array_triggers', {
triggers: ['user.profile', 'settings.theme'], // Both paths use bubbling
states: ['user', 'settings']
});
// Mixed: specific values and bubbling behavior
await Doh.use_snapshot('object_triggers', {
triggers: {
'user.active': true, // ONLY when user.active becomes exactly true
'user.profile': '*', // ANY change to user.profile.* (same as string format)
'page.path': '/dashboard', // ONLY when path becomes '/dashboard'
'document.content': '*', // ANY change to document.content (bubbling)
'game.health': 0 // ONLY when health reaches exactly 0
},
states: ['user', 'page', 'document', 'game']
});
Key Point: '*'
in object format provides the same path bubbling behavior as string/array format.
String Trigger - Single path with bubbling:
await Doh.use_snapshot('simple_page', {
triggers: 'currentPage', // Equivalent to { 'currentPage': '*' }
states: ['currentPage', 'pageData']
});
Array Triggers - Multiple paths with bubbling:
await Doh.use_snapshot('form_fields', {
triggers: ['form.name', 'form.email', 'form.message'], // All treated as '*'
states: 'form'
});
Object Triggers - Precise control:
await Doh.use_snapshot('mixed_conditions', {
triggers: {
'user.loggedIn': true, // Specific value: must be true
'document.modified': '*', // Any change to modified flag
'session.timeout': 0, // Specific value: must reach 0
'ui.theme': '*' // Any theme change
},
states: ['user', 'document', 'session', 'ui']
});
AND Logic (Default) - ALL triggers must be satisfied:
// Snapshot only triggers when BOTH conditions are met
Doh.use_snapshot('and_logic', {
triggers: {
'user.loggedIn': true, // First condition
'user.profile': '*' // Second condition (any profile change)
},
states: ['user.profile', 'user.session']
});
// Sequence required:
state.user.loggedIn = true; // First trigger met, no snapshot yet
state.user.profile.name = 'Alice'; // Both triggers met - snapshot captured!
OR Logic - ANY trigger will execute snapshot:
// Snapshot triggers on any form field change
Doh.use_snapshot('or_logic', {
triggers: ['form.name', 'form.email', 'form.message'],
trigger_mode_or: true, // Enable OR logic
states: 'form'
});
// Any of these will trigger a snapshot:
state.form.name = 'John'; // Triggers snapshot
state.form.email = 'john@test'; // Triggers snapshot
state.form.message = 'Hello'; // Triggers snapshot
Custom Logic Functions - Complete control:
// Advanced business logic: save if form is valid OR user is premium
Doh.use_snapshot('custom_logic', {
triggers: { 'form.errors': 0, 'form.submitted': true, 'user.premium': true },
trigger_mode_or: function(triggers) {
// triggers object contains state for each trigger path:
// { 'form.errors': { met: boolean, value: any, expectedValue: any, timestamp: number }, ... }
// Business rule: snapshot if (no errors AND submitted) OR (user is premium)
const validSubmission = triggers['form.errors'].met && // errors === 0
triggers['form.submitted'].met; // submitted === true
const premiumUser = triggers['user.premium'].met; // premium === true
return validSubmission || premiumUser;
},
states: ['form.data', 'user.account']
});
// Time-based custom logic
Doh.use_snapshot('time_based', {
triggers: { 'document.modified': true, 'session.lastSave': '*' },
trigger_mode_or: function(triggers) {
const modified = triggers['document.modified'];
const lastSave = triggers['session.lastSave'];
// Only save if document was modified AND enough time has passed
if (modified.met && lastSave.met) {
const timeSinceLastSave = Date.now() - lastSave.timestamp;
return timeSinceLastSave > 5 * 60 * 1000; // 5 minutes
}
return false;
},
states: 'document'
});
Trigger State Properties: Each trigger in custom functions receives:
met
: Boolean - whether the trigger condition was satisfiedvalue
: The current state value at the trigger pathexpectedValue
: The value the trigger was watching for (or true
for wildcard)timestamp
: When this trigger condition was last met (Date.now())Replay triggers define conditions that automatically restore (replay) the most recent snapshot when met. They work independently from save triggers, enabling powerful auto-recovery and state synchronization patterns.
// Auto-restore on app crash detection
Doh.use_snapshot('crash_recovery', {
// Save triggers - when to capture state
triggers: { 'document.saved': true },
states: ['document.content', 'document.metadata'],
// Replay triggers - when to auto-restore
replay_triggers: { 'app.crashed': true },
in_timeline: true
});
// When app.crashed becomes true, automatically replays the last captured snapshot
AND Logic (Default) - All replay triggers must be met:
Doh.use_snapshot('secure_restore', {
triggers: 'document.content',
states: 'document.content',
replay_triggers: {
'user.authenticated': true,
'app.ready': true
},
// Both conditions must be true to auto-replay
replay_trigger_mode_or: false // Default AND logic
});
OR Logic - Any replay trigger activates restoration:
Doh.use_snapshot('flexible_restore', {
triggers: 'form.data',
states: ['form.data', 'form.validation'],
replay_triggers: {
'user.clickedUndo': true,
'app.errorRecovered': true,
'session.restored': true
},
replay_trigger_mode_or: true // Any trigger causes replay
});
Custom Function Logic - Complex business rules:
Doh.use_snapshot('smart_restore', {
triggers: 'workspace.state',
states: ['workspace.tabs', 'workspace.layout'],
replay_triggers: {
'session.type': 'restored',
'user.premium': true,
'workspace.lastSaved': '*'
},
replay_trigger_mode_or: function(triggers) {
// Only auto-restore for premium users during session restoration
// OR if enough time has passed since last save
const sessionRestore = triggers['session.type'].met;
const isPremium = triggers['user.premium'].met;
const recentSave = triggers['workspace.lastSaved'];
if (sessionRestore && isPremium) {
return true;
}
// Auto-restore if last save was over 10 minutes ago
if (recentSave.met) {
const timeSince = Date.now() - recentSave.timestamp;
return timeSince > 10 * 60 * 1000;
}
return false;
}
});
Error Recovery:
Doh.use_snapshot('error_recovery', {
triggers: { 'form.valid': true },
states: 'form',
replay_triggers: { 'form.error': false }, // Restore when error clears
replay_trigger_mode_or: true
});
Navigation Restoration:
Doh.use_snapshot('nav_restore', {
triggers: 'router.currentRoute',
states: ['router.currentRoute', 'router.params'],
replay_triggers: { 'router.restoreRequested': true },
as_history: true
});
Session Recovery:
Doh.use_snapshot('session_state', {
triggers: ['user.preferences', 'ui.layout'],
states: ['user.preferences', 'ui.layout', 'workspace.openFiles'],
replay_triggers: { 'session.type': 'restored' },
in_timeline: true
});
Time-based Auto-save with Recovery:
Doh.use_snapshot('auto_recovery', {
triggers: { 'document.content': '*' },
states: 'document',
replay_triggers: { 'app.restarted': true },
callback: function(data) {
console.log('Document auto-saved:', data.timestamp);
}
});
[USE SNAPSHOT] Auto-replaying...
messagesString States - Single state path:
Doh.use_snapshot('theme_changes', {
triggers: 'ui.theme',
states: 'ui.theme' // Capture just the theme
});
Array States - Multiple state paths:
Doh.use_snapshot('search_activity', {
triggers: 'search.query',
states: ['search.query', 'search.filters', 'search.results', 'search.timestamp']
});
Object States - States with defaults:
Doh.use_snapshot('user_activity', {
triggers: 'activity.type',
states: {
'user.id': null, // Default if undefined
'activity.type': 'unknown', // Default activity type
'activity.timestamp': Date.now, // Function called for default
'activity.duration': 0, // Static default
'session.context': {} // Default object
}
});
// When triggered, snapshot contains the current values or defaults for missing paths
State paths use dot notation and support deep nesting:
const state = Doh.use_state({
user: {
profile: {
name: 'John',
settings: { theme: 'light', notifications: true }
},
activity: {
lastLogin: Date.now(),
pageViews: 42
}
}
});
Doh.use_snapshot('user_session', {
triggers: 'user.profile.name',
states: [
'user.profile', // Entire profile object
'user.profile.name', // Just the name string
'user.profile.settings.theme', // Deep nested value
'user.activity.lastLogin' // Activity timestamp
]
});
// Snapshot data structure:
// {
// name: 'user_session',
// timestamp: 1234567890,
// triggerCount: 1,
// states: {
// 'user.profile': { name: 'John', settings: { theme: 'light', notifications: true } },
// 'user.profile.name': 'John',
// 'user.profile.settings.theme': 'light',
// 'user.activity.lastLogin': 1234567890
// }
// }
Store snapshots in browser history for navigation-based state restoration:
Doh.use_snapshot('navigation_state', {
triggers: 'router.currentRoute',
states: ['router.currentRoute', 'router.params', 'router.query'],
as_history: true // Add to browser history
});
// Each route change adds a history entry with the snapshot
// Browser back/forward buttons can trigger state restoration
// History entries include the snapshot data for manual replay
History State Structure:
// Browser history.state contains:
{
doh_snapshot: {
name: 'navigation_state',
timestamp: 1234567890,
triggerCount: 1,
states: { /* captured state data */ }
}
}
// Access via history API:
const currentSnapshot = history.state?.doh_snapshot;
if (currentSnapshot) {
await Doh.use_replay_snapshot(currentSnapshot);
}
The snapshot system provides comprehensive URL query parameter management through two complementary features:
Add save_url_query: true
to snapshot definitions to capture and restore URL query parameters along with state:
// Define snapshot that saves URL query parameters
Doh.use_snapshot('navigation_state', {
triggers: 'router.currentRoute', // Watches router.currentRoute and all its properties (bubbling)
states: ['router.currentRoute', 'router.params', 'ui.filters'],
save_url_query: true, // Capture URL query parameters with snapshot
clear_url_query: true, // Default behavior for replay (optional)
in_timeline: true
});
// When triggered, captures both state and current URL query like:
// ?page=2&filter=active&search=user
Important: The use_replay_snapshot
function is async and returns a Promise that must be awaited. This ensures proper cascade prevention and queue ordering during replay operations.
The use_replay_snapshot
function and related timeline functions support an optional options
parameter for controlling replay behavior:
// Basic replay (uses snapshot's default clear_url_query setting)
await Doh.use_replay_snapshot(snapshotData);
// Explicitly control URL clearing during replay
await Doh.use_replay_snapshot(snapshotData, {
clear_url_query: true // Override snapshot's default setting
});
// Timeline functions also support the same options
await Doh.use_timeline_goto('snapshot_name', 0, { clear_url_query: true });
await Doh.use_timeline_undo('snapshot_name', { clear_url_query: true });
await Doh.use_timeline_redo('snapshot_name', { clear_url_query: true });
Snapshot Definition Options:
save_url_query
(boolean, default: false) - Captures current URL query parameters as part of the snapshot dataclear_url_query
(boolean, default: undefined) - Default behavior for URL clearing during replayReplay Options:
clear_url_query
(boolean, optional) - Controls URL clearing during replay:true
: Clear URL and restore saved query (if available)false
: Keep current URL, don't restore saved queryundefined
: Use snapshot's default settingSmart URL Handling: When a snapshot has saved URL query parameters:
clear_url_query
defaults to true
unless explicitly set to false
Use Cases:
The timeline system provides the most robust storage with dedicated IndexedDB databases, automatic rotation, and built-in navigation:
// Basic timeline storage
Doh.use_snapshot('document_history', {
triggers: { 'document.content': '*' },
states: ['document.content', 'document.title', 'document.metadata'],
in_timeline: true // Enable IndexedDB timeline
});
// Timeline with automatic rotation
Doh.use_snapshot('user_actions', {
triggers: { 'user.action': '*' },
states: ['user.profile', 'user.activity', 'user.context'],
in_timeline: 50 // Keep only last 50 entries
});
Timeline Database Architecture:
_use_ss_[snapshot_name]
timeline
timestamp
(ensures chronological ordering)Timeline Navigation API:
// All timeline functions are async and return Promises
async function demonstrateTimeline() {
// Get timeline information
const info = await Doh.use_timeline_info('document_history');
console.log(`Timeline has ${info.length} entries`);
console.log('Latest entry timestamp:', info.latest?.timestamp);
// Navigate backwards (undo)
const undoSuccess = await Doh.use_timeline_undo('document_history');
if (undoSuccess) {
console.log('Successfully undid last change');
} else {
console.log('Nothing to undo');
}
// Navigate forwards (redo)
const redoSuccess = await Doh.use_timeline_redo('document_history');
if (redoSuccess) {
console.log('Successfully redid last change');
} else {
console.log('Nothing to redo');
}
// Jump to specific position
const gotoSuccess = await Doh.use_timeline_goto('document_history', 5);
if (gotoSuccess) {
console.log('Jumped to timeline position 5');
}
// Get all timeline entries (for debugging/inspection)
const allSnapshots = await Doh.use_get_timeline('document_history');
console.log(`Retrieved ${allSnapshots.length} timeline entries`);
// Get limited entries (most recent first)
const recentSnapshots = await Doh.use_get_timeline('document_history', 10);
console.log('10 most recent snapshots:', recentSnapshots);
// Clear entire timeline
await Doh.use_clear_timeline('document_history');
console.log('Timeline cleared');
}
Queue-Based Operations and Microtask Batching: The timeline system uses per-snapshot-name queues to ensure atomic operations and maintain chronological order. However, due to the USE system's microtask batching, rapid state changes to the same path create only one snapshot:
// ⚠️ Important: Rapid changes are batched - only creates ONE snapshot
state.document.content = 'Version 1';
state.document.content = 'Version 2';
state.document.content = 'Version 3';
// Result: Only one snapshot with 'Version 3' (final value)
// ✅ To create separate snapshots, wait for microtasks between changes:
state.document.content = 'Version 1';
await Doh.microtask(); // Let snapshot capture
state.document.content = 'Version 2';
await Doh.microtask(); // Let snapshot capture
state.document.content = 'Version 3';
await Doh.microtask(); // Let snapshot capture
// Result: Three separate snapshots
// ✅ Or change different paths (different triggers):
state.document.content = 'New content'; // Triggers content snapshot
state.document.title = 'New title'; // Triggers metadata snapshot
state.document.styles = { bold: true }; // Triggers formatting snapshot
// Result: Multiple snapshots if watching different paths
Browser History:
IndexedDB Timeline:
The save_as
feature allows multiple snapshot definitions to share the same storage containers, creating unified undo/redo streams while maintaining separate triggers and captured states.
// Main document content snapshots
Doh.use_snapshot('document_content', {
triggers: { 'document.content': '*' },
states: ['document.content', 'document.cursor'],
in_timeline: 100
});
// Document metadata changes - share the same timeline
Doh.use_snapshot('document_metadata', {
triggers: { 'document.title': '*' },
states: ['document.title', 'document.metadata'],
save_as: 'document_content', // Store in shared timeline
in_timeline: 100
});
// Document formatting - also shares the timeline
Doh.use_snapshot('document_formatting', {
triggers: { 'document.styles': '*' },
states: ['document.styles', 'document.layout'],
save_as: 'document_content', // Same shared timeline
in_timeline: 100
});
// Now all three snapshots contribute to one unified undo/redo stream
await Doh.use_timeline_undo('document_content'); // Undoes latest from any of the three
// Example: Creating multiple entries in shared timeline
state.document.content = 'New content'; // Triggers document_content snapshot
await Doh.microtask(); // Let it process
state.document.title = 'New title'; // Triggers document_metadata snapshot
await Doh.microtask(); // Let it process
state.document.styles = { bold: true }; // Triggers document_formatting snapshot
// All stored in 'document_content' timeline in chronological order
Multiple Shared Timelines:
// Save to multiple shared containers
Doh.use_snapshot('user_action', {
triggers: { 'user.action': '*' },
states: ['user.profile', 'user.activity'],
save_as: ['document_content', 'user_activity'], // Store in both timelines
in_timeline: 50
});
Key Benefits:
Complete implementation of a document editor with timeline-based undo/redo:
// Document state setup
const state = Doh.use_state({
document: {
content: '',
title: 'Untitled Document',
cursor: { line: 0, column: 0 },
metadata: { wordCount: 0, lastModified: Date.now() }
}
});
// Document history with timeline
Doh.use_snapshot('document_history', {
triggers: { 'document.content': '*' }, // Any content change
states: [
'document.content',
'document.cursor',
'document.metadata'
],
in_timeline: 100, // Keep last 100 changes
callback: function(snapshotData) {
const content = snapshotData.states['document.content'];
console.log(`Auto-saved: "${content.substring(0, 30)}..."`);
}
});
// Editor interface functions
const editor = {
// Check if undo is available
async canUndo() {
const info = await Doh.use_timeline_info('document_history');
return info.length > 1;
},
// Check if redo is available
async canRedo() {
const info = await Doh.use_timeline_info('document_history');
return info.currentIndex < info.length - 1;
},
// Undo last change
async undo() {
const success = await Doh.use_timeline_undo('document_history');
if (success) {
this.updateUI();
return true;
}
return false;
},
// Redo last undone change
async redo() {
const success = await Doh.use_timeline_redo('document_history');
if (success) {
this.updateUI();
return true;
}
return false;
},
// Update UI to reflect current state
updateUI() {
document.getElementById('content').value = state.document.content;
document.getElementById('title').value = state.document.title;
this.updateToolbar();
},
// Update toolbar button states
async updateToolbar() {
document.getElementById('undo-btn').disabled = !(await this.canUndo());
document.getElementById('redo-btn').disabled = !(await this.canRedo());
},
// Insert text at cursor
insertText(text) {
const cursor = state.document.cursor;
const content = state.document.content;
const before = content.substring(0, cursor.column);
const after = content.substring(cursor.column);
state.document.content = before + text + after;
state.document.cursor.column += text.length;
state.document.metadata.lastModified = Date.now();
}
};
// Keyboard shortcuts
document.addEventListener('keydown', async function(e) {
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case 'z':
if (e.shiftKey) {
e.preventDefault();
await editor.redo();
} else {
e.preventDefault();
await editor.undo();
}
break;
case 'y':
e.preventDefault();
await editor.redo();
break;
}
}
});
Comprehensive session backup and recovery system:
// Session state
const state = Doh.use_state({
session: {
userId: null,
startTime: null,
lastActivity: null,
preferences: {},
openTabs: [],
workspaceLayout: 'default'
},
user: {
profile: null,
isLoggedIn: false
}
});
// Session backup with multiple trigger conditions
Doh.use_snapshot('session_backup', {
triggers: {
'user.isLoggedIn': true, // When user logs in
'session.preferences': '*', // Any preference change
'session.workspaceLayout': '*', // Layout changes
'session.lastActivity': '*' // Activity updates
},
trigger_mode_or: true, // Any of these triggers a backup
states: {
'user.profile': null,
'session.preferences': {},
'session.startTime': Date.now,
'session.workspaceLayout': 'default',
'session.openTabs': [],
'app.version': '1.0.0' // Include app version for migration
},
in_timeline: 20, // Keep last 20 session states
callback: function(snapshot) {
console.log('Session backed up:', new Date(snapshot.timestamp));
}
});
// Application startup - restore last session
async function restoreSession() {
try {
const sessions = await Doh.use_get_timeline('session_backup');
if (sessions.length > 0) {
const lastSession = sessions[sessions.length - 1];
// Check version compatibility
const savedVersion = lastSession.states['app.version'];
const currentVersion = '1.0.0';
if (savedVersion === currentVersion) {
// Restore the last session
await Doh.use_replay_snapshot(lastSession);
console.log('Session restored from:', new Date(lastSession.timestamp));
return true;
} else {
console.log('Session version mismatch, starting fresh');
return false;
}
}
} catch (error) {
console.warn('Failed to restore session:', error);
}
return false;
}
// Activity tracking
function trackActivity(activityType) {
state.session.lastActivity = Date.now();
// This will trigger a session backup due to OR logic
}
// Graceful shutdown - manual session save
window.addEventListener('beforeunload', function() {
// Force a final session backup
Doh.use_trigger_snapshot('session_backup');
});
// Initialize on app start
window.addEventListener('load', async function() {
const restored = await restoreSession();
if (!restored) {
// Initialize fresh session
state.session.startTime = Date.now();
state.session.preferences = getDefaultPreferences();
}
});
Using browser history for application navigation state:
// Navigation state
const state = Doh.use_state({
router: {
currentRoute: '/',
params: {},
query: {},
history: []
}
});
// Navigation snapshot with browser history
Doh.use_snapshot('navigation_history', {
triggers: {
'router.currentRoute': '*', // Any route change
'router.query': '*' // Any query parameter change
},
states: [
'router.currentRoute',
'router.params',
'router.query',
'user.context' // Include user context for route-specific state
],
as_history: true, // Store in browser history
callback: function(snapshot) {
console.log('Navigation saved:', snapshot.states['router.currentRoute']);
}
});
// Navigation functions
const router = {
// Navigate to new route
navigate(route, params = {}, query = {}) {
state.router.currentRoute = route;
state.router.params = params;
state.router.query = query;
// This triggers the snapshot which adds to browser history
},
// Handle browser back/forward
async handlePopState(event) {
if (event.state?.doh_snapshot) {
// Restore state from browser history
await Doh.use_replay_snapshot(event.state.doh_snapshot);
console.log('Restored from browser history');
}
}
};
// Listen for browser navigation
window.addEventListener('popstate', router.handlePopState);
// Usage examples
router.navigate('/users', {}, { page: 1, filter: 'active' });
router.navigate('/users/123', { id: 123 });
router.navigate('/settings');
// Browser back/forward will now restore the exact route state
Create snapshots on demand without automatic triggers:
// Define snapshot without automatic triggers
Doh.use_snapshot('manual_checkpoint', {
states: ['player.position', 'game.inventory', 'world.state']
// No triggers - manual only
});
// Trigger manually when needed
function createCheckpoint() {
const snapshot = Doh.use_trigger_snapshot('manual_checkpoint');
console.log('Checkpoint created:', snapshot.timestamp);
return snapshot;
}
// Manual triggering with dynamic state selection
function createCustomSnapshot(statePaths) {
const snapshotName = `custom_${Date.now()}`;
Doh.use_snapshot(snapshotName, {
states: statePaths,
in_timeline: 1 // Just store this one snapshot
});
return Doh.use_trigger_snapshot(snapshotName);
}
// Get and replay specific snapshots
async function demonstrateReplay() {
// Get all snapshots for inspection
const snapshots = await Doh.use_get_timeline('document_history');
console.log(`Found ${snapshots.length} snapshots`);
// Show snapshot info
snapshots.forEach((snapshot, index) => {
console.log(`${index}: ${new Date(snapshot.timestamp)} - Trigger #${snapshot.triggerCount}`);
});
// Replay specific snapshot
if (snapshots.length > 0) {
const targetSnapshot = snapshots[2]; // Third snapshot
await Doh.use_replay_snapshot(targetSnapshot);
console.log('State restored to snapshot #2');
}
// Replay with URL query clearing (useful for clean state restoration)
if (snapshots.length > 0) {
const cleanSnapshot = snapshots[0]; // First snapshot
await Doh.use_replay_snapshot(cleanSnapshot, { clear_url_query: true });
console.log('State restored with clean URL (all query parameters cleared)');
}
// If snapshots have saved URL query, they restore automatically
if (snapshots.length > 0 && snapshots[0].url_query) {
await Doh.use_replay_snapshot(snapshots[0]); // Restores saved URL query
console.log('State and URL restored from saved query parameters:', snapshots[0].url_query);
}
// Navigate through timeline
await Doh.use_timeline_goto('document_history', 0); // Go to first
await Doh.use_timeline_goto('document_history', -1); // Go to last
// Navigate with URL clearing - useful for clean state restoration
await Doh.use_timeline_goto('document_history', 0, { clear_url_query: true }); // Go to first with clean URL
await Doh.use_timeline_undo('document_history', { clear_url_query: true }); // Undo with clean URL
await Doh.use_timeline_redo('document_history', { clear_url_query: true }); // Redo with clean URL
// Step through manually
let position = 0;
while (await Doh.use_timeline_goto('document_history', position)) {
console.log(`Viewing snapshot at position ${position}`);
position++;
}
}
The snapshot system provides completion callbacks that enable reliable async coordination without arbitrary timeouts. Understanding when each callback fires is essential for proper usage.
Callback Execution Order:
1. State change triggers observer
↓
2. Snapshot queued for processing
↓
3. Snapshot captured (states extracted)
↓
4. `callback()` fires ← During capture phase
↓
5. IndexedDB storage operations (if in_timeline: true)
↓
6. All storage operations complete
↓
7. `onSnapshot()` fires ← After ALL operations complete
Detailed Examples:
// Track completion across the entire async pipeline
let captureStarted = false;
let storageComplete = false;
await Doh.use_snapshot('async_tracking', {
triggers: { 'document.content': '*' },
states: ['document.content', 'document.metadata'],
in_timeline: true,
// Fires DURING capture (before storage)
callback: function(snapshotData) {
captureStarted = true;
console.log('Captured:', snapshotData.states['document.content']);
// At this point: snapshot data extracted, storage NOT complete
},
// Fires AFTER all async operations complete
onSnapshot: function(snapshotData) {
storageComplete = true;
console.log('Fully stored in IndexedDB');
// At this point: timeline updated, safe to query use_timeline_info()
}
});
// Trigger a change
state.document.content = 'Hello World';
await Doh.microtask();
// Wait for full completion
while (!storageComplete) await Doh.microtask();
// Now timeline queries are guaranteed accurate
const info = await Doh.use_timeline_info('async_tracking');
console.log(`Timeline length: ${info.length}`); // Accurate!
Replay Completion Tracking:
let replayComplete = false;
await Doh.use_snapshot('replay_tracking', {
triggers: { 'state.value': '*' },
states: ['state.value', 'state.metadata'],
in_timeline: true,
onSnapshot: function(data) {
console.log('Snapshot stored');
},
onReplay: function(data) {
replayComplete = true;
console.log('Replay complete, state restored');
// Safe to update UI, run assertions, etc.
}
});
// Create some history
state.state = { value: 'Version 1', metadata: {} };
await Doh.microtask();
state.state.value = 'Version 2';
await Doh.microtask();
// Undo and wait for completion
replayComplete = false;
await Doh.use_timeline_undo('replay_tracking');
while (!replayComplete) await Doh.microtask();
// State is now guaranteed restored to Version 1
Test Pattern: Robust Waiting
// Helper function for waiting on snapshot completion
async function waitForSnapshotComplete(flagRef, timeout = 2000) {
const start = Date.now();
while (!flagRef.value && (Date.now() - start) < timeout) {
await Doh.microtask();
}
if (!flagRef.value) {
throw new Error('Snapshot did not complete within timeout');
}
flagRef.value = false; // Reset for next use
}
// Usage in tests
const snapshotFlag = { value: false };
await Doh.use_snapshot('test_snapshot', {
triggers: { 'document.content': '*' },
states: ['document.content'],
in_timeline: true,
onSnapshot: () => { snapshotFlag.value = true; }
});
// Make changes and wait for each
state.document.content = 'First';
await waitForSnapshotComplete(snapshotFlag);
let info = await Doh.use_timeline_info('test_snapshot');
console.assert(info.length === 2, 'Should have 2 entries (initial + first)');
state.document.content = 'Second';
await waitForSnapshotComplete(snapshotFlag);
info = await Doh.use_timeline_info('test_snapshot');
console.assert(info.length === 3, 'Should have 3 entries');
Async Callbacks Support:
All callbacks can be async functions:
await Doh.use_snapshot('async_callbacks', {
triggers: { 'data': '*' },
states: ['data'],
in_timeline: true,
// Async callback for processing during capture
callback: async function(snapshotData) {
await performValidation(snapshotData.states.data);
console.log('Validation complete');
},
// Async completion notification
onSnapshot: async function(snapshotData) {
await updateRemoteBackup(snapshotData);
await notifyObservers('snapshot_saved');
console.log('All post-storage tasks complete');
},
// Async replay notification
onReplay: async function(snapshotData) {
await refreshUI(snapshotData.states);
await recalculateDependencies();
console.log('Replay and UI refresh complete');
}
});
Key Differences:
Callback | Timing | Purpose | Storage Complete? |
---|---|---|---|
callback |
During capture | Process snapshot data, logging | ❌ No |
onSnapshot |
After all storage | Signal completion, tests, coordination | ✅ Yes |
onReplay |
After replay complete | Update UI, signal test completion | ✅ Yes |
Common Patterns:
// Pattern 1: Test synchronization
let done = false;
await Doh.use_snapshot('test', {
triggers: { 'x': '*' },
states: ['x'],
onSnapshot: () => { done = true; }
});
state.x = 'value';
while (!done) await Doh.microtask();
// Test assertions here
// Pattern 2: UI updates after replay
await Doh.use_snapshot('ui_state', {
triggers: { 'ui': '*' },
states: ['ui'],
onReplay: async function(data) {
await updateDOMFromState(data.states.ui);
showNotification('State restored');
}
});
// Pattern 3: Progress tracking
let saveCount = 0;
await Doh.use_snapshot('autosave', {
triggers: { 'document.content': '*' },
states: ['document.content'],
in_timeline: true,
callback: () => {
showStatus('Saving...');
},
onSnapshot: () => {
saveCount++;
showStatus(`Saved (${saveCount} times)`);
}
});
// Comprehensive cleanup functions
const snapshotManager = {
// Remove snapshot definition (stops watching, keeps data)
removeDefinition(name) {
Doh.use_remove_snapshot(name);
console.log(`Stopped watching ${name}, data preserved`);
},
// Remove definition and clear all stored data
async purgeSnapshot(name, clearStorage = true) {
Doh.use_remove_snapshot(name, clearStorage);
if (clearStorage) {
// Also clear timeline if it exists
try {
await Doh.use_clear_timeline(name);
console.log(`Purged ${name} completely`);
} catch (error) {
console.log(`Purged ${name} (no timeline data)`);
}
}
},
// Get snapshot statistics
async getSnapshotStats(name) {
try {
const info = await Doh.use_timeline_info(name);
const snapshots = await Doh.use_get_timeline(name, 5); // Last 5 for analysis
let totalSize = 0;
let avgTriggerCount = 0;
snapshots.forEach(snapshot => {
totalSize += JSON.stringify(snapshot).length;
avgTriggerCount += snapshot.triggerCount;
});
return {
name: name,
totalEntries: info.length,
latestTimestamp: info.latest?.timestamp,
avgSize: snapshots.length > 0 ? Math.round(totalSize / snapshots.length) : 0,
avgTriggerCount: snapshots.length > 0 ? Math.round(avgTriggerCount / snapshots.length) : 0,
estimatedTotalSize: Math.round((totalSize / snapshots.length) * info.length) || 0
};
} catch (error) {
return { name: name, error: error.message };
}
},
// Analyze all active snapshots
async analyzeAllSnapshots() {
const activeSnapshots = Array.from(Doh._snapshots.keys());
const stats = [];
for (const name of activeSnapshots) {
const stat = await this.getSnapshotStats(name);
stats.push(stat);
}
console.table(stats);
return stats;
}
};
// Usage examples
await snapshotManager.analyzeAllSnapshots();
const stats = await snapshotManager.getSnapshotStats('document_history');
console.log('Document history stats:', stats);
The USE snapshot system uses per-snapshot-name operation queues that provide several performance advantages:
// Multiple rapid changes are handled gracefully
state.document.content = 'Version 1';
state.document.content = 'Version 2';
state.document.content = 'Version 3';
state.document.content = 'Version 4';
// All versions will be queued and stored in order
// No timestamp collisions or lost snapshots
// Each waits for the previous to complete
// Meanwhile, other snapshots can operate in parallel
state.user.preferences.theme = 'dark'; // Different queue
state.game.level = 5; // Different queue
// Best practices for memory efficiency
// 1. Use appropriate storage backends
Doh.use_snapshot('navigation_state', {
triggers: 'router.currentRoute',
states: ['router.currentRoute', 'router.params'],
as_history: true // Lightweight navigation, use browser history
});
Doh.use_snapshot('heavy_document', {
triggers: 'document.content',
states: ['document.content', 'document.metadata'],
in_timeline: 50 // Large data, use IndexedDB with rotation
});
// 2. Limit captured state to essentials
Doh.use_snapshot('efficient_user', {
triggers: 'user.profile.name',
states: ['user.profile.name', 'user.profile.email'], // Only what's needed
// Don't capture: user.cache, user.temporaryData, user.ui
});
// 3. Use rotation to prevent unbounded growth
Doh.use_snapshot('bounded_history', {
triggers: 'document.content',
states: 'document.content',
in_timeline: 100 // Automatically removes entries beyond 100
});
// 4. Clean up unused snapshots
function cleanupOldSnapshots() {
const unusedSnapshots = ['temp_debug', 'test_scenario', 'old_feature'];
unusedSnapshots.forEach(name => {
Doh.use_remove_snapshot(name, true); // Remove definition + clear storage
});
}
// Efficient trigger strategies
// 1. Use specific values instead of wildcards when possible
Doh.use_snapshot('optimized_saves', {
triggers: {
'game.status': 'completed', // Specific value - efficient
'user.authenticated': true // Specific value - efficient
},
// Instead of: 'game.status': '*' (triggers on any change)
});
// 2. Combine related triggers with AND logic
Doh.use_snapshot('compound_condition', {
triggers: {
'user.loggedIn': true, // Must be logged in
'document.hasChanges': true // AND must have changes
},
// More efficient than separate snapshots for each condition
});
// 3. Use custom functions for complex logic
Doh.use_snapshot('smart_saving', {
triggers: { 'document.modified': true, 'user.activity': '*' },
trigger_mode_or: function(triggers) {
// Only save if user is active AND document was modified recently
const modified = triggers['document.modified'];
const activity = triggers['user.activity'];
if (modified.met && activity.met) {
const timeSinceModification = Date.now() - modified.timestamp;
return timeSinceModification < 60000; // Within last minute
}
return false;
}
});
Snapshots integrate seamlessly with all USE state features:
// URL-synchronized state with snapshots
const state = Doh.use_state();
// URL sync
Doh.use_url_query(['page', 'query', 'filters']);
// Snapshot navigation state for browser history
Doh.use_snapshot('navigation_history', {
triggers: ['page', 'query', 'filters'],
states: ['page', 'query', 'filters', 'user.context'],
as_history: true // Browser back/forward integration
});
// Changes to URL parameters automatically trigger snapshots
state.page = 2; // Updates URL AND creates snapshot
state.query = 'search term'; // Updates URL AND creates snapshot
// Persistent state with snapshots
Doh.use_local_storage(['userPrefs', 'workspaceLayout']);
// Snapshot preference changes
Doh.use_snapshot('preference_history', {
triggers: ['userPrefs', 'workspaceLayout'],
states: ['userPrefs', 'workspaceLayout', 'app.version'],
in_timeline: 20 // Keep preference history
});
// Changes automatically save to localStorage AND create snapshots
state.userPrefs.theme = 'dark'; // Persists in localStorage + snapshot
state.workspaceLayout = 'sidebar'; // Persists in localStorage + snapshot
// Pattern-driven UI with snapshots
Doh.use_state_pattern('todoItems');
// Snapshot UI state changes
Doh.use_snapshot('ui_history', {
triggers: 'todoItems',
states: ['todoItems', 'ui.selectedItem', 'ui.filter'],
in_timeline: 30
});
// Adding/removing todo items triggers both pattern updates AND snapshots
state.todoItems.push({ pattern: 'TodoItem', text: 'New task', id: Date.now() });
Critical: Timeline operations return Promises and must be awaited:
// ✅ Correct - using await
const info = await Doh.use_timeline_info('my_snapshot');
const success = await Doh.use_timeline_undo('my_snapshot');
const snapshots = await Doh.use_get_timeline('my_snapshot');
// ✅ Correct - using .then()
Doh.use_timeline_info('my_snapshot').then(info => {
console.log(`Timeline has ${info.length} entries`);
});
// ❌ Incorrect - missing await
const info = Doh.use_timeline_info('my_snapshot'); // Returns Promise, not info
console.log(info.length); // Error: Promise doesn't have length property
// ❌ Incorrect - treating as synchronous
const snapshots = Doh.use_get_timeline('my_snapshot'); // Returns Promise
snapshots.forEach(snap => console.log(snap)); // Error: Promise doesn't have forEach
// Snapshot triggers are async - use microtask to wait for completion
state.document.content = 'New content';
// Wait for snapshot to be captured
await Doh.microtask();
// Now the snapshot has been processed and stored
const snapshots = await Doh.use_get_timeline('document_history');
console.log('Latest snapshot captured'); // Includes the new content
async function safeTimelineOperation() {
try {
const success = await Doh.use_timeline_undo('document_history');
if (success) {
console.log('Undo successful');
} else {
console.log('Nothing to undo');
}
} catch (error) {
console.error('Timeline operation failed:', error);
// Handle error gracefully - maybe disable undo button
}
}
// Robust snapshot management
async function robustSnapshotManagement() {
try {
// Check if timeline exists and has content
const info = await Doh.use_timeline_info('document_history');
if (info.length === 0) {
console.log('No snapshots available');
return false;
}
// Perform operation
const result = await Doh.use_timeline_undo('document_history');
return result;
} catch (error) {
if (error.name === 'DatabaseError') {
console.log('Timeline database not available');
} else {
console.error('Unexpected error:', error);
}
return false;
}
}
The USE snapshot system provides a comprehensive solution for state capture and time travel with fully asynchronous top-to-bottom architecture:
onSnapshot
and onReplay
callbacks eliminate arbitrary timeoutsKey Async Principles:
onSnapshot
/onReplay
callbacks instead of arbitrary timeoutsThe system scales from simple navigation tracking to complex document editing workflows, providing reliable, non-blocking state management for modern web applications.