/** * IndexedDB wrapper for offline storage * Provides persistent storage for large objects and file data */ class OfflineStorage { private dbName = 'veza_offline_db'; private dbVersion = 1; private db: IDBDatabase | null = null; /** * Open database connection */ async open(): Promise { if (this.db) { return this.db; } return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, this.dbVersion); request.onerror = () => { reject(new Error('Failed to open IndexedDB')); }; request.onsuccess = () => { this.db = request.result; resolve(this.db); }; request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; // Create object stores for different data types if (!db.objectStoreNames.contains('queued_actions')) { const actionStore = db.createObjectStore('queued_actions', { keyPath: 'id', }); actionStore.createIndex('timestamp', 'timestamp', { unique: false }); actionStore.createIndex('type', 'type', { unique: false }); } if (!db.objectStoreNames.contains('uploaded_files')) { const fileStore = db.createObjectStore('uploaded_files', { keyPath: 'id', }); fileStore.createIndex('filename', 'filename', { unique: false }); fileStore.createIndex('status', 'status', { unique: false }); } if (!db.objectStoreNames.contains('messages')) { const messageStore = db.createObjectStore('messages', { keyPath: 'id', }); messageStore.createIndex('conversationId', 'conversationId', { unique: false, }); messageStore.createIndex('timestamp', 'timestamp', { unique: false }); } }; }); } /** * Save data to a store */ async save(storeName: string, data: T): Promise { const db = await this.open(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, 'readwrite'); const store = transaction.objectStore(storeName); const request = store.add(data); request.onsuccess = () => resolve(); request.onerror = () => reject(new Error(`Failed to save data to ${storeName}`)); }); } /** * Update data in a store */ async update(storeName: string, data: T & { id: string }): Promise { const db = await this.open(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, 'readwrite'); const store = transaction.objectStore(storeName); const request = store.put(data); request.onsuccess = () => resolve(); request.onerror = () => reject(new Error(`Failed to update data in ${storeName}`)); }); } /** * Get data from a store by ID */ async get(storeName: string, id: string): Promise { const db = await this.open(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, 'readonly'); const store = transaction.objectStore(storeName); const request = store.get(id); request.onsuccess = () => { resolve(request.result || null); }; request.onerror = () => reject(new Error(`Failed to get data from ${storeName}`)); }); } /** * Get all data from a store */ async getAll(storeName: string): Promise { const db = await this.open(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, 'readonly'); const store = transaction.objectStore(storeName); const request = store.getAll(); request.onsuccess = () => { resolve(request.result || []); }; request.onerror = () => reject(new Error(`Failed to get all data from ${storeName}`)); }); } /** * Delete data from a store by ID */ async delete(storeName: string, id: string): Promise { const db = await this.open(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, 'readwrite'); const store = transaction.objectStore(storeName); const request = store.delete(id); request.onsuccess = () => resolve(); request.onerror = () => reject(new Error(`Failed to delete data from ${storeName}`)); }); } /** * Clear all data from a store */ async clear(storeName: string): Promise { const db = await this.open(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, 'readwrite'); const store = transaction.objectStore(storeName); const request = store.clear(); request.onsuccess = () => resolve(); request.onerror = () => reject(new Error(`Failed to clear ${storeName}`)); }); } /** * Query data by index */ async query( storeName: string, indexName: string, query: IDBKeyRange | string, ): Promise { const db = await this.open(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, 'readonly'); const store = transaction.objectStore(storeName); const index = store.index(indexName); const request = index.getAll(query); request.onsuccess = () => { resolve(request.result || []); }; request.onerror = () => reject(new Error(`Failed to query ${storeName}`)); }); } /** * Get count of items in a store */ async count(storeName: string): Promise { const db = await this.open(); return new Promise((resolve, reject) => { const transaction = db.transaction(storeName, 'readonly'); const store = transaction.objectStore(storeName); const request = store.count(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(new Error(`Failed to count ${storeName}`)); }); } } // Export singleton instance export const offlineStorage = new OfflineStorage(); // Export types export interface QueuedAction { id: string; type: 'send_message' | 'upload_track' | 'update_profile'; payload: Record; timestamp: number; retries: number; status: 'pending' | 'processing' | 'failed' | 'completed'; } export interface UploadedFile { id: string; filename: string; file: File; status: 'pending' | 'uploading' | 'completed' | 'failed'; progress: number; error?: string; } export interface OfflineMessage { id: string; conversationId: string; content: string; timestamp: number; userId: string; }