@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
text/typescript
/**
* 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;
}
}