// 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() {
console.log('๐ด PWA Offline Manager initializing...');
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();
}
console.log('โ
PWA Offline Manager initialized');
} 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;
console.log('๐พ Offline database opened successfully');
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)) {
console.log(`๐ฆ Creating object store: ${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);
});
}
}
});
};
});
}
setupEventListeners() {
// Network status changes
window.addEventListener('online', () => {
console.log('๐ Connection restored');
this.isOnline = true;
this.reconnectAttempts = 0;
this.updateConnectionStatus(true);
this.handleConnectionRestored();
});
window.addEventListener('offline', () => {
console.log('๐ด Connection lost');
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 = `
`;
this.offlineIndicator.classList.remove('hidden');
// Hide after 3 seconds
setTimeout(() => {
this.offlineIndicator.classList.add('hidden');
}, 3000);
} else {
this.offlineIndicator.innerHTML = `
`;
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++;
console.log(`๐ Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
// Try to detect if we're actually back online
this.checkOnlineStatus();
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
clearInterval(this.reconnectInterval);
this.reconnectInterval = null;
console.log('โ Max reconnection attempts reached');
}
}, 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) {
// We're actually online but navigator.onLine is wrong
console.log('๐ Detected online status, updating...');
this.isOnline = true;
this.handleConnectionRestored();
}
} catch (error) {
// Still offline
console.log('๐ด Still offline');
}
}
async queueOfflineAction(action) {
if (!this.offlineDB) {
console.warn('โ ๏ธ Offline database not available');
this.offlineQueue.push(action);
return;
}
const queueItem = {
...action,
id: Date.now() + Math.random(),
timestamp: Date.now(),
priority: action.priority || 1,
retryCount: 0,
maxRetries: action.maxRetries || 3
};
try {
const transaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
const store = transaction.objectStore('offlineQueue');
await this.promisifyRequest(store.add(queueItem));
console.log('๐ค Action queued for offline sync:', action.type);
this.offlineQueue.push(queueItem);
// Try to register background sync
if (this.registration) {
await this.registration.sync.register('offline-sync');
}
} catch (error) {
console.error('โ Failed to queue offline action:', error);
// Fallback to memory queue
this.offlineQueue.push(queueItem);
}
}
async processOfflineQueue() {
if (this.syncInProgress || !this.isOnline) {
return;
}
this.syncInProgress = true;
console.log('๐ Processing offline queue...');
let processedCount = 0;
let errorCount = 0;
try {
// Process database queue
if (this.offlineDB) {
const transaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
const store = transaction.objectStore('offlineQueue');
const allItems = await this.promisifyRequest(store.getAll());
// 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
});
for (const item of allItems) {
try {
await this.processQueueItem(item);
await this.promisifyRequest(store.delete(item.id));
processedCount++;
console.log('โ
Processed offline action:', item.type);
} 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
await this.promisifyRequest(store.delete(item.id));
console.log('โ Max retries reached for action:', item.type);
} else {
// Update retry count in database
await this.promisifyRequest(store.put(item));
}
}
}
}
// 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 = `
`;
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) {
console.log('๐ Attempting WebRTC reconnection...');
// 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 {
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));
console.log('๐พ Application state saved for offline recovery');
} catch (error) {
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');
}
const transaction = this.offlineDB.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
return this.promisifyRequest(store.put(data));
}
async getStoredData(storeName, key) {
if (!this.offlineDB) {
return null;
}
try {
const transaction = this.offlineDB.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const result = await this.promisifyRequest(store.get(key));
return result;
} catch (error) {
console.error(`โ Failed to get stored data from ${storeName}:`, error);
return null;
}
}
async clearStoredData(storeName, key = null) {
if (!this.offlineDB) return;
try {
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());
}
console.log(`๐๏ธ Cleared stored data from ${storeName}`);
} catch (error) {
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;
console.log('๐ก Background sync registered');
});
} 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 {
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) {
console.error('โ Failed to cleanup old data:', error);
}
}
handleOfflineDisconnection(details) {
console.log('๐ WebRTC disconnected while offline:', 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) {
console.log('๐ Network failure detected:', error?.message);
// 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 = `
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);
}
if (this.offlineDB) {
this.offlineDB.close();
}
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();
}
}
}