UNPKG

@jager-ai/holy-editor

Version:

Rich text editor with Bible verse slash commands and PWA keyboard tracking, extracted from Holy Habit project

326 lines (273 loc) â€ĸ 8.26 kB
/** * Auto Save Manager * * Manages automatic saving of editor content to localStorage * Extracted from Holy Habit auto-save functionality */ import { AutoSaveData, AutoSaveOptions } from '../types/Editor'; export class AutoSaveManager { private editorId: string; private storageKey: string; private saveInterval: number; private maxSize: number; private intervalId: NodeJS.Timeout | null = null; private lastSaveTime = 0; private version = '1.0.0'; // Callbacks private onSave?: (data: AutoSaveData) => void; private onRestore?: (data: AutoSaveData) => void; private onError?: (error: Error) => void; // Storage availability flag private storageAvailable = false; constructor(editorId: string, options?: AutoSaveOptions) { this.editorId = editorId; this.storageKey = options?.key || `holy-editor-autosave-${editorId}`; this.saveInterval = options?.interval || 30000; // 30 seconds default this.maxSize = options?.maxSize || 1048576; // 1MB default // Set callbacks this.onSave = options?.onSave; this.onRestore = options?.onRestore; this.onError = options?.onError; // Check localStorage availability this.checkStorageAvailability(); console.log('💾 AutoSaveManager initialized for editor:', editorId); } /** * Check if localStorage is available */ private checkStorageAvailability(): boolean { try { const testKey = '__holy_editor_test__'; localStorage.setItem(testKey, 'test'); localStorage.removeItem(testKey); this.storageAvailable = true; return true; } catch (error) { console.warn('âš ī¸ localStorage not available:', error); this.storageAvailable = false; this.handleError(new Error('localStorage not available')); return false; } } /** * Start auto-saving */ public start(getContent: () => string): void { if (!this.storageAvailable) { console.warn('âš ī¸ Cannot start auto-save: localStorage not available'); return; } // Clear any existing interval this.stop(); // Save immediately this.save(getContent()); // Set up interval this.intervalId = setInterval(() => { this.save(getContent()); }, this.saveInterval); console.log('â–ļī¸ Auto-save started with interval:', this.saveInterval); } /** * Stop auto-saving */ public stop(): void { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; console.log('âšī¸ Auto-save stopped'); } } /** * Save content to localStorage */ public save(content: string): boolean { if (!this.storageAvailable) return false; try { // Check content size const contentSize = new Blob([content]).size; if (contentSize > this.maxSize) { throw new Error(`Content size (${contentSize} bytes) exceeds maximum (${this.maxSize} bytes)`); } // Create save data const saveData: AutoSaveData = { content, timestamp: Date.now(), version: this.version, editorId: this.editorId }; // Save to localStorage localStorage.setItem(this.storageKey, JSON.stringify(saveData)); this.lastSaveTime = saveData.timestamp; // Call onSave callback if (this.onSave) { this.onSave(saveData); } console.log('💾 Content auto-saved:', { size: contentSize, timestamp: new Date(saveData.timestamp).toISOString() }); return true; } catch (error) { console.error('❌ Auto-save failed:', error); this.handleError(error as Error); return false; } } /** * Restore content from localStorage */ public restore(): string | null { if (!this.storageAvailable) return null; try { const savedData = localStorage.getItem(this.storageKey); if (!savedData) { console.log('💾 No saved content found'); return null; } const data: AutoSaveData = JSON.parse(savedData); // Validate data structure if (!data.content || !data.timestamp || !data.version) { throw new Error('Invalid save data structure'); } // Check if saved data is from same editor if (data.editorId !== this.editorId) { console.warn('âš ī¸ Saved data is from different editor:', data.editorId); } // Call onRestore callback if (this.onRestore) { this.onRestore(data); } const age = Date.now() - data.timestamp; const ageMinutes = Math.floor(age / 60000); console.log('✅ Content restored:', { age: `${ageMinutes} minutes`, timestamp: new Date(data.timestamp).toISOString(), size: new Blob([data.content]).size }); return data.content; } catch (error) { console.error('❌ Failed to restore content:', error); this.handleError(error as Error); return null; } } /** * Clear saved content */ public clear(): boolean { if (!this.storageAvailable) return false; try { localStorage.removeItem(this.storageKey); console.log('đŸ—‘ī¸ Auto-saved content cleared'); return true; } catch (error) { console.error('❌ Failed to clear saved content:', error); this.handleError(error as Error); return false; } } /** * Get save info without restoring content */ public getSaveInfo(): { timestamp: number; size: number } | null { if (!this.storageAvailable) return null; try { const savedData = localStorage.getItem(this.storageKey); if (!savedData) return null; const data: AutoSaveData = JSON.parse(savedData); const size = new Blob([data.content]).size; return { timestamp: data.timestamp, size }; } catch (error) { console.error('❌ Failed to get save info:', error); return null; } } /** * Check if there's saved content */ public hasSavedContent(): boolean { return this.getSaveInfo() !== null; } /** * Get time since last save */ public getTimeSinceLastSave(): number { if (this.lastSaveTime === 0) return -1; return Date.now() - this.lastSaveTime; } /** * Update save interval */ public updateInterval(newInterval: number, getContent?: () => string): void { this.saveInterval = newInterval; // Restart if currently running if (this.intervalId && getContent) { this.start(getContent); } } /** * Handle errors */ private handleError(error: Error): void { if (this.onError) { this.onError(error); } } /** * Get storage key */ public getStorageKey(): string { return this.storageKey; } /** * Check if auto-save is running */ public isRunning(): boolean { return this.intervalId !== null; } /** * Get all saved editor keys */ public static getAllSavedEditors(): string[] { const keys: string[] = []; const prefix = 'holy-editor-autosave-'; try { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(prefix)) { keys.push(key.substring(prefix.length)); } } } catch (error) { console.error('❌ Failed to get saved editors:', error); } return keys; } /** * Clear all saved content for all editors */ public static clearAll(): number { let cleared = 0; const prefix = 'holy-editor-autosave-'; try { const keysToRemove: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(prefix)) { keysToRemove.push(key); } } keysToRemove.forEach(key => { localStorage.removeItem(key); cleared++; }); console.log(`đŸ—‘ī¸ Cleared ${cleared} auto-saved editors`); } catch (error) { console.error('❌ Failed to clear all saved content:', error); } return cleared; } }