### How to run Source: https://github.com/mutativejs/travels/blob/main/examples/travels-vite/README.md Commands to install dependencies and start the development server using npm. ```bash npm install npm run dev ``` -------------------------------- ### Development Setup Commands Source: https://github.com/mutativejs/travels/blob/main/CONTRIBUTING.md Commands to install dependencies, build the project, and run tests. ```bash yarn install yarn build yarn test ``` -------------------------------- ### Persistence Migration Example Source: https://github.com/mutativejs/travels/blob/main/docs/migration-guide.md Example of migrating older hand-rolled persistence formats to the current Mutative Travels format using the `migrate` option during deserialization. ```typescript const history = Travels.deserialize(stored, { migrate(snapshot) { if (snapshot && typeof snapshot === 'object' && !('version' in snapshot)) { return { version: 1, state: snapshot.state, patches: snapshot.patches, position: snapshot.position, }; } return snapshot; }, }); ``` -------------------------------- ### Quick Start Source: https://github.com/mutativejs/travels/blob/main/README.md A basic example demonstrating how to create a Travels instance, subscribe to state changes, update state using mutation syntax, and manage history. ```typescript import { createTravels } from 'travels'; // Create a travels instance with initial state const travels = createTravels({ count: 0 }); // Subscribe to state changes const unsubscribe = travels.subscribe((state, patches, position) => { console.log('State:', state); console.log('Position:', position); }); // Update state using mutation syntax (preferred - more intuitive) travels.setState((draft) => { draft.count += 1; // Mutate the draft directly }); // Or set state directly by providing a new value travels.setState({ count: 2 }); // Undo the last change travels.back(); // Redo the undone change travels.forward(); // Get current state console.log(travels.getState()); // { count: 1 } // Cleanup when done unsubscribe(); ``` -------------------------------- ### Install idb Source: https://github.com/mutativejs/travels/blob/main/docs/persistence-integrations.md Command to install the idb library. ```bash npm install idb ``` -------------------------------- ### Install localForage Source: https://github.com/mutativejs/travels/blob/main/docs/persistence-integrations.md Command to install the localForage library. ```bash npm install localforage ``` -------------------------------- ### localspace Adapter Example Source: https://github.com/mutativejs/travels/blob/main/docs/persistence-integrations.md TypeScript example demonstrating how to create a localspace instance and use its methods for loading and saving snapshots. ```typescript import localspace from 'localspace'; import type { TravelsSerializedHistory } from 'travels'; const SNAPSHOT_KEY = 'document:main'; const store = localspace.createInstance({ name: 'travels', storeName: 'snapshots', driver: [localspace.INDEXEDDB, localspace.LOCALSTORAGE], }); async function loadSnapshotFromLocalspace() { await store.ready(); return store.getItem>(SNAPSHOT_KEY); } async function saveSnapshotToLocalspace( snapshot: TravelsSerializedHistory ) { await store.setItem(SNAPSHOT_KEY, snapshot); } async function initLocalspacePersistence() { const travels = restoreTravels(await loadSnapshotFromLocalspace()); const persistence = attachAutoSave(travels, saveSnapshotToLocalspace); return { travels, persistence }; } ``` -------------------------------- ### Dexie.js Installation Source: https://github.com/mutativejs/travels/blob/main/docs/persistence-integrations.md Installs the Dexie.js library using npm. ```bash npm install dexie ``` -------------------------------- ### Installation Source: https://github.com/mutativejs/travels/blob/main/README.md Install the Travels and Mutative packages using npm, yarn, or pnpm. ```bash npm install travels mutative # or yarn add travels mutative # or pnpm add travels mutative ``` -------------------------------- ### Install localspace Source: https://github.com/mutativejs/travels/blob/main/docs/persistence-integrations.md Command to install the localspace package using npm. ```bash npm install localspace ``` -------------------------------- ### Enabling Mutable Mode Source: https://github.com/mutativejs/travels/blob/main/docs/mutable-mode.md Example of how to enable mutable mode when creating a Travels instance and using setState to mutate the store in place. ```typescript import { createTravels } from 'travels'; const store = reactive({ count: 0 }); // Vue/Pinia example const travels = createTravels(store, { mutable: true }); travels.setState((draft) => { draft.count += 1; // Mutates `store` in place }); ``` -------------------------------- ### Migration Function Example Source: https://github.com/mutativejs/travels/blob/main/docs/persistence-integrations.md Example of a migrate function used with Travels.deserialize to handle legacy document snapshot schemas. ```typescript type LegacyDocumentSnapshot = { version: 0; state: DocumentState; history: TravelsSerializedHistory['patches']; cursor: number; }; const history = Travels.deserialize(stored, { migrate(snapshot) { if ( snapshot && typeof snapshot === 'object' && (snapshot as { version?: unknown }).version === 0 ) { const legacy = snapshot as LegacyDocumentSnapshot; return { version: TRAVELS_HISTORY_SCHEMA_VERSION, state: legacy.state, patches: legacy.history, position: legacy.cursor, }; } return snapshot; }, fallback: createEmptySnapshot, }); ``` -------------------------------- ### React Integration Example Source: https://github.com/mutativejs/travels/blob/main/README.md A React component demonstrating integration with Mutative Travels using the useSyncExternalStore hook. ```jsx import { useSyncExternalStore } from 'react'; import { createTravels } from 'travels'; const travels = createTravels({ count: 0 }); function useTravel() { const state = useSyncExternalStore( travels.subscribe.bind(travels), travels.getState.bind(travels) ); return [state, travels.setState.bind(travels), travels.getControls()] as const; } function Counter() { const [state, setState, controls] = useTravel(); return (
Count: {state.count}
); } ``` -------------------------------- ### MobX Integration Source: https://github.com/mutativejs/travels/blob/main/docs/mutable-mode.md Example of integrating TravelsJS with MobX using mutable mode for state management. ```typescript const mobxStore = makeAutoObservable({ todos: [] }); const travels = createTravels(mobxStore, { mutable: true }); autorun(() => { // mobxStore reference never changes console.log(mobxStore.todos.length); }); function addTodo(title: string) { travels.setState((draft) => { draft.todos.push({ id: nanoid(), title, done: false }); }); } ``` -------------------------------- ### Vue / Pinia Integration Source: https://github.com/mutativejs/travels/blob/main/docs/mutable-mode.md Example of integrating TravelsJS with Vue and Pinia using mutable mode. ```typescript export const useTodosStore = defineStore('todos', () => { const state = reactive({ items: [] }); const travels = createTravels(state, { mutable: true }); const controls = travels.getControls(); function addTodo(text: string) { travels.setState((draft) => { draft.items.push({ id: crypto.randomUUID(), text, done: false }); }); } return { state, addTodo, travels, controls }; }); The reactive `state` reference is the same object that Vue components bind to, so they instantly see mutations while retaining undo/redo controls. ``` -------------------------------- ### Using getControls() Source: https://github.com/mutativejs/travels/blob/main/README.md Demonstrates how to get and use the controls object returned by getControls() for navigation and state inspection. ```typescript const travels = createTravels({ count: 0 }); const controls = travels.getControls(); // Use controls controls.back(); controls.forward(); console.log(controls.position); console.log(controls.patches); ``` -------------------------------- ### Saving Multiple Snapshots Source: https://github.com/mutativejs/travels/blob/main/docs/persistence-integrations.md Example demonstrating how to save multiple localspace snapshots concurrently using the `setItems` method. ```typescript async function saveMultipleLocalspaceSnapshots( entries: Array <{ key: string; snapshot: TravelsSerializedHistory; }> ) { await store.setItems( entries.map(({ key, snapshot }) => ({ key, value: snapshot })) ); } ``` -------------------------------- ### Manual Archive + Mutable Mode Source: https://github.com/mutativejs/travels/blob/main/docs/mutable-mode.md Demonstrates using manual archiving with mutable mode in TravelsJS. ```typescript const travels = createTravels(store, { mutable: true, autoArchive: false }); function commitTransaction(cb: () => void) { cb(); // Run multiple travels.setState calls travels.archive(); // Save them as one undoable step } Because the state is mutated in place, your UI keeps updating during the transaction, but history only grows when you call `archive()`. ``` -------------------------------- ### maxHistory Option Example Source: https://github.com/mutativejs/travels/blob/main/README.md Illustrates the behavior of the maxHistory option, showing how history entries are trimmed and how the position is capped. ```typescript const travels = createTravels({ count: 0 }, { maxHistory: 3 }); const controls = travels.getControls(); const increment = () => travels.setState((draft) => { draft.count += 1; }); // Make 5 changes increment(); // 1 increment(); // 2 increment(); // 3 increment(); // 4 increment(); // 5 expect(travels.getState().count).toBe(5); // Position is capped at maxHistory (3), so we're at position 3 // The library keeps only the last 3 patches, representing states: [2, 3, 4, 5] // Why 4 states? Because patches represent *transitions*: // - patch 0: 2→3 // - patch 1: 3→4 // - patch 2: 4→5 // So you can access 4 states total: the window start (2) plus 3 transitions // Go back 1 step: from 5 to 4 controls.back(); expect(travels.getPosition()).toBe(2); expect(travels.getState().count).toBe(4); // Go back 1 step: from 4 to 3 controls.back(); expect(travels.getPosition()).toBe(1); expect(travels.getState().count).toBe(3); // Go back 1 step: from 3 to 2 (the window start) controls.back(); expect(travels.getPosition()).toBe(0); expect(travels.getState().count).toBe(2); // Can only go back to the window start expect(controls.canBack()).toBe(false); // Can't go further back // However, reset() can still return to the true initial state controls.reset(); expect(travels.getState().count).toBe(0); // Back to the original initial state ``` -------------------------------- ### Pinia/Vue Store Example Source: https://github.com/mutativejs/travels/blob/main/README.md Demonstrates how to integrate Mutative Travels with a Pinia/Vue store, allowing Vue components to use the original state reference while Travels handles history and controls. ```typescript import { defineStore } from 'pinia'; import { reactive } from 'vue'; import { createTravels } from 'travels'; export const useTodosStore = defineStore('todos', () => { const state = reactive({ items: [] }); const travels = createTravels(state, { mutable: true }); const controls = travels.getControls(); function addTodo(text: string) { travels.setState((draft) => { draft.items.push({ id: crypto.randomUUID(), text, done: false }); }); } return { state, addTodo, controls }; }); ``` -------------------------------- ### Customize Benchmark Parameters Source: https://github.com/mutativejs/travels/blob/main/benchmarks/README.md Examples of how to adjust parameters in the benchmark scripts to modify object size and the number of operations. ```javascript // Adjust object size const initialState = generateComplexObject(200); // change to 200KB // Adjust number of operations results.push(testTravels(500)); // change to 500 operations ``` -------------------------------- ### Migrating Older Snapshots Source: https://github.com/mutativejs/travels/blob/main/README.md Example of using the `migrate` option in `Travels.deserialize` to upgrade older snapshot versions before validation. ```typescript const history = Travels.deserialize(stored, { migrate(snapshot) { if (snapshot && typeof snapshot === 'object' && snapshot.version === 0) { return { version: 1, state: snapshot.state, patches: snapshot.history, position: snapshot.cursor, }; } return snapshot; }, }); ``` -------------------------------- ### Vue Integration Source: https://github.com/mutativejs/travels/blob/main/README.md Example of integrating Mutative Travels with Vue using Composition API. ```typescript import { ref, readonly } from 'vue'; import { createTravels } from 'travels'; export function useTravel(initialState, options) { const travels = createTravels(initialState, options); const state = ref(travels.getState()); travels.subscribe((newState) => { state.value = newState; }); const setState = (updater) => { travels.setState(updater); }; return { state: readonly(state), setState, controls: travels.getControls(), }; } ``` -------------------------------- ### Auto Archive Mode Example Source: https://github.com/mutativejs/travels/blob/main/README.md Illustrates the default auto archive mode where each `setState` call creates a new history entry. It also shows how no-op updates are optimized away. ```typescript const travels = createTravels({ count: 0 }); // or explicitly: createTravels({ count: 0 }, { autoArchive: true }) // Each setState creates a new history entry travels.setState({ count: 1 }); // History: [0, 1], position: 1 travels.setState({ count: 2 }); // History: [0, 1, 2], position: 2 travels.setState({ count: 3 }); // History: [0, 1, 2, 3], position: 3 // No-op update - position stays the same (optimization) travels.setState(state => state); // History: [0, 1, 2, 3], position: 3 // Conditional update that changes nothing travels.setState(draft => { if (draft.count > 10) { // false, so no changes draft.count = 0; } }); // History: [0, 1, 2, 3], position: 3 travels.back(); // Go back to count: 2 ``` -------------------------------- ### Common Patterns: Composing Wrappers Source: https://github.com/mutativejs/travels/blob/main/docs/advanced-patterns.md Example of composing multiple wrapper patterns (validation, logging, permissions) for a Travels instance. ```typescript // Compose all wrappers const travels = createTravels({ count: 0 }); withValidation( travels, (state) => state.count >= 0 || 'Count must be non-negative' ); withLogging(travels); withPermissions(travels, (action) => currentUser.role === 'admin'); ``` -------------------------------- ### Dexie.js Transaction Example Source: https://github.com/mutativejs/travels/blob/main/docs/persistence-integrations.md Saves a Dexie.js snapshot within a transaction, ensuring atomicity with related table updates. ```typescript async function saveDexieSnapshotWithRelatedRows( travels: ReturnType ) { const updatedAt = Date.now(); await db.transaction('rw', db.snapshots, db.snapshotAudit, async () => { await db.snapshots.put({ key: SNAPSHOT_KEY, value: travels.serialize(), updatedAt, }); await db.snapshotAudit.add({ key: SNAPSHOT_KEY, action: 'save', updatedAt, }); }); } ``` -------------------------------- ### setState with Draft Mutation and Metadata Source: https://github.com/mutativejs/travels/blob/main/README.md Example of updating state using draft mutation and providing metadata for history labeling. ```typescript travels.setState( (draft) => { draft.layer.name = 'Header'; }, { label: 'Rename Layer', source: 'layers-panel', timestamp: Date.now() } ); ``` -------------------------------- ### Saving Snapshot with Related Rows Source: https://github.com/mutativejs/travels/blob/main/docs/persistence-integrations.md Example of saving a localspace snapshot with related rows using `runTransaction` for transactional writes with the IndexedDB driver. ```typescript async function saveLocalspaceSnapshotWithRelatedRows( travels: ReturnType ) { await store.runTransaction('readwrite', async (tx) => { await tx.set(SNAPSHOT_KEY, travels.serialize()); await tx.set(`${SNAPSHOT_KEY}:updatedAt`, Date.now()); }); } ``` -------------------------------- ### Extending Travels with Custom Logic (setState Wrapper) Source: https://github.com/mutativejs/travels/blob/main/README.md A basic example of extending `Travels` by wrapping the `setState` method to add custom validation logic. ```typescript const travels = createTravels({ count: 0 }); const originalSetState = travels.setState.bind(travels); // Add validation travels.setState = function (updater: any) { if (typeof updater === 'object' && updater.count > 100) { console.error('Count cannot exceed 100'); return; // Block the operation } return originalSetState(updater); } as any; ``` -------------------------------- ### How to build Source: https://github.com/mutativejs/travels/blob/main/examples/travels-vite/README.md Command to build the project for production using npm. ```bash npm run build ``` -------------------------------- ### Run all tests Source: https://github.com/mutativejs/travels/blob/main/benchmarks/README.md Runs all benchmark tests in order: Simulated implementations, Real libraries, Scenario matrix. ```bash npm run test:all ``` -------------------------------- ### Local Verification Commands Source: https://github.com/mutativejs/travels/blob/main/docs/release-checklist.md Commands to run locally to verify the build before tagging a release. ```bash yarn install yarn build yarn test yarn test:types yarn test:browser yarn coverage yarn benchmark:ci ``` -------------------------------- ### Pushing Main and Creating Release Tag Source: https://github.com/mutativejs/travels/blob/main/docs/release-checklist.md Commands to push the main branch and create a new release tag. ```bash git push origin main git tag v1.3.0 git push origin v1.3.0 ``` -------------------------------- ### Zustand Integration Source: https://github.com/mutativejs/travels/blob/main/README.md Example of integrating Mutative Travels with Zustand for state management. ```typescript import { create } from 'zustand'; import { createTravels } from 'travels'; const travels = createTravels({ count: 0 }); const useStore = create((set) => ({ ...travels.getState(), setState: (updater) => { travels.setState(updater); set(travels.getState()); }, controls: travels.getControls(), })); // Subscribe to travels changes travels.subscribe((state) => { useStore.setState(state); }); ``` -------------------------------- ### Benchmarking Best Practices - Command Line Source: https://github.com/mutativejs/travels/blob/main/benchmarks/README.md A bash script demonstrating steps to ensure a consistent environment and run benchmarks multiple times for averaging results. ```bash # 1. Ensure consistent Node.js version node --version # 2. Clear npm cache npm cache clean --force # 3. Reinstall dependencies rm -rf node_modules package-lock.json npm install # 4. Restart terminal before running # 5. Close other apps # 6. Run multiple times and average for i in {1..3}; do echo "=== Run $i ===" npm run test:real sleep 5 done ``` -------------------------------- ### Smoke Testing Published Package Source: https://github.com/mutativejs/travels/blob/main/docs/release-checklist.md Commands to smoke test the published package. ```bash npm view travels version npm pack travels --dry-run ``` -------------------------------- ### TypeScript Helpers for Durable State Source: https://github.com/mutativejs/travels/blob/main/README.md Example of using TypeScript helpers to enforce the durable subset of state within an application. ```typescript import { createTravels, type JsonValue, type PatchableState } from 'travels'; const initialDocumentState = { title: 'Draft', blocks: [] as Array<{ id: string; text: string }> } satisfies JsonValue; const travels = createTravels(initialDocumentState); function createHistoryFor(state: S) { return createTravels(state); } ``` -------------------------------- ### memory-performance-test.js - Simulated implementations Source: https://github.com/mutativejs/travels/blob/main/benchmarks/README.md Uses simplified simulated implementations to quickly compare core differences. Pros: No dependencies, fast to run, clearly shows core algorithm differences. ```bash node --expose-gc memory-performance-test.js ``` -------------------------------- ### Transaction for Batching State Updates Source: https://github.com/mutativejs/travels/blob/main/README.md Example of using the transaction function to group multiple setState calls into a single undo step. ```typescript travels.transaction({ label: 'Move Selection' }, () => { travels.setState((draft) => { draft.selection.x += 10; }); travels.setState((draft) => { draft.selection.y += 20; }); }); ``` -------------------------------- ### Type-Safe State Updates with TypeScript Source: https://github.com/mutativejs/travels/blob/main/README.md Illustrates how to use TypeScript generics with `createTravels` for type-safe state management and updates. ```typescript import { createTravels, type TravelsOptions, type TravelPatches, } from 'travels'; interface State { count: number; todos: Array<{ id: number; text: string }>; } const travels = createTravels({ count: 0, todos: [] }); // Type-safe state updates travels.setState((draft) => { draft.count += 1; draft.todos.push({ id: 1, text: 'Buy milk' }); }); ``` -------------------------------- ### Saving and Loading State from Local Storage Source: https://github.com/mutativejs/travels/blob/main/README.md Demonstrates how to serialize the current state and history to localStorage and deserialize it back, including error handling and fallback mechanisms. ```typescript import { createTravels, Travels, TravelsPersistenceError, } from 'travels'; function saveToStorage(travels) { localStorage.setItem('travels:document', JSON.stringify(travels.serialize())); } function loadFromStorage() { const stored = localStorage.getItem('travels:document'); if (!stored) return createTravels(defaultState); const history = Travels.deserialize(stored, { fallback: { version: 1, state: defaultState, patches: { patches: [], inversePatches: [] }, position: 0, }, onError(error) { if (error instanceof TravelsPersistenceError) { console.warn('Ignoring invalid persisted history:', error.code); } }, }); return createTravels(history.state, { history, maxHistory: 50, strictInitialPatches: true, }); } ``` -------------------------------- ### External Form Manager Integration Source: https://github.com/mutativejs/travels/blob/main/README.md Example of integrating Mutative Travels with an external form manager to keep the form manager as the single source of truth. ```tsx import { createTravels } from 'travels'; type FormValues = { title: string; description: string; }; type FormApi = { getValues: () => S; setValues: (values: S) => void; }; const travels = createTravels( { title: '', description: '', }, { autoArchive: false } ); function bindHistoryToForm(form: FormApi) { const syncToHistory = () => { travels.setState(structuredClone(form.getValues())); }; const commitHistoryStep = () => { if (travels.canArchive()) { travels.archive(); } }; const handleKeyDown = (event: KeyboardEvent) => { const modifier = event.metaKey || event.ctrlKey; if (modifier && event.key === 'z' && !event.shiftKey) { event.preventDefault(); if (!travels.canBack()) return; travels.back(); form.setValues(travels.getState()); return; } if ( (modifier && event.key === 'z' && event.shiftKey) || (modifier && event.key === 'y') ) { event.preventDefault(); if (!travels.canForward()) return; travels.forward(); form.setValues(travels.getState()); } }; return { syncToHistory, commitHistoryStep, handleKeyDown, }; } ``` -------------------------------- ### Useful Checks Source: https://github.com/mutativejs/travels/blob/main/CONTRIBUTING.md Commands for running specific checks like type checking, browser tests, coverage, and benchmarks. ```bash yarn test:types yarn test:browser yarn coverage yarn benchmark:ci ``` -------------------------------- ### Implementing Rate Limiting and Throttling Source: https://github.com/mutativejs/travels/blob/main/docs/advanced-patterns.md Wrap methods to control how frequently they can be called. ```typescript let lastCallTime = 0; const throttleInterval = 100; // ms const originalSetState = travels.setState.bind(travels); travels.setState = function (updater: any) { const now = Date.now(); if (now - lastCallTime < throttleInterval) { console.warn('Too many updates, throttled'); return; } lastCallTime = now; return originalSetState(updater); } as any; ``` -------------------------------- ### Snapshot Contract and Restoration Source: https://github.com/mutativejs/travels/blob/main/docs/persistence-integrations.md Demonstrates the structure of a serialized Travels snapshot and how to restore it using the `Travels.deserialize` and `createTravels` functions. Includes definitions for document state, empty snapshot, and a restoration function with error handling. ```typescript import { createTravels, Travels, TravelsPersistenceError, TRAVELS_HISTORY_SCHEMA_VERSION, type TravelsSerializedHistory, } from 'travels'; type DocumentState = { title: string; blocks: Array<{ id: string; text: string }>; }; const createDefaultDocument = (): DocumentState => ({ title: 'Untitled', blocks: [], }); const createEmptySnapshot = (): TravelsSerializedHistory => ({ version: TRAVELS_HISTORY_SCHEMA_VERSION, state: createDefaultDocument(), patches: { patches: [], inversePatches: [] }, position: 0, }); function restoreTravels(raw: unknown) { const history = Travels.deserialize( raw ?? createEmptySnapshot(), { fallback: createEmptySnapshot, onError(error) { if (error instanceof TravelsPersistenceError) { console.warn('Ignoring invalid persisted history:', error.code); } }, } ); return createTravels(history.state, { history, maxHistory: 100, strictInitialPatches: true, }); } ``` -------------------------------- ### Adding validation Source: https://github.com/mutativejs/travels/blob/main/docs/advanced-patterns.md Wrap the `setState` method to add validation logic before or after state updates. This example shows how to prevent the `count` from exceeding 10 and how to add a `timestamp` to state changes. ```typescript const travels = createTravels({ count: 0 }); // Save the original method const originalSetState = travels.setState.bind(travels); // Wrap setState with validation travels.setState = function (updater: any) { // Only validate direct values (not functions) if (typeof updater === 'object' && updater !== null) { // Validate if (updater.count > 10) { console.error('Count cannot exceed 10!'); return; // Prevent execution } // Modify input - add metadata updater = { ...updater, count: Math.min(updater.count, 10), timestamp: Date.now(), }; } // For mutation functions, wrap to validate after execution if (typeof updater === 'function') { const wrappedUpdater = (draft: any) => { // Execute the original mutation updater(draft); // Validate after mutation if (draft.count > 10) { draft.count = 10; // Fix invalid state console.warn('Count was capped at 10'); } // Add metadata draft.timestamp = Date.now(); }; originalSetState(wrappedUpdater); return; } // Execute for direct values originalSetState(updater); } as any; travels.setState({ count: 5 }); // ✅ Works travels.setState({ count: 100 }); // ❌ Blocked, capped at 10 // Also works with mutation functions travels.setState((draft) => { draft.count = 100; // Will be capped at 10 }); ``` -------------------------------- ### real-library-benchmark.js - Real libraries Source: https://github.com/mutativejs/travels/blob/main/benchmarks/README.md Uses real npm packages to reflect real-world scenarios. Pros: Real environment behavior, accurate performance numbers, includes full library overhead. ```bash # Install dependencies npm install # Run benchmark npm run test:real # Or run manually node --expose-gc real-library-benchmark.js ``` -------------------------------- ### Implementing Operation Logging and Auditing Source: https://github.com/mutativejs/travels/blob/main/docs/advanced-patterns.md Wrap methods to record all operations before and after execution. ```typescript const auditLog: any[] = []; const originalSetState = travels.setState.bind(travels); travels.setState = function (updater: any) { // Log before auditLog.push({ type: 'setState', timestamp: Date.now(), user: currentUser.id, before: travels.getState(), }); // Execute const result = originalSetState(updater); // Log after auditLog.push({ type: 'setState', timestamp: Date.now(), user: currentUser.id, after: travels.getState(), }); return result; } as any; ``` -------------------------------- ### Manual Archive Mode - Batch multiple changes Source: https://github.com/mutativejs/travels/blob/main/README.md Demonstrates how to group multiple setState calls into a single undo/redo step using travels.archive() when autoArchive is false. ```typescript const travels = createTravels({ count: 0 }, { autoArchive: false }); // Multiple setState calls travels.setState({ count: 1 }); // Temporary change (not in history yet) travels.setState({ count: 2 }); // Temporary change (not in history yet) travels.setState({ count: 3 }); // Temporary change (not in history yet) // Commit all changes as a single history entry travels.archive(); // History: [0, 3] // Now undo will go back to 0, not 2 or 1 travels.back(); // Back to 0 ``` -------------------------------- ### idb Adapter Source: https://github.com/mutativejs/travels/blob/main/docs/persistence-integrations.md TypeScript code for integrating idb for persistence. ```typescript import { openDB, type DBSchema } from 'idb'; import type { TravelsSerializedHistory } from 'travels'; interface TravelsPersistenceDB extends DBSchema { snapshots: { key: string; value: { key: string; value: TravelsSerializedHistory; updatedAt: number; }; indexes: { 'by-updatedAt': number; }; }; } const SNAPSHOT_KEY = 'document:main'; const dbPromise = openDB('travels', 1, { upgrade(db) { const store = db.createObjectStore('snapshots', { keyPath: 'key', }); store.createIndex('by-updatedAt', 'updatedAt'); }, }); async function loadSnapshotFromIdb() { const db = await dbPromise; const row = await db.get('snapshots', SNAPSHOT_KEY); return row?.value ?? null; } async function saveSnapshotToIdb( snapshot: TravelsSerializedHistory ) { const db = await dbPromise; const tx = db.transaction('snapshots', 'readwrite'); await tx.store.put({ key: SNAPSHOT_KEY, value: snapshot, updatedAt: Date.now(), }); await tx.done; } async function deleteIdbSnapshotsOlderThan(cutoff: number) { const db = await dbPromise; const tx = db.transaction('snapshots', 'readwrite'); const index = tx.store.index('by-updatedAt'); let cursor = await index.openCursor(IDBKeyRange.upperBound(cutoff, true)); while (cursor) { await cursor.delete(); cursor = await cursor.continue(); } await tx.done; } async function initIdbPersistence() { const travels = restoreTravels(await loadSnapshotFromIdb()); const persistence = attachAutoSave(travels, saveSnapshotToIdb); return { travels, persistence }; } ``` -------------------------------- ### Common Patterns: Permissions Wrapper Source: https://github.com/mutativejs/travels/blob/main/docs/advanced-patterns.md A reusable wrapper pattern to enforce permissions for specific actions on a Travels instance. ```typescript // Pattern 3: Permissions wrapper function withPermissions( travels: Travels, checkPermission: (action: string) => boolean ) { const methods = ['setState', 'back', 'forward', 'reset', 'archive', 'rebase']; methods.forEach((method) => { const original = (travels as any)[method]?.bind(travels); if (original) { (travels as any)[method] = function (...args: any[]) { if (!checkPermission(method)) { throw new Error(`Permission denied: ${method}`); } return original(...args); }; } }); return travels; } ``` -------------------------------- ### Adding Permission Checks Source: https://github.com/mutativejs/travels/blob/main/docs/advanced-patterns.md Wrap methods like `back` and `forward` to enforce user permissions, preventing unauthorized actions. ```typescript const currentUser = { role: 'viewer' }; // Read-only user // Prevent undo/redo for viewers const originalBack = travels.back.bind(travels); travels.back = function (amount?: number) { if (currentUser.role === 'viewer') { throw new Error('Permission denied: viewers cannot undo'); } return originalBack(amount); } as any; // Same for other methods const originalForward = travels.forward.bind(travels); travels.forward = function (amount?: number) { if (currentUser.role === 'viewer') { throw new Error('Permission denied: viewers cannot redo'); } return originalForward(amount); } as any; ``` -------------------------------- ### Composing Multiple Wrappers Source: https://github.com/mutativejs/travels/blob/main/docs/advanced-patterns.md Create a reusable function that applies multiple enhancements. ```typescript const currentUser = { id: 'user123', role: 'admin' }; // Helper function to wrap travels with multiple enhancers function enhanceTravels( travels: Travels, config: { validation?: (state: any, draft?: any) => boolean | string; permissions?: (action: string) => boolean; logging?: boolean; metadata?: boolean; } ) { // Wrap setState if (config.validation || config.metadata || config.logging) { const original = travels.setState.bind(travels); travels.setState = function (updater: any) { // Logging - before if (config.logging) { console.log('[setState] before:', travels.getState()); } // Handle direct value if (typeof updater === 'object' && updater !== null) { // Validation for direct values if (config.validation) { const result = config.validation(updater); if (result !== true) { throw new Error( typeof result === 'string' ? result : 'Validation failed' ); } } // Add metadata for direct values if (config.metadata) { updater = { ...updater, _meta: { timestamp: Date.now(), user: currentUser.id }, }; } const res = original(updater); // Logging - after if (config.logging) { console.log('[setState] after:', travels.getState()); } return res; } // Handle mutation function if (typeof updater === 'function') { const wrappedUpdater = (draft: any) => { updater(draft); // Validation for mutations if (config.validation) { const result = config.validation(travels.getState(), draft); if (result !== true) { throw new Error( typeof result === 'string' ? result : 'Validation failed' ); } } // Add metadata for mutations if (config.metadata) { draft._meta = { timestamp: Date.now(), user: currentUser.id }; } }; const res = original(wrappedUpdater); // Logging - after if (config.logging) { console.log('[setState] after:', travels.getState()); } return res; } return original(updater); } as any; } // Wrap navigation methods with permissions if (config.permissions) { ['back', 'forward', 'reset', 'archive', 'rebase'].forEach((method) => { const original = (travels as any)[method]?.bind(travels); if (original) { (travels as any)[method] = function (...args: any[]) { if (!config.permissions!(method)) { throw new Error(`Permission denied: ${method}`); } return original(...args); }; } }); } return travels; } // Usage const travels = createTravels({ count: 0 }); const enhanced = enhanceTravels(travels, { validation: (state, draft) => { const target = draft || state; if (target.count < 0) return 'Count cannot be negative'; if (target.count > 100) return 'Count cannot exceed 100'; return true; }, permissions: (action) => { return currentUser.role !== 'viewer' || action === 'setState'; }, logging: true, metadata: true, }); // Now works with both styles enhanced.setState({ count: 50 }); // ✅ Direct value enhanced.setState((draft) => { draft.count = 75; }); // ✅ Mutation ```