// PWA Offline Manager for SecureBit.chat // Enhanced Security Edition v4.02.442 // Handles offline functionality, data synchronization, and user experience class PWAOfflineManager { constructor() { this.isOnline = navigator.onLine; this.offlineDB = null; this.offlineQueue = []; this.syncInProgress = false; this.lastSyncTime = null; this.offlineIndicator = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectInterval = null; // Offline storage configuration this.dbConfig = { name: 'SecureBitOffline', version: 2, stores: { offlineQueue: { keyPath: 'id', autoIncrement: true, indexes: { timestamp: { unique: false }, type: { unique: false }, priority: { unique: false } } }, sessionData: { keyPath: 'key' }, messageQueue: { keyPath: 'id', autoIncrement: true, indexes: { timestamp: { unique: false }, channelId: { unique: false } } }, appState: { keyPath: 'component' } } }; this.init(); } async init() { try { // Initialize offline database await this.initOfflineDB(); // Setup event listeners this.setupEventListeners(); // Create offline indicator this.createOfflineIndicator(); // Register background sync this.registerBackgroundSync(); // Setup periodic cleanup this.setupPeriodicCleanup(); // Show initial connection status this.updateConnectionStatus(this.isOnline); // Try to process any pending queue items if (this.isOnline) { await this.processOfflineQueue(); } } catch (error) { console.error('โŒ Offline Manager initialization failed:', error); this.handleInitializationError(error); } } async initOfflineDB() { if (!('indexedDB' in window)) { throw new Error('IndexedDB not supported'); } return new Promise((resolve, reject) => { const request = indexedDB.open(this.dbConfig.name, this.dbConfig.version); request.onerror = () => { reject(new Error('Failed to open offline database')); }; request.onsuccess = () => { this.offlineDB = request.result; // Listen for database close events this.offlineDB.onclose = () => { console.log('๐Ÿ”’ IndexedDB connection closed'); this.offlineDB = null; }; // Listen for database errors this.offlineDB.onerror = (event) => { console.error('โŒ IndexedDB error:', event); }; resolve(this.offlineDB); }; request.onupgradeneeded = (event) => { const db = event.target.result; // Create object stores Object.entries(this.dbConfig.stores).forEach(([storeName, config]) => { if (!db.objectStoreNames.contains(storeName)) { const store = db.createObjectStore(storeName, { keyPath: config.keyPath, autoIncrement: config.autoIncrement || false }); // Create indexes if (config.indexes) { Object.entries(config.indexes).forEach(([indexName, indexConfig]) => { store.createIndex(indexName, indexName, indexConfig); }); } } }); }; }); } /** * Ensure database is open, reopen if necessary */ async ensureDatabaseOpen() { // Check if database exists and is open if (this.offlineDB && this.offlineDB.objectStoreNames.length > 0) { // Check if database connection is still valid try { // Try to access objectStoreNames to verify connection is active const storeNames = this.offlineDB.objectStoreNames; if (storeNames.length > 0) { return; // Database is open and valid } } catch (error) { // Database connection is invalid, need to reopen console.warn('โš ๏ธ Database connection invalid, reopening...'); this.offlineDB = null; } } // Database is closed or invalid, reopen it if (!this.offlineDB) { try { await this.initOfflineDB(); } catch (error) { console.error('โŒ Failed to reopen database:', error); throw new Error('Database unavailable'); } } } setupEventListeners() { // Network status changes window.addEventListener('online', () => { this.isOnline = true; this.reconnectAttempts = 0; this.updateConnectionStatus(true); this.handleConnectionRestored(); }); window.addEventListener('offline', () => { this.isOnline = false; this.updateConnectionStatus(false); this.handleConnectionLost(); }); // App visibility changes document.addEventListener('visibilitychange', () => { if (!document.hidden && this.isOnline) { // Try to sync when app becomes visible setTimeout(() => this.processOfflineQueue(), 1000); } }); // Listen for WebRTC connection events document.addEventListener('peer-disconnect', (event) => { if (!this.isOnline) { this.handleOfflineDisconnection(event.detail); } }); // Listen for failed network requests window.addEventListener('unhandledrejection', (event) => { if (this.isNetworkError(event.reason)) { this.handleNetworkFailure(event.reason); } }); // Listen for beforeunload to save state window.addEventListener('beforeunload', () => { this.saveApplicationState(); }); } createOfflineIndicator() { this.offlineIndicator = document.createElement('div'); this.offlineIndicator.id = 'pwa-connection-status'; this.offlineIndicator.className = 'hidden fixed top-4 left-1/2 transform -translate-x-1/2 z-50 transition-all duration-300'; document.body.appendChild(this.offlineIndicator); } updateConnectionStatus(isOnline) { if (!this.offlineIndicator) return; if (isOnline) { this.offlineIndicator.innerHTML = `
๐ŸŒ Back online
`; this.offlineIndicator.classList.remove('hidden'); // Hide after 3 seconds setTimeout(() => { this.offlineIndicator.classList.add('hidden'); }, 3000); } else { this.offlineIndicator.innerHTML = `
๐Ÿ“ด Offline mode
`; this.offlineIndicator.classList.remove('hidden'); } } async handleConnectionRestored() { console.log('๐Ÿ”„ Handling connection restoration...'); try { // Process offline queue await this.processOfflineQueue(); // Restore WebRTC connections if needed await this.attemptWebRTCReconnection(); // Show success notification this.showReconnectionSuccess(); } catch (error) { console.error('โŒ Connection restoration failed:', error); this.showReconnectionError(error); } } handleConnectionLost() { console.log('๐Ÿ“ด Handling connection loss...'); // Show offline guidance this.showOfflineGuidance(); // Save current application state this.saveApplicationState(); // Start reconnection attempts this.startReconnectionAttempts(); } showOfflineGuidance() { // Don't show if already shown recently const lastShown = localStorage.getItem('offline_guidance_shown'); if (lastShown && Date.now() - parseInt(lastShown) < 60000) { // 1 minute return; } const guidance = document.createElement('div'); guidance.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 backdrop-blur-sm'; guidance.innerHTML = `

Connection Lost

SecureBit.chat is now in offline mode. Some features are limited, but your data is safe.

Your session and keys are preserved
No data is stored on servers
Messages will sync when online
`; document.body.appendChild(guidance); // Save that we showed the guidance localStorage.setItem('offline_guidance_shown', Date.now().toString()); // Auto-remove after 15 seconds setTimeout(() => { if (guidance.parentElement) { guidance.remove(); } }, 15000); } startReconnectionAttempts() { if (this.reconnectInterval) { clearInterval(this.reconnectInterval); } this.reconnectInterval = setInterval(() => { if (this.isOnline) { clearInterval(this.reconnectInterval); this.reconnectInterval = null; return; } this.reconnectAttempts++; // Try to detect if we're actually back online this.checkOnlineStatus(); if (this.reconnectAttempts >= this.maxReconnectAttempts) { clearInterval(this.reconnectInterval); this.reconnectInterval = null; } }, 10000); // Try every 10 seconds } async checkOnlineStatus() { try { // Try to fetch a small resource to check connectivity const response = await fetch('/favicon.ico', { method: 'HEAD', cache: 'no-cache', signal: AbortSignal.timeout(5000) }); if (response.ok && !this.isOnline) { this.isOnline = true; this.handleConnectionRestored(); } } catch (error) { // Still offline console.log('๐Ÿ“ด Still offline'); } } async queueOfflineAction(action) { const queueItem = { ...action, id: Date.now() + Math.random(), timestamp: Date.now(), priority: action.priority || 1, retryCount: 0, maxRetries: action.maxRetries || 3 }; // Always add to memory queue as fallback this.offlineQueue.push(queueItem); if (!this.offlineDB) { console.warn('โš ๏ธ Offline database not available, using memory queue only'); return; } try { await this.ensureDatabaseOpen(); const transaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite'); const store = transaction.objectStore('offlineQueue'); await this.promisifyRequest(store.add(queueItem)); // Try to register background sync if (this.registration) { await this.registration.sync.register('offline-sync'); } } catch (error) { if (error.name === 'InvalidStateError' || error.message.includes('closing')) { console.warn('โš ๏ธ Database was closing, item added to memory queue only'); } else { console.error('โŒ Failed to queue offline action:', error); } // Item already in memory queue, so no action needed } } async processOfflineQueue() { if (this.syncInProgress || !this.isOnline) { return; } this.syncInProgress = true; let processedCount = 0; let errorCount = 0; try { // Process database queue if (this.offlineDB) { // Ensure database is open before processing await this.ensureDatabaseOpen(); // Check if database is still open if (!this.offlineDB || this.offlineDB.objectStoreNames.length === 0) { console.warn('โš ๏ธ Database not available, skipping queue processing'); return; } // Get all items first in a single transaction let allItems = []; try { const readTransaction = this.offlineDB.transaction(['offlineQueue'], 'readonly'); const readStore = readTransaction.objectStore('offlineQueue'); allItems = await this.promisifyRequest(readStore.getAll()); } catch (error) { if (error.name === 'InvalidStateError' || error.message.includes('closing')) { console.warn('โš ๏ธ Database was closing during read, retrying...'); await this.ensureDatabaseOpen(); const retryTransaction = this.offlineDB.transaction(['offlineQueue'], 'readonly'); const retryStore = retryTransaction.objectStore('offlineQueue'); allItems = await this.promisifyRequest(retryStore.getAll()); } else { throw error; } } // Sort by priority and timestamp allItems.sort((a, b) => { if (a.priority !== b.priority) { return b.priority - a.priority; // Higher priority first } return a.timestamp - b.timestamp; // Older first }); // Process each item with its own transaction to avoid "database closing" errors for (const item of allItems) { try { await this.processQueueItem(item); // Create a new transaction for each delete operation await this.ensureDatabaseOpen(); const deleteTransaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite'); const deleteStore = deleteTransaction.objectStore('offlineQueue'); await this.promisifyRequest(deleteStore.delete(item.id)); processedCount++; } catch (error) { console.error('โŒ Failed to process offline action:', error); errorCount++; // Increment retry count item.retryCount = (item.retryCount || 0) + 1; if (item.retryCount >= item.maxRetries) { // Max retries reached, remove from queue try { await this.ensureDatabaseOpen(); const removeTransaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite'); const removeStore = removeTransaction.objectStore('offlineQueue'); await this.promisifyRequest(removeStore.delete(item.id)); console.log('โŒ Max retries reached for action:', item.type); } catch (removeError) { console.error('โŒ Failed to remove item after max retries:', removeError); } } else { // Update retry count in database try { await this.ensureDatabaseOpen(); const updateTransaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite'); const updateStore = updateTransaction.objectStore('offlineQueue'); await this.promisifyRequest(updateStore.put(item)); } catch (updateError) { console.error('โŒ Failed to update retry count:', updateError); } } } } } // Process in-memory queue as fallback const memoryQueue = [...this.offlineQueue]; this.offlineQueue = []; for (const item of memoryQueue) { try { await this.processQueueItem(item); processedCount++; } catch (error) { console.error('โŒ Failed to process memory queue item:', error); errorCount++; item.retryCount = (item.retryCount || 0) + 1; if (item.retryCount < item.maxRetries) { this.offlineQueue.push(item); // Re-queue for retry } } } this.lastSyncTime = Date.now(); if (processedCount > 0 || errorCount > 0) { this.showSyncNotification(processedCount, errorCount); } } catch (error) { console.error('โŒ Error processing offline queue:', error); } finally { this.syncInProgress = false; } } async processQueueItem(item) { switch (item.type) { case 'message': return this.retryMessage(item.data); case 'connection': return this.retryConnection(item.data); case 'payment_check': return this.retryPaymentCheck(item.data); case 'session_verification': return this.retrySessionVerification(item.data); case 'key_exchange': return this.retryKeyExchange(item.data); default: console.warn('Unknown queue item type:', item.type); throw new Error(`Unknown queue item type: ${item.type}`); } } async retryMessage(messageData) { // Retry sending message when back online if (window.webrtcManager && window.webrtcManager.isConnected()) { return window.webrtcManager.sendMessage(messageData.content); } throw new Error('WebRTC not connected'); } async retryConnection(connectionData) { // Retry connection establishment if (window.webrtcManager) { return window.webrtcManager.retryConnection(); } throw new Error('WebRTC manager not available'); } async retryPaymentCheck(paymentData) { // Retry payment verification if (window.sessionManager) { return window.sessionManager.checkPaymentStatus(paymentData.checkingId); } throw new Error('Session manager not available'); } async retrySessionVerification(sessionData) { // Retry session verification if (window.sessionManager) { return window.sessionManager.verifyPayment(sessionData.preimage, sessionData.paymentHash); } throw new Error('Session manager not available'); } async retryKeyExchange(keyData) { // Retry key exchange if (window.webrtcManager) { return window.webrtcManager.handleKeyExchange(keyData); } throw new Error('WebRTC manager not available'); } showSyncNotification(successCount, errorCount) { const notification = document.createElement('div'); notification.className = 'fixed bottom-4 right-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm transform translate-x-full transition-transform duration-300'; let message = ''; if (successCount > 0 && errorCount === 0) { message = `โœ… Synced ${successCount} offline action${successCount > 1 ? 's' : ''}`; } else if (successCount > 0 && errorCount > 0) { message = `โš ๏ธ Synced ${successCount}, ${errorCount} failed`; } else if (errorCount > 0) { message = `โŒ ${errorCount} sync error${errorCount > 1 ? 's' : ''}`; } notification.innerHTML = `
Sync Complete
${message}
`; document.body.appendChild(notification); // Animate in setTimeout(() => { notification.classList.remove('translate-x-full'); }, 100); // Auto-remove after 4 seconds setTimeout(() => { notification.classList.add('translate-x-full'); setTimeout(() => notification.remove(), 300); }, 4000); } async attemptWebRTCReconnection() { if (!window.webrtcManager) return; try { // Check if we had an active connection before going offline const savedConnectionState = await this.getStoredData('sessionData', 'connection_state'); if (savedConnectionState && savedConnectionState.wasConnected) { // Show reconnection indicator this.showReconnectionIndicator(); // Attempt to restore connection // This would depend on your specific WebRTC implementation if (window.webrtcManager.attemptReconnection) { await window.webrtcManager.attemptReconnection(savedConnectionState.data); } } } catch (error) { console.error('โŒ WebRTC reconnection failed:', error); } } showReconnectionIndicator() { const indicator = document.createElement('div'); indicator.className = 'fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-blue-500/90 text-white px-6 py-4 rounded-lg backdrop-blur-sm z-50'; indicator.innerHTML = `
Reconnecting...
Restoring secure connection
`; document.body.appendChild(indicator); // Remove after 5 seconds or when connection is restored setTimeout(() => { if (indicator.parentElement) { indicator.remove(); } }, 5000); } showReconnectionSuccess() { const notification = document.createElement('div'); notification.className = 'fixed top-4 right-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm'; notification.innerHTML = `
Reconnected!
All services restored
`; document.body.appendChild(notification); setTimeout(() => notification.remove(), 3000); } showReconnectionError(error) { const notification = document.createElement('div'); notification.className = 'fixed top-4 right-4 bg-yellow-500 text-black p-4 rounded-lg shadow-lg z-50 max-w-sm'; notification.innerHTML = `
Reconnection Issue
Some features may need manual restart
`; document.body.appendChild(notification); setTimeout(() => notification.remove(), 5000); } async saveApplicationState() { if (!this.offlineDB) return; try { await this.ensureDatabaseOpen(); const appState = { component: 'app_state', timestamp: Date.now(), url: window.location.href, // Save WebRTC connection state webrtc: window.webrtcManager ? { isConnected: window.webrtcManager.isConnected(), connectionState: window.webrtcManager.getConnectionInfo(), } : null, // Save session state session: window.sessionManager ? { hasActiveSession: window.sessionManager.hasActiveSession(), sessionInfo: window.sessionManager.getSessionInfo(), } : null, // Save UI state ui: { currentTab: document.querySelector('.active')?.id, scrollPosition: window.pageYOffset, } }; const transaction = this.offlineDB.transaction(['appState'], 'readwrite'); const store = transaction.objectStore('appState'); await this.promisifyRequest(store.put(appState)); } catch (error) { if (error.name === 'InvalidStateError' || error.message.includes('closing')) { console.warn('โš ๏ธ Database was closing, could not save application state'); } else { console.error('โŒ Failed to save application state:', error); } } } async restoreApplicationState() { if (!this.offlineDB) return null; try { const savedState = await this.getStoredData('appState', 'app_state'); if (savedState && Date.now() - savedState.timestamp < 24 * 60 * 60 * 1000) { // 24 hours console.log('๐Ÿ”„ Restoring application state from offline storage'); return savedState; } } catch (error) { console.error('โŒ Failed to restore application state:', error); } return null; } async storeData(storeName, data) { if (!this.offlineDB) { throw new Error('Offline database not available'); } try { await this.ensureDatabaseOpen(); const transaction = this.offlineDB.transaction([storeName], 'readwrite'); const store = transaction.objectStore(storeName); return await this.promisifyRequest(store.put(data)); } catch (error) { if (error.name === 'InvalidStateError' || error.message.includes('closing')) { await this.ensureDatabaseOpen(); const retryTransaction = this.offlineDB.transaction([storeName], 'readwrite'); const retryStore = retryTransaction.objectStore(storeName); return await this.promisifyRequest(retryStore.put(data)); } throw error; } } async getStoredData(storeName, key) { if (!this.offlineDB) { return null; } try { await this.ensureDatabaseOpen(); const transaction = this.offlineDB.transaction([storeName], 'readonly'); const store = transaction.objectStore(storeName); const result = await this.promisifyRequest(store.get(key)); return result; } catch (error) { if (error.name === 'InvalidStateError' || error.message.includes('closing')) { console.warn(`โš ๏ธ Database was closing during get from ${storeName}, retrying...`); try { await this.ensureDatabaseOpen(); const retryTransaction = this.offlineDB.transaction([storeName], 'readonly'); const retryStore = retryTransaction.objectStore(storeName); return await this.promisifyRequest(retryStore.get(key)); } catch (retryError) { console.error(`โŒ Failed to get stored data from ${storeName} after retry:`, retryError); return null; } } console.error(`โŒ Failed to get stored data from ${storeName}:`, error); return null; } } async clearStoredData(storeName, key = null) { if (!this.offlineDB) return; try { await this.ensureDatabaseOpen(); const transaction = this.offlineDB.transaction([storeName], 'readwrite'); const store = transaction.objectStore(storeName); if (key) { await this.promisifyRequest(store.delete(key)); } else { await this.promisifyRequest(store.clear()); } } catch (error) { if (error.name === 'InvalidStateError' || error.message.includes('closing')) { console.warn(`โš ๏ธ Database was closing during clear from ${storeName}`); } else { console.error(`โŒ Failed to clear stored data from ${storeName}:`, error); } } } registerBackgroundSync() { if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) { navigator.serviceWorker.ready.then(registration => { this.registration = registration; }); } else { console.warn('โš ๏ธ Background sync not supported'); } } setupPeriodicCleanup() { // Clean up old data every hour setInterval(() => { this.cleanupOldData(); }, 60 * 60 * 1000); } async cleanupOldData() { if (!this.offlineDB) return; const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days const cutoffTime = Date.now() - maxAge; try { await this.ensureDatabaseOpen(); const transaction = this.offlineDB.transaction(['offlineQueue', 'messageQueue'], 'readwrite'); // Clean offline queue const queueStore = transaction.objectStore('offlineQueue'); const queueIndex = queueStore.index('timestamp'); const queueRange = IDBKeyRange.upperBound(cutoffTime); const queueRequest = queueIndex.openCursor(queueRange); queueRequest.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { cursor.delete(); cursor.continue(); } }; // Clean message queue const messageStore = transaction.objectStore('messageQueue'); const messageIndex = messageStore.index('timestamp'); const messageRange = IDBKeyRange.upperBound(cutoffTime); const messageRequest = messageIndex.openCursor(messageRange); messageRequest.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { cursor.delete(); cursor.continue(); } }; console.log('๐Ÿงน Old offline data cleaned up'); } catch (error) { if (error.name === 'InvalidStateError' || error.message.includes('closing')) { console.warn('โš ๏ธ Database was closing during cleanup, skipping...'); } else { console.error('โŒ Failed to cleanup old data:', error); } } } handleOfflineDisconnection(details) { // Save connection state for recovery this.storeData('sessionData', { key: 'connection_state', wasConnected: true, disconnectReason: details.reason, timestamp: Date.now(), data: details }); // Show user feedback this.showOfflineDisconnectionNotice(); } showOfflineDisconnectionNotice() { const notice = document.createElement('div'); notice.className = 'fixed bottom-4 left-4 right-4 bg-yellow-500/90 text-black p-4 rounded-lg backdrop-blur-sm z-50'; notice.innerHTML = `
Connection Interrupted
Your secure connection was lost due to network issues. It will be restored automatically when you're back online.
`; document.body.appendChild(notice); setTimeout(() => { if (notice.parentElement) { notice.remove(); } }, 8000); } handleNetworkFailure(error) { // Queue the failed action for retry if appropriate if (this.shouldQueueFailedRequest(error)) { this.queueOfflineAction({ type: 'network_retry', data: { error: error?.message }, priority: 1, maxRetries: 2 }); } } shouldQueueFailedRequest(error) { if (!error) return false; const queueableErrors = [ 'fetch', 'network', 'connection', 'timeout', 'offline', 'ERR_NETWORK', 'ERR_INTERNET_DISCONNECTED' ]; const errorString = error.toString().toLowerCase(); return queueableErrors.some(err => errorString.includes(err)) && !this.isOnline; } isNetworkError(error) { if (!error) return false; const networkErrorPatterns = [ /fetch/i, /network/i, /connection/i, /timeout/i, /offline/i, /ERR_NETWORK/i, /ERR_INTERNET_DISCONNECTED/i ]; const errorString = error.toString(); return networkErrorPatterns.some(pattern => pattern.test(errorString)); } handleInitializationError(error) { console.error('๐Ÿšจ Offline manager initialization error:', error); // Show fallback UI const fallback = document.createElement('div'); fallback.className = 'fixed bottom-4 right-4 bg-red-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm'; fallback.innerHTML = `
Offline Mode Unavailable
Some offline features may not work properly. Please ensure you have a stable internet connection.
`; document.body.appendChild(fallback); setTimeout(() => fallback.remove(), 8000); } showOfflineHelp() { const helpModal = document.createElement('div'); helpModal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 backdrop-blur-sm'; helpModal.innerHTML = `

Offline Mode Guide

What works offline:

  • โ€ข App interface and navigation
  • โ€ข Previously cached resources
  • โ€ข Session data and keys (preserved in memory)
  • โ€ข Message queuing for later delivery
  • โ€ข Basic cryptographic operations

What requires internet:

  • โ€ข P2P connections (WebRTC)
  • โ€ข Real-time messaging
  • โ€ข Session verification
  • โ€ข Key exchange with new peers

Automatic sync:

When you're back online, all queued messages and actions will be automatically synchronized. No data is lost.

Security Notice

Your encryption keys and session data remain secure even offline. SecureBit.chat never stores sensitive information on servers.

`; document.body.appendChild(helpModal); } promisifyRequest(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } // Public API methods async addToQueue(type, data, priority = 1) { return this.queueOfflineAction({ type, data, priority }); } async forceSync() { if (this.isOnline) { return this.processOfflineQueue(); } else { console.warn('โš ๏ธ Cannot sync while offline'); return Promise.resolve(); } } getStatus() { return { isOnline: this.isOnline, queueLength: this.offlineQueue.length, syncInProgress: this.syncInProgress, hasOfflineDB: !!this.offlineDB, lastSyncTime: this.lastSyncTime, reconnectAttempts: this.reconnectAttempts }; } clearOfflineData() { return Promise.all([ this.clearStoredData('offlineQueue'), this.clearStoredData('messageQueue'), this.clearStoredData('sessionData'), this.clearStoredData('appState') ]).then(() => { this.offlineQueue = []; console.log('๐Ÿ—‘๏ธ All offline data cleared'); }); } // Cleanup method destroy() { if (this.reconnectInterval) { clearInterval(this.reconnectInterval); this.reconnectInterval = null; } // Set sync flag to prevent new operations this.syncInProgress = true; // Close database connection if (this.offlineDB) { try { // Only close if database is not in a transaction // IndexedDB will automatically close when all transactions complete if (this.offlineDB.objectStoreNames.length > 0) { this.offlineDB.close(); } this.offlineDB = null; } catch (error) { console.warn('โš ๏ธ Error closing database:', error); this.offlineDB = null; } } console.log('๐Ÿงน Offline Manager destroyed'); } } // Singleton pattern let instance = null; const PWAOfflineManagerAPI = { getInstance() { if (!instance) { instance = new PWAOfflineManager(); } return instance; }, init() { return this.getInstance(); } }; // Export for module use if (typeof module !== 'undefined' && module.exports) { module.exports = PWAOfflineManagerAPI; } else if (typeof window !== 'undefined' && !window.PWAOfflineManager) { window.PWAOfflineManager = PWAOfflineManagerAPI; } // Auto-initialize when DOM is ready if (typeof window !== 'undefined' && !window.pwaOfflineManager) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { if (!window.pwaOfflineManager) { window.pwaOfflineManager = PWAOfflineManagerAPI.init(); } }); } else { if (!window.pwaOfflineManager) { window.pwaOfflineManager = PWAOfflineManagerAPI.init(); } } }