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

1,390 lines (1,224 loc) β€’ 52.1 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.IndexedStorage = {})); })(this, (function (exports) { 'use strict'; /** * IndexedStorage - A modern wrapper for IndexedDB with multi-store and multi-database support * * Usage patterns: * 1. Instance-based (new API): const storage = new IndexedStorage({ dbName: 'myApp', version: 1 }); * 2. Static methods (legacy API): IndexedStorage.setItem('key', 'value'); * * @class IndexedStorage */ class IndexedStorage { /** * Create a new IndexedStorage instance * @param {Object} config - Configuration options * @param {string} [config.dbName='IndexedStorage'] - Database name * @param {number} [config.version=1] - Database version * @param {string} [config.storeName='data'] - Default store name * @param {Object} [config.storeConfig] - Default store configuration * @param {string[]} [config.additionalStores] - Additional stores to create during initialization * @param {Object} [config.storeConfigs] - Configuration for additional stores (key: storeName, value: config) * @param {boolean} [config.logging=false] - Enable logging */ constructor(config = {}) { this.config = { dbName: config.dbName || 'IndexedStorage', version: config.version || 1, storeName: config.storeName || 'data', storeConfig: config.storeConfig || { keyPath: null, autoIncrement: false }, additionalStores: config.additionalStores || [], storeConfigs: config.storeConfigs || {}, logging: config.logging || false }; this.db = null; this.isInitialized = false; // Auto-initialize if not in static context if (config.dbName || config.version || config.storeName) { this._initializeAsync(); } } /** * Initialize the database connection asynchronously * @private */ async _initializeAsync() { try { await this.init(); } catch (error) { if (this.config.logging) { console.error('IndexedStorage auto-initialization failed:', error); } } } /** * Initialize the database connection * @returns {Promise<IDBDatabase>} */ async init() { if (this.isInitialized && this.db) { return this.db; } return new Promise((resolve, reject) => { const request = indexedDB.open(this.config.dbName, this.config.version); request.onerror = () => { const error = new Error(`Failed to open database: ${this.config.dbName}`); if (this.config.logging) { console.error('IndexedStorage init error:', error); } reject(error); }; request.onsuccess = (event) => { this.db = event.target.result; this.isInitialized = true; if (this.config.logging) { console.log(`IndexedStorage initialized: ${this.config.dbName}`); } resolve(this.db); }; request.onupgradeneeded = (event) => { const db = event.target.result; // Create default store if it doesn't exist if (!db.objectStoreNames.contains(this.config.storeName)) { db.createObjectStore(this.config.storeName, this.config.storeConfig); if (this.config.logging) { console.log(`Created store: ${this.config.storeName}`); } } // Create additional stores if specified for (const storeName of this.config.additionalStores) { if (!db.objectStoreNames.contains(storeName)) { const storeConfig = this.config.storeConfigs[storeName] || { keyPath: null, autoIncrement: false }; db.createObjectStore(storeName, storeConfig); if (this.config.logging) { console.log(`Created additional store: ${storeName}`); } } } }; }); } /** * Get an item from the store * @param {string} key - The key to retrieve * @param {string} [storeName] - Store name (defaults to configured store) * @returns {Promise<any>} */ async getItem(key, storeName = this.config.storeName) { await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction([storeName], 'readonly'); const store = transaction.objectStore(storeName); const request = store.get(key); request.onsuccess = () => { const result = request.result; if (this.config.logging) { console.log(`Retrieved item: ${key} from ${storeName}`, result); } resolve(result); }; request.onerror = () => { const error = new Error(`Failed to get item: ${key}`); if (this.config.logging) { console.error('getItem error:', error); } reject(error); }; }); } /** * Set an item in the store * @param {string} key - The key to store * @param {any} value - The value to store * @param {string} [storeName] - Store name (defaults to configured store) * @returns {Promise<void>} */ async setItem(key, value, storeName = this.config.storeName) { await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction([storeName], 'readwrite'); const store = transaction.objectStore(storeName); const request = store.put(value, key); request.onsuccess = () => { if (this.config.logging) { console.log(`Stored item: ${key} in ${storeName}`, value); } resolve(); }; request.onerror = () => { const error = new Error(`Failed to set item: ${key}`); if (this.config.logging) { console.error('setItem error:', error); } reject(error); }; }); } /** * Remove an item from the store * @param {string} key - The key to remove * @param {string} [storeName] - Store name (defaults to configured store) * @returns {Promise<void>} */ async removeItem(key, storeName = this.config.storeName) { await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction([storeName], 'readwrite'); const store = transaction.objectStore(storeName); const request = store.delete(key); request.onsuccess = () => { if (this.config.logging) { console.log(`Removed item: ${key} from ${storeName}`); } resolve(); }; request.onerror = () => { const error = new Error(`Failed to remove item: ${key}`); if (this.config.logging) { console.error('removeItem error:', error); } reject(error); }; }); } /** * Clear all items from a store * @param {string} [storeName] - Store name (defaults to configured store) * @returns {Promise<void>} */ async clear(storeName = this.config.storeName) { await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction([storeName], 'readwrite'); const store = transaction.objectStore(storeName); const request = store.clear(); request.onsuccess = () => { if (this.config.logging) { console.log(`Cleared store: ${storeName}`); } resolve(); }; request.onerror = () => { const error = new Error(`Failed to clear store: ${storeName}`); if (this.config.logging) { console.error('clear error:', error); } reject(error); }; }); } /** * Get all keys from a store * @param {string} [storeName] - Store name (defaults to configured store) * @returns {Promise<string[]>} */ async getAllKeys(storeName = this.config.storeName) { await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction([storeName], 'readonly'); const store = transaction.objectStore(storeName); const request = store.getAllKeys(); request.onsuccess = () => { const keys = request.result; if (this.config.logging) { console.log(`Retrieved keys from ${storeName}:`, keys); } resolve(keys); }; request.onerror = () => { const error = new Error(`Failed to get keys from store: ${storeName}`); if (this.config.logging) { console.error('getAllKeys error:', error); } reject(error); }; }); } /** * Get all values from a store * @param {string} [storeName] - Store name (defaults to configured store) * @returns {Promise<any[]>} */ async getAllValues(storeName = this.config.storeName) { await this.init(); return new Promise((resolve, reject) => { const transaction = this.db.transaction([storeName], 'readonly'); const store = transaction.objectStore(storeName); const request = store.getAll(); request.onsuccess = () => { const values = request.result; if (this.config.logging) { console.log(`Retrieved values from ${storeName}:`, values); } resolve(values); }; request.onerror = () => { const error = new Error(`Failed to get values from store: ${storeName}`); if (this.config.logging) { console.error('getAllValues error:', error); } reject(error); }; }); } /** * Create a new store in the database * @param {string} storeName - Name of the store to create * @param {Object} [storeConfig] - Store configuration * @returns {Promise<void>} */ async createStore(storeName, storeConfig = { keyPath: null, autoIncrement: false }) { return new Promise((resolve, reject) => { // Close current connection if (this.db) { this.db.close(); } // Increment version to trigger upgrade const newVersion = this.config.version + 1; const request = indexedDB.open(this.config.dbName, newVersion); request.onerror = () => { const error = new Error(`Failed to create store: ${storeName}`); if (this.config.logging) { console.error('createStore error:', error); } reject(error); }; request.onsuccess = (event) => { this.db = event.target.result; this.config.version = newVersion; if (this.config.logging) { console.log(`Store created: ${storeName}`); } resolve(); }; request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName, storeConfig); } }; }); } /** * Delete a store from the database * @param {string} storeName - Name of the store to delete * @returns {Promise<void>} */ async deleteStore(storeName) { return new Promise((resolve, reject) => { // Close current connection if (this.db) { this.db.close(); } // Increment version to trigger upgrade const newVersion = this.config.version + 1; const request = indexedDB.open(this.config.dbName, newVersion); request.onerror = () => { const error = new Error(`Failed to delete store: ${storeName}`); if (this.config.logging) { console.error('deleteStore error:', error); } reject(error); }; request.onsuccess = (event) => { this.db = event.target.result; this.config.version = newVersion; if (this.config.logging) { console.log(`Store deleted: ${storeName}`); } resolve(); }; request.onupgradeneeded = (event) => { const db = event.target.result; if (db.objectStoreNames.contains(storeName)) { db.deleteObjectStore(storeName); } }; }); } /** * Get store information * @param {string} [storeName] - Store name (defaults to configured store) * @returns {Promise<Object>} */ async getStoreInfo(storeName = this.config.storeName) { await this.init(); const exists = this.db.objectStoreNames.contains(storeName); if (!exists) { throw new Error(`Store '${storeName}' does not exist`); } const transaction = this.db.transaction([storeName], 'readonly'); const store = transaction.objectStore(storeName); const info = { name: storeName, keyPath: store.keyPath, autoIncrement: store.autoIncrement, indexNames: Array.from(store.indexNames) }; // Get count return new Promise((resolve, reject) => { const countRequest = store.count(); countRequest.onsuccess = () => { info.count = countRequest.result; resolve(info); }; countRequest.onerror = () => reject(new Error(`Failed to get store info: ${storeName}`)); }); } /** * List all stores in the database * @returns {Promise<string[]>} */ async listStores() { await this.init(); return Array.from(this.db.objectStoreNames); } /** * Close the database connection */ close() { if (this.db) { this.db.close(); this.db = null; this.isInitialized = false; if (this.config.logging) { console.log(`Closed database: ${this.config.dbName}`); } } } /** * Get a store-specific API for easier multi-store operations * @param {string} storeName - Name of the store * @returns {Object} Store-specific API object */ getStore(storeName) { const self = this; return { setItem: (key, value) => self.setItem(key, value, storeName), getItem: (key) => self.getItem(key, storeName), removeItem: (key) => self.removeItem(key, storeName), clear: () => self.clear(storeName), getAllKeys: () => self.getAllKeys(storeName), getAllValues: () => self.getAllValues(storeName), getInfo: () => self.getStoreInfo(storeName) }; } /** * Get data from all stores in the database * @returns {Promise<Object>} Object with store names as keys and their data as values */ async getAllStoresData() { await this.init(); const stores = await this.listStores(); const result = {}; for (const storeName of stores) { try { const values = await this.getAllValues(storeName); const keys = await this.getAllKeys(storeName); result[storeName] = {}; // Map keys to values for (let i = 0; i < keys.length; i++) { result[storeName][keys[i]] = values[i]; } } catch (error) { if (this.config.logging) { console.warn(`Failed to get data from store ${storeName}:`, error); } result[storeName] = null; } } return result; } /** * Clear all stores in the database * @returns {Promise<void>} */ async clearAllStores() { await this.init(); const stores = await this.listStores(); for (const storeName of stores) { try { await this.clear(storeName); if (this.config.logging) { console.log(`Cleared store: ${storeName}`); } } catch (error) { if (this.config.logging) { console.error(`Failed to clear store ${storeName}:`, error); } } } } /** * Get available stores (alias for listStores for consistency with multi-store API) * @returns {Promise<string[]>} */ async getAvailableStores() { return this.listStores(); } /** * Set multiple items across different stores * @param {Object} storeData - Object with store names as keys and data objects as values * @returns {Promise<void>} */ async setMultipleStores(storeData) { await this.init(); for (const [storeName, data] of Object.entries(storeData)) { for (const [key, value] of Object.entries(data)) { await this.setItem(key, value, storeName); } } } /** * Get multiple items from different stores * @param {Object} storeKeys - Object with store names as keys and key arrays as values * @returns {Promise<Object>} Object with store names as keys and retrieved data as values */ async getMultipleStores(storeKeys) { await this.init(); const result = {}; for (const [storeName, keys] of Object.entries(storeKeys)) { result[storeName] = {}; for (const key of keys) { result[storeName][key] = await this.getItem(key, storeName); } } return result; } // Static methods for backward compatibility (legacy API) static _defaultInstance = null; /** * Get the default static instance * @private */ static _getDefaultInstance() { if (!IndexedStorage._defaultInstance) { IndexedStorage._defaultInstance = new IndexedStorage({ dbName: 'IndexedStorage', version: 1, storeName: 'data', logging: false }); } return IndexedStorage._defaultInstance; } /** * Static method: Get an item * @param {string} key * @param {string} [storeName] * @returns {Promise<any>} */ static async getItem(key, storeName) { return IndexedStorage._getDefaultInstance().getItem(key, storeName); } /** * Static method: Set an item * @param {string} key * @param {any} value * @param {string} [storeName] * @returns {Promise<void>} */ static async setItem(key, value, storeName) { return IndexedStorage._getDefaultInstance().setItem(key, value, storeName); } /** * Static method: Remove an item * @param {string} key * @param {string} [storeName] * @returns {Promise<void>} */ static async removeItem(key, storeName) { return IndexedStorage._getDefaultInstance().removeItem(key, storeName); } /** * Static method: Clear a store * @param {string} [storeName] * @returns {Promise<void>} */ static async clear(storeName) { return IndexedStorage._getDefaultInstance().clear(storeName); } /** * Static method: Get all keys * @param {string} [storeName] * @returns {Promise<string[]>} */ static async getAllKeys(storeName) { return IndexedStorage._getDefaultInstance().getAllKeys(storeName); } /** * Static method: Get all values * @param {string} [storeName] * @returns {Promise<any[]>} */ static async getAllValues(storeName) { return IndexedStorage._getDefaultInstance().getAllValues(storeName); } /** * Static method: Create a store * @param {string} storeName * @param {Object} [storeConfig] * @returns {Promise<void>} */ static async createStore(storeName, storeConfig) { return IndexedStorage._getDefaultInstance().createStore(storeName, storeConfig); } /** * Static method: Delete a store * @param {string} storeName * @returns {Promise<void>} */ static async deleteStore(storeName) { return IndexedStorage._getDefaultInstance().deleteStore(storeName); } /** * Static method: Get store info * @param {string} [storeName] * @returns {Promise<Object>} */ static async getStoreInfo(storeName) { return IndexedStorage._getDefaultInstance().getStoreInfo(storeName); } /** * Static method: List stores * @returns {Promise<string[]>} */ static async listStores() { return IndexedStorage._getDefaultInstance().listStores(); } /** * Static method: Initialize * @returns {Promise<IDBDatabase>} */ static async init() { return IndexedStorage._getDefaultInstance().init(); } /** * Static method: Close */ static close() { if (IndexedStorage._defaultInstance) { IndexedStorage._defaultInstance.close(); IndexedStorage._defaultInstance = null; } } /** * Static method: Get store-specific API * @param {string} storeName * @returns {Object} */ static getStore(storeName) { return IndexedStorage._getDefaultInstance().getStore(storeName); } /** * Static method: Get all stores data * @returns {Promise<Object>} */ static async getAllStoresData() { return IndexedStorage._getDefaultInstance().getAllStoresData(); } /** * Static method: Clear all stores * @returns {Promise<void>} */ static async clearAllStores() { return IndexedStorage._getDefaultInstance().clearAllStores(); } /** * Static method: Get available stores * @returns {Promise<string[]>} */ static async getAvailableStores() { return IndexedStorage._getDefaultInstance().getAvailableStores(); } /** * Static method: Set multiple stores * @param {Object} storeData * @returns {Promise<void>} */ static async setMultipleStores(storeData) { return IndexedStorage._getDefaultInstance().setMultipleStores(storeData); } /** * Static method: Get multiple stores * @param {Object} storeKeys * @returns {Promise<Object>} */ static async getMultipleStores(storeKeys) { return IndexedStorage._getDefaultInstance().getMultipleStores(storeKeys); } } // Export for both CommonJS and ES modules if (typeof module !== 'undefined' && module.exports) { module.exports = IndexedStorage; } else if (typeof window !== 'undefined') { window.IndexedStorage = IndexedStorage; } // Configuration for IndexedStorage // Internal logging function function log(type, message, ...args) { } // IndexedDB utility composable for state management /** * IndexedDB composable with configurable database settings * @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) * @returns {Object} IndexedDB operations */ function useIndexedDB(config = {}) { // Database configuration with defaults const DB_NAME = config.dbName || 'LocalStorageDB'; const DB_VERSION = config.dbVersion || 1; const STORE_NAME = config.storeName || 'localStates'; const ADDITIONAL_STORES = config.additionalStores || []; // All store names (primary + additional) const ALL_STORES = [STORE_NAME, ...ADDITIONAL_STORES]; /** * Opens IndexedDB connection with proper error handling * @returns {Promise<IDBDatabase>} Database instance */ function openDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => { log('error', 'πŸ—„οΈβŒ IndexedDB open error:', request.error); reject(request.error); }; request.onsuccess = () => { resolve(request.result); }; request.onupgradeneeded = (event) => { const db = event.target.result; // Create all required stores ALL_STORES.forEach(storeName => { if (!db.objectStoreNames.contains(storeName)) { const store = db.createObjectStore(storeName, { keyPath: 'id' }); // Create indexes for better querying store.createIndex('timestamp', 'timestamp', { unique: false }); store.createIndex('version', 'version', { unique: false }); } }); }; }); } /** * Retrieves data from IndexedDB by key * @param {string} key - The key to retrieve * @param {string} storeName - Optional store name (defaults to primary store) * @returns {Promise<Object|null>} Retrieved data or null if not found */ async function get(key, storeName = STORE_NAME) { try { const db = await openDB(); const transaction = db.transaction([storeName], 'readonly'); const store = transaction.objectStore(storeName); const request = store.get(key); return new Promise((resolve, reject) => { request.onsuccess = () => { const result = request.result; resolve(result || null); }; request.onerror = () => reject(request.error); }); } catch (error) { return null; } } /** * Saves data to IndexedDB * @param {string} key - The key to save under * @param {Object} data - The data to save * @param {string} storeName - Optional store name (defaults to primary store) * @returns {Promise<void>} */ async function save(key, data, storeName = STORE_NAME) { try { const db = await openDB(); const transaction = db.transaction([storeName], 'readwrite'); const store = transaction.objectStore(storeName); const dataToSave = { id: key, ...data, timestamp: data.timestamp || new Date().toISOString(), version: data.version || '1.0' }; const request = store.put(dataToSave); return new Promise((resolve, reject) => { request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } catch (error) { throw error; } } /** * Removes data from IndexedDB by key * @param {string} key - The key to remove * @param {string} storeName - Optional store name (defaults to primary store) * @returns {Promise<void>} */ async function remove(key, storeName = STORE_NAME) { try { const db = await openDB(); const transaction = db.transaction([storeName], 'readwrite'); const store = transaction.objectStore(storeName); const request = store.delete(key); return new Promise((resolve, reject) => { request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } catch (error) { throw error; } } /** * Gets all data from IndexedDB * @param {string} storeName - Optional store name (defaults to primary store) * @returns {Promise<Array>} Array of all stored items */ async function getAll(storeName = STORE_NAME) { try { const db = await openDB(); const transaction = db.transaction([storeName], 'readonly'); const store = transaction.objectStore(storeName); const request = store.getAll(); return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result || []); request.onerror = () => reject(request.error); }); } catch (error) { return []; } } /** * Checks if a key exists in IndexedDB * @param {string} key - The key to check * @param {string} storeName - Optional store name (defaults to primary store) * @returns {Promise<boolean>} True if key exists */ async function exists(key, storeName = STORE_NAME) { try { const data = await get(key, storeName); return data !== null; } catch (error) { return false; } } /** * Clears all data from IndexedDB * @param {string} storeName - Optional store name (defaults to primary store) * @returns {Promise<void>} */ async function clear(storeName = STORE_NAME) { try { const db = await openDB(); const transaction = db.transaction([storeName], 'readwrite'); const store = transaction.objectStore(storeName); const request = store.clear(); return new Promise((resolve, reject) => { request.onsuccess = () => { log('log', `πŸ—„οΈπŸ§Ή IndexedDB store '${storeName}' cleared successfully`); resolve(); }; request.onerror = () => reject(request.error); }); } catch (error) { throw error; } } /** * Calculates storage usage for all items in IndexedDB * @param {string} storeName - Optional store name (defaults to primary store) * @returns {Promise<Object>} Usage information */ async function calculateUsage(storeName = STORE_NAME) { try { const allData = await getAll(storeName); let totalSize = 0; const items = {}; allData.forEach(item => { const size = new Blob([JSON.stringify(item)]).size; totalSize += size; items[item.id] = size; }); return { totalSize, items }; } catch (error) { return { totalSize: 0, items: {} }; } } /** * Tests if IndexedDB is available and working * @returns {Promise<boolean>} True if available */ async function testAvailability() { try { const testKey = 'indexeddb-test'; const testData = { test: true, timestamp: new Date().toISOString() }; await save(testKey, testData); const retrieved = await get(testKey); await remove(testKey); return retrieved !== null && retrieved.test === true; } catch (error) { return false; } } /** * Get list of all available store names in the database * @returns {string[]} Array of store names */ function getAvailableStores() { return ALL_STORES; } /** * Create a store-specific API that operates only on a particular store * @param {string} storeName - The store name to create API for * @returns {Object} Store-specific API */ function createStoreAPI(storeName) { if (!ALL_STORES.includes(storeName)) { throw new Error(`Store '${storeName}' is not configured. Available stores: ${ALL_STORES.join(', ')}`); } return { get: (key) => get(key, storeName), save: (key, data) => save(key, data, storeName), remove: (key) => remove(key, storeName), getAll: () => getAll(storeName), exists: (key) => exists(key, storeName), clear: () => clear(storeName), calculateUsage: () => calculateUsage(storeName), storeName }; } /** * Clear all stores in the database * @returns {Promise<void>} */ async function clearAllStores() { try { for (const storeName of ALL_STORES) { await clear(storeName); } log('log', 'πŸ—„οΈπŸ§Ή All stores cleared successfully'); } catch (error) { throw error; } } /** * Get data from all stores * @returns {Promise<Object>} Object with store names as keys and their data as values */ async function getAllStoresData() { try { const result = {}; for (const storeName of ALL_STORES) { result[storeName] = await getAll(storeName); } return result; } catch (error) { return {}; } } return { // Core operations (work with primary store or specified store) get, save, remove, getAll, exists, clear, // Database management openDB, // Utility functions calculateUsage, testAvailability, // Multi-store operations getAvailableStores, createStoreAPI, clearAllStores, getAllStoresData, // Configuration info dbName: DB_NAME, dbVersion: DB_VERSION, primaryStore: STORE_NAME, allStores: ALL_STORES }; } /** * 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) */ 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) { // 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) { 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) { 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) { 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) {