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:

CodeMeaning
INVALID_CONFIGConfiguration failed normalization before driver initialization
DRIVER_UNAVAILABLEEvery requested driver failed; details.driverErrors preserves each
QUOTA_EXCEEDEDA driver reported a storage quota failure
OPERATION_FAILEDA driver operation failed for another stable category
UNSUPPORTED_OPERATIONThe selected driver/plugin combination cannot honor the contract
INSTANCE_CLOSEDA terminally closed instance received another operation
SERIALIZATION_FAILEDA value could not be safely transformed or serialized
DESERIALIZATION_FAILEDStored 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 an Error object rather than rejecting, so a try/catch will not catch it. Inspect the return value when you pass options that can fail. Only the driver form 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/storeName are 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. Set name and storeName explicitly for a fresh, app-owned namespace.