UNPKG

@amitkhare/indexed-storage

Version:

A modern, localStorage-like API for IndexedDB with instance-based and static APIs, multi-store support, configurable databases, and enhanced capabilities

407 lines (349 loc) β€’ 12 kB
import { useIndexedDB } from './useIndexedDB.js'; import { log } from './config.js'; /** * Storage management composable for handling storage estimates, monitoring, and migration * Provides a localStorage-like API for IndexedDB operations * @param {Object} config - Configuration options * @param {string} config.dbName - Database name (default: 'LocalStorageDB') * @param {number} config.dbVersion - Database version (default: 1) * @param {string} config.storeName - Object store name (default: 'localStates') * @param {string[]} config.additionalStores - Additional store names to create (optional) */ export function useStorageManager(config = {}) { const indexedDB = useIndexedDB(config); // Cache for storage estimates to avoid repeated API calls let storageEstimateCache = null; let storageEstimatePromise = null; /** * localStorage-like API for IndexedDB operations */ const storage = { /** * Get item from storage * @param {string} key - Storage key * @returns {Promise<any>} - Stored value or null */ async getItem(key) { const result = await indexedDB.get(key); return result ? result : null; }, /** * Set item in storage * @param {string} key - Storage key * @param {any} value - Value to store * @returns {Promise<void>} */ async setItem(key, value) { const data = { timestamp: new Date().toISOString(), version: '1.0', ...value }; await indexedDB.save(key, data); }, /** * Remove item from storage * @param {string} key - Storage key * @returns {Promise<void>} */ async removeItem(key) { await indexedDB.remove(key); }, /** * Check if item exists in storage * @param {string} key - Storage key * @returns {Promise<boolean>} */ async hasItem(key) { return await indexedDB.exists(key); }, /** * Clear all items from storage * @returns {Promise<void>} */ async clear() { await indexedDB.clear(); }, /** * Get all keys from storage * @returns {Promise<string[]>} */ async getAllKeys() { const allData = await indexedDB.getAll(); return allData.map(item => item.id); }, /** * Test storage availability * @returns {Promise<boolean>} */ async testAvailability() { return await indexedDB.testAvailability(); } }; /** * Format bytes to human readable format * @param {number} bytes - Number of bytes * @returns {string} Formatted string */ function formatBytes(bytes) { if (bytes === 0 || bytes === null || bytes === undefined) return '0 Bytes'; // Handle negative numbers if (bytes < 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); // Ensure we don't exceed the sizes array const sizeIndex = Math.min(i, sizes.length - 1); return parseFloat((bytes / Math.pow(k, sizeIndex)).toFixed(2)) + ' ' + sizes[sizeIndex]; } /** * Get storage estimate using Storage API with caching * @returns {Promise<Object>} Storage estimate information */ async function getStorageEstimate() { // Return cached result if available if (storageEstimateCache) { return storageEstimateCache; } // Return existing promise if already in progress if (storageEstimatePromise) { return storageEstimatePromise; } // Create new promise and cache it storageEstimatePromise = (async () => { let quota = null; let usage = null; let exactQuotaAvailable = false; // Use Storage API for IndexedDB quota (much more accurate than localStorage) if ('storage' in navigator && 'estimate' in navigator.storage) { try { const estimate = await navigator.storage.estimate(); quota = estimate.quota; usage = estimate.usage; exactQuotaAvailable = true; log('log', 'πŸ’ΎπŸ“Š IndexedDB storage estimate:', { totalQuota: formatBytes(estimate.quota), totalUsage: formatBytes(estimate.usage), note: 'Using Storage API for accurate IndexedDB quota' }); } catch (apiError) { log('warn', 'πŸ’Ύβš οΈ StorageManager API not available:', apiError); // Fallback to conservative estimate quota = 50 * 1024 * 1024; // 50MB fallback for IndexedDB exactQuotaAvailable = false; } } else { // Fallback quota for older browsers - IndexedDB typically has much more space quota = 50 * 1024 * 1024; // 50MB fallback exactQuotaAvailable = false; } log('log', 'πŸ’ΎπŸ“ Using IndexedDB quota:', formatBytes(quota), exactQuotaAvailable ? '(exact)' : '(estimated)'); const result = { quota, usage, exactQuotaAvailable }; // Cache the result for 5 minutes storageEstimateCache = result; setTimeout(() => { storageEstimateCache = null; storageEstimatePromise = null; }, 5 * 60 * 1000); return result; })(); return storageEstimatePromise; } /** * Calculate current storage usage * @returns {Promise<Object>} Storage usage information */ async function calculateUsage() { const usage = await indexedDB.calculateUsage(); return usage; } /** * Get comprehensive storage usage information * @param {string} specificKey - Optional key to get specific item size * @returns {Promise<Object>} Storage usage information */ async function getStorageUsage(specificKey = null) { try { // Get IndexedDB usage const { totalSize, items } = await calculateUsage(); // Get specific item size if requested let specificItemSize = 0; if (specificKey && items[specificKey]) { specificItemSize = items[specificKey]; } // Get IndexedDB quota from Storage API const { quota, usage, exactQuotaAvailable } = await getStorageEstimate(); // Calculate usage percentages const calculatedUsagePercentage = Math.round((totalSize / quota) * 100); const apiUsagePercentage = exactQuotaAvailable && usage !== null ? Math.round((usage / quota) * 100) : null; return { totalSize, specificItemSize, quota, usage: exactQuotaAvailable ? usage : totalSize, exactQuotaAvailable, usagePercentage: calculatedUsagePercentage, apiUsagePercentage, formattedTotalSize: formatBytes(totalSize), formattedSpecificSize: formatBytes(specificItemSize), formattedQuota: formatBytes(quota), formattedUsage: formatBytes(exactQuotaAvailable ? usage : totalSize), items: Object.keys(items).map(key => ({ key, size: items[key], formattedSize: formatBytes(items[key]) })) }; } catch (error) { log('error', 'πŸ’ΎβŒ Error calculating IndexedDB storage usage:', error); return null; } } /** * Get storage warning level based on usage * @returns {Promise<string>} - 'normal', 'warning', or 'critical' */ async function getStorageWarningLevel() { const storageUsage = await getStorageUsage(); if (!storageUsage) return 'normal'; const { usagePercentage, exactQuotaAvailable, apiUsagePercentage } = storageUsage; const displayUsage = exactQuotaAvailable && apiUsagePercentage !== null ? apiUsagePercentage : usagePercentage; if (displayUsage >= 95) return 'critical'; if (displayUsage >= 80) return 'warning'; return 'normal'; } /** * Test storage availability by attempting a write operation * @returns {Promise<boolean>} */ async function testStorageAvailability() { try { const testKey = 'storage-availability-test'; const testData = { test: true, timestamp: new Date().toISOString() }; await storage.setItem(testKey, testData); await storage.removeItem(testKey); return true; } catch (error) { log('error', 'πŸ’ΎβŒ Storage availability test failed:', error); return false; } } /** * Migrate data from localStorage to IndexedDB * @param {string} key - Storage key to migrate * @returns {Promise<boolean>} - Success status */ async function migrateFromLocalStorage(key) { try { log('log', `πŸ”„πŸ“ Checking for existing localStorage data to migrate: ${key}`); const existingData = localStorage.getItem(key); if (!existingData) { log('log', `πŸ”„βŒ No localStorage data found for key: ${key}`); return false; } // Check if IndexedDB already has data const existingIndexedDBData = await storage.getItem(key); if (existingIndexedDBData) { log('log', `πŸ”„β­οΈ IndexedDB already has data for key: ${key}, skipping migration`); return false; } log('log', `πŸ”„β¬†οΈ Migrating data from localStorage to IndexedDB: ${key}`); // Parse and migrate the data const parsedData = JSON.parse(existingData); await storage.setItem(key, parsedData); // Remove from localStorage after successful migration localStorage.removeItem(key); log('log', `πŸ”„βœ… Successfully migrated data to IndexedDB: ${key}`); return true; } catch (error) { log('error', `πŸ”„βŒ Error migrating data for key ${key}:`, error); return false; } } /** * Create a store-specific storage manager * @param {string} storeName - Name of the store to work with * @returns {Object} Store-specific storage API */ function createStoreManager(storeName) { const storeAPI = indexedDB.createStoreAPI(storeName); return { storage: { async getItem(key) { const result = await storeAPI.get(key); return result ? result : null; }, async setItem(key, value) { const data = { timestamp: new Date().toISOString(), version: '1.0', ...value }; await storeAPI.save(key, data); }, async removeItem(key) { await storeAPI.remove(key); }, async hasItem(key) { return await storeAPI.exists(key); }, async clear() { await storeAPI.clear(); }, async getAllKeys() { const allData = await storeAPI.getAll(); return allData.map(item => item.id); }, async testAvailability() { return await indexedDB.testAvailability(); } }, storeName }; } /** * Get data from all stores * @returns {Promise<Object>} Object with store names as keys */ async function getAllStoresData() { return await indexedDB.getAllStoresData(); } /** * Clear all stores * @returns {Promise<void>} */ async function clearAllStores() { await indexedDB.clearAllStores(); } /** * Get list of available stores * @returns {string[]} Array of store names */ function getAvailableStores() { return indexedDB.getAvailableStores(); } return { // localStorage-like API storage, // Storage monitoring and management getStorageEstimate, getStorageUsage, getStorageWarningLevel, calculateUsage, formatBytes, testStorageAvailability, // Migration utilities migrateFromLocalStorage, // Multi-store operations createStoreManager, getAllStoresData, clearAllStores, getAvailableStores, // Configuration info dbName: indexedDB.dbName, dbVersion: indexedDB.dbVersion, primaryStore: indexedDB.primaryStore, allStores: indexedDB.allStores }; }