@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
JavaScript
/**
* 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;