@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
JavaScript
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
};
}