/** * 🔍 REAL-TIME ELEMENT & PROPERTY CHANGE TRACKER * * Tracks all element changes, property modifications, and canvas updates * in real-time with detailed logging and diff detection. */ import type { Element } from '../types/elements'; interface ElementSnapshot { id: string; type: string; x: number; y: number; width: number; height: number; visible: boolean; locked: boolean; [key: string]: unknown; timestamp: number; } interface PropertyChange { elementId: string; property: string; oldValue: unknown; newValue: unknown; timestamp: number; changeType: 'created' | 'updated' | 'deleted' | 'property_changed'; } export class ElementChangeTracker { private previousSnapshots: Map = new Map(); private changeHistory: PropertyChange[] = []; private maxHistorySize = 500; private listeners: ((change: PropertyChange) => void)[] = []; public debugEnabled = false; // Set to true to enable logging /** * Track element state at a specific point in time */ public trackElements(elements: Element[]): PropertyChange[] { const currentSnapshots = new Map(); const changes: PropertyChange[] = []; elements.forEach((element: Element) => { const snapshot: ElementSnapshot = { ...element, timestamp: Date.now() } as ElementSnapshot; currentSnapshots.set(element.id as string, snapshot); const previousSnapshot = this.previousSnapshots.get(element.id as string); if (!previousSnapshot) { // Element created changes.push({ elementId: element.id as string, property: '__created__', oldValue: undefined, newValue: snapshot, timestamp: snapshot.timestamp, changeType: 'created' }); } else { // Check for property changes this.detectPropertyChanges(previousSnapshot, snapshot, changes); } }); // Check for deleted elements this.previousSnapshots.forEach((snapshot, elementId) => { if (!currentSnapshots.has(elementId)) { changes.push({ elementId, property: '__deleted__', oldValue: snapshot, newValue: undefined, timestamp: Date.now(), changeType: 'deleted' }); } }); this.previousSnapshots = currentSnapshots; this.addChangesToHistory(changes); this.notifyListeners(changes); return changes; } /** * Detect all property changes between two snapshots */ private detectPropertyChanges( previous: ElementSnapshot, current: ElementSnapshot, changes: PropertyChange[] ): void { const allKeys = new Set([ ...Object.keys(previous), ...Object.keys(current) ]); allKeys.forEach(key => { if (key === 'timestamp') return; const oldValue = previous[key]; const newValue = current[key]; // Deep comparison for objects/arrays if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) { changes.push({ elementId: previous.id, property: key, oldValue, newValue, timestamp: current.timestamp, changeType: 'property_changed' }); } }); } /** * Get emoji for different property types */ private getPropertyEmoji(property: string): string { const emojiMap: { [key: string]: string } = { x: '📍', y: '📍', width: '📏', height: '📏', rotation: '🔄', opacity: '👁️', visible: '👁️', locked: '🔒', color: '🎨', fillColor: '🎨', strokeColor: '🖌️', fontSize: '📝', fontFamily: '📝', text: '📄', src: '🖼️', url: '🔗', backgroundColor: '🎨', textColor: '🎨', borderColor: '🖌️', borderRadius: '🔲', showHeaders: '📊', showBorders: '📊', showAlternatingRows: '📊' }; return emojiMap[property] || '🔧'; } /** * Determine the type of property */ private getPropertyType(value: unknown): string { if (value === null) return 'null'; if (Array.isArray(value)) return `array[${value.length}]`; if (typeof value === 'object') return `object{${Object.keys(value as Record).length}}`; return typeof value; } /** * Format value for logging */ private formatValue(value: unknown, maxLength = 50): string { if (value === undefined) return 'undefined'; if (value === null) return 'null'; if (typeof value === 'boolean') return value ? '✓ true' : '✗ false'; if (typeof value === 'number') return value.toFixed(2); if (typeof value === 'string') { const truncated = value.length > maxLength ? value.slice(0, maxLength) + '...' : value; return `"${truncated}"`; } if (Array.isArray(value)) return `[${value.length} items]`; if (typeof value === 'object') return `{${Object.keys(value as Record).length} props}`; return String(value); } /** * Add changes to history with size limit */ private addChangesToHistory(changes: PropertyChange[]): void { this.changeHistory.push(...changes); if (this.changeHistory.length > this.maxHistorySize) { this.changeHistory = this.changeHistory.slice(-this.maxHistorySize); } } /** * Notify all registered listeners */ private notifyListeners(changes: PropertyChange[]): void { changes.forEach(change => { this.listeners.forEach(listener => listener(change)); }); } /** * Subscribe to changes */ public onChange( callback: (change: PropertyChange) => void ): () => void { this.listeners.push(callback); return () => { this.listeners = this.listeners.filter(l => l !== callback); }; } /** * Get change history */ public getHistory(): PropertyChange[] { return [...this.changeHistory]; } /** * Get changes for a specific element */ public getElementHistory(elementId: string): PropertyChange[] { return this.changeHistory.filter(c => c.elementId === elementId); } /** * Get all changes for a specific property */ public getPropertyHistory(property: string): PropertyChange[] { return this.changeHistory.filter( c => c.property === property || c.changeType === 'created' ); } /** * Get changes within a time range */ public getChangesBetween( startTime: number, endTime: number ): PropertyChange[] { return this.changeHistory.filter( c => c.timestamp >= startTime && c.timestamp <= endTime ); } /** * Reset history (but keep current snapshots) */ public clearHistory(): void { this.changeHistory = []; } /** * Get current snapshots */ public getSnapshots(): Map { return new Map(this.previousSnapshots); } /** * Generate a detailed report */ public generateReport(): string { const totalChanges = this.changeHistory.length; const elementsTracked = this.previousSnapshots.size; const changeSummary = this.changeHistory.reduce( (acc, change) => { acc[change.changeType] = (acc[change.changeType] || 0) + 1; return acc; }, {} as Record ); return ` 📊 ELEMENT TRACKER REPORT ======================== Total Changes: ${totalChanges} Elements Tracked: ${elementsTracked} Change Breakdown: - Created: ${changeSummary.created || 0} - Updated: ${changeSummary.updated || 0} - Deleted: ${changeSummary.deleted || 0} - Property Changes: ${changeSummary.property_changed || 0} ======================== `.trim(); } } // Export singleton instance export const elementChangeTracker = new ElementChangeTracker();