localspace is a Promise-first storage toolkit for IndexedDB, localStorage, and React Native AsyncStorage, with TypeScript types and a focused plugin system.
localspace keeps the familiar key-value shape of localForage while making Promise semantics, explicit batching, and driver capabilities the actual contract. It adds first-class TypeScript types, IndexedDB transactions, multi-platform drivers, and a plugin architecture.
It is not a callback-compatible drop-in replacement. Existing Promise-based
localForage usage is usually straightforward to migrate; callback-based code
must be converted to await or .then().
Get started in 5 minutes:
pnpm add localspace
# or: npm install localspaceimport localspace from 'localspace';
// Store and retrieve data
await localspace.setItem('user', { name: 'Ada', role: 'admin' });
const user = await localspace.getItem<{ name: string; role: string }>('user');
// TypeScript generics for type safety
interface User {
name: string;
role: string;
}
const typedUser = await localspace.getItem<User>('user');const cache = localspace.createInstance({
name: 'my-app',
storeName: 'cache',
});
await cache.setItem('token', 'abc123');// Write multiple items in one transaction (IndexedDB)
await localspace.setItems([
{ key: 'user:1', value: { name: 'Ada' } },
{ key: 'user:2', value: { name: 'Grace' } },
]);
// Read multiple items
const users = await localspace.getItems(['user:1', 'user:2']);import localspace, { ttlPlugin, encryptionPlugin } from 'localspace';
const secureStore = localspace.createInstance({
name: 'secure',
plugins: [
ttlPlugin({ defaultTTL: 60_000 }), // Auto-expire after 1 minute
encryptionPlugin({ key: 'your-32-byte-key' }), // Encrypt data
],
});That's it! For more details, see the sections below.
- Installation
- Core API
- Batch Operations
- Plugin System
- Configuration
- Performance Notes
- Troubleshooting
- Documentation
- License
pnpm add localspace
# or
npm install localspaceimport localspace from 'localspace';Bundles included: ES modules, CommonJS, UMD, plus .d.ts files.
// Set and get items
await localspace.setItem('key', value);
const value = await localspace.getItem<T>('key');
// Remove items
await localspace.removeItem('key');
await localspace.clear();
// Query
const count = await localspace.length();
const keys = await localspace.keys();
// Iterate
await localspace.iterate<T, void>((value, key, index) => {
console.log(key, value);
});// Web fallback order (default bundled drivers)
await localspace.setDriver([localspace.INDEXEDDB, localspace.LOCALSTORAGE]);
// Check current driver
console.log(localspace.driver());
// 'asyncStorage' | 'localStorageWrapper'When browser persistent storage is unavailable (for example, cookies/site data are blocked), opt in to the memory driver explicitly:
await localspace.setDriver([
localspace.INDEXEDDB,
localspace.LOCALSTORAGE,
localspace.MEMORY,
]);The memory driver is runtime-only: data is shared by name/storeName while the
page is alive and is lost on reload. It is not part of the default fallback order
so persistent-storage failures remain visible unless you opt in.
import AsyncStorage from '@react-native-async-storage/async-storage';
import localspace from 'localspace';
import { createReactNativeInstance } from 'localspace/react-native';
const mobileStore = await createReactNativeInstance(localspace, {
name: 'myapp',
storeName: 'kv',
reactNativeAsyncStorage: AsyncStorage,
});The default localspace entry does not bundle the React Native driver; it is included only when importing localspace/react-native. Explicit reactNativeAsyncStorage injection is recommended.
Advanced usage is still available:
import localspace from 'localspace';
import { installReactNativeAsyncStorageDriver } from 'localspace/react-native';
await installReactNativeAsyncStorageDriver(localspace);
await localspace.setDriver(localspace.REACTNATIVEASYNCSTORAGE);Integration smoke (official AsyncStorage Jest mock):
pnpm test:rn:integrationSee integration/react-native-jest/README.md for details.
Manual Detox smoke workflow (real simulator/emulator runtime):
- Workflow:
.github/workflows/detox-mobile.yml - Fixture app folder:
integration/react-native-detox/ - Fixture README:
integration/react-native-detox/README.md
π Full API Reference: docs/api-reference.md
Use batch APIs for better performance. IndexedDB runs each batch chunk inside a
transaction; when maxBatchSize is unset, the entire call is one chunk. Other
drivers expose the same API but may perform grouped work sequentially.
// One transaction on IndexedDB when maxBatchSize is unset
await localspace.setItems([
{ key: 'user:1', value: { name: 'Ada' } },
{ key: 'user:2', value: { name: 'Lin' } },
]);
// Ordered bulk read
const result = await localspace.getItems(['user:1', 'user:2']);
// [{ key: 'user:1', value: {...} }, { key: 'user:2', value: {...} }]
// One transaction on IndexedDB when maxBatchSize is unset
await localspace.removeItems(['user:1', 'user:2']);For atomic multi-step operations on IndexedDB:
await localspace.runTransaction('readwrite', async (tx) => {
const current = (await tx.get<number>('counter')) ?? 0;
await tx.set('counter', current + 1);
await tx.set('lastUpdated', Date.now());
});runTransaction() is supported by the IndexedDB and memory drivers. On
localStorage and React Native AsyncStorage it rejects with
UNSUPPORTED_OPERATION; use explicit operations when atomicity is unnecessary.
localspace ships with a powerful plugin engine:
import localspace, {
ttlPlugin,
compressionPlugin,
encryptionPlugin,
} from 'localspace';
const store = localspace.createInstance({
name: 'secure-store',
plugins: [
ttlPlugin({ defaultTTL: 60_000 }), // Auto-expire
compressionPlugin({ threshold: 1024 }), // Compress > 1KB
encryptionPlugin({ key: '32-byte-key-here' }), // Encrypt
],
pluginErrorPolicy: 'strict', // Recommended for encryption
});| Plugin | Purpose |
|---|---|
| TTL | Auto-expire items with { data, expiresAt } wrapper |
| Encryption | AES-GCM encryption via Web Crypto API |
| Compression | LZ-string compression for large values |
Cross-context replication is intentionally not built in. For best-effort
single-item notifications, adapt
examples/broadcast-notification-plugin.ts
to your application-level synchronization protocol.
Serialized-size limits are also application policy rather than browser quota
management. The non-published
examples/size-limit-plugin.ts example
shows a best-effort guard without automatic data eviction.
π Full Plugin Documentation: docs/plugins.md π Real-World Examples: docs/examples.md
const store = localspace.createInstance({
// Database
name: 'myapp', // Database name
storeName: 'data', // Store name
version: 1, // Schema version
// Driver
driver: [localspace.INDEXEDDB, localspace.LOCALSTORAGE],
// Add localspace.MEMORY explicitly for runtime-only fallback.
// IndexedDB performance
durability: 'relaxed', // 'relaxed' (fast) or 'strict'
prewarmTransactions: true, // Pre-warm connection
// Batching
maxBatchSize: 200, // Split large batches
// Plugins
plugins: [],
pluginErrorPolicy: 'lenient', // 'strict' for encryption
});// React Native one-step instance
import localspace from 'localspace';
import { createReactNativeInstance } from 'localspace/react-native';
const mobileStore = await createReactNativeInstance(localspace, {
name: 'myapp',
storeName: 'data',
reactNativeAsyncStorage: AsyncStorage,
});π Full Configuration Options: docs/api-reference.md#configuration-options
- Batch APIs outperform loops:
setItems()~6x faster,getItems()~7.7x faster than per-item loops - Transaction helpers:
runTransaction()is atomic on IndexedDB and rolls back on the memory driver - IndexedDB durability: Chrome 121+ uses relaxed durability by default
- Non-transactional drivers: localStorage and React Native AsyncStorage reject
runTransaction()
| Issue | Solution |
|---|---|
| Driver not ready | Call await localspace.ready() before first operation |
| Browser storage is full | Check error.code === 'QUOTA_EXCEEDED' |
| Persistent storage blocked | Add localspace.MEMORY as an explicit fallback |
| Plugin errors swallowed | Set pluginErrorPolicy: 'strict' |
Errors are LocalSpaceError with code, details, and cause properties.
- Browsers: Modern Chromium/Edge, Firefox, Safari
- Drivers: IndexedDB (primary), localStorage, memory (opt-in fallback)
- React Native: AsyncStorage driver available via
localspace/react-nativeopt-in entry - WebSQL: Not supported (migrate to IndexedDB)
- Node/SSR: Custom driver required
| Document | Description |
|---|---|
| API Reference | Complete method documentation |
| Plugin System | Built-in plugins & custom development |
| Real-World Examples | Production-ready code patterns |
| Migration Guide | Migrating from localForage and 1.x |
- IndexedDB and localStorage drivers
- Opt-in memory fallback driver
- React Native AsyncStorage driver
- Familiar Promise-based key-value API
- TypeScript-first implementation
- Explicit batch operations
- Plugin system (TTL, Encryption, Compression)
- OPFS driver (Origin Private File System)
- Node.js (File system, SQLite)
- React Native SQLite driver
- Deno (Native KV store)