API Reference
July 14, 2026 ยท View on GitHub
Complete reference for all localspace methods with TypeScript signatures.
Core Methods
getItem<T>(key: string): Promise<T | null>
Retrieves an item from storage.
// Basic usage
const user = await localspace.getItem<User>('user');
// Returns null for non-existent keys
const missing = await localspace.getItem('nonexistent'); // null
setItem<T>(key: string, value: T): Promise<T>
Stores an item. Returns the stored value.
// Basic usage
await localspace.setItem('user', { name: 'Ada', role: 'admin' });
// Store various types
await localspace.setItem('count', 42);
await localspace.setItem('tags', ['a', 'b', 'c']);
await localspace.setItem('active', true);
await localspace.setItem('config', null);
// undefined is converted to null
await localspace.setItem('empty', undefined); // stored as null
removeItem(key: string): Promise<void>
Removes an item from storage.
await localspace.removeItem('user');
clear(): Promise<void>
Removes all items from the current store.
await localspace.clear();
length(): Promise<number>
Returns the number of items in the store.
const count = await localspace.length();
console.log(`Store has ${count} items`);
key(index: number): Promise<string | null>
Returns the key at the given index.
const firstKey = await localspace.key(0);
const lastKey = await localspace.key((await localspace.length()) - 1);
keys(): Promise<string[]>
Returns all keys in the store.
const allKeys = await localspace.keys();
console.log('Keys:', allKeys);
iterate<T, U>(iterator: (value: T, key: string, index: number) => U): Promise<U>
Iterates over all items. Return a non-undefined value to stop early.
When the built-in encryption, compression, or TTL plugin is active, LocalSpace
2.1 rejects iterate() with UNSUPPORTED_OPERATION before invoking the
iterator. Use keys() plus getItems() when logical, plugin-processed values
are required.
// Process all items
await localspace.iterate<User, void>((value, key, index) => {
console.log(`${index}. ${key}:`, value);
});
// Early termination - find first admin
const admin = await localspace.iterate<User, User>((value, key) => {
if (value.role === 'admin') {
return value; // stops iteration and returns this value
}
});
Error Contract
Rejected operations use LocalSpaceError. Branch on error.code, not browser
exception text; the original failure is available through error.cause, while
error.details carries stable context such as driver, operation, key,
configKey, and attempted driver failures.
Common codes include:
| Code | Meaning |
|---|---|
INVALID_CONFIG | Configuration failed normalization before driver initialization |
DRIVER_UNAVAILABLE | Every requested driver failed; details.driverErrors preserves each |
QUOTA_EXCEEDED | A driver reported a storage quota failure |
OPERATION_FAILED | A driver operation failed for another stable category |
UNSUPPORTED_OPERATION | The selected driver/plugin combination cannot honor the contract |
INSTANCE_CLOSED | A terminally closed instance received another operation |
SERIALIZATION_FAILED | A value could not be safely transformed or serialized |
DESERIALIZATION_FAILED | Stored data was malformed, unsupported, or could not be transformed |
import { LocalSpaceError } from 'localspace';
try {
await store.setItem('key', value);
} catch (error) {
if (error instanceof LocalSpaceError && error.code === 'QUOTA_EXCEEDED') {
reportStoragePressure(error.details);
}
throw error;
}
Batch Operations
Each batch chunk executes in a transaction on IndexedDB. When maxBatchSize is
unset, the entire call is one chunk. Other drivers expose the same API but may
run grouped work sequentially.
setItems<T>(entries: BatchItems<T>): Promise<BatchResponse<T>>
Stores multiple items atomically on IndexedDB when maxBatchSize is unset or
the input fits in one chunk. On other drivers this is a grouped operation with
driver-specific atomicity.
// Array format
await localspace.setItems([
{ key: 'user:1', value: { name: 'Ada' } },
{ key: 'user:2', value: { name: 'Grace' } },
]);
// Map format
await localspace.setItems(
new Map([
['user:1', { name: 'Ada' }],
['user:2', { name: 'Grace' }],
])
);
// Object format
await localspace.setItems({
'user:1': { name: 'Ada' },
'user:2': { name: 'Grace' },
});
// Returns array of { key, value } pairs
const result = await localspace.setItems([
{ key: 'a', value: 1 },
{ key: 'b', value: 2 },
]);
// [{ key: 'a', value: 1 }, { key: 'b', value: 2 }]
getItems<T>(keys: string[]): Promise<BatchResponse<T>>
Retrieves multiple items in order.
const users = await localspace.getItems(['user:1', 'user:2', 'user:3']);
// [
// { key: 'user:1', value: { name: 'Ada' } },
// { key: 'user:2', value: { name: 'Grace' } },
// { key: 'user:3', value: null } // doesn't exist
// ]
// Access values
users.forEach(({ key, value }) => {
if (value) console.log(key, value);
});
removeItems(keys: string[]): Promise<void>
Removes multiple items atomically on IndexedDB when maxBatchSize is unset or
the input fits in one chunk. On other drivers this is a grouped operation with
driver-specific atomicity.
await localspace.removeItems(['user:1', 'user:2', 'temp:session']);
Transaction API
runTransaction<T>(mode: 'readonly' | 'readwrite', runner: (scope: TransactionScope) => Promise<T> | T): Promise<T>
Executes multiple operations using the selected driver's 2.x transaction
behavior. IndexedDB provides a native transaction while the runner is issuing
transaction-scope requests. The memory driver provides snapshot rollback but
does not isolate concurrent callers. localStorage and React Native AsyncStorage
reject this method with UNSUPPORTED_OPERATION.
The permissive 2.x runner may await ordinary instance operations, but those operations are not part of the transaction. A stricter transaction-bound runner and cross-driver isolation contract are planned for 3.0.
When the built-in encryption, compression, or TTL plugin is active, LocalSpace
2.1 rejects runTransaction() with UNSUPPORTED_OPERATION before creating a
driver transaction or invoking the runner. This prevents transaction writes
from bypassing the configured storage transformation.
// Atomic counter increment on IndexedDB
const newValue = await localspace.runTransaction('readwrite', async (tx) => {
const current = (await tx.get<number>('counter')) ?? 0;
const next = current + 1;
await tx.set('counter', next);
return next;
});
// Read multiple values consistently
const snapshot = await localspace.runTransaction('readonly', async (tx) => {
const user = await tx.get<User>('user');
const settings = await tx.get<Settings>('settings');
return { user, settings };
});
// Transaction scope methods:
// tx.get<T>(key) - read a value
// tx.set<T>(key, value) - write a value (readwrite only)
// tx.remove(key) - delete a value (readwrite only)
// tx.keys() - get all keys
// tx.iterate(fn) - iterate all items
// tx.clear() - clear all items (readwrite only)
Configuration Methods
config(): LocalSpaceConfig
Returns current configuration.
const config = localspace.config();
console.log(config.name, config.storeName);
The 2.x getter still returns its historical mutable internal reference, but
mutating that object is deprecated. Treat it as readonly and pass configuration
to createInstance(); 3.0 returns a readonly snapshot.
config<K>(key: K): LocalSpaceConfig[K]
Returns a specific configuration value.
const name = localspace.config('name');
const driver = localspace.config('driver');
config(options: LocalSpaceConfig): true | Error | Promise<void>
Updates configuration. Must be called before the first storage operation.
Configuration without driver returns synchronously. Supplying driver
returns the setDriver() promise, while invalid or locked configuration is
returned as an Error value.
The constructor and config(options) use the same validation rules.
Database/store names must be non-empty strings. version must be a positive
safe integer; maxBatchSize, connectionIdleMs, and
maxConcurrentTransactions must be non-negative safe integers. Zero preserves
the 2.x disabled/unbounded behavior: no batch split, no idle close, and no
transaction cap, respectively. Invalid constructor options throw
LocalSpaceError(INVALID_CONFIG) before driver selection begins.
For 2.x data compatibility, the legacy config(options) setter still replaces
non-word storeName characters with _; constructor names are preserved.
Pass the exact stored namespace when moving between the two entry points.
Note: validation and lock errors are returned, not thrown or rejected (a localForage-compatible contract). This means
await localspace.config({ version: 'bad' })resolves to anErrorobject rather than rejecting, so atry/catchwill not catch it. Inspect the return value when you pass options that can fail. Only thedriverform returns a real promise.
localspace.config({
name: 'myapp',
storeName: 'data',
version: 2,
});
// Non-driver config: check the return value, do not rely on try/catch.
const result = localspace.config({ version: 2 });
if (result instanceof Error) {
// handle invalid/locked configuration
}
// Only the driver form returns a promise you can await.
await localspace.config({
driver: [localspace.INDEXEDDB, localspace.LOCALSTORAGE],
});
createInstance(options?: LocalSpaceOptions): LocalSpaceInstance
Creates a new independent instance.
Invalid initial configuration throws LocalSpaceError(INVALID_CONFIG)
synchronously; it is not converted into a later DRIVER_UNAVAILABLE error.
If every configured driver fails initialization, DRIVER_UNAVAILABLE.details
contains an ordered driverErrors entry for each attempted driver and the
original errors remain available through cause. IndexedDB quota failures use
QUOTA_EXCEEDED; browser-specific DOMException text is retained in
details.causeMessage rather than used as the public error message.
const cache = localspace.createInstance({
name: 'cache',
storeName: 'api-responses',
plugins: [ttlPlugin({ defaultTTL: 60_000 })],
});
ready(): Promise<void>
Waits for driver initialization to complete.
await localspace.ready();
console.log('Driver ready:', localspace.driver());
Driver Methods
driver(): string | null
Returns the current driver name.
const driverName = localspace.driver();
// 'asyncStorage' | 'localStorageWrapper'
setDriver(drivers: string | string[]): Promise<void>
Sets the driver(s) to use with fallback order.
// Single driver
await localspace.setDriver(localspace.INDEXEDDB);
// With fallback
await localspace.setDriver([localspace.INDEXEDDB, localspace.LOCALSTORAGE]);
// Runtime-only fallback when persistent browser storage is blocked
await localspace.setDriver([
localspace.INDEXEDDB,
localspace.LOCALSTORAGE,
localspace.MEMORY,
]);
localspace.MEMORY uses the built-in in-memory driver
('memoryStorageWrapper'). It is shared by name/storeName during the current
page lifetime, supports the full storage API, and loses data on reload. It is
opt-in and is not included in the default driver order.
Call setDriver() only while the instance is idle. While a storage operation
is active it rejects with LocalSpaceError(OPERATION_FAILED) and
details.reason === 'active-operations'; await the operation and retry.
supports(driverName: string): boolean
Checks if a driver is supported.
if (localspace.supports(localspace.INDEXEDDB)) {
console.log('IndexedDB is available');
}
React Native AsyncStorage is opt-in from a separate entry:
import AsyncStorage from '@react-native-async-storage/async-storage';
import localspace from 'localspace';
import { createReactNativeInstance } from 'localspace/react-native';
const store = await createReactNativeInstance(localspace, {
name: 'myapp',
storeName: 'kv',
reactNativeAsyncStorage: AsyncStorage,
});
For existing instances, use:
import localspace from 'localspace';
import { installReactNativeAsyncStorageDriver } from 'localspace/react-native';
await installReactNativeAsyncStorageDriver(localspace);
await localspace.setDriver(localspace.REACTNATIVEASYNCSTORAGE);
Integration smoke test harness (official AsyncStorage Jest mock) lives in integration/react-native-jest/.
Real device-runtime template (Detox on simulator/emulator) lives in integration/react-native-detox/ with CI workflow .github/workflows/detox-mobile.yml.
defineDriver(driver: Driver): Promise<void>
Registers a custom driver.
import localspace, { type Driver } from 'localspace';
const values = new Map<string, unknown>();
const customDriver: Driver = {
_driver: 'customDriver',
async _initStorage() {
// `this` is the LocalSpaceInstance selecting this driver.
},
getItem: async <T>(key: string) =>
values.has(key) ? (values.get(key) as T) : null,
setItem: async <T>(key: string, value: T) => {
values.set(key, value);
return value;
},
removeItem: async (key) => {
values.delete(key);
},
clear: async () => {
values.clear();
},
length: async () => values.size,
key: async (index) => [...values.keys()][index] ?? null,
keys: async () => [...values.keys()],
iterate: async <T, U>(iterator: (value: T, key: string, n: number) => U) => {
let iteration = 1;
for (const [key, value] of values) {
const result = iterator(value as T, key, iteration++);
if (result !== undefined) return result;
}
return undefined as U;
},
};
await localspace.defineDriver(customDriver);
await localspace.setDriver('customDriver');
_initStorage() and optional _closeStorage() are typed with the selecting
LocalSpaceInstance as their this receiver. Use a method or a non-arrow
function when the callback needs that receiver. It is lifecycle-guarded while
the callback is pending, so same-instance storage and lifecycle calls reject
with details.reason === 'lifecycle-reentrancy' instead of self-deadlocking.
The same receiver object is used for initialization, driver operations, and
cleanup, so identity-keyed driver state remains available throughout its
lifetime. If _closeStorage() rejects, a later close() or setDriver() can
invoke it again; custom drivers must make cleanup retries safe.
_support, batch methods, runTransaction(), and dropInstance() are optional
driver capabilities. Calling an omitted capability through a selected instance
rejects with UNSUPPORTED_OPERATION.
getDriver(driverName: string): Promise<Driver>
Returns a registered driver by name.
const idbDriver = await localspace.getDriver(localspace.INDEXEDDB);
Built-in web driver exports are also available:
import { indexedDBDriver, localStorageDriver, memoryDriver } from 'localspace';
dropInstance(options?: LocalSpaceConfig): Promise<void>
Deletes the database or specific store.
// Drop current instance's store
await localspace.dropInstance();
// Drop specific store
await localspace.dropInstance({
name: 'myapp',
storeName: 'temp-data',
});
// Drop entire database (all stores)
await localspace.dropInstance({
name: 'myapp',
// omit storeName to drop entire DB
});
Plugin Methods
use(plugin: LocalSpacePlugin | LocalSpacePlugin[]): LocalSpaceInstance
Registers plugins after instance creation.
const store = localspace.createInstance({ name: 'mystore' });
store.use(ttlPlugin({ defaultTTL: 60_000 }));
store.use([compressionPlugin(), encryptionPlugin({ key: myKey })]);
close(): Promise<void>
Closes the instance without deleting stored data. Calls cleanup hooks only for
plugins that were initialized, releases the active driver connection, and is
safe to call more than once. A closed instance rejects later storage operations
with
LocalSpaceError(INSTANCE_CLOSED) before initializing plugins or invoking their
operation hooks.
await store.close();
Calling close() on an unused instance does not initialize its driver or
plugins. Use clear() or dropInstance() when data should be deleted.
If custom-driver cleanup rejects, the instance remains terminal and rejects
storage operations with INSTANCE_CLOSED, but another close() call retries
the unfinished driver cleanup. Concurrent calls share an attempt, and cleanup
that has already completed is not repeated. A failed setDriver() release is
similarly retained for the next driver-switch attempt.
If a concurrent legacy destroy() has already started plugin initialization,
close() waits for that complete initialization pass before teardown.
If the built-in TTL plugin is sweeping expired entries, close() stops its
timer and waits for that sweep before releasing the driver.
Call it only while the instance is idle. While a storage operation is active it
rejects with LocalSpaceError(OPERATION_FAILED) and
details.reason === 'active-operations'; await the operation and retry. The
same rule prevents hooks, transaction runners, and custom drivers from awaiting
a lifecycle transition that is waiting for their own operation.
context.instance remains the public instance with stable identity across all
plugin hooks. Plugin lifecycle callbacks must use the callback-scoped
context.lifecycleInstance for same-instance calls across an async boundary;
custom-driver lifecycle callbacks must use their this receiver. Those guarded
receivers reject same-instance lifecycle and storage calls with
details.reason === 'lifecycle-reentrancy' while the callback is pending,
including across await. After settlement, retained receivers forward normally
for later timer or event-handler work. A custom driver reuses a stable receiver
for its lifecycle and operation methods, so identity-keyed state remains
available. Guard state is isolated per plugin lifecycle callback and per
selected custom driver, so unrelated retained receivers and concurrent callers
are not treated as lifecycle reentry.
destroy(): Promise<void>
Deprecated in 2.1. Use close(), which runs initialized plugin cleanup and also
releases the active driver without deleting data. destroy() retains its 2.0
plugin-only behavior throughout the 2.x line.
// Legacy only; new code should call await store.close()
await legacyStore.destroy();
Configuration Options
Full LocalSpaceConfig interface:
interface LocalSpaceConfig {
// Database configuration
name?: string; // Database name (default: 'localforage')
storeName?: string; // Store/table name (default: 'keyvaluepairs')
version?: number; // Database version (default: 1)
description?: string; // Database description
size?: number; // Deprecated compatibility hint; built-in drivers ignore it
// Driver configuration
driver?: string | string[]; // Driver(s) to use
// Built-ins: localspace.INDEXEDDB, localspace.LOCALSTORAGE, localspace.MEMORY
reactNativeAsyncStorage?: ReactNativeAsyncStorage; // Optional adapter used by the react-native driver
// IndexedDB specific
durability?: 'relaxed' | 'strict'; // Transaction durability hint
bucket?: {
// Storage Buckets API (Chromium 122+)
name: string;
durability?: 'relaxed' | 'strict';
persisted?: boolean;
};
prewarmTransactions?: boolean; // Pre-warm connection (default: true)
connectionIdleMs?: number; // Auto-close idle connections
maxConcurrentTransactions?: number; // Throttle concurrent transactions
// Batch operations
maxBatchSize?: number; // Split large batches into chunks
// Plugin configuration
pluginInitPolicy?: 'fail' | 'disable-and-continue';
pluginErrorPolicy?: 'strict' | 'lenient';
}
When the requested Storage Bucket cannot be opened, IndexedDB falls back to the default storage backend. Instances that resolve to that same backend share one connection context even if one of them originally requested a bucket.
Default database name. When
name/storeNameare omitted, localspace uses'localforage'/'keyvaluepairs'. This lets an app migrating from localForage reuse data without rewriting it when both libraries select the same IndexedDB or localStorage backend (their serializer and key layout are compatible). localspace does not support WebSQL, so migrate WebSQL data first and confirm the selected driver as described in the migration guide. SetnameandstoreNameexplicitly for a fresh, app-owned namespace.