// Translation cache management for offline support import { TranslationCacheEntry, CacheStats } from './types'; import { Validator } from './validator'; export class TranslationCache { private static DB_NAME = 'VoiceTranslatorDB'; private static DB_VERSION = 2; // Increment version for cache store private static CACHE_STORE = 'translationCache'; // private static MAX_CACHE_SIZE = 50 * 1024 * 1024; // 50MB limit - Reserved for future use private static MAX_ENTRIES = 1000; // Maximum number of cached translations private static CACHE_EXPIRY_DAYS = 30; // Expire entries after 30 days // Generate cache key from input parameters static generateCacheKey(text: string, sourceLang: string, targetLang: string): string { // Normalize and sanitize text to create a consistent key const normalizedText = text.trim().toLowerCase(); const sanitized = Validator.sanitizeCacheKey(normalizedText); return `${sourceLang}:${targetLang}:${sanitized}`; } // Open or create the cache database static async openDB(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(this.DB_NAME, this.DB_VERSION); request.onupgradeneeded = (event: IDBVersionChangeEvent) => { const db = (event.target as IDBOpenDBRequest).result; // Create cache store if it doesn't exist if (!db.objectStoreNames.contains(this.CACHE_STORE)) { const store = db.createObjectStore(this.CACHE_STORE, { keyPath: 'key' }); store.createIndex('timestamp', 'timestamp', { unique: false }); store.createIndex('lastAccessed', 'lastAccessed', { unique: false }); store.createIndex('sourceLanguage', 'sourceLanguage', { unique: false }); store.createIndex('targetLanguage', 'targetLanguage', { unique: false }); } }; request.onsuccess = (event: Event) => { resolve((event.target as IDBOpenDBRequest).result); }; request.onerror = () => { reject('Failed to open translation cache database'); }; }); } // Get cached translation static async getCachedTranslation( text: string, sourceLang: string, targetLang: string ): Promise { try { const db = await this.openDB(); const transaction = db.transaction([this.CACHE_STORE], 'readwrite'); const store = transaction.objectStore(this.CACHE_STORE); const key = this.generateCacheKey(text, sourceLang, targetLang); const request = store.get(key); return new Promise((resolve) => { request.onsuccess = (event: Event) => { const entry = (event.target as IDBRequest).result as TranslationCacheEntry; if (entry) { // Check if entry is not expired const expiryTime = entry.timestamp + (this.CACHE_EXPIRY_DAYS * 24 * 60 * 60 * 1000); if (Date.now() < expiryTime) { // Update access count and last accessed time entry.accessCount++; entry.lastAccessed = Date.now(); store.put(entry); console.log(`Cache hit for translation: ${sourceLang} -> ${targetLang}`); resolve(entry.targetText); } else { // Entry expired, delete it store.delete(key); resolve(null); } } else { resolve(null); } }; request.onerror = () => { console.error('Failed to get cached translation'); resolve(null); }; }); } catch (error) { console.error('Cache lookup error:', error); return null; } } // Save translation to cache static async cacheTranslation( sourceText: string, sourceLang: string, targetText: string, targetLang: string ): Promise { try { const db = await this.openDB(); const transaction = db.transaction([this.CACHE_STORE], 'readwrite'); const store = transaction.objectStore(this.CACHE_STORE); const key = this.generateCacheKey(sourceText, sourceLang, targetLang); const entry: TranslationCacheEntry = { key, sourceText, sourceLanguage: sourceLang, targetText, targetLanguage: targetLang, timestamp: Date.now(), accessCount: 1, lastAccessed: Date.now() }; // Check cache size before adding await this.ensureCacheSize(db); store.put(entry); console.log(`Cached translation: ${sourceLang} -> ${targetLang}`); } catch (error) { console.error('Failed to cache translation:', error); } } // Ensure cache doesn't exceed size limits static async ensureCacheSize(db: IDBDatabase): Promise { const transaction = db.transaction([this.CACHE_STORE], 'readwrite'); const store = transaction.objectStore(this.CACHE_STORE); // Count entries const countRequest = store.count(); countRequest.onsuccess = async () => { const count = countRequest.result; if (count >= this.MAX_ENTRIES) { // Delete least recently accessed entries const index = store.index('lastAccessed'); const cursor = index.openCursor(); let deleted = 0; const toDelete = Math.floor(count * 0.2); // Delete 20% of entries cursor.onsuccess = (event: Event) => { const cursor = (event.target as IDBRequest).result; if (cursor && deleted < toDelete) { cursor.delete(); deleted++; cursor.continue(); } }; } }; } // Get cache statistics static async getCacheStats(): Promise { try { const db = await this.openDB(); const transaction = db.transaction([this.CACHE_STORE], 'readonly'); const store = transaction.objectStore(this.CACHE_STORE); return new Promise((resolve) => { const stats: CacheStats = { totalEntries: 0, totalSize: 0, oldestEntry: Date.now(), newestEntry: 0 }; const countRequest = store.count(); countRequest.onsuccess = () => { stats.totalEntries = countRequest.result; }; const cursor = store.openCursor(); cursor.onsuccess = (event: Event) => { const cursor = (event.target as IDBRequest).result; if (cursor) { const entry = cursor.value as TranslationCacheEntry; // Estimate size (rough calculation) stats.totalSize += (entry.sourceText.length + entry.targetText.length) * 2; stats.oldestEntry = Math.min(stats.oldestEntry, entry.timestamp); stats.newestEntry = Math.max(stats.newestEntry, entry.timestamp); cursor.continue(); } else { resolve(stats); } }; }); } catch (error) { console.error('Failed to get cache stats:', error); return { totalEntries: 0, totalSize: 0, oldestEntry: 0, newestEntry: 0 }; } } // Clear all cache static async clearCache(): Promise { try { const db = await this.openDB(); const transaction = db.transaction([this.CACHE_STORE], 'readwrite'); const store = transaction.objectStore(this.CACHE_STORE); store.clear(); console.log('Translation cache cleared'); } catch (error) { console.error('Failed to clear cache:', error); } } // Export cache for backup static async exportCache(): Promise { try { const db = await this.openDB(); const transaction = db.transaction([this.CACHE_STORE], 'readonly'); const store = transaction.objectStore(this.CACHE_STORE); const request = store.getAll(); return new Promise((resolve) => { request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { resolve([]); }; }); } catch (error) { console.error('Failed to export cache:', error); return []; } } }