# Advanced Patterns: Extending Travels with Custom Logic You can enhance Travels by wrapping its methods to add validation, permissions, logging, or other custom behavior. ## Table of Contents - [Intercepting and Modifying Operations](#intercepting-and-modifying-operations) - [Adding Permission Checks](#adding-permission-checks) - [Automatically Adding Metadata](#automatically-adding-metadata-to-state-changes) - [Implementing Operation Logging and Auditing](#implementing-operation-logging-and-auditing) - [Implementing Rate Limiting and Throttling](#implementing-rate-limiting-and-throttling) - [Composing Multiple Wrappers](#composing-multiple-wrappers) - [Detecting History Overflow](#detecting-history-overflow) - [Common Patterns](#common-patterns) ## Intercepting and Modifying Operations While `subscribe()` lets you observe state changes, it cannot prevent or modify operations. To add validation, permissions, or transform data before execution, wrap the Travels methods: ### Adding validation: ```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 }); ``` ## Adding Permission Checks Wrap methods to verify permissions before allowing execution: ```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; ``` ## Automatically Adding Metadata to State Changes Wrap `setState` to inject metadata like timestamps or user IDs: ```typescript const travels = createTravels({ items: [] }); const currentUser = { id: 'user123' }; const originalSetState = travels.setState.bind(travels); travels.setState = function (updater: any) { // Handle direct value if (typeof updater === 'object' && updater !== null) { if (updater.items) { updater = { ...updater, items: updater.items.map((item: any) => ({ ...item, timestamp: Date.now(), userId: currentUser.id, version: (item.version || 0) + 1, })), }; } return originalSetState(updater); } // Handle mutation function if (typeof updater === 'function') { const wrappedUpdater = (draft: any) => { updater(draft); // Execute original mutation // Add metadata after mutation if (draft.items) { draft.items.forEach((item: any) => { if (!item.timestamp) { item.timestamp = Date.now(); item.userId = currentUser.id; item.version = (item.version || 0) + 1; } }); } }; return originalSetState(wrappedUpdater); } return originalSetState(updater); } as any; // Works with direct value travels.setState({ items: [{ name: 'Task 1' }] }); // Result: { items: [{ name: 'Task 1', timestamp: ..., userId: ..., version: 1 }] } // Also works with mutation travels.setState((draft) => { draft.items.push({ name: 'Task 2' }); // Metadata will be added automatically }); ``` ## Implementing Operation Logging and Auditing 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; ``` ## Implementing Rate Limiting and Throttling 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; ``` ## Composing Multiple Wrappers 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 ``` ## Detecting History Overflow Use `subscribe()` to detect when history reaches the maximum limit: ```typescript const travels = createTravels({ count: 0 }, { maxHistory: 5 }); const archive: any[] = []; let lastPosition = 0; travels.subscribe((state, patches, position) => { // Detect overflow: position stops growing if (position === lastPosition && position >= 5) { // Archive to external storage archive.push({ state: travels.getState(), patches: travels.getPatches(), timestamp: Date.now(), }); // You can save to localStorage, IndexedDB, or API localStorage.setItem('archive', JSON.stringify(archive)); } lastPosition = position; }); ``` ## Common Patterns Here are some reusable wrapper patterns: ```typescript // Pattern 1: Validation wrapper function withValidation( travels: Travels, validator: (state: any, draft?: any) => boolean | string ) { const original = travels.setState.bind(travels); travels.setState = function (updater: any) { // Handle direct value if (typeof updater === 'object' && updater !== null) { const result = validator(updater); if (result !== true) { throw new Error( typeof result === 'string' ? result : 'Validation failed' ); } return original(updater); } // Handle mutation function if (typeof updater === 'function') { const wrapped = (draft: any) => { updater(draft); const result = validator(travels.getState(), draft); if (result !== true) { throw new Error( typeof result === 'string' ? result : 'Validation failed' ); } }; return original(wrapped); } return original(updater); } as any; return travels; } // Pattern 2: Logging wrapper function withLogging(travels: Travels) { 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[]) { console.log(`[${method}] called with:`, args); const result = original(...args); console.log(`[${method}] result:`, travels.getState()); return result; }; } }); return travels; } // 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; } // 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'); ``` ## Back to Main Documentation See the [main README](../README.md) for basic usage, API reference, and getting started guide.