Browser APIs

JavaScript IndexedDB: Complete Guide to Client-Side Storage

Master IndexedDB for large-scale client-side storage in JavaScript. Learn database operations, transactions, and building offline-capable applications.

By JavaScriptDoc Team
indexeddbstoragedatabaseofflinejavascript

JavaScript IndexedDB: Complete Guide to Client-Side Storage

IndexedDB is a powerful, low-level API for client-side storage of large amounts of structured data, including files and blobs. It provides a database-like storage mechanism for web applications.

Understanding IndexedDB

IndexedDB is a transactional database system that lets you store and retrieve objects indexed with keys.

// Basic IndexedDB concepts
// 1. Database - Contains object stores
// 2. Object Store - Like a table, stores records
// 3. Index - Provides alternate keys for accessing data
// 4. Transaction - Groups operations together
// 5. Cursor - Iterates over records

// Check for IndexedDB support
if ('indexedDB' in window) {
  console.log('IndexedDB is supported');
} else {
  console.log('IndexedDB is not supported');
}

// Open a database
const request = indexedDB.open('MyDatabase', 1);

request.onerror = function (event) {
  console.error('Database error:', event.target.error);
};

request.onsuccess = function (event) {
  const db = event.target.result;
  console.log('Database opened successfully');
};

request.onupgradeneeded = function (event) {
  const db = event.target.result;

  // Create object store
  if (!db.objectStoreNames.contains('customers')) {
    const objectStore = db.createObjectStore('customers', { keyPath: 'id' });

    // Create indexes
    objectStore.createIndex('name', 'name', { unique: false });
    objectStore.createIndex('email', 'email', { unique: true });
  }
};

Database Operations

Creating and Managing Databases

class IndexedDBManager {
  constructor(dbName, version) {
    this.dbName = dbName;
    this.version = version;
    this.db = null;
  }

  async open() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve(this.db);
      };

      request.onupgradeneeded = (event) => {
        this.db = event.target.result;
        this.setupDatabase(event);
      };
    });
  }

  setupDatabase(event) {
    const db = event.target.result;
    const oldVersion = event.oldVersion;

    // Version 1: Create initial schema
    if (oldVersion < 1) {
      // Users store
      const userStore = db.createObjectStore('users', {
        keyPath: 'id',
        autoIncrement: true,
      });

      userStore.createIndex('email', 'email', { unique: true });
      userStore.createIndex('createdAt', 'createdAt', { unique: false });

      // Products store
      const productStore = db.createObjectStore('products', {
        keyPath: 'sku',
      });

      productStore.createIndex('category', 'category', { unique: false });
      productStore.createIndex('price', 'price', { unique: false });
      productStore.createIndex('name', 'name', { unique: false });

      // Orders store with compound index
      const orderStore = db.createObjectStore('orders', {
        keyPath: 'orderId',
        autoIncrement: true,
      });

      orderStore.createIndex('userId', 'userId', { unique: false });
      orderStore.createIndex('status', 'status', { unique: false });
      orderStore.createIndex('userStatus', ['userId', 'status'], {
        unique: false,
      });
    }

    // Version 2: Add new indexes
    if (oldVersion < 2) {
      const userStore = event.target.transaction.objectStore('users');
      userStore.createIndex('username', 'username', { unique: true });
    }
  }

  async deleteDatabase() {
    if (this.db) {
      this.db.close();
    }

    return new Promise((resolve, reject) => {
      const deleteReq = indexedDB.deleteDatabase(this.dbName);
      deleteReq.onsuccess = () => resolve();
      deleteReq.onerror = () => reject(deleteReq.error);
    });
  }
}

CRUD Operations

class IndexedDBStore {
  constructor(db, storeName) {
    this.db = db;
    this.storeName = storeName;
  }

  // Create/Update
  async put(data) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      const request = store.put(data);

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  // Create (will fail if key exists)
  async add(data) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      const request = store.add(data);

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  // Read
  async get(key) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readonly');
      const store = transaction.objectStore(this.storeName);
      const request = store.get(key);

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  // Read by index
  async getByIndex(indexName, value) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readonly');
      const store = transaction.objectStore(this.storeName);
      const index = store.index(indexName);
      const request = index.get(value);

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  // Get all matching index
  async getAllByIndex(indexName, value) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readonly');
      const store = transaction.objectStore(this.storeName);
      const index = store.index(indexName);
      const request = index.getAll(value);

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  // Update
  async update(key, updates) {
    const existing = await this.get(key);

    if (!existing) {
      throw new Error('Record not found');
    }

    const updated = { ...existing, ...updates };
    return this.put(updated);
  }

  // Delete
  async delete(key) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      const request = store.delete(key);

      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  // Clear all
  async clear() {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      const request = store.clear();

      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  // Count records
  async count() {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readonly');
      const store = transaction.objectStore(this.storeName);
      const request = store.count();

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
}

// Usage
const dbManager = new IndexedDBManager('MyApp', 1);
const db = await dbManager.open();

const userStore = new IndexedDBStore(db, 'users');

// Add user
await userStore.add({
  name: 'John Doe',
  email: 'john@example.com',
  createdAt: new Date(),
});

// Get user by ID
const user = await userStore.get(1);

// Get user by email
const userByEmail = await userStore.getByIndex('email', 'john@example.com');

// Update user
await userStore.update(1, { name: 'John Smith' });

// Delete user
await userStore.delete(1);

Cursors and Queries

Working with Cursors

class CursorQuery {
  constructor(db, storeName) {
    this.db = db;
    this.storeName = storeName;
  }

  // Iterate all records
  async iterate(callback, direction = 'next') {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readonly');
      const store = transaction.objectStore(this.storeName);
      const request = store.openCursor(null, direction);
      const results = [];

      request.onsuccess = (event) => {
        const cursor = event.target.result;

        if (cursor) {
          const shouldContinue = callback(cursor.value, cursor.key);

          if (shouldContinue !== false) {
            results.push(cursor.value);
            cursor.continue();
          }
        } else {
          resolve(results);
        }
      };

      request.onerror = () => reject(request.error);
    });
  }

  // Query with range
  async queryRange(options = {}) {
    const {
      index: indexName,
      lower,
      upper,
      lowerOpen = false,
      upperOpen = false,
      direction = 'next',
      limit,
    } = options;

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readonly');
      const store = transaction.objectStore(this.storeName);
      const source = indexName ? store.index(indexName) : store;

      let range;
      if (lower !== undefined && upper !== undefined) {
        range = IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen);
      } else if (lower !== undefined) {
        range = IDBKeyRange.lowerBound(lower, lowerOpen);
      } else if (upper !== undefined) {
        range = IDBKeyRange.upperBound(upper, upperOpen);
      }

      const request = source.openCursor(range, direction);
      const results = [];

      request.onsuccess = (event) => {
        const cursor = event.target.result;

        if (cursor && (!limit || results.length < limit)) {
          results.push(cursor.value);
          cursor.continue();
        } else {
          resolve(results);
        }
      };

      request.onerror = () => reject(request.error);
    });
  }

  // Advanced filtering
  async filter(predicate, options = {}) {
    const { index, range } = options;

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readonly');
      const store = transaction.objectStore(this.storeName);
      const source = index ? store.index(index) : store;
      const request = source.openCursor(range);
      const results = [];

      request.onsuccess = (event) => {
        const cursor = event.target.result;

        if (cursor) {
          if (predicate(cursor.value)) {
            results.push(cursor.value);
          }
          cursor.continue();
        } else {
          resolve(results);
        }
      };

      request.onerror = () => reject(request.error);
    });
  }

  // Pagination
  async paginate(page = 1, pageSize = 20, options = {}) {
    const { index, direction = 'next' } = options;
    const offset = (page - 1) * pageSize;

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readonly');
      const store = transaction.objectStore(this.storeName);
      const source = index ? store.index(index) : store;
      const request = source.openCursor(null, direction);

      let skipped = 0;
      const results = [];

      request.onsuccess = (event) => {
        const cursor = event.target.result;

        if (cursor) {
          if (skipped < offset) {
            skipped++;
            cursor.continue();
          } else if (results.length < pageSize) {
            results.push(cursor.value);
            cursor.continue();
          } else {
            resolve(results);
          }
        } else {
          resolve(results);
        }
      };

      request.onerror = () => reject(request.error);
    });
  }
}

// Usage examples
const query = new CursorQuery(db, 'products');

// Get all products
const allProducts = await query.iterate((product) => {
  console.log(product);
  return true; // Continue iteration
});

// Get products in price range
const affordableProducts = await query.queryRange({
  index: 'price',
  lower: 10,
  upper: 50,
  limit: 10,
});

// Filter products
const electronics = await query.filter(
  (product) => product.category === 'Electronics',
  { index: 'category' }
);

// Paginate results
const page1 = await query.paginate(1, 20, { index: 'name' });

Transactions

Transaction Management

class TransactionManager {
  constructor(db) {
    this.db = db;
  }

  // Execute multiple operations in transaction
  async executeTransaction(storeNames, mode, operations) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(storeNames, mode);
      const results = [];

      transaction.oncomplete = () => resolve(results);
      transaction.onerror = () => reject(transaction.error);
      transaction.onabort = () => reject(new Error('Transaction aborted'));

      // Execute operations
      operations(transaction, results);
    });
  }

  // Batch operations
  async batchOperation(storeName, operations) {
    return this.executeTransaction(
      [storeName],
      'readwrite',
      (transaction, results) => {
        const store = transaction.objectStore(storeName);

        operations.forEach((op) => {
          let request;

          switch (op.type) {
            case 'put':
              request = store.put(op.data);
              break;
            case 'add':
              request = store.add(op.data);
              break;
            case 'delete':
              request = store.delete(op.key);
              break;
            default:
              throw new Error(`Unknown operation: ${op.type}`);
          }

          request.onsuccess = () =>
            results.push({
              success: true,
              operation: op.type,
              result: request.result,
            });

          request.onerror = () =>
            results.push({
              success: false,
              operation: op.type,
              error: request.error,
            });
        });
      }
    );
  }

  // Atomic transfer between stores
  async transfer(fromStore, toStore, key, transform) {
    return this.executeTransaction(
      [fromStore, toStore],
      'readwrite',
      (transaction) => {
        const from = transaction.objectStore(fromStore);
        const to = transaction.objectStore(toStore);

        // Get from source
        const getRequest = from.get(key);

        getRequest.onsuccess = () => {
          const data = getRequest.result;

          if (!data) {
            transaction.abort();
            return;
          }

          // Transform if needed
          const transformed = transform ? transform(data) : data;

          // Add to destination
          const addRequest = to.add(transformed);

          addRequest.onsuccess = () => {
            // Delete from source
            from.delete(key);
          };
        };
      }
    );
  }
}

// Usage
const txManager = new TransactionManager(db);

// Batch insert
await txManager.batchOperation('users', [
  { type: 'add', data: { name: 'User 1', email: 'user1@example.com' } },
  { type: 'add', data: { name: 'User 2', email: 'user2@example.com' } },
  { type: 'add', data: { name: 'User 3', email: 'user3@example.com' } },
]);

// Complex transaction
await txManager.executeTransaction(
  ['orders', 'inventory'],
  'readwrite',
  (transaction) => {
    const orders = transaction.objectStore('orders');
    const inventory = transaction.objectStore('inventory');

    // Create order
    orders.add({
      userId: 1,
      items: [{ sku: 'ABC123', quantity: 2 }],
      status: 'pending',
    });

    // Update inventory
    const getInventory = inventory.get('ABC123');

    getInventory.onsuccess = () => {
      const item = getInventory.result;
      item.quantity -= 2;
      inventory.put(item);
    };
  }
);

Complex Queries

Advanced Query Patterns

class QueryBuilder {
  constructor(db) {
    this.db = db;
  }

  // Join-like operations
  async join(primaryStore, foreignStore, primaryKey, foreignKey) {
    const transaction = this.db.transaction(
      [primaryStore, foreignStore],
      'readonly'
    );
    const primary = transaction.objectStore(primaryStore);
    const foreign = transaction.objectStore(foreignStore);

    return new Promise((resolve, reject) => {
      const results = [];
      const primaryRequest = primary.openCursor();

      primaryRequest.onsuccess = (event) => {
        const cursor = event.target.result;

        if (cursor) {
          const primaryRecord = cursor.value;
          const foreignRequest = foreign.get(primaryRecord[foreignKey]);

          foreignRequest.onsuccess = () => {
            results.push({
              ...primaryRecord,
              [foreignStore]: foreignRequest.result,
            });

            cursor.continue();
          };
        } else {
          resolve(results);
        }
      };

      primaryRequest.onerror = () => reject(primaryRequest.error);
    });
  }

  // Aggregation
  async aggregate(storeName, operations) {
    const transaction = this.db.transaction([storeName], 'readonly');
    const store = transaction.objectStore(storeName);

    return new Promise((resolve, reject) => {
      const aggregates = {
        count: 0,
        sum: {},
        avg: {},
        min: {},
        max: {},
        values: {},
      };

      const request = store.openCursor();

      request.onsuccess = (event) => {
        const cursor = event.target.result;

        if (cursor) {
          const record = cursor.value;
          aggregates.count++;

          // Process each operation
          operations.forEach((op) => {
            const value = record[op.field];

            switch (op.type) {
              case 'sum':
                aggregates.sum[op.field] =
                  (aggregates.sum[op.field] || 0) + value;
                break;

              case 'avg':
                aggregates.values[op.field] = aggregates.values[op.field] || [];
                aggregates.values[op.field].push(value);
                break;

              case 'min':
                aggregates.min[op.field] = Math.min(
                  aggregates.min[op.field] ?? Infinity,
                  value
                );
                break;

              case 'max':
                aggregates.max[op.field] = Math.max(
                  aggregates.max[op.field] ?? -Infinity,
                  value
                );
                break;
            }
          });

          cursor.continue();
        } else {
          // Calculate averages
          Object.keys(aggregates.values).forEach((field) => {
            const values = aggregates.values[field];
            aggregates.avg[field] =
              values.reduce((a, b) => a + b, 0) / values.length;
          });

          delete aggregates.values;
          resolve(aggregates);
        }
      };

      request.onerror = () => reject(request.error);
    });
  }

  // Full-text search simulation
  async search(storeName, searchFields, query) {
    const keywords = query
      .toLowerCase()
      .split(' ')
      .filter((k) => k.length > 2);

    const transaction = this.db.transaction([storeName], 'readonly');
    const store = transaction.objectStore(storeName);

    return new Promise((resolve, reject) => {
      const results = [];
      const scores = new Map();

      const request = store.openCursor();

      request.onsuccess = (event) => {
        const cursor = event.target.result;

        if (cursor) {
          const record = cursor.value;
          let score = 0;

          searchFields.forEach((field) => {
            const fieldValue = String(record[field] || '').toLowerCase();

            keywords.forEach((keyword) => {
              if (fieldValue.includes(keyword)) {
                score += fieldValue.split(keyword).length - 1;
              }
            });
          });

          if (score > 0) {
            results.push(record);
            scores.set(record, score);
          }

          cursor.continue();
        } else {
          // Sort by relevance
          results.sort((a, b) => scores.get(b) - scores.get(a));
          resolve(results);
        }
      };

      request.onerror = () => reject(request.error);
    });
  }
}

// Usage
const queryBuilder = new QueryBuilder(db);

// Join orders with users
const ordersWithUsers = await queryBuilder.join(
  'orders',
  'users',
  'orderId',
  'userId'
);

// Aggregate product statistics
const productStats = await queryBuilder.aggregate('products', [
  { type: 'sum', field: 'price' },
  { type: 'avg', field: 'price' },
  { type: 'min', field: 'price' },
  { type: 'max', field: 'price' },
]);

// Search products
const searchResults = await queryBuilder.search(
  'products',
  ['name', 'description', 'category'],
  'wireless headphones'
);

Performance Optimization

IndexedDB Performance Tips

class PerformanceOptimizer {
  constructor(db) {
    this.db = db;
  }

  // Bulk insert with batching
  async bulkInsert(storeName, data, batchSize = 100) {
    const batches = [];

    for (let i = 0; i < data.length; i += batchSize) {
      batches.push(data.slice(i, i + batchSize));
    }

    let inserted = 0;

    for (const batch of batches) {
      await new Promise((resolve, reject) => {
        const transaction = this.db.transaction([storeName], 'readwrite');
        const store = transaction.objectStore(storeName);

        transaction.oncomplete = () => {
          inserted += batch.length;
          console.log(`Inserted ${inserted}/${data.length} records`);
          resolve();
        };

        transaction.onerror = () => reject(transaction.error);

        batch.forEach((item) => store.put(item));
      });
    }

    return inserted;
  }

  // Lazy loading with virtual scrolling
  createVirtualScroller(storeName, pageSize = 50) {
    return {
      cache: new Map(),
      currentPage: 0,

      async loadPage(page) {
        if (this.cache.has(page)) {
          return this.cache.get(page);
        }

        const transaction = this.db.transaction([storeName], 'readonly');
        const store = transaction.objectStore(storeName);

        return new Promise((resolve, reject) => {
          const results = [];
          let skipped = 0;
          const offset = page * pageSize;

          const request = store.openCursor();

          request.onsuccess = (event) => {
            const cursor = event.target.result;

            if (cursor) {
              if (skipped < offset) {
                skipped++;
                cursor.advance(1);
              } else if (results.length < pageSize) {
                results.push(cursor.value);
                cursor.continue();
              } else {
                this.cache.set(page, results);
                resolve(results);
              }
            } else {
              this.cache.set(page, results);
              resolve(results);
            }
          };

          request.onerror = () => reject(request.error);
        });
      },

      clearCache() {
        this.cache.clear();
      },
    };
  }

  // Index usage optimizer
  async analyzeIndexUsage(storeName) {
    const transaction = this.db.transaction([storeName], 'readonly');
    const store = transaction.objectStore(storeName);

    const analysis = {
      storeName,
      recordCount: 0,
      indexes: {},
      recommendations: [],
    };

    // Get record count
    const countRequest = store.count();
    await new Promise((resolve) => {
      countRequest.onsuccess = () => {
        analysis.recordCount = countRequest.result;
        resolve();
      };
    });

    // Analyze indexes
    const indexNames = Array.from(store.indexNames);

    for (const indexName of indexNames) {
      const index = store.index(indexName);
      const unique = index.unique;
      const multiEntry = index.multiEntry;

      analysis.indexes[indexName] = {
        unique,
        multiEntry,
        keyPath: index.keyPath,
      };
    }

    // Recommendations
    if (analysis.recordCount > 1000 && indexNames.length === 0) {
      analysis.recommendations.push(
        'Consider adding indexes for frequently queried fields'
      );
    }

    return analysis;
  }
}

// Usage
const optimizer = new PerformanceOptimizer(db);

// Bulk insert
const largeDataset = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  name: `Item ${i}`,
  value: Math.random() * 1000,
}));

await optimizer.bulkInsert('items', largeDataset);

// Virtual scrolling
const scroller = optimizer.createVirtualScroller('items');
const firstPage = await scroller.loadPage(0);

Sync and Backup

Data Synchronization

class SyncManager {
  constructor(db, apiEndpoint) {
    this.db = db;
    this.apiEndpoint = apiEndpoint;
    this.syncInProgress = false;
  }

  async sync(storeName) {
    if (this.syncInProgress) {
      console.log('Sync already in progress');
      return;
    }

    this.syncInProgress = true;

    try {
      // Pull changes from server
      await this.pull(storeName);

      // Push local changes
      await this.push(storeName);

      console.log('Sync completed successfully');
    } catch (error) {
      console.error('Sync failed:', error);
      throw error;
    } finally {
      this.syncInProgress = false;
    }
  }

  async pull(storeName) {
    // Get last sync timestamp
    const lastSync = await this.getLastSync(storeName);

    // Fetch changes from server
    const response = await fetch(`${this.apiEndpoint}/${storeName}/changes`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ since: lastSync }),
    });

    const changes = await response.json();

    // Apply changes locally
    const transaction = this.db.transaction([storeName], 'readwrite');
    const store = transaction.objectStore(storeName);

    for (const change of changes) {
      switch (change.operation) {
        case 'create':
        case 'update':
          store.put(change.data);
          break;
        case 'delete':
          store.delete(change.key);
          break;
      }
    }

    // Update sync timestamp
    await this.updateLastSync(storeName);
  }

  async push(storeName) {
    // Get local changes
    const changes = await this.getLocalChanges(storeName);

    if (changes.length === 0) {
      return;
    }

    // Send to server
    const response = await fetch(`${this.apiEndpoint}/${storeName}/sync`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ changes }),
    });

    if (response.ok) {
      // Clear change tracking
      await this.clearLocalChanges(storeName);
    }
  }

  async backup() {
    const backup = {
      version: this.db.version,
      timestamp: new Date().toISOString(),
      stores: {},
    };

    const storeNames = Array.from(this.db.objectStoreNames);

    for (const storeName of storeNames) {
      const transaction = this.db.transaction([storeName], 'readonly');
      const store = transaction.objectStore(storeName);

      backup.stores[storeName] = await new Promise((resolve, reject) => {
        const records = [];
        const request = store.openCursor();

        request.onsuccess = (event) => {
          const cursor = event.target.result;

          if (cursor) {
            records.push(cursor.value);
            cursor.continue();
          } else {
            resolve(records);
          }
        };

        request.onerror = () => reject(request.error);
      });
    }

    return backup;
  }

  async restore(backup) {
    // Clear existing data
    const storeNames = Array.from(this.db.objectStoreNames);

    for (const storeName of storeNames) {
      const transaction = this.db.transaction([storeName], 'readwrite');
      const store = transaction.objectStore(storeName);
      await new Promise((resolve) => {
        const request = store.clear();
        request.onsuccess = () => resolve();
      });
    }

    // Restore data
    for (const [storeName, records] of Object.entries(backup.stores)) {
      const transaction = this.db.transaction([storeName], 'readwrite');
      const store = transaction.objectStore(storeName);

      records.forEach((record) => store.put(record));

      await new Promise((resolve, reject) => {
        transaction.oncomplete = () => resolve();
        transaction.onerror = () => reject(transaction.error);
      });
    }
  }

  async getLastSync(storeName) {
    const syncStore = await this.getSyncMetadata();
    return syncStore[storeName]?.lastSync || null;
  }

  async updateLastSync(storeName) {
    const syncStore = await this.getSyncMetadata();
    syncStore[storeName] = {
      ...syncStore[storeName],
      lastSync: new Date().toISOString(),
    };
    await this.saveSyncMetadata(syncStore);
  }

  async getSyncMetadata() {
    // Implementation depends on where you store sync metadata
    return JSON.parse(localStorage.getItem('syncMetadata') || '{}');
  }

  async saveSyncMetadata(metadata) {
    localStorage.setItem('syncMetadata', JSON.stringify(metadata));
  }
}

Best Practices

  1. Handle Errors Gracefully

    request.onerror = () => {
      console.error('IndexedDB error:', request.error);
      // Fallback to localStorage or other storage
    };
    
  2. Version Management

    // Always handle schema changes in onupgradeneeded
    request.onupgradeneeded = (event) => {
      // Migrate data if needed
    };
    
  3. Transaction Scope

    // Use minimal transaction scope
    const transaction = db.transaction(['store1'], 'readonly');
    // Not: db.transaction(db.objectStoreNames, 'readwrite');
    
  4. Index Strategy

    // Create indexes for frequently queried fields
    // But don't over-index - each index increases storage
    

Conclusion

IndexedDB provides powerful client-side storage:

  • Large storage capacity compared to other methods
  • Structured data with indexes
  • Transactional for data integrity
  • Asynchronous to avoid blocking
  • Complex queries with cursors
  • Offline capabilities for PWAs

Key takeaways:

  • Plan your schema carefully
  • Use transactions appropriately
  • Implement proper error handling
  • Consider performance implications
  • Test across browsers
  • Implement sync strategies for offline apps

Master IndexedDB to build robust offline-capable web applications!