# Implementation Plan: Migrating from LocalStorage to IndexedDB ## Overview This document outlines the plan for migrating the WebAssembly crypto example application from using `localStorage` to `IndexedDB` for persisting encrypted key spaces. The primary motivations for this migration are: 1. Transaction capabilities for better data integrity 2. Improved performance for larger data operations 3. More structured approach to data storage ## Current Implementation The current implementation uses localStorage with the following key functions: ```javascript // LocalStorage functions for key spaces const STORAGE_PREFIX = 'crypto_space_'; // Save encrypted space to localStorage function saveSpaceToStorage(spaceName, encryptedData) { localStorage.setItem(`${STORAGE_PREFIX}${spaceName}`, encryptedData); } // Get encrypted space from localStorage function getSpaceFromStorage(spaceName) { return localStorage.getItem(`${STORAGE_PREFIX}${spaceName}`); } // List all spaces in localStorage function listSpacesFromStorage() { const spaces = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key.startsWith(STORAGE_PREFIX)) { spaces.push(key.substring(STORAGE_PREFIX.length)); } } return spaces; } // Remove space from localStorage function removeSpaceFromStorage(spaceName) { localStorage.removeItem(`${STORAGE_PREFIX}${spaceName}`); } ``` ## Implementation Plan ### 1. Database Structure - Create a database named 'CryptoSpaceDB' - Create an object store named 'keySpaces' with 'name' as the key path - Add indexes for efficient querying: 'name' (unique) and 'lastAccessed' ```mermaid erDiagram KeySpaces { string name PK string encryptedData date created date lastAccessed } ``` ### 2. Database Initialization Create a module for initializing and managing the IndexedDB database: ```javascript // Database constants const DB_NAME = 'CryptoSpaceDB'; const DB_VERSION = 1; const STORE_NAME = 'keySpaces'; // Initialize the database function initDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = (event) => { reject('Error opening database: ' + event.target.error); }; request.onsuccess = (event) => { const db = event.target.result; resolve(db); }; request.onupgradeneeded = (event) => { const db = event.target.result; // Create object store for key spaces if it doesn't exist if (!db.objectStoreNames.contains(STORE_NAME)) { const store = db.createObjectStore(STORE_NAME, { keyPath: 'name' }); store.createIndex('name', 'name', { unique: true }); store.createIndex('lastAccessed', 'lastAccessed', { unique: false }); } }; }); } // Get database connection function getDB() { return initDatabase(); } ``` ### 3. Replace Storage Functions Replace the localStorage functions with IndexedDB equivalents: ```javascript // Save encrypted space to IndexedDB async function saveSpaceToStorage(spaceName, encryptedData) { const db = await getDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); const space = { name: spaceName, encryptedData: encryptedData, created: new Date(), lastAccessed: new Date() }; const request = store.put(space); request.onsuccess = () => { resolve(); }; request.onerror = (event) => { reject('Error saving space: ' + event.target.error); }; transaction.oncomplete = () => { db.close(); }; }); } // Get encrypted space from IndexedDB async function getSpaceFromStorage(spaceName) { const db = await getDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], 'readonly'); const store = transaction.objectStore(STORE_NAME); const request = store.get(spaceName); request.onsuccess = (event) => { const space = event.target.result; if (space) { // Update last accessed timestamp updateLastAccessed(spaceName).catch(console.error); resolve(space.encryptedData); } else { resolve(null); } }; request.onerror = (event) => { reject('Error retrieving space: ' + event.target.error); }; transaction.oncomplete = () => { db.close(); }; }); } // Update last accessed timestamp async function updateLastAccessed(spaceName) { const db = await getDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); const request = store.get(spaceName); request.onsuccess = (event) => { const space = event.target.result; if (space) { space.lastAccessed = new Date(); store.put(space); resolve(); } else { resolve(); } }; transaction.oncomplete = () => { db.close(); }; }); } // List all spaces in IndexedDB async function listSpacesFromStorage() { const db = await getDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], 'readonly'); const store = transaction.objectStore(STORE_NAME); const request = store.openCursor(); const spaces = []; request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { spaces.push(cursor.value.name); cursor.continue(); } else { resolve(spaces); } }; request.onerror = (event) => { reject('Error listing spaces: ' + event.target.error); }; transaction.oncomplete = () => { db.close(); }; }); } // Remove space from IndexedDB async function removeSpaceFromStorage(spaceName) { const db = await getDB(); return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); const request = store.delete(spaceName); request.onsuccess = () => { resolve(); }; request.onerror = (event) => { reject('Error removing space: ' + event.target.error); }; transaction.oncomplete = () => { db.close(); }; }); } ``` ### 4. Update Application Flow Update the login, logout, and other functions to handle the asynchronous nature of IndexedDB: ```javascript // Login to a space async function performLogin() { const spaceName = document.getElementById('space-name').value.trim(); const password = document.getElementById('space-password').value; if (!spaceName || !password) { document.getElementById('space-result').textContent = 'Please enter both space name and password'; return; } try { // Get encrypted space from IndexedDB const encryptedSpace = await getSpaceFromStorage(spaceName); if (!encryptedSpace) { document.getElementById('space-result').textContent = `Space "${spaceName}" not found`; return; } // Decrypt the space const result = decrypt_key_space(encryptedSpace, password); if (result === 0) { isLoggedIn = true; currentSpace = spaceName; updateLoginUI(); updateKeypairsList(); document.getElementById('space-result').textContent = `Successfully logged in to space "${spaceName}"`; // Setup auto-logout updateActivity(); setupAutoLogout(); // Add activity listeners document.addEventListener('click', updateActivity); document.addEventListener('keypress', updateActivity); } else { document.getElementById('space-result').textContent = `Error logging in: ${result}`; } } catch (e) { document.getElementById('space-result').textContent = `Error: ${e}`; } } // Create a new space async function performCreateSpace() { const spaceName = document.getElementById('space-name').value.trim(); const password = document.getElementById('space-password').value; if (!spaceName || !password) { document.getElementById('space-result').textContent = 'Please enter both space name and password'; return; } try { // Check if space already exists const existingSpace = await getSpaceFromStorage(spaceName); if (existingSpace) { document.getElementById('space-result').textContent = `Space "${spaceName}" already exists`; return; } // Create new space const result = create_key_space(spaceName); if (result === 0) { // Encrypt and save the space const encryptedSpace = encrypt_key_space(password); await saveSpaceToStorage(spaceName, encryptedSpace); isLoggedIn = true; currentSpace = spaceName; updateLoginUI(); updateKeypairsList(); document.getElementById('space-result').textContent = `Successfully created space "${spaceName}"`; // Setup auto-logout updateActivity(); setupAutoLogout(); // Add activity listeners document.addEventListener('click', updateActivity); document.addEventListener('keypress', updateActivity); } else { document.getElementById('space-result').textContent = `Error creating space: ${result}`; } } catch (e) { document.getElementById('space-result').textContent = `Error: ${e}`; } } // Delete a space from storage async function deleteSpace(spaceName) { if (!spaceName) return false; try { // Check if space exists const existingSpace = await getSpaceFromStorage(spaceName); if (!existingSpace) { return false; } // Remove from IndexedDB await removeSpaceFromStorage(spaceName); // If this was the current space, logout if (isLoggedIn && currentSpace === spaceName) { performLogout(); } return true; } catch (e) { console.error('Error deleting space:', e); return false; } } // Update the spaces dropdown list async function updateSpacesList() { const spacesList = document.getElementById('space-list'); // Clear existing options while (spacesList.options.length > 1) { spacesList.remove(1); } try { // Get spaces list const spaces = await listSpacesFromStorage(); // Add options for each space spaces.forEach(spaceName => { const option = document.createElement('option'); option.value = spaceName; option.textContent = spaceName; spacesList.appendChild(option); }); } catch (e) { console.error('Error updating spaces list:', e); } } // Save the current space to storage async function saveCurrentSpace() { if (!isLoggedIn || !currentSpace) return; try { // Store the password in a session variable when logging in // and use it here to avoid issues when the password field is cleared const password = document.getElementById('space-password').value; if (!password) { console.error('Password not available for saving space'); alert('Please re-enter your password to save changes'); return; } const encryptedSpace = encrypt_key_space(password); await saveSpaceToStorage(currentSpace, encryptedSpace); } catch (e) { console.error('Error saving space:', e); } } ``` ### 5. Update Event Handlers Update the event handlers in the `run()` function to handle asynchronous operations: ```javascript document.getElementById('delete-space-button').addEventListener('click', async () => { if (confirm(`Are you sure you want to delete the space "${currentSpace}"? This action cannot be undone.`)) { try { if (await deleteSpace(currentSpace)) { document.getElementById('space-result').textContent = `Space "${currentSpace}" deleted successfully`; } else { document.getElementById('space-result').textContent = `Error deleting space "${currentSpace}"`; } } catch (e) { document.getElementById('space-result').textContent = `Error: ${e}`; } } }); document.getElementById('delete-selected-space-button').addEventListener('click', async () => { const selectedSpace = document.getElementById('space-list').value; if (!selectedSpace) { document.getElementById('space-result').textContent = 'Please select a space to delete'; return; } if (confirm(`Are you sure you want to delete the space "${selectedSpace}"? This action cannot be undone.`)) { try { if (await deleteSpace(selectedSpace)) { document.getElementById('space-result').textContent = `Space "${selectedSpace}" deleted successfully`; await updateSpacesList(); } else { document.getElementById('space-result').textContent = `Error deleting space "${selectedSpace}"`; } } catch (e) { document.getElementById('space-result').textContent = `Error: ${e}`; } } }); ``` ## Testing Strategy 1. **Unit Tests**: - Test individual IndexedDB functions - Verify CRUD operations work correctly 2. **Integration Tests**: - Test full application flow with IndexedDB - Verify UI updates correctly 3. **Error Handling Tests**: - Test database connection errors - Test transaction rollbacks 4. **Performance Tests**: - Compare performance with localStorage - Verify improved performance for larger data sets ## Potential Challenges and Solutions 1. **Browser Compatibility**: - IndexedDB is supported in all modern browsers, but older browsers might have compatibility issues - Consider using a feature detection approach before initializing IndexedDB - Provide a fallback mechanism for browsers that don't support IndexedDB 2. **Transaction Management**: - Properly manage transactions to maintain data integrity - Ensure all operations within a transaction are completed or rolled back - Use appropriate transaction modes ('readonly' or 'readwrite') 3. **Error Handling**: - Implement comprehensive error handling for all IndexedDB operations - Provide user-friendly error messages - Log detailed error information for debugging 4. **Asynchronous Operations**: - Handle Promise rejections with try/catch blocks - Provide loading indicators for operations that might take time - Consider using async/await for cleaner code and better error handling ## Implementation Steps 1. Create the database initialization module 2. Implement the IndexedDB storage functions 3. Update the UI functions to handle asynchronous operations 4. Add comprehensive error handling 5. Test all functionality 6. Deploy the updated application ## Conclusion Migrating from localStorage to IndexedDB will provide better performance, transaction capabilities, and a more structured approach to data storage. The asynchronous nature of IndexedDB requires updates to the application flow, but the benefits outweigh the implementation effort.