fix: prevent encryption key loss and IndexedDB connection errors
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled

- Disable timer-based key rotation for Double Ratchet mode
- Auto-reinitialize encryption keys when missing but ECDH available
- Preserve active keys during periodic cleanup in ratchet sessions
- Fix IndexedDB "database closing" errors with connection checking
- Add individual transactions per queue item to prevent race conditions
This commit is contained in:
lockbitchat
2025-11-04 13:14:24 -04:00
parent 207e51361c
commit 79c8d08782
12 changed files with 315 additions and 67 deletions
+11 -17
View File
@@ -1,4 +1,4 @@
# SecureBit.chat v4.4.99 # SecureBit.chat v4.5.22
<div align="center"> <div align="center">
@@ -31,22 +31,16 @@ SecureBit.chat is a revolutionary peer-to-peer messenger that prioritizes your p
--- ---
## ✨ What's New in v4.4.99 ## ✨ What's New in v4.5.22
### 🔔 Secure Browser Notifications ### fix: prevent encryption key loss and IndexedDB connection errors
- Smart delivery when user is away from chat tab
- Cross-browser compatibility (Chrome, Firefox, Safari, Edge) - Disable timer-based key rotation for Double Ratchet mode
- Page Visibility API integration with proper tab focus detection - Auto-reinitialize encryption keys when missing but ECDH available
- XSS protection with text sanitization and URL validation - Preserve active keys during periodic cleanup in ratchet sessions
- Rate limiting and spam protection - Fix IndexedDB "database closing" errors with connection checking
- Automatic cleanup and memory management - Add individual transactions per queue item to prevent race conditions
### 🧹 Code Cleanup & Architecture
- Removed session management logic for simplified architecture
- Eliminated experimental Bluetooth module
- Cleaned debug logging from production code
- Removed test functions from production build
- Enhanced error handling for production stability
### 🛡️ Security Enhancements ### 🛡️ Security Enhancements
- **ECDH + DTLS + SAS System** - Triple-layer security verification - **ECDH + DTLS + SAS System** - Triple-layer security verification
@@ -170,7 +164,7 @@ Modern browser with WebRTC support (Chrome 60+, Firefox 60+, Safari 12+), HTTPS
## 🗺️ Roadmap ## 🗺️ Roadmap
**Current: v4.4.99** - Browser Notifications & Code Cleanup ✅ **Current: v4.5.22** - Browser Notifications & Code Cleanup ✅
**Next Releases:** **Next Releases:**
@@ -336,7 +330,7 @@ MIT License - see **LICENSE** file for details.
--- ---
**Latest Release: v4.4.99** - Browser Notifications & Code Cleanup **Latest Release: v4.5.22** - Browser Notifications & Code Cleanup
[🚀 Try Now](https://securebitchat.github.io/securebit-chat/) • [⭐ Star on GitHub](https://github.com/SecureBitChat/securebit-chat) [🚀 Try Now](https://securebitchat.github.io/securebit-chat/) • [⭐ Star on GitHub](https://github.com/SecureBitChat/securebit-chat)
+53 -7
View File
@@ -4724,7 +4724,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
}; };
this.onFileReceived = null; this.onFileReceived = null;
this.onFileError = null; this.onFileError = null;
this.keyRotationInterval = _EnhancedSecureWebRTCManager.TIMEOUTS.KEY_ROTATION_INTERVAL; this.keyRotationInterval = null;
this.lastKeyRotation = Date.now(); this.lastKeyRotation = Date.now();
this.currentKeyVersion = 0; this.currentKeyVersion = 0;
this.keyVersions = /* @__PURE__ */ new Map(); this.keyVersions = /* @__PURE__ */ new Map();
@@ -4761,6 +4761,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
packetPadding: this._config.packetPadding.enabled, packetPadding: this._config.packetPadding.enabled,
antiFingerprinting: this._config.antiFingerprinting.enabled antiFingerprinting: this._config.antiFingerprinting.enabled
}); });
this.sessionMode = "ratchet";
this._hardenDebugModeReferences(); this._hardenDebugModeReferences();
this._initializeUnifiedScheduler(); this._initializeUnifiedScheduler();
this._syncSecurityFeaturesWithTariff(); this._syncSecurityFeaturesWithTariff();
@@ -5357,9 +5358,6 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
if (this._keyStorageStats.activeKeys > 10) { if (this._keyStorageStats.activeKeys > 10) {
this._secureLog("warn", "\u26A0\uFE0F High number of active keys detected. Consider rotation."); this._secureLog("warn", "\u26A0\uFE0F High number of active keys detected. Consider rotation.");
} }
if (Date.now() - (this._keyStorageStats.lastRotation || 0) > 36e5) {
this._rotateKeys();
}
} }
/** /**
* Send heartbeat message (called by unified scheduler) * Send heartbeat message (called by unified scheduler)
@@ -6791,7 +6789,12 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
async _performPeriodicMemoryCleanup() { async _performPeriodicMemoryCleanup() {
try { try {
this._secureMemoryManager.isCleaning = true; this._secureMemoryManager.isCleaning = true;
this._secureCleanupCryptographicMaterials(); const shouldPreserveActiveKeys = this.sessionMode === "ratchet" && this.isConnected && this.dataChannel && this.dataChannel.readyState === "open";
if (shouldPreserveActiveKeys) {
this._secureLog("debug", "\u{1F9F9} Skipping crypto key wipe during periodic cleanup (ratchet mode, active connection)");
} else {
this._secureCleanupCryptographicMaterials();
}
if (this.messageQueue && this.messageQueue.length > 100) { if (this.messageQueue && this.messageQueue.length > 100) {
const excessMessages = this.messageQueue.splice(0, this.messageQueue.length - 50); const excessMessages = this.messageQueue.splice(0, this.messageQueue.length - 50);
excessMessages.forEach((message, index) => { excessMessages.forEach((message, index) => {
@@ -7296,6 +7299,14 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
} }
const normalizedReceived = receivedFingerprint.toLowerCase().replace(/:/g, ""); const normalizedReceived = receivedFingerprint.toLowerCase().replace(/:/g, "");
const normalizedExpected = expectedFingerprint.toLowerCase().replace(/:/g, ""); const normalizedExpected = expectedFingerprint.toLowerCase().replace(/:/g, "");
if (this.sessionMode === "ratchet" && normalizedExpected === normalizedReceived) {
this._secureLog("info", "Same fingerprint detected \u2014 skip MITM warning (ratchet mode)", {
context,
timestamp: Date.now()
});
this.isVerified = true;
return true;
}
if (normalizedReceived !== normalizedExpected) { if (normalizedReceived !== normalizedExpected) {
this._secureLog("error", "DTLS fingerprint mismatch - possible MITM attack", { this._secureLog("error", "DTLS fingerprint mismatch - possible MITM attack", {
context, context,
@@ -7664,6 +7675,38 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
} }
return hasAllKeys; return hasAllKeys;
} }
/**
* Attempt to reinitialize encryption keys if missing
* Uses existing ECDH key pair, peer public key, and session salt
* Returns true if keys were (re)initialized successfully
*/
async _tryReinitializeEncryptionKeys() {
try {
if (this.encryptionKey && this.macKey && this.metadataKey) {
return true;
}
const hasECDH = !!(this.ecdhKeyPair?.privateKey && (this.peerPublicKey || this.peerECDHPublicKey));
const peerPublicKey = this.peerPublicKey || this.peerECDHPublicKey;
if (!hasECDH || !peerPublicKey || !this.sessionSalt) {
return false;
}
const derivedKeys = await window.EnhancedSecureCryptoUtils.deriveSharedKeys(
this.ecdhKeyPair.privateKey,
peerPublicKey,
this.sessionSalt
);
await this._setEncryptionKeys(
derivedKeys.messageKey,
derivedKeys.macKey,
derivedKeys.metadataKey,
derivedKeys.fingerprint
);
return !!(this.encryptionKey && this.macKey && this.metadataKey);
} catch (error) {
this._secureLog("error", "Failed to reinitialize encryption keys", { error: error.message });
return false;
}
}
/** /**
* Checks whether a message is a file-transfer message * Checks whether a message is a file-transfer message
* @param {string|object} data - message payload * @param {string|object} data - message payload
@@ -9261,6 +9304,9 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
throw new Error("Data channel not ready"); throw new Error("Data channel not ready");
} }
try { try {
if (!(this.encryptionKey && this.macKey && this.metadataKey)) {
await this._tryReinitializeEncryptionKeys();
}
this._secureLog("debug", "sendMessage called", { this._secureLog("debug", "sendMessage called", {
hasDataChannel: !!this.dataChannel, hasDataChannel: !!this.dataChannel,
dataChannelReady: this.dataChannel?.readyState === "open", dataChannelReady: this.dataChannel?.readyState === "open",
@@ -15265,7 +15311,7 @@ Right-click or Ctrl+click to disconnect`,
React.createElement("p", { React.createElement("p", {
key: "subtitle", key: "subtitle",
className: "text-xs sm:text-sm text-muted hidden sm:block" className: "text-xs sm:text-sm text-muted hidden sm:block"
}, "End-to-end freedom v4.4.99") }, "End-to-end freedom v4.5.22")
]) ])
]), ]),
// Status and Controls - Responsive // Status and Controls - Responsive
@@ -16020,7 +16066,7 @@ function Roadmap() {
}, },
// current and future phases // current and future phases
{ {
version: "v4.4.99", version: "v4.5.22",
title: "Enhanced Security Edition", title: "Enhanced Security Edition",
status: "current", status: "current",
date: "Now", date: "Now",
+2 -2
View File
File diff suppressed because one or more lines are too long
Vendored
+1 -1
View File
@@ -1688,7 +1688,7 @@ var EnhancedSecureP2PChat = () => {
} catch (error) { } catch (error) {
} }
} }
handleMessage(" SecureBit.chat Enhanced Security Edition v4.4.99 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.", "system"); handleMessage(" SecureBit.chat Enhanced Security Edition v4.5.22 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.", "system");
const handleBeforeUnload = (event) => { const handleBeforeUnload = (event) => {
if (event.type === "beforeunload" && !isTabSwitching) { if (event.type === "beforeunload" && !isTabSwitching) {
if (webrtcManagerRef2.current && webrtcManagerRef2.current.isConnected()) { if (webrtcManagerRef2.current && webrtcManagerRef2.current.isConnected()) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "SecureBit.chat v4.4.99 - ECDH + DTLS + SAS", "name": "SecureBit.chat v4.5.22 - ECDH + DTLS + SAS",
"short_name": "SecureBit", "short_name": "SecureBit",
"description": "P2P messenger with ECDH + DTLS + SAS security, military-grade cryptography and Lightning Network payments", "description": "P2P messenger with ECDH + DTLS + SAS security, military-grade cryptography and Lightning Network payments",
"start_url": "./", "start_url": "./",
+1 -1
View File
@@ -1924,7 +1924,7 @@
} }
} }
handleMessage(' SecureBit.chat Enhanced Security Edition v4.4.99 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system'); handleMessage(' SecureBit.chat Enhanced Security Edition v4.5.22 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
const handleBeforeUnload = (event) => { const handleBeforeUnload = (event) => {
if (event.type === 'beforeunload' && !isTabSwitching) { if (event.type === 'beforeunload' && !isTabSwitching) {
+1 -1
View File
@@ -539,7 +539,7 @@ const EnhancedMinimalHeader = ({
React.createElement('p', { React.createElement('p', {
key: 'subtitle', key: 'subtitle',
className: 'text-xs sm:text-sm text-muted hidden sm:block' className: 'text-xs sm:text-sm text-muted hidden sm:block'
}, 'End-to-end freedom v4.4.99') }, 'End-to-end freedom v4.5.22')
]) ])
]), ]),
+1 -1
View File
@@ -75,7 +75,7 @@ function Roadmap() {
// current and future phases // current and future phases
{ {
version: "v4.4.99", version: "v4.5.22",
title: "Enhanced Security Edition", title: "Enhanced Security Edition",
status: "current", status: "current",
date: "Now", date: "Now",
+66 -6
View File
@@ -330,7 +330,7 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
this.onFileError = null; this.onFileError = null;
// PFS (Perfect Forward Secrecy) Implementation // PFS (Perfect Forward Secrecy) Implementation
this.keyRotationInterval = EnhancedSecureWebRTCManager.TIMEOUTS.KEY_ROTATION_INTERVAL; this.keyRotationInterval = null; // отключаем таймерную ротацию
this.lastKeyRotation = Date.now(); this.lastKeyRotation = Date.now();
this.currentKeyVersion = 0; this.currentKeyVersion = 0;
this.keyVersions = new Map(); // Store key versions for PFS this.keyVersions = new Map(); // Store key versions for PFS
@@ -372,6 +372,9 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
antiFingerprinting: this._config.antiFingerprinting.enabled antiFingerprinting: this._config.antiFingerprinting.enabled
}); });
// Session mode: 'time' | 'ratchet' (Double Ratchet controls key lifecycle)
this.sessionMode = 'ratchet';
// XSS Hardening - replace all window.DEBUG_MODE references // XSS Hardening - replace all window.DEBUG_MODE references
this._hardenDebugModeReferences(); this._hardenDebugModeReferences();
@@ -1183,9 +1186,6 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
this._secureLog('warn', '⚠️ High number of active keys detected. Consider rotation.'); this._secureLog('warn', '⚠️ High number of active keys detected. Consider rotation.');
} }
if (Date.now() - (this._keyStorageStats.lastRotation || 0) > 3600000) {
this._rotateKeys();
}
} }
/** /**
@@ -2900,8 +2900,13 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
try { try {
this._secureMemoryManager.isCleaning = true; this._secureMemoryManager.isCleaning = true;
// Clean up any remaining sensitive data // Clean up sensitive data, but DO NOT wipe active crypto in ratchet session
this._secureCleanupCryptographicMaterials(); const shouldPreserveActiveKeys = (this.sessionMode === 'ratchet') && this.isConnected && this.dataChannel && this.dataChannel.readyState === 'open';
if (shouldPreserveActiveKeys) {
this._secureLog('debug', '🧹 Skipping crypto key wipe during periodic cleanup (ratchet mode, active connection)');
} else {
this._secureCleanupCryptographicMaterials();
}
// Clean up message queue if it's too large // Clean up message queue if it's too large
if (this.messageQueue && this.messageQueue.length > 100) { if (this.messageQueue && this.messageQueue.length > 100) {
@@ -3545,6 +3550,16 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
const normalizedReceived = receivedFingerprint.toLowerCase().replace(/:/g, ''); const normalizedReceived = receivedFingerprint.toLowerCase().replace(/:/g, '');
const normalizedExpected = expectedFingerprint.toLowerCase().replace(/:/g, ''); const normalizedExpected = expectedFingerprint.toLowerCase().replace(/:/g, '');
// Ratchet mode: if fingerprint hasn't changed, treat as verified and skip warnings
if (this.sessionMode === 'ratchet' && normalizedExpected === normalizedReceived) {
this._secureLog('info', 'Same fingerprint detected — skip MITM warning (ratchet mode)', {
context: context,
timestamp: Date.now()
});
this.isVerified = true;
return true;
}
if (normalizedReceived !== normalizedExpected) { if (normalizedReceived !== normalizedExpected) {
this._secureLog('error', 'DTLS fingerprint mismatch - possible MITM attack', { this._secureLog('error', 'DTLS fingerprint mismatch - possible MITM attack', {
context: context, context: context,
@@ -4014,6 +4029,46 @@ this._secureLog('info', '🔒 Enhanced Mutex system fully initialized and valida
return hasAllKeys; return hasAllKeys;
} }
/**
* Attempt to reinitialize encryption keys if missing
* Uses existing ECDH key pair, peer public key, and session salt
* Returns true if keys were (re)initialized successfully
*/
async _tryReinitializeEncryptionKeys() {
try {
// If keys already present, nothing to do
if (this.encryptionKey && this.macKey && this.metadataKey) {
return true;
}
// Require ECDH materials and session salt to derive keys
const hasECDH = !!(this.ecdhKeyPair?.privateKey && (this.peerPublicKey || this.peerECDHPublicKey));
const peerPublicKey = this.peerPublicKey || this.peerECDHPublicKey;
if (!hasECDH || !peerPublicKey || !this.sessionSalt) {
return false;
}
// Derive fresh keys
const derivedKeys = await window.EnhancedSecureCryptoUtils.deriveSharedKeys(
this.ecdhKeyPair.privateKey,
peerPublicKey,
this.sessionSalt
);
await this._setEncryptionKeys(
derivedKeys.messageKey,
derivedKeys.macKey,
derivedKeys.metadataKey,
derivedKeys.fingerprint
);
return !!(this.encryptionKey && this.macKey && this.metadataKey);
} catch (error) {
this._secureLog('error', 'Failed to reinitialize encryption keys', { error: error.message });
return false;
}
}
/** /**
* Checks whether a message is a file-transfer message * Checks whether a message is a file-transfer message
* @param {string|object} data - message payload * @param {string|object} data - message payload
@@ -6056,6 +6111,11 @@ async processOrderedPackets() {
} }
try { try {
// Ensure encryption keys are available; try to reinitialize if needed
if (!(this.encryptionKey && this.macKey && this.metadataKey)) {
await this._tryReinitializeEncryptionKeys();
}
this._secureLog('debug', 'sendMessage called', { this._secureLog('debug', 'sendMessage called', {
hasDataChannel: !!this.dataChannel, hasDataChannel: !!this.dataChannel,
dataChannelReady: this.dataChannel?.readyState === 'open', dataChannelReady: this.dataChannel?.readyState === 'open',
+173 -25
View File
@@ -95,6 +95,18 @@ class PWAOfflineManager {
request.onsuccess = () => { request.onsuccess = () => {
this.offlineDB = request.result; 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); resolve(this.offlineDB);
}; };
@@ -122,6 +134,37 @@ class PWAOfflineManager {
}); });
} }
/**
* 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() { setupEventListeners() {
// Network status changes // Network status changes
window.addEventListener('online', () => { window.addEventListener('online', () => {
@@ -345,12 +388,6 @@ class PWAOfflineManager {
} }
async queueOfflineAction(action) { async queueOfflineAction(action) {
if (!this.offlineDB) {
console.warn('⚠️ Offline database not available');
this.offlineQueue.push(action);
return;
}
const queueItem = { const queueItem = {
...action, ...action,
id: Date.now() + Math.random(), id: Date.now() + Math.random(),
@@ -360,21 +397,32 @@ class PWAOfflineManager {
maxRetries: action.maxRetries || 3 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 { try {
await this.ensureDatabaseOpen();
const transaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite'); const transaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
const store = transaction.objectStore('offlineQueue'); const store = transaction.objectStore('offlineQueue');
await this.promisifyRequest(store.add(queueItem)); await this.promisifyRequest(store.add(queueItem));
this.offlineQueue.push(queueItem);
// Try to register background sync // Try to register background sync
if (this.registration) { if (this.registration) {
await this.registration.sync.register('offline-sync'); await this.registration.sync.register('offline-sync');
} }
} catch (error) { } catch (error) {
console.error('❌ Failed to queue offline action:', error); if (error.name === 'InvalidStateError' || error.message.includes('closing')) {
// Fallback to memory queue console.warn('⚠️ Database was closing, item added to memory queue only');
this.offlineQueue.push(queueItem); } else {
console.error('❌ Failed to queue offline action:', error);
}
// Item already in memory queue, so no action needed
} }
} }
@@ -391,9 +439,32 @@ class PWAOfflineManager {
try { try {
// Process database queue // Process database queue
if (this.offlineDB) { if (this.offlineDB) {
const transaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite'); // Ensure database is open before processing
const store = transaction.objectStore('offlineQueue'); await this.ensureDatabaseOpen();
const allItems = await this.promisifyRequest(store.getAll());
// 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 // Sort by priority and timestamp
allItems.sort((a, b) => { allItems.sort((a, b) => {
@@ -403,10 +474,17 @@ class PWAOfflineManager {
return a.timestamp - b.timestamp; // Older 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) { for (const item of allItems) {
try { try {
await this.processQueueItem(item); await this.processQueueItem(item);
await this.promisifyRequest(store.delete(item.id));
// 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++; processedCount++;
} catch (error) { } catch (error) {
console.error('❌ Failed to process offline action:', error); console.error('❌ Failed to process offline action:', error);
@@ -417,11 +495,25 @@ class PWAOfflineManager {
if (item.retryCount >= item.maxRetries) { if (item.retryCount >= item.maxRetries) {
// Max retries reached, remove from queue // Max retries reached, remove from queue
await this.promisifyRequest(store.delete(item.id)); try {
console.log('❌ Max retries reached for action:', item.type); 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 { } else {
// Update retry count in database // Update retry count in database
await this.promisifyRequest(store.put(item)); 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);
}
} }
} }
} }
@@ -640,6 +732,8 @@ class PWAOfflineManager {
if (!this.offlineDB) return; if (!this.offlineDB) return;
try { try {
await this.ensureDatabaseOpen();
const appState = { const appState = {
component: 'app_state', component: 'app_state',
timestamp: Date.now(), timestamp: Date.now(),
@@ -669,7 +763,11 @@ class PWAOfflineManager {
await this.promisifyRequest(store.put(appState)); await this.promisifyRequest(store.put(appState));
} catch (error) { } catch (error) {
console.error('❌ Failed to save application state:', 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);
}
} }
} }
@@ -695,9 +793,20 @@ class PWAOfflineManager {
throw new Error('Offline database not available'); throw new Error('Offline database not available');
} }
const transaction = this.offlineDB.transaction([storeName], 'readwrite'); try {
const store = transaction.objectStore(storeName); await this.ensureDatabaseOpen();
return this.promisifyRequest(store.put(data)); 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) { async getStoredData(storeName, key) {
@@ -706,11 +815,24 @@ class PWAOfflineManager {
} }
try { try {
await this.ensureDatabaseOpen();
const transaction = this.offlineDB.transaction([storeName], 'readonly'); const transaction = this.offlineDB.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName); const store = transaction.objectStore(storeName);
const result = await this.promisifyRequest(store.get(key)); const result = await this.promisifyRequest(store.get(key));
return result; return result;
} catch (error) { } 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); console.error(`❌ Failed to get stored data from ${storeName}:`, error);
return null; return null;
} }
@@ -720,6 +842,7 @@ class PWAOfflineManager {
if (!this.offlineDB) return; if (!this.offlineDB) return;
try { try {
await this.ensureDatabaseOpen();
const transaction = this.offlineDB.transaction([storeName], 'readwrite'); const transaction = this.offlineDB.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName); const store = transaction.objectStore(storeName);
@@ -730,7 +853,11 @@ class PWAOfflineManager {
} }
} catch (error) { } catch (error) {
console.error(`❌ Failed to clear stored data from ${storeName}:`, 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);
}
} }
} }
@@ -758,6 +885,8 @@ class PWAOfflineManager {
const cutoffTime = Date.now() - maxAge; const cutoffTime = Date.now() - maxAge;
try { try {
await this.ensureDatabaseOpen();
const transaction = this.offlineDB.transaction(['offlineQueue', 'messageQueue'], 'readwrite'); const transaction = this.offlineDB.transaction(['offlineQueue', 'messageQueue'], 'readwrite');
// Clean offline queue // Clean offline queue
@@ -790,7 +919,11 @@ class PWAOfflineManager {
console.log('🧹 Old offline data cleaned up'); console.log('🧹 Old offline data cleaned up');
} catch (error) { } catch (error) {
console.error('❌ Failed to cleanup old data:', 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);
}
} }
} }
@@ -1030,10 +1163,25 @@ class PWAOfflineManager {
destroy() { destroy() {
if (this.reconnectInterval) { if (this.reconnectInterval) {
clearInterval(this.reconnectInterval); clearInterval(this.reconnectInterval);
this.reconnectInterval = null;
} }
// Set sync flag to prevent new operations
this.syncInProgress = true;
// Close database connection
if (this.offlineDB) { if (this.offlineDB) {
this.offlineDB.close(); 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'); console.log('🧹 Offline Manager destroyed');
+4 -4
View File
@@ -1,9 +1,9 @@
// SecureBit.chat Service Worker // SecureBit.chat Service Worker
// Conservative PWA Edition v4.4.99 - Minimal Caching Strategy // Conservative PWA Edition v4.5.22 - Minimal Caching Strategy
const CACHE_NAME = 'securebit-pwa-v4.4.99'; const CACHE_NAME = 'securebit-pwa-v4.5.22';
const STATIC_CACHE = 'securebit-pwa-static-v4.4.99'; const STATIC_CACHE = 'securebit-pwa-static-v4.5.22';
const DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.4.99'; const DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.5.22';
// Essential files for PWA offline functionality // Essential files for PWA offline functionality
const STATIC_ASSETS = [ const STATIC_ASSETS = [