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
+173 -25
View File
@@ -95,6 +95,18 @@ class PWAOfflineManager {
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);
};
@@ -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() {
// Network status changes
window.addEventListener('online', () => {
@@ -345,12 +388,6 @@ class PWAOfflineManager {
}
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(),
@@ -360,21 +397,32 @@ class PWAOfflineManager {
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));
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);
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
}
}
@@ -391,9 +439,32 @@ class PWAOfflineManager {
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());
// 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) => {
@@ -403,10 +474,17 @@ class PWAOfflineManager {
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);
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++;
} catch (error) {
console.error('❌ Failed to process offline action:', error);
@@ -417,11 +495,25 @@ class PWAOfflineManager {
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);
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
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;
try {
await this.ensureDatabaseOpen();
const appState = {
component: 'app_state',
timestamp: Date.now(),
@@ -669,7 +763,11 @@ class PWAOfflineManager {
await this.promisifyRequest(store.put(appState));
} 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');
}
const transaction = this.offlineDB.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
return this.promisifyRequest(store.put(data));
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) {
@@ -706,11 +815,24 @@ class PWAOfflineManager {
}
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;
}
@@ -720,6 +842,7 @@ class PWAOfflineManager {
if (!this.offlineDB) return;
try {
await this.ensureDatabaseOpen();
const transaction = this.offlineDB.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
@@ -730,7 +853,11 @@ class PWAOfflineManager {
}
} 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;
try {
await this.ensureDatabaseOpen();
const transaction = this.offlineDB.transaction(['offlineQueue', 'messageQueue'], 'readwrite');
// Clean offline queue
@@ -790,7 +919,11 @@ class PWAOfflineManager {
console.log('🧹 Old offline data cleaned up');
} 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() {
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) {
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');