← back to Watches

src/utils/offlineStorage.js

484 lines

/**
 * Offline Storage Utilities using IndexedDB
 * Provides persistent storage for watches, favorites, and pending actions
 */

const DB_NAME = 'OmegaWatchesDB';
const DB_VERSION = 1;

// Object stores
const STORES = {
  WATCHES: 'watches',
  FAVORITES: 'favorites',
  WATCHLIST: 'watchlist',
  PENDING_ACTIONS: 'pendingActions',
  CACHE_METADATA: 'cacheMetadata'
};

/**
 * Open IndexedDB database
 */
function openDatabase() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(DB_NAME, DB_VERSION);

    request.onerror = () => {
      reject(new Error('Failed to open database'));
    };

    request.onsuccess = () => {
      resolve(request.result);
    };

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

      // Create object stores if they don't exist
      if (!db.objectStoreNames.contains(STORES.WATCHES)) {
        db.createObjectStore(STORES.WATCHES, { keyPath: 'id' });
      }

      if (!db.objectStoreNames.contains(STORES.FAVORITES)) {
        const favStore = db.createObjectStore(STORES.FAVORITES, { keyPath: 'id', autoIncrement: true });
        favStore.createIndex('watchId', 'watchId', { unique: false });
      }

      if (!db.objectStoreNames.contains(STORES.WATCHLIST)) {
        const watchlistStore = db.createObjectStore(STORES.WATCHLIST, { keyPath: 'id', autoIncrement: true });
        watchlistStore.createIndex('watchId', 'watchId', { unique: false });
      }

      if (!db.objectStoreNames.contains(STORES.PENDING_ACTIONS)) {
        db.createObjectStore(STORES.PENDING_ACTIONS, { keyPath: 'id', autoIncrement: true });
      }

      if (!db.objectStoreNames.contains(STORES.CACHE_METADATA)) {
        db.createObjectStore(STORES.CACHE_METADATA, { keyPath: 'key' });
      }

      console.log('Database upgraded to version', DB_VERSION);
    };
  });
}

/**
 * Save watches to offline storage
 */
export async function saveWatchesOffline(watches) {
  try {
    const db = await openDatabase();
    const tx = db.transaction(STORES.WATCHES, 'readwrite');
    const store = tx.objectStore(STORES.WATCHES);

    // Clear existing watches
    await store.clear();

    // Add all watches
    for (const watch of watches) {
      await store.put(watch);
    }

    // Update cache metadata
    await updateCacheMetadata('watches', {
      count: watches.length,
      lastUpdated: Date.now()
    });

    console.log(`Saved ${watches.length} watches offline`);
    return true;
  } catch (error) {
    console.error('Failed to save watches offline:', error);
    return false;
  }
}

/**
 * Get all watches from offline storage
 */
export async function getWatchesOffline() {
  try {
    const db = await openDatabase();
    const tx = db.transaction(STORES.WATCHES, 'readonly');
    const store = tx.objectStore(STORES.WATCHES);

    return new Promise((resolve, reject) => {
      const request = store.getAll();

      request.onsuccess = () => {
        resolve(request.result);
      };

      request.onerror = () => {
        reject(new Error('Failed to get watches'));
      };
    });
  } catch (error) {
    console.error('Failed to get watches offline:', error);
    return [];
  }
}

/**
 * Get single watch by ID from offline storage
 */
export async function getWatchOffline(id) {
  try {
    const db = await openDatabase();
    const tx = db.transaction(STORES.WATCHES, 'readonly');
    const store = tx.objectStore(STORES.WATCHES);

    return new Promise((resolve, reject) => {
      const request = store.get(id);

      request.onsuccess = () => {
        resolve(request.result);
      };

      request.onerror = () => {
        reject(new Error('Failed to get watch'));
      };
    });
  } catch (error) {
    console.error('Failed to get watch offline:', error);
    return null;
  }
}

/**
 * Add watch to favorites
 */
export async function addToFavorites(watchId) {
  try {
    const db = await openDatabase();
    const tx = db.transaction(STORES.FAVORITES, 'readwrite');
    const store = tx.objectStore(STORES.FAVORITES);

    // Check if already in favorites
    const index = store.index('watchId');
    const existing = await new Promise((resolve) => {
      const request = index.get(watchId);
      request.onsuccess = () => resolve(request.result);
    });

    if (existing) {
      console.log('Watch already in favorites');
      return false;
    }

    // Add to favorites
    await store.add({
      watchId,
      addedAt: Date.now()
    });

    console.log('Added to favorites:', watchId);
    return true;
  } catch (error) {
    console.error('Failed to add to favorites:', error);
    return false;
  }
}

/**
 * Remove watch from favorites
 */
export async function removeFromFavorites(watchId) {
  try {
    const db = await openDatabase();
    const tx = db.transaction(STORES.FAVORITES, 'readwrite');
    const store = tx.objectStore(STORES.FAVORITES);
    const index = store.index('watchId');

    // Find the favorite by watchId
    const request = index.openCursor(IDBKeyRange.only(watchId));

    return new Promise((resolve, reject) => {
      request.onsuccess = (event) => {
        const cursor = event.target.result;
        if (cursor) {
          cursor.delete();
          console.log('Removed from favorites:', watchId);
          resolve(true);
        } else {
          resolve(false);
        }
      };

      request.onerror = () => {
        reject(new Error('Failed to remove from favorites'));
      };
    });
  } catch (error) {
    console.error('Failed to remove from favorites:', error);
    return false;
  }
}

/**
 * Get all favorite watch IDs
 */
export async function getFavorites() {
  try {
    const db = await openDatabase();
    const tx = db.transaction(STORES.FAVORITES, 'readonly');
    const store = tx.objectStore(STORES.FAVORITES);

    return new Promise((resolve, reject) => {
      const request = store.getAll();

      request.onsuccess = () => {
        const favorites = request.result.map(f => f.watchId);
        resolve(favorites);
      };

      request.onerror = () => {
        reject(new Error('Failed to get favorites'));
      };
    });
  } catch (error) {
    console.error('Failed to get favorites:', error);
    return [];
  }
}

/**
 * Check if watch is in favorites
 */
export async function isInFavorites(watchId) {
  try {
    const db = await openDatabase();
    const tx = db.transaction(STORES.FAVORITES, 'readonly');
    const store = tx.objectStore(STORES.FAVORITES);
    const index = store.index('watchId');

    return new Promise((resolve) => {
      const request = index.get(watchId);
      request.onsuccess = () => {
        resolve(!!request.result);
      };
      request.onerror = () => resolve(false);
    });
  } catch (error) {
    return false;
  }
}

/**
 * Add watch to watchlist
 */
export async function addToWatchlist(watchId, notes = '') {
  try {
    const db = await openDatabase();
    const tx = db.transaction(STORES.WATCHLIST, 'readwrite');
    const store = tx.objectStore(STORES.WATCHLIST);

    await store.add({
      watchId,
      notes,
      addedAt: Date.now()
    });

    console.log('Added to watchlist:', watchId);
    return true;
  } catch (error) {
    console.error('Failed to add to watchlist:', error);
    return false;
  }
}

/**
 * Get watchlist
 */
export async function getWatchlist() {
  try {
    const db = await openDatabase();
    const tx = db.transaction(STORES.WATCHLIST, 'readonly');
    const store = tx.objectStore(STORES.WATCHLIST);

    return new Promise((resolve, reject) => {
      const request = store.getAll();

      request.onsuccess = () => {
        resolve(request.result);
      };

      request.onerror = () => {
        reject(new Error('Failed to get watchlist'));
      };
    });
  } catch (error) {
    console.error('Failed to get watchlist:', error);
    return [];
  }
}

/**
 * Add pending action (for background sync)
 */
export async function addPendingAction(type, data) {
  try {
    const db = await openDatabase();
    const tx = db.transaction(STORES.PENDING_ACTIONS, 'readwrite');
    const store = tx.objectStore(STORES.PENDING_ACTIONS);

    await store.add({
      type,
      data,
      createdAt: Date.now()
    });

    console.log('Added pending action:', type);
    return true;
  } catch (error) {
    console.error('Failed to add pending action:', error);
    return false;
  }
}

/**
 * Get all pending actions
 */
export async function getPendingActions() {
  try {
    const db = await openDatabase();
    const tx = db.transaction(STORES.PENDING_ACTIONS, 'readonly');
    const store = tx.objectStore(STORES.PENDING_ACTIONS);

    return new Promise((resolve, reject) => {
      const request = store.getAll();

      request.onsuccess = () => {
        resolve(request.result);
      };

      request.onerror = () => {
        reject(new Error('Failed to get pending actions'));
      };
    });
  } catch (error) {
    console.error('Failed to get pending actions:', error);
    return [];
  }
}

/**
 * Clear pending actions
 */
export async function clearPendingActions() {
  try {
    const db = await openDatabase();
    const tx = db.transaction(STORES.PENDING_ACTIONS, 'readwrite');
    const store = tx.objectStore(STORES.PENDING_ACTIONS);

    await store.clear();
    console.log('Cleared pending actions');
    return true;
  } catch (error) {
    console.error('Failed to clear pending actions:', error);
    return false;
  }
}

/**
 * Update cache metadata
 */
export async function updateCacheMetadata(key, metadata) {
  try {
    const db = await openDatabase();
    const tx = db.transaction(STORES.CACHE_METADATA, 'readwrite');
    const store = tx.objectStore(STORES.CACHE_METADATA);

    await store.put({
      key,
      ...metadata,
      updatedAt: Date.now()
    });

    return true;
  } catch (error) {
    console.error('Failed to update cache metadata:', error);
    return false;
  }
}

/**
 * Get cache metadata
 */
export async function getCacheMetadata(key) {
  try {
    const db = await openDatabase();
    const tx = db.transaction(STORES.CACHE_METADATA, 'readonly');
    const store = tx.objectStore(STORES.CACHE_METADATA);

    return new Promise((resolve) => {
      const request = store.get(key);
      request.onsuccess = () => {
        resolve(request.result || null);
      };
      request.onerror = () => resolve(null);
    });
  } catch (error) {
    return null;
  }
}

/**
 * Clear all offline data
 */
export async function clearOfflineData() {
  try {
    const db = await openDatabase();

    // Clear all object stores
    for (const storeName of Object.values(STORES)) {
      const tx = db.transaction(storeName, 'readwrite');
      await tx.objectStore(storeName).clear();
    }

    console.log('Cleared all offline data');
    return true;
  } catch (error) {
    console.error('Failed to clear offline data:', error);
    return false;
  }
}

/**
 * Get storage size estimate
 */
export async function getStorageSize() {
  if ('storage' in navigator && 'estimate' in navigator.storage) {
    try {
      const estimate = await navigator.storage.estimate();
      return {
        usage: estimate.usage,
        quota: estimate.quota,
        percentage: (estimate.usage / estimate.quota) * 100
      };
    } catch (error) {
      console.error('Failed to get storage estimate:', error);
    }
  }

  return null;
}

export default {
  saveWatchesOffline,
  getWatchesOffline,
  getWatchOffline,
  addToFavorites,
  removeFromFavorites,
  getFavorites,
  isInFavorites,
  addToWatchlist,
  getWatchlist,
  addPendingAction,
  getPendingActions,
  clearPendingActions,
  updateCacheMetadata,
  getCacheMetadata,
  clearOfflineData,
  getStorageSize
};