indexeddb-keyvalue
v1.0.12
Published
Lightweight IndexedDB wrapper with table management, CRUD operations, and HTTP request caching
Maintainers
Readme
indexeddb-keyvalue
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.
Features
- Ultra Small - Only ~1KB gzipped, zero dependencies, extremely lightweight
- Dual-Layer Caching - Memory + IndexedDB, subsequent reads return directly from memory, 100-500x performance boost
- Single Instance Principle - Multiple
CachedStorageinstances with the same dbName+tableName share one underlying IndexedDB table and connection - localStorage-Compatible API - Drop-in replacement for localStorage with async support and object storage
- Transparent Factory - Automatic global instance caching, no manual factory management needed
- Automatic Version Management - No manual database upgrades needed
- Automatic Table Creation - Tables are created automatically when used
- Promise API - Fully asynchronous, supports async/await
- HTTP Caching - Automatically cache fetch request results to IndexedDB
- TypeScript Support - Complete type definitions
- Zero Dependencies - No external dependencies
Architecture
┌─────────────────────────────────────────────────────────────┐
│ 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:
- Simplified Public API: Only
CachedStorageandCachedFetchare exported - Transparent Factory: Users don't need to manage
StorageFactorymanually - Single Instance: Same dbName+tableName always shares one IndexedDB connection
- Clean Separation: Private implementation classes are hidden in
src/private/
Performance Comparison
| 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.
Installation
npm install indexeddb-keyvalueUsage
CachedStorage (Recommended)
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:
- First read: Load from IndexedDB → ~1-5ms
- Subsequent reads: Return from memory → ~0.01ms, 100-500x faster
Single Instance Principle
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 cacheThis design ensures:
- ✅ Resource efficiency (single connection per table)
- ✅ Data consistency across instances
- ✅ Independent memory caching per instance
localStorage API Compatibility
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:
- ✅ Stores any JavaScript object (not just strings)
- ✅ Asynchronous - doesn't block the main thread
- ✅ Much larger storage capacity (typically 50MB+ vs 5-10MB)
- ✅ Memory caching for instant subsequent reads
- ✅ Structured data with multiple tables
CachedFetch (HTTP Request Caching)
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 cachesMulti-Table Usage
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 }Subscription (Reactive)
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:
- ✅ Immediate callback - Fires once on subscription with current value
- ✅ Deep equality check - Only notifies when value actually changes
- ✅ Multiple subscribers - One key can have multiple independent subscribers
- ✅ Error isolation - One failing callback doesn't affect others
- ✅ Cross-tab sync - Changes automatically sync across browser tabs/iframes
- ✅ Batch unsubscribe - Clear all subscriptions at once
Batch Unsubscribe
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: Cleanup everything (subscriptions + cross-tab communication)
storage.destroy();Cross-Tab Synchronization
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 changesHow it works:
- Uses
BroadcastChannelAPI for cross-tab communication - Changes are automatically synchronized across all open tabs/iframes
- Memory cache is updated in real-time on all tabs
- No extra code needed - works with the same
subscribe()API - Same-origin only (no cross-domain support)
- Gracefully degrades if
BroadcastChannelis not available
React Hook Example
import { 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>;
}Vue 3 Composable Example
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');Svelte Store Example
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 };
}API Reference
CachedStorage
Constructor
| 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.
Methods
| 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 |
| destroy() | - | void | Cleanup resources, stop cross-tab communication, and clear all subscriptions |
CachedFetch
| 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 management
TypeScript
import { 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);Architecture Details
Directory Structure
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.jsKey Design Decisions
- Single Instance Principle: Same dbName+tableName always returns the same underlying IndexedDB connection
- Transparent Factory: Factory pattern is internal, users just use
new CachedStorage() - Clean API Surface: Only 2 classes exported, reducing cognitive load
- Dual-Layer Caching: Memory for speed, IndexedDB for persistence
- localStorage Compatibility: Familiar API with enhanced capabilities
Migration from v1.x
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 appliesDevelopment
# Install dependencies
npm install
# Development mode (start local server)
npm run dev
# Build production version
npm run build
# Build development version
npm run build:devBrowser Compatibility
- Chrome/Edge 24+
- Firefox 16+
- Safari 10+
- iOS Safari 10+
- Android Chrome 25+
License
MIT
