← 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
};