A lightweight IndexedDB wrapper with dual-layer caching (Memory + IndexedDB) and HTTP request caching. Features a refactored architecture with clean separation of public and private APIs.
CachedStorage instances with the same dbName+tableName share one underlying IndexedDB table and connection┌─────────────────────────────────────────────────────────────┐ │ Public API (index.js) │ ├─────────────────────────────────────────────────────────────┤ │ CachedStorage CachedFetch │ │ (default export) (network caching) │ │ │ │ │ │ └──────────────────────┘ │ │ │ │ └────────────────┼────────────────────────────────────────────┘ │ (transparent factory access) ┌────────────────┼────────────────────────────────────────────┐ │ ▼ Private Implementation │ │ ┌─────────────────────┐ ┌──────────────┐ │ │ │ PrivateStorage │───>│ PrivateTiny │ │ │ │ Factory │ │ IndexDB │ │ │ │ (global caching) │ │ (low-level) │ │ │ └─────────────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────────┘
Key architectural improvements:
CachedStorage and CachedFetch are exportedStorageFactory manuallysrc/private/| Operation | Pure IndexedDB | indexeddb-keyvalue (Memory Cache) | Performance Boost |
|---|---|---|---|
| First Read | ~1-5ms | ~1-5ms | Same |
| Subsequent Reads | ~1-5ms | ~0.01ms | 100-500x |
| Write | ~2-8ms | ~2-8ms (Memory + Persistence) | Reliable persistence |
Based on Chrome/Edge browser testing, actual performance varies by data size and device. CachedStorage automatically caches read data to memory, making subsequent access nearly instant.
npm install indexeddb-keyvalue
The simplest way to use it, with built-in memory caching, one line for high-performance data storage:
import { CachedStorage } from 'indexeddb-keyvalue';
// Create storage instance (with dual-layer Memory + IndexedDB caching)
const storage = new CachedStorage('myDB', 'myTable');
// Save data (writes to both memory and IndexedDB)
await storage.setItem('user1', { name: 'John', age: 25 });
// First read - loads from IndexedDB and caches to memory
const user1 = await storage.getItem('user1');
// Second read - returns directly from memory, 100x+ faster!
const user2 = await storage.getItem('user1'); // ⚡ Lightning fast, almost no delay
// Check memory cache status
console.log('Memory cache entries:', storage.getMemoryCacheSize());
// Delete data (removes from both memory and IndexedDB)
await storage.removeItem('user1');
// Clear table (clears both memory and IndexedDB)
await storage.clear();
// Get all keys (enhanced API beyond localStorage)
const allKeys = await storage.keys();
console.log('All keys:', allKeys);
// Get key by index (localStorage-compatible)
const firstKey = await storage.key(0);
console.log('First key:', firstKey);
// Get total count (localStorage-compatible, async version of length)
const count = await storage.length();
console.log('Total entries:', count);
// Clear only memory cache (keeps IndexedDB data)
storage.clearMemoryCache();
Performance Benefits:
Multiple instances with the same dbName and tableName automatically share the same underlying IndexedDB table and connection:
import { CachedStorage } from 'indexeddb-keyvalue';
// Create two instances with same dbName + tableName
const storageA = new CachedStorage('myDB', 'myTable');
const storageB = new CachedStorage('myDB', 'myTable');
// Write through instance A
await storageA.setItem('key', 'value from A');
// Read through instance B - gets the same data!
const data = await storageB.getItem('key'); // 'value from A'
// Both instances share the same IndexedDB table and connection
// but each has independent memory cache
This design ensures:
This library provides a localStorage-compatible API that makes migration effortless:
| localStorage | indexeddb-keyvalue | Difference |
|---|---|---|
setItem(key, value) | await storage.setItem(key, value) | Async + supports objects |
getItem(key) | await storage.getItem(key) | Async + returns objects |
removeItem(key) | await storage.removeItem(key) | Async |
clear() | await storage.clear() | Async |
key(index) | await storage.key(index) | Async |
length | await storage.length() | Async method |
Key advantages over localStorage:
Automatically cache network request results to IndexedDB:
import { CachedFetch } from 'indexeddb-keyvalue';
const cachedFetch = new CachedFetch('cacheDB', 'apiCache');
// First request hits the network and caches the result
const data = await cachedFetch.fetchJson('https://api.example.com/data');
// Subsequent requests read directly from IndexedDB, no network access
const cachedData = await cachedFetch.fetchJson('https://api.example.com/data');
// Support for other response types
const text = await cachedFetch.fetchText('https://api.example.com/text');
const blob = await cachedFetch.fetchBlob('https://api.example.com/image.png');
const buffer = await cachedFetch.fetchArrayBuffer('https://api.example.com/binary');
// Use converter function to process data
const users = await cachedFetch.fetchJson('https://api.example.com/users', (data) => {
return data.map(user => ({ ...user, fullName: `${user.firstName} ${user.lastName}` }));
});
// Access underlying CachedStorage for cache management
await cachedFetch.cachedStorage.clear(); // Clear all request caches
Each table is completely isolated:
import { CachedStorage } from 'indexeddb-keyvalue';
// Create separate tables in the same database
const userStorage = new CachedStorage('appDB', 'users');
const orderStorage = new CachedStorage('appDB', 'orders');
const productStorage = new CachedStorage('appDB', 'products');
// Each table is independent
await userStorage.setItem('user1', { name: 'John' });
await orderStorage.setItem('order1', { total: 100 });
await productStorage.setItem('product1', { name: 'Product A' });
// Data is completely isolated between tables
const user = await userStorage.getItem('user1'); // { name: 'John' }
const order = await orderStorage.getItem('order1'); // { total: 100 }
Subscribe to data changes with automatic initialization callback:
const storage = new CachedStorage('myDB', 'myTable');
// Subscribe to changes
const unsubscribe = storage.subscribe('user', (value, oldValue, key) => {
console.log(`Key "${key}" changed:`, oldValue, '->', value);
});
// Note: Callback fires immediately with current value from memory cache
// Update data - triggers notification
await storage.setItem('user', { name: '张三' });
// Unsubscribe
unsubscribe();
Alternative unsubscribe method:
function onChange(value, oldValue, key) {
console.log('Changed:', value);
}
storage.subscribe('user', onChange);
// Unsubscribe using method (when you can't save the return value)
storage.unsubscribe('user', onChange);
Key features:
Clear all subscriptions at once (useful when component unmounts):
const storage = new CachedStorage('myDB', 'myTable');
// Create multiple subscriptions
storage.subscribe('user', onUserChange);
storage.subscribe('settings', onSettingsChange);
storage.subscribe('theme', onThemeChange);
// Option 1: Clear subscriptions for a specific key
const cleared = storage.clearKeySubscriptions('user');
console.log(`Cleared ${cleared} subscriptions for 'user'`);
// Option 2: Clear all subscriptions at once
const total = storage.clearAllSubscriptions();
console.log(`Cleared ${total} total subscriptions`);
// Option 3: Get subscription statistics
const stats = storage.getSubscriptionStats();
console.log('Total subscriptions:', stats.total);
console.log('By key:', stats.byKey);
// { total: 5, byKey: { user: 2, settings: 2, theme: 1 } }
// Option 4: Clear memory cache only
storage.clearMemoryCache();
For test environments or when you need to completely delete database resources:
import { CachedStorage } from 'indexeddb-keyvalue';
// Delete entire database (closes connections, clears caches, deletes IndexedDB)
await CachedStorage.dropDB('my_database');
// Clear all data from current table (keeps table structure)
const storage = new CachedStorage('my_database', 'my_table');
await storage.dropTable();
Use cases:
Note: dropDB() is a static method called on the class, while dropTable() is an instance method.
Subscribe to data changes across browser tabs and iframes (same-origin only):
// Tab 1
const storage1 = new CachedStorage('myDB', 'myTable');
storage1.subscribe('user', (value, oldValue, key) => {
console.log('Tab 1 received:', value);
});
await storage1.setItem('user', { name: '张三' });
// Automatically broadcasts to other tabs
// Tab 2 (same origin)
const storage2 = new CachedStorage('myDB', 'myTable');
storage2.subscribe('user', (value, oldValue, key) => {
console.log('Tab 2 received:', value); // { name: '张三' }
});
// Callback fires immediately with current value, then updates when Tab 1 changes
How it works:
BroadcastChannel API for cross-tab communicationsubscribe() APIBroadcastChannel is not availableimport { useState, useEffect } from 'react';
function useStorageItem(storage, key) {
const [value, setValue] = useState(undefined);
useEffect(() => {
// subscribe fires immediately with current value
return storage.subscribe(key, setValue);
}, [storage, key]);
return value;
}
// Usage
function UserProfile() {
const user = useStorageItem(storage, 'user');
if (user === undefined) return <div>Loading...</div>;
return <div>{user?.name || 'No data'}</div>;
}
import { ref, onUnmounted } from 'vue';
export function useStorageItem(storage, key) {
const data = ref(undefined);
const unsubscribe = storage.subscribe(key, (value) => {
data.value = value;
});
onUnmounted(unsubscribe);
return data;
}
// Usage
const user = useStorageItem(storage, 'user');
import { writable } from 'svelte/store';
function storageStore(storage, key) {
const { subscribe, set } = writable(undefined);
const unsubscribe = storage.subscribe(key, (value) => set(value));
return { subscribe, unsubscribe };
}
| Parameter | Type | Default | Description |
|---|---|---|---|
dbName | string | 'linushp_default' | Database name |
tableName | string | 'linushp_t' | Table name |
options | Object | {} | Configuration options |
options.enableCrossTab | boolean | true | Enable cross-tab synchronization (BroadcastChannel) |
Usage Examples:
// Traditional way (backward compatible)
const storage = new CachedStorage('myDB', 'myTable');
// Configuration object way
const storage = new CachedStorage({
dbName: 'myDB',
tableName: 'myTable',
enableCrossTab: false // Disable cross-tab sync
});
Note: For the same dbName + tableName, the configuration from the first created instance takes effect. Subsequent instances will reuse the same underlying PrivateCachedStorage instance regardless of their configuration.
| Method | Parameters | Return | Description |
|---|---|---|---|
setItem(key, value) | key: string, value: any | Promise<void> | Save or update data (Memory + IndexedDB) |
getItem(key) | key: string | Promise<any> | Get data (priority from memory cache) |
removeItem(key) | key: string | Promise<void> | Delete data (Memory + IndexedDB) |
clear() | - | Promise<void> | Clear table (Memory + IndexedDB) |
key(index) | index: number | Promise<string | null> | Get key at specified index |
keys() | - | Promise<string[]> | Get all keys (enhanced API) |
length() | - | Promise<number> | Get total number of entries |
clearMemoryCache() | - | void | Clear only memory cache |
getMemoryCacheSize() | - | number | Get memory cache entry count |
subscribe(key, callback) | key: string, callback: (value, oldValue, key) => void | () => void | Subscribe to key changes (supports cross-tab sync), returns unsubscribe function |
unsubscribe(key, callback) | key: string, callback: Function | boolean | Unsubscribe a specific callback |
clearKeySubscriptions(key) | key: string | number | Clear all subscriptions for a specific key, returns count cleared |
clearAllSubscriptions() | - | number | Clear all subscriptions (all keys), returns total count cleared |
getSubscriptionStats() | - | { total, byKey } | Get subscription statistics |
CachedStorage.dropDB(dbName) | dbName: string | Promise<void> | Static - Delete entire database (close connections, clear caches, delete IndexedDB) |
storage.dropTable() | - | Promise<void> | Clear all data from current table (keeps table structure) |
| Method | Parameters | Return | Description |
|---|---|---|---|
fetchJson(url, converter?) | url: string, converter?: (data) => any | Promise<T> | Get JSON data |
fetchText(url, converter?) | url: string, converter?: (data) => string | Promise<string> | Get text data |
fetchBlob(url, converter?) | url: string, converter?: (data) => Blob | Promise<Blob> | Get Blob data |
fetchArrayBuffer(url, converter?) | url: string, converter?: (data) => ArrayBuffer | Promise<ArrayBuffer> | Get binary data |
Properties:
cachedStorage: CachedStorage - The underlying storage instance for direct cache managementimport { CachedStorage, CachedFetch, ResponseType, Converter } from 'indexeddb-keyvalue';
const storage = new CachedStorage('myDB', 'myTable');
const cachedFetch = new CachedFetch('cacheDB', 'apiCache');
// Type-safe usage
interface User {
id: string;
name: string;
}
const user = await storage.getItem('user1') as User;
const converter: Converter<User[]> = (data) => {
return data.map((item: any) => ({ id: item.id, name: item.name }));
};
const users = await cachedFetch.fetchJson<User[]>('https://api.example.com/users', converter);
src/ ├── index.js # Public API: exports CachedStorage, CachedFetch ├── CachedStorage.js # Public class: dual-layer caching storage ├── CachedFetch.js # Public class: HTTP request caching └── private/ # Private implementation (not exported) ├── PrivateStorageFactory.js ├── PrivateTableStorage.js ├── PrivateDatabase.js └── utils/ └── promisifyStore.js
new CachedStorage()If you were using StorageFactory or TinyIndexDB directly:
// Old (v1.x) - Using StorageFactory
import { StorageFactory } from 'indexeddb-keyvalue';
const storage = StorageFactory.getStorage('db', 'table');
// New (v2.x) - Simplified API
import { CachedStorage } from 'indexeddb-keyvalue';
const storage = new CachedStorage('db', 'table');
// Factory is now transparent, single instance principle still applies
# Install dependencies
npm install
# Development mode (start local server)
npm run dev
# Build production version
npm run build
# Build development version
npm run build:dev
MIT