UNPKG

cs-element

Version:

Advanced reactive data management library with state machines, blueprints, persistence, compression, networking, and multithreading support

905 lines (756 loc) 29.5 kB
# 📚 History System - Система истории изменений CSElement включает мощную систему истории изменений с поддержкой undo/redo операций, создания снимков состояния и отслеживания всех изменений в данных. ## 📋 Содержание - [Основы History System](#основы-history-system) - [Конфигурация и настройка](#конфигурация-и-настройка) - [Операции и снимки](#операции-и-снимки) - [Undo/Redo функциональность](#undoredo-функциональность) - [События и мониторинг](#события-и-мониторинг) - [Экспорт и импорт истории](#экспорт-и-импорт-истории) - [Полный пример](#полный-пример-редактор-документов) ## Основы History System ### Создание и инициализация ```typescript import { HistoryManagerImpl, HistoryConfig, HistoryOperation } from 'cs-element'; // Базовая инициализация const historyManager = new HistoryManagerImpl(); // Расширенная конфигурация const config: HistoryConfig = { maxOperations: 200, // Максимум 200 операций в истории maxSize: 50 * 1024 * 1024, // Максимум 50MB snapshotInterval: 15, // Снимок каждые 15 операций autoCleanup: true, // Автоматическая очистка compression: true, // Сжатие снимков trackOperations: ['update', 'delete'], // Отслеживать только эти операции ignoreOperations: ['temp'] // Игнорировать временные операции }; const historyManager = new HistoryManagerImpl(config); ``` ### Интеграция с CSElement ```typescript // Автоматическое отслеживание изменений class HistoryAwareElement extends CSElement { private historyManager: HistoryManagerImpl; constructor(name: string, historyManager: HistoryManagerImpl) { super(name); this.historyManager = historyManager; // Подписываемся на события изменения данных this.on('data:set', (key, newValue, oldValue) => { this.historyManager.addOperation({ type: 'update', description: `Updated ${key} in ${this.name}`, before: { [key]: oldValue }, after: { [key]: newValue }, path: [this.id, key], canUndo: true, canRedo: true }); }); } async setDataWithHistory(key: string, value: any): Promise<void> { const oldValue = this.getData(key); await this.setData(key, value); // История автоматически записывается через событие } } ``` ## Конфигурация и настройка ### Параметры конфигурации ```typescript interface HistoryConfig { // Ограничения размера maxOperations?: number; // Максимальное количество операций maxSize?: number; // Максимальный размер в байтах // Снимки состояния snapshotInterval?: number; // Интервал создания снимков compression?: boolean; // Сжатие данных снимков // Управление операциями trackOperations?: string[]; // Отслеживать только эти типы ignoreOperations?: string[]; // Игнорировать эти типы // Оптимизация autoCleanup?: boolean; // Автоматическая очистка } // Пример специализированной конфигурации const documentEditorConfig: HistoryConfig = { maxOperations: 500, snapshotInterval: 20, compression: true, trackOperations: ['update', 'delete', 'create'], ignoreOperations: ['cursor-move', 'scroll', 'focus'], autoCleanup: true }; ``` ### Динамическая настройка ```typescript class ConfigurableHistoryManager extends HistoryManagerImpl { updateConfig(newConfig: Partial<HistoryConfig>): void { // Обновляем конфигурацию на лету Object.assign(this.config, newConfig); // Применяем новые ограничения if (newConfig.maxOperations && this.operations.length > newConfig.maxOperations) { this.cleanup(); } } enableVerboseLogging(): void { this.updateConfig({ trackOperations: [], // Отслеживать все операции ignoreOperations: [] }); } enableMinimalTracking(): void { this.updateConfig({ trackOperations: ['update', 'delete'], ignoreOperations: ['temp', 'cache', 'ui-state'] }); } } ``` ## Операции и снимки ### Ручное добавление операций ```typescript // Простая операция historyManager.addOperation({ type: 'update', description: 'Changed user name', before: { name: 'John' }, after: { name: 'Jane' }, canUndo: true, canRedo: true }); // Операция с метаданными historyManager.addOperation({ type: 'delete', description: 'Deleted comment', before: { id: 'comment-123', text: 'This is a comment', author: 'user-456' }, after: null, path: ['comments', 'comment-123'], canUndo: true, canRedo: false, // Нельзя повторить удаление metadata: { author: 'user-789', reason: 'inappropriate-content', timestamp: Date.now() } }); // Batch операция historyManager.addOperation({ type: 'batch', description: 'Bulk update users', before: [ { id: 'user-1', status: 'inactive' }, { id: 'user-2', status: 'inactive' } ], after: [ { id: 'user-1', status: 'active' }, { id: 'user-2', status: 'active' } ], canUndo: true, canRedo: true, metadata: { batchSize: 2, operation: 'activate-users' } }); ``` ### Создание снимков ```typescript // Автоматические снимки создаются каждые N операций // Ручное создание снимка const element = new CSElement('document'); await element.setData('title', 'My Document'); await element.setData('content', 'Document content...'); const snapshot = historyManager.createSnapshot( element.export(), 'Document saved checkpoint' ); console.log('Снимок создан:', { id: snapshot.id, size: snapshot.size, timestamp: new Date(snapshot.timestamp) }); // Снимок с дополнительными метаданными const detailedSnapshot = historyManager.createSnapshot( { elements: [element.export()], metadata: { version: '1.2.0', user: 'user-123', session: 'session-456' } }, 'Version 1.2.0 release' ); ``` ### Получение информации об операциях ```typescript // Получить последние 10 операций const recentOperations = historyManager.getOperations(10); console.log('Последние операции:', recentOperations); // Получить конкретную операцию const operation = historyManager.getOperation('operation-id-123'); if (operation) { console.log('Операция найдена:', operation.description); } // Получить все снимки const snapshots = historyManager.getSnapshots(); console.log(`Всего снимков: ${snapshots.length}`); // Получить состояние истории const state = historyManager.getState(); console.log('Состояние истории:', { canUndo: state.canUndo, canRedo: state.canRedo, currentIndex: state.currentIndex, totalOperations: state.totalOperations, totalSize: `${(state.totalSize / 1024 / 1024).toFixed(2)} MB` }); ``` ## Undo/Redo функциональность ### Базовые операции отмены и повтора ```typescript // Проверяем возможность отмены/повтора const state = historyManager.getState(); console.log(`Можно отменить: ${state.canUndo}`); console.log(`Можно повторить: ${state.canRedo}`); // Отмена последней операции if (state.canUndo) { try { const restoredData = await historyManager.undo(); console.log('Операция отменена, восстановлены данные:', restoredData); } catch (error) { console.error('Ошибка отмены:', error); } } // Повтор отмененной операции if (historyManager.getState().canRedo) { try { const redoneData = await historyManager.redo(); console.log('Операция повторена:', redoneData); } catch (error) { console.error('Ошибка повтора:', error); } } ``` ### Отмена/повтор до конкретной операции ```typescript // Получаем список операций для выбора const operations = historyManager.getOperations(); console.log('Доступные операции:'); operations.forEach((op, index) => { console.log(`${index}: ${op.description} (${new Date(op.timestamp)})`); }); // Отменяем до конкретной операции const targetOperationId = operations[5].id; try { const result = await historyManager.undoTo(targetOperationId); console.log('Отмена до операции выполнена:', result); } catch (error) { console.error('Ошибка отмены до операции:', error); } // Повторяем до конкретной операции const redoTargetId = operations[8].id; try { const result = await historyManager.redoTo(redoTargetId); console.log('Повтор до операции выполнен:', result); } catch (error) { console.error('Ошибка повтора до операции:', error); } ``` ### Интеграция с UI ```typescript class HistoryUI { private historyManager: HistoryManagerImpl; private undoButton: HTMLButtonElement; private redoButton: HTMLButtonElement; private historyList: HTMLElement; constructor(historyManager: HistoryManagerImpl) { this.historyManager = historyManager; this.initializeUI(); this.bindEvents(); } private initializeUI(): void { // Создаем UI элементы this.undoButton = document.createElement('button'); this.undoButton.textContent = 'Undo'; this.redoButton = document.createElement('button'); this.redoButton.textContent = 'Redo'; this.historyList = document.createElement('ul'); this.historyList.className = 'history-list'; this.updateUI(); } private bindEvents(): void { // Обработчики кнопок this.undoButton.addEventListener('click', async () => { try { await this.historyManager.undo(); this.updateUI(); } catch (error) { console.error('Undo failed:', error); } }); this.redoButton.addEventListener('click', async () => { try { await this.historyManager.redo(); this.updateUI(); } catch (error) { console.error('Redo failed:', error); } }); // Подписка на события истории this.historyManager.on('operation-added', () => this.updateUI()); this.historyManager.on('undo-performed', () => this.updateUI()); this.historyManager.on('redo-performed', () => this.updateUI()); } private updateUI(): void { const state = this.historyManager.getState(); // Обновляем состояние кнопок this.undoButton.disabled = !state.canUndo; this.redoButton.disabled = !state.canRedo; // Обновляем список операций this.updateHistoryList(); } private updateHistoryList(): void { const operations = this.historyManager.getOperations(20); const state = this.historyManager.getState(); this.historyList.innerHTML = ''; operations.forEach((operation, index) => { const li = document.createElement('li'); li.textContent = operation.description; li.className = index <= state.currentIndex ? 'applied' : 'unapplied'; // Клик для отмены/повтора до этой операции li.addEventListener('click', async () => { try { if (index < state.currentIndex) { await this.historyManager.undoTo(operation.id); } else if (index > state.currentIndex) { await this.historyManager.redoTo(operation.id); } this.updateUI(); } catch (error) { console.error('Navigation failed:', error); } }); this.historyList.appendChild(li); }); } } ``` ## События и мониторинг ### Подписка на события истории ```typescript // Подписка на все типы событий historyManager.on('operation-added', (data) => { console.log('Новая операция:', data.operation.description); console.log('Состояние истории:', data.state); }); historyManager.on('snapshot-created', (data) => { console.log('Создан снимок:', data.snapshot.metadata.operation); console.log('Размер снимка:', data.snapshot.size, 'байт'); }); historyManager.on('undo-performed', (data) => { console.log('Выполнена отмена:', data.operation.description); console.log('Восстановлено состояние:', data.operation.before); }); historyManager.on('redo-performed', (data) => { console.log('Выполнен повтор:', data.operation.description); console.log('Применено состояние:', data.operation.after); }); historyManager.on('history-cleared', (data) => { console.log('История очищена в:', new Date(data.timestamp)); }); historyManager.on('cleanup-performed', (data) => { console.log('Выполнена очистка истории'); console.log('Новое состояние:', data.state); }); ``` ### Мониторинг производительности ```typescript class HistoryMonitor { private historyManager: HistoryManagerImpl; private metrics = { operationsPerSecond: 0, averageOperationSize: 0, memoryUsage: 0, undoRedoRatio: 0 }; constructor(historyManager: HistoryManagerImpl) { this.historyManager = historyManager; this.startMonitoring(); } private startMonitoring(): void { // Мониторинг каждые 5 секунд setInterval(() => { this.updateMetrics(); this.logMetrics(); }, 5000); // Отслеживание операций let operationCount = 0; let lastReset = Date.now(); this.historyManager.on('operation-added', () => { operationCount++; // Сброс счетчика каждую минуту const now = Date.now(); if (now - lastReset >= 60000) { this.metrics.operationsPerSecond = operationCount / 60; operationCount = 0; lastReset = now; } }); } private updateMetrics(): void { const state = this.historyManager.getState(); const operations = this.historyManager.getOperations(); // Средний размер операции if (operations.length > 0) { const totalSize = operations.reduce((sum, op) => { return sum + JSON.stringify(op).length; }, 0); this.metrics.averageOperationSize = totalSize / operations.length; } // Использование памяти this.metrics.memoryUsage = state.totalSize; // Соотношение undo/redo if (state.stats.undoCount + state.stats.redoCount > 0) { this.metrics.undoRedoRatio = state.stats.undoCount / (state.stats.undoCount + state.stats.redoCount); } } private logMetrics(): void { console.log('📊 История - метрики производительности:'); console.log(` Операций в секунду: ${this.metrics.operationsPerSecond.toFixed(2)}`); console.log(` Средний размер операции: ${this.metrics.averageOperationSize.toFixed(0)} байт`); console.log(` Использование памяти: ${(this.metrics.memoryUsage / 1024 / 1024).toFixed(2)} MB`); console.log(` Соотношение Undo/Redo: ${(this.metrics.undoRedoRatio * 100).toFixed(1)}%`); } getMetrics() { return { ...this.metrics }; } } ``` ## Экспорт и импорт истории ### Экспорт истории ```typescript // Полный экспорт истории const historyExport = historyManager.export(); console.log('Экспорт истории:', { version: historyExport.version, timestamp: new Date(historyExport.timestamp), operationsCount: historyExport.operations.length, snapshotsCount: historyExport.snapshots.length }); // Сохранение в файл (Node.js) import { writeFileSync } from 'fs'; const historyData = JSON.stringify(historyExport, null, 2); writeFileSync('history-backup.json', historyData); console.log('История сохранена в файл'); // Сохранение в localStorage (браузер) localStorage.setItem('app-history', JSON.stringify(historyExport)); ``` ### Импорт истории ```typescript // Загрузка из файла (Node.js) import { readFileSync } from 'fs'; try { const historyData = readFileSync('history-backup.json', 'utf8'); const importData = JSON.parse(historyData); historyManager.import(importData); console.log('История успешно импортирована'); } catch (error) { console.error('Ошибка импорта истории:', error); } // Загрузка из localStorage (браузер) const savedHistory = localStorage.getItem('app-history'); if (savedHistory) { try { const importData = JSON.parse(savedHistory); historyManager.import(importData); console.log('История восстановлена из localStorage'); } catch (error) { console.error('Ошибка восстановления истории:', error); } } // Слияние историй class HistoryMerger { static merge( primary: HistoryExport, secondary: HistoryExport ): HistoryExport { // Объединяем операции по времени const allOperations = [ ...primary.operations, ...secondary.operations ].sort((a, b) => a.timestamp - b.timestamp); // Объединяем снимки const allSnapshots = [ ...primary.snapshots, ...secondary.snapshots ].sort((a, b) => a.timestamp - b.timestamp); return { version: primary.version, timestamp: Date.now(), operations: allOperations, snapshots: allSnapshots, state: primary.state, config: primary.config }; } } ``` ## Полный пример: Редактор документов ```typescript class DocumentEditor { private document: CSElement; private historyManager: HistoryManagerImpl; private historyUI: HistoryUI; private monitor: HistoryMonitor; constructor() { this.initializeHistory(); this.initializeDocument(); this.setupAutoSave(); } private initializeHistory(): void { // Конфигурация для редактора документов const config: HistoryConfig = { maxOperations: 1000, maxSize: 100 * 1024 * 1024, // 100MB snapshotInterval: 25, autoCleanup: true, compression: true, trackOperations: ['update', 'delete', 'create', 'format'], ignoreOperations: ['cursor', 'selection', 'scroll', 'focus'] }; this.historyManager = new HistoryManagerImpl(config); this.historyUI = new HistoryUI(this.historyManager); this.monitor = new HistoryMonitor(this.historyManager); // Обработка событий истории this.historyManager.on('operation-added', (data) => { this.onHistoryChanged('added', data.operation); }); this.historyManager.on('undo-performed', (data) => { this.applyHistoryChange(data.operation.before); this.onHistoryChanged('undo', data.operation); }); this.historyManager.on('redo-performed', (data) => { this.applyHistoryChange(data.operation.after); this.onHistoryChanged('redo', data.operation); }); } private initializeDocument(): void { this.document = new CSElement('document'); // Начальное содержимое this.document.setData('title', 'Untitled Document'); this.document.setData('content', ''); this.document.setData('metadata', { created: Date.now(), modified: Date.now(), version: '1.0.0' }); } // Операции редактирования с отслеживанием истории async updateTitle(newTitle: string): Promise<void> { const oldTitle = this.document.getData('title'); await this.document.setData('title', newTitle); this.historyManager.addOperation({ type: 'update', description: `Changed title from "${oldTitle}" to "${newTitle}"`, before: { title: oldTitle }, after: { title: newTitle }, path: ['title'], canUndo: true, canRedo: true, metadata: { field: 'title', timestamp: Date.now() } }); } async updateContent(newContent: string): Promise<void> { const oldContent = this.document.getData('content'); await this.document.setData('content', newContent); // Создаем diff для больших изменений const diff = this.historyManager.createDiff( { content: oldContent }, { content: newContent }, ['content'] ); this.historyManager.addOperation({ type: 'update', description: `Updated document content (${diff.length} changes)`, before: { content: oldContent }, after: { content: newContent }, path: ['content'], canUndo: true, canRedo: true, metadata: { field: 'content', diff: diff, timestamp: Date.now() } }); } async formatText(range: { start: number; end: number }, format: any): Promise<void> { const content = this.document.getData('content'); const formatting = this.document.getData('formatting') || {}; const oldFormatting = { ...formatting }; const newFormatting = { ...formatting, [range.start + '-' + range.end]: format }; await this.document.setData('formatting', newFormatting); this.historyManager.addOperation({ type: 'format', description: `Applied ${format.type} formatting to range ${range.start}-${range.end}`, before: { formatting: oldFormatting }, after: { formatting: newFormatting }, path: ['formatting'], canUndo: true, canRedo: true, metadata: { field: 'formatting', range: range, format: format, timestamp: Date.now() } }); } // Применение изменений из истории private async applyHistoryChange(data: any): Promise<void> { if (data.title !== undefined) { await this.document.setData('title', data.title); } if (data.content !== undefined) { await this.document.setData('content', data.content); } if (data.formatting !== undefined) { await this.document.setData('formatting', data.formatting); } // Обновляем UI this.updateEditorUI(); } private onHistoryChanged(type: string, operation: HistoryOperation): void { console.log(`История: ${type} - ${operation.description}`); // Обновляем метаданные документа const metadata = this.document.getData('metadata'); this.document.setData('metadata', { ...metadata, modified: Date.now(), lastOperation: operation.description }); // Показываем уведомление пользователю this.showNotification(`${type}: ${operation.description}`); } private setupAutoSave(): void { // Автосохранение каждые 30 секунд setInterval(async () => { const snapshot = this.historyManager.createSnapshot( this.document.export(), 'Auto-save checkpoint' ); // Сохраняем в localStorage const historyExport = this.historyManager.export(); localStorage.setItem('document-history', JSON.stringify(historyExport)); console.log('Автосохранение выполнено'); }, 30000); } private updateEditorUI(): void { // Обновление UI редактора console.log('Обновление UI редактора'); } private showNotification(message: string): void { // Показ уведомления пользователю console.log(`📝 ${message}`); } // Публичные методы для управления историей async undo(): Promise<void> { try { await this.historyManager.undo(); } catch (error) { console.error('Ошибка отмены:', error); } } async redo(): Promise<void> { try { await this.historyManager.redo(); } catch (error) { console.error('Ошибка повтора:', error); } } getHistoryState() { return this.historyManager.getState(); } exportDocument() { return { document: this.document.export(), history: this.historyManager.export() }; } importDocument(data: any) { // Импорт документа и истории this.document = CSElement.import(data.document); this.historyManager.import(data.history); } } // Использование const editor = new DocumentEditor(); // Редактирование с отслеживанием истории await editor.updateTitle('My Amazing Document'); await editor.updateContent('This is the content of my document...'); await editor.formatText({ start: 0, end: 10 }, { type: 'bold', value: true }); // Отмена последнего действия await editor.undo(); // Повтор отмененного действия await editor.redo(); // Проверка состояния истории const state = editor.getHistoryState(); console.log(`Можно отменить: ${state.canUndo}, можно повторить: ${state.canRedo}`); ``` ## 🔧 Лучшие практики ### 1. Оптимизация производительности ```typescript // Используйте batch операции для групповых изменений const batchOperations = []; for (let i = 0; i < 100; i++) { batchOperations.push({ type: 'update', elementId: `element-${i}`, before: oldValues[i], after: newValues[i] }); } historyManager.addOperation({ type: 'batch', description: 'Bulk update operation', before: batchOperations.map(op => op.before), after: batchOperations.map(op => op.after), canUndo: true, canRedo: true, metadata: { batchSize: batchOperations.length } }); ``` ### 2. Управление памятью ```typescript // Настройте ограничения для предотвращения утечек памяти const memoryEfficientConfig: HistoryConfig = { maxOperations: 50, maxSize: 10 * 1024 * 1024, // 10MB autoCleanup: true, compression: true }; ``` ### 3. Селективное отслеживание ```typescript // Отслеживайте только важные операции const selectiveConfig: HistoryConfig = { trackOperations: ['update', 'delete', 'create'], ignoreOperations: ['temp', 'ui-state', 'cursor-move'] }; ``` History System в CSElement предоставляет полный контроль над историей изменений в ваших данных, обеспечивая надежную функциональность undo/redo и отслеживание всех операций! 📚✨