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
Markdown
# 📚 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 и отслеживание всех операций! 📚✨