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

737 lines (652 loc) 24.9 kB
/** * 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)) { const store = 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 }; const store = 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; } export default IndexedStorage;