webassembly/implementation_plan_indexeddb.md
2025-04-21 13:02:32 +02:00

15 KiB

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:

// 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'
erDiagram
    KeySpaces {
        string name PK
        string encryptedData
        date created
        date lastAccessed
    }

2. Database Initialization

Create a module for initializing and managing the IndexedDB database:

// 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:

// 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:

// 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:

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.