@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
JavaScript
(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) {