/** * @ai-summary IndexedDB storage adapter for Auth0 and Zustand persistence * @ai-context Replaces localStorage with IndexedDB for mobile browser compatibility */ import logger from '../../utils/logger'; interface StorageAdapter { getItem(key: string): string | null; setItem(key: string, value: string): void; removeItem(key: string): void; clear(): void; key(index: number): string | null; readonly length: number; } interface Auth0Cache { get(key: string): Promise; set(key: string, value: any): Promise; remove(key: string): Promise; } class IndexedDBStorage implements StorageAdapter, Auth0Cache { private dbName = 'motovaultpro-storage'; private dbVersion = 1; private storeName = 'keyvalue'; private db: IDBDatabase | null = null; private memoryCache = new Map(); private initPromise: Promise; private isReady = false; constructor() { this.initPromise = this.initialize(); } private async initialize(): Promise { try { this.db = await this.openDatabase(); await this.loadCacheFromDB(); this.isReady = true; logger.debug('IndexedDB storage initialized successfully'); } catch (error) { logger.error('IndexedDB initialization failed, using memory only', { error: String(error) }); this.isReady = false; } } private openDatabase(): Promise { return new Promise((resolve) => { const request = indexedDB.open(this.dbName, this.dbVersion); request.onerror = () => { logger.error(`IndexedDB open failed: ${request.error?.message}`); resolve(null as any); // Fallback to memory-only mode }; request.onsuccess = () => { resolve(request.result); }; request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains(this.storeName)) { db.createObjectStore(this.storeName); } }; }); } private async loadCacheFromDB(): Promise { if (!this.db) return; return new Promise((resolve) => { const transaction = this.db!.transaction([this.storeName], 'readonly'); const store = transaction.objectStore(this.storeName); const request = store.openCursor(); this.memoryCache.clear(); request.onsuccess = () => { const cursor = request.result; if (cursor) { const key = cursor.key as string; const value = cursor.value; if (typeof key === 'string' && typeof value === 'string') { this.memoryCache.set(key, value); } cursor.continue(); } else { logger.debug(`IndexedDB loaded ${this.memoryCache.size} items into cache`); resolve(); } }; request.onerror = () => { logger.warn('IndexedDB failed to load cache from DB', { error: String(request.error) }); resolve(); }; }); } private async persistToDB(key: string, value: string | null): Promise { if (!this.db) return; return new Promise((resolve) => { const transaction = this.db!.transaction([this.storeName], 'readwrite'); const store = transaction.objectStore(this.storeName); if (value === null) { const request = store.delete(key); request.onsuccess = () => resolve(); request.onerror = () => { logger.warn(`IndexedDB failed to delete ${key}`, { error: String(request.error) }); resolve(); }; } else { const request = store.put(value, key); request.onsuccess = () => resolve(); request.onerror = () => { logger.warn(`IndexedDB failed to persist ${key}`, { error: String(request.error) }); resolve(); }; } }); } // Synchronous Storage interface (uses memory cache) getItem(key: string): string | null { return this.memoryCache.get(key) || null; } setItem(key: string, value: string): void { this.memoryCache.set(key, value); // Async persist to IndexedDB (non-blocking) if (this.isReady) { this.persistToDB(key, value).catch(error => { logger.warn(`IndexedDB background persist failed for ${key}`, { error: String(error) }); }); } } removeItem(key: string): void { this.memoryCache.delete(key); // Async remove from IndexedDB (non-blocking) if (this.isReady) { this.persistToDB(key, null).catch(error => { logger.warn(`IndexedDB background removal failed for ${key}`, { error: String(error) }); }); } } clear(): void { this.memoryCache.clear(); // Async clear IndexedDB (non-blocking) if (this.db) { const transaction = this.db.transaction([this.storeName], 'readwrite'); const store = transaction.objectStore(this.storeName); store.clear(); } } async clearAll(): Promise { await this.initPromise; if (!this.db) { this.memoryCache.clear(); return; } const tx = this.db.transaction(this.storeName, 'readwrite'); tx.objectStore(this.storeName).clear(); await new Promise((resolve, reject) => { tx.oncomplete = () => { this.memoryCache.clear(); resolve(); }; tx.onerror = () => { this.memoryCache.clear(); reject(tx.error); }; tx.onabort = () => { this.memoryCache.clear(); reject(new Error('Transaction aborted')); }; }); } key(index: number): string | null { const keys = Array.from(this.memoryCache.keys()); return keys[index] || null; } get length(): number { return this.memoryCache.size; } // Auth0 Cache interface implementation // allKeys() eliminates Auth0 SDK's CacheKeyManifest fallback (auth0-spa-js line 2319) allKeys(): string[] { return Array.from(this.memoryCache.keys()); } async get(key: string): Promise { await this.initPromise; const value = this.getItem(key); return value ? JSON.parse(value) : undefined; } async set(key: string, value: any): Promise { await this.initPromise; const stringValue = JSON.stringify(value); this.memoryCache.set(key, stringValue); // Fire-and-forget: persist to IndexedDB for page reload survival this.persistToDB(key, stringValue).catch(error => { logger.warn(`IndexedDB background persist failed for ${key}`, { error: String(error) }); }); } async remove(key: string): Promise { await this.initPromise; this.memoryCache.delete(key); // Fire-and-forget: remove from IndexedDB this.persistToDB(key, null).catch(error => { logger.warn(`IndexedDB background removal failed for ${key}`, { error: String(error) }); }); } // Additional methods for enhanced functionality async waitForReady(): Promise { return this.initPromise; } get isInitialized(): boolean { return this.isReady; } // For debugging getStats() { return { cacheSize: this.memoryCache.size, isReady: this.isReady, hasDB: !!this.db }; } } // Create singleton instance export const indexedDBStorage = new IndexedDBStorage(); // For Auth0 compatibility - ensure storage is ready before use export const createIndexedDBAdapter = () => { return indexedDBStorage; };