Skip to content

unadlib/localspace

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

154 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

localspace

Node CI npm license

localspace is a Promise-first storage toolkit for IndexedDB, localStorage, and React Native AsyncStorage, with TypeScript types and a focused plugin system.

Motivation

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().

Quick Start

Get started in 5 minutes:

1. Install

pnpm add localspace
# or: npm install localspace

2. Basic Usage

import 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');

3. Create Isolated Instances

const cache = localspace.createInstance({
  name: 'my-app',
  storeName: 'cache',
});

await cache.setItem('token', 'abc123');

4. Batch Operations

// 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']);

5. Use Plugins

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.


Table of Contents


Installation

pnpm add localspace
# or
npm install localspace
import localspace from 'localspace';

Bundles included: ES modules, CommonJS, UMD, plus .d.ts files.


Core API

Storage Methods

// 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);
});

Driver Selection

// Web fallback order (default bundled drivers)
await localspace.setDriver([localspace.INDEXEDDB, localspace.LOCALSTORAGE]);

// Check current driver
console.log(localspace.driver());
// 'asyncStorage' | 'localStorageWrapper'

In-Memory Fallback

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.

React Native AsyncStorage

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:integration

See 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


Batch Operations

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']);

Transactions

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.


Plugin System

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
});

Built-in Plugins

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


Configuration

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


Performance Notes

  • 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()

Troubleshooting

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.


Compatibility

  • Browsers: Modern Chromium/Edge, Firefox, Safari
  • Drivers: IndexedDB (primary), localStorage, memory (opt-in fallback)
  • React Native: AsyncStorage driver available via localspace/react-native opt-in entry
  • WebSQL: Not supported (migrate to IndexedDB)
  • Node/SSR: Custom driver required

Documentation

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

Roadmap

Complete βœ…

  • 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)

Coming Soon

  • OPFS driver (Origin Private File System)
  • Node.js (File system, SQLite)
  • React Native SQLite driver
  • Deno (Native KV store)

License

MIT

About

A library that unifies the APIs of IndexedDB, localStorage and other storage into a consistent API

Topics

Resources

License

Stars

197 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors