npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

localspace

v3.0.1

Published

A Promise-first storage toolkit for IndexedDB, localStorage, memory, and React Native AsyncStorage

Readme

localspace

Node CI npm license

LocalSpace is a Promise-first storage toolkit for IndexedDB, localStorage, in-memory storage, and React Native AsyncStorage. It provides one stable key-value facade, explicit driver capabilities, scoped transactions, and a plugin pipeline for TTL, compression, encryption, and application extensions.

LocalSpace keeps a familiar localForage-style API, but it is not a callback- compatible drop-in replacement. All operations return promises, and LocalSpace 3.0 intentionally narrows stored values to a cross-driver-safe contract.

Install

pnpm add localspace
# or: npm install localspace

The package publishes ESM, CommonJS, and UMD builds plus TypeScript declarations. The supported public entry points are localspace, localspace/react-native, and localspace/package.json; source files and other deep imports are not part of the package contract.

Quick start

import localspace from 'localspace';

await localspace.setItem('user', { name: 'Ada', role: 'admin' });

const user = await localspace.getItem<{
  name: string;
  role: string;
}>('user');

await localspace.removeItem('user');

Create an isolated namespace when an application owns the data:

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

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

The historical defaults are permanently frozen as name: 'localforage' and storeName: 'keyvaluepairs'. They preserve access to compatible existing IndexedDB and localStorage data. Set both explicitly for every new application; changing either default would silently orphan existing data.

StorageValue contract

Every 3.0 write is validated before storage or plugin side effects. A value may contain:

  • null, booleans, finite numbers, and strings;
  • ArrayBuffer and the standard integer/float typed arrays;
  • dense arrays of supported values;
  • ordinary plain objects whose enumerable own data properties contain supported values.

This contract round-trips consistently across IndexedDB, localStorage, memory, and React Native AsyncStorage. Values such as undefined, Date, Map, Set, RegExp, bigint, Blob, DataView, SharedArrayBuffer, null-prototype or class instances, accessors, sparse arrays, symbol properties, cycles, and non-finite numbers are rejected with SERIALIZATION_FAILED. The exact top-level localspace.plugin envelope namespace is also reserved for built-in storage transforms.

Convert richer application values at the boundary:

await cache.setItem('created-at', new Date().toISOString());
await cache.setItem('labels', [...new Set(['urgent', 'review'])]);
await cache.setItem('counters', Object.fromEntries(new Map([['open', 3]])));

Ordinary JSON-compatible and native binary values are stored without a universal wrapper. A compact versioned codec is used only when a string-backed driver or byte transform must preserve binary nested inside an array or object. Objects that merely contain a __localspace__ property remain ordinary application data unless they claim the exact reserved plugin namespace.

Write types are checked recursively without forcing DTO interfaces to declare a string index signature:

interface StoredUser {
  id: string;
  roles: string[];
}

const saved: StoredUser = await cache.setItem('user', user);

Runtime validation remains authoritative because TypeScript's structural type system cannot distinguish every data-only class instance from an interface.

Stable facade and configuration

Storage-operation references stay stable across ready(), driver fallback, setDriver(), and plugin registration. Those operations are safe to destructure:

const { getItem, setItem } = cache;
await setItem('theme', 'dark');
await getItem('theme');

Configuration is construction-time state. config() returns a detached, deeply frozen snapshot; config(key) reads one field. The old config(options) setter has been removed.

const store = localspace.createInstance({
  name: 'my-app',
  storeName: 'data',
  driver: [localspace.INDEXEDDB, localspace.LOCALSTORAGE],
  durability: 'relaxed',
  maxBatchSize: 200,
  pluginInitPolicy: 'fail',
  pluginErrorPolicy: 'strict',
});

const snapshot = store.config();
console.log(snapshot.name); // my-app

Plugins may be passed in the constructor or added with use() only before the first ready() or storage call. Later registration rejects with CONFIG_LOCKED.

Drivers and capabilities

The default web fallback order is IndexedDB, then localStorage. The memory driver is deliberately not an automatic fallback: persistent-storage failure remains visible unless the application explicitly accepts volatile data.

await store.setDriver([store.INDEXEDDB, store.LOCALSTORAGE, store.MEMORY]);
await store.ready();

console.log(store.driver());
console.log(store.capabilities());

capabilities() is available after initialization and returns the same frozen snapshot for the selected driver session:

| Capability | Meaning | | ---------------- | ------------------------------------------------------- | | transactions | runTransaction() is supported | | atomicBatch | an unchunked batch is one driver-level atomic unit | | dropInstance | the selected driver can remove its namespace | | persistent | the driver survives the current runtime session | | storageBuckets | the selected IndexedDB backend supports Storage Buckets |

Optional facade methods remain present for stable typing. If the selected driver does not support one, the call rejects early with UNSUPPORTED_OPERATION, before plugin or driver side effects.

Driver matrix

| Driver | Persistence | Transactions | Atomic batch | Storage Buckets | | ------------------------- | ------------------------ | ------------ | -------------------------------- | -------------------------------------- | | IndexedDB | yes | yes | yes when maxBatchSize is unset | when the requested backend supports it | | localStorage | yes | no | no | no | | memory | current JavaScript realm | yes | no | no | | React Native AsyncStorage | yes | no | no | no |

The memory driver serializes read-write transactions per JavaScript realm, name, and storeName. IndexedDB uses native transactions. LocalSpace does not add a distributed lock or replication protocol across tabs/processes; IndexedDB retains its native cross-context scheduling, while memory isolation does not extend beyond its realm. The broadcast example is notification-only, not transaction coordination.

Explicit memory fallback

const volatileAllowed = localspace.createInstance({
  name: 'my-app',
  driver: [localspace.INDEXEDDB, localspace.LOCALSTORAGE, localspace.MEMORY],
});

Custom drivers

Prefer construction-scoped definitions:

import { LocalSpace } from 'localspace';

const customStore = new LocalSpace({
  driver: customDriver._driver,
  drivers: [customDriver],
});

LocalSpace snapshots the definition without mutating the caller's object. Each selection creates a private driver session, so driver state is never injected onto the public facade. For deliberate realm-wide registration, use the exported registerDriver():

import { registerDriver } from 'localspace';

await registerDriver(customDriver);

instance.defineDriver() was removed in 3.0. Custom drivers declare optional guarantees through _capabilities; LocalSpace validates that each declaration matches the implemented methods.

Storage Buckets

Storage Buckets are explicit IndexedDB configuration:

const bucketStore = localspace.createInstance({
  name: 'my-app',
  bucket: {
    name: 'durable-data',
    durability: 'strict',
    persisted: true,
  },
});

When bucket is provided, LocalSpace never silently opens the default IndexedDB database or falls back to another driver. An unavailable API, failed bucket open, or bucket without IndexedDB rejects readiness with structured error details.

Batch operations

await store.setItems([
  { key: 'user:1', value: { name: 'Ada' } },
  { key: 'user:2', value: { name: 'Grace' } },
]);

const users = await store.getItems(['user:1', 'user:2']);
// [{ key: 'user:1', value: ... }, { key: 'user:2', value: ... }]

await store.removeItems(['user:1', 'user:2']);

IndexedDB runs an unchunked batch in one transaction. Setting maxBatchSize splits a large call into multiple chunks, so atomicBatch becomes false. Other drivers preserve the public result shape without promising atomicity.

Transactions

IndexedDB and memory expose scoped transactions:

await store.runTransaction('readwrite', async (tx) => {
  const current = (await tx.get<number>('counter')) ?? 0;
  await tx.set('counter', current + 1);
  await tx.set('updated-at', Date.now());
});

The scope contains get, set, remove, keys, iterate, and clear. During the runner, every operation on that same instance must go through the provided scope; ordinary facade calls reject with TRANSACTION_SCOPE_REQUIRED. The scope becomes invalid as soon as the runner settles, and readonly scopes reject mutations with TRANSACTION_READONLY.

Only await Promises returned by those scope methods inside the runner. Timers, network requests, prompts, and other arbitrary waits are outside the contract. With IndexedDB, the browser may commit when no native request remains; a later scope call then rejects with TRANSACTION_INACTIVE. Writes in that already completed native transaction remain committed, so this rejection must not be interpreted as rollback. Move external work before or after runTransaction().

Plugins run inside the same transaction and receive the active scope as context.transactionScope. Async iterate() callbacks are awaited sequentially; the first non-undefined result stops iteration and becomes the return value. Iteration reads at most a bounded driver page ahead instead of materializing the complete value set.

localStorage and React Native AsyncStorage report transactions: false and reject runTransaction() with UNSUPPORTED_OPERATION.

Plugins

import localspace, {
  compressionPlugin,
  encryptionPlugin,
  ttlPlugin,
} from 'localspace';

const secureStore = localspace.createInstance({
  name: 'secure-store',
  plugins: [
    ttlPlugin({ defaultTTL: 60_000 }),
    compressionPlugin({ threshold: 1024 }),
    encryptionPlugin({ key: '0123456789abcdef0123456789abcdef' }),
  ],
  pluginErrorPolicy: 'strict',
});

| Plugin | Contract | | ----------- | --------------------------------------------------------------------------------------------------- | | TTL | expired values disappear consistently from item, batch, iteration, key, and length views | | Compression | bytes-to-bytes codec; stores an envelope only when the complete persisted representation is smaller | | Encryption | AES-GCM writes through Web Crypto; malformed data and crypto failures fail closed |

Legacy AES-CBC/AES-CTR data can be read only through legacyEncryptionMigrationPlugin(). That plugin rejects every write; migrate values into a separate AES-GCM instance.

For each plugin and phase, a batch call invokes the batch hook once when it is defined, otherwise it maps the matching single hook over entries. It never runs both forms for the same plugin phase. Query/destructive observers cover iterate, keys, key, length, clear, dropInstance, and the outer runTransaction lifecycle. Errors from void after observers are reported without rejecting the operation; validation that must veto a write belongs in a before hook.

See Plugin System for ordering, envelopes, policies, and custom hook examples. Application-level notification examples live in examples/.

React Native

Runtime auto-detection was removed. The AsyncStorage adapter is required and a missing or malformed adapter fails instead of falling through to another driver. It must implement getItem, setItem, removeItem, and getAllKeys so every public query and namespace operation is available after readiness; clear and the multi* methods remain optional optimizations.

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: 'my-app',
  storeName: 'data',
  reactNativeAsyncStorage: AsyncStorage,
});

For deliberate realm-wide driver installation:

import AsyncStorage from '@react-native-async-storage/async-storage';
import localspace from 'localspace';
import { installReactNativeAsyncStorageDriver } from 'localspace/react-native';

await installReactNativeAsyncStorageDriver();
const mobileStore = localspace.createInstance({
  driver: localspace.REACTNATIVEASYNCSTORAGE,
  reactNativeAsyncStorage: AsyncStorage,
});
await mobileStore.ready();

The repository provides an official AsyncStorage Jest integration and a React Native 0.83.x Detox fixture:

pnpm test:rn:integration

See integration/react-native-detox/README.md for simulator/emulator commands and the manual gate that installs an exact, integrity-checked published RC tarball.

Lifecycle

close() is idempotent and non-destructive. It stops initialized plugins and releases the active driver session; later operations reject with INSTANCE_CLOSED. Use clear() or dropInstance() only when data should be deleted.

await cache.close();

close() does not wait for storage work: while any operation or transaction on the instance is still pending, it rejects with OPERATION_FAILED (details.reason: 'active-operations') and leaves the instance open. Await outstanding operations first, then close. Waiting instead would deadlock when close() is awaited from inside an iterator, transaction runner, or plugin hook of the pending operation.

destroy() was removed in 3.0. If custom-driver cleanup rejects, the closed instance retains that cleanup and a later close() retries it. Calls that would re-enter a pending plugin or driver lifecycle callback are rejected instead of deadlocking.

Errors

Operational failures are LocalSpaceError objects with a stable code, structured details, and an optional original cause.

import { LocalSpaceError } from 'localspace';

try {
  await store.runTransaction('readwrite', async () => {});
} catch (error) {
  if (error instanceof LocalSpaceError) {
    console.error(error.code, error.details, error.cause);
  }
}

Common codes include DRIVER_UNAVAILABLE, DRIVER_NOT_INITIALIZED, UNSUPPORTED_OPERATION, TRANSACTION_SCOPE_REQUIRED, TRANSACTION_INACTIVE, TRANSACTION_READONLY, SERIALIZATION_FAILED, DESERIALIZATION_FAILED, QUOTA_EXCEEDED, and INSTANCE_CLOSED.

Supported platforms

| Platform | 3.0 support policy | Release evidence | | ------------ | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | Chromium | exact engine build resolved by the frozen Playwright lockfile, using unprefixed IndexedDB | Chromium project plus logged Playwright and engine versions | | Firefox | exact engine build resolved by the frozen Playwright lockfile | Firefox project plus logged Playwright and engine versions | | WebKit | exact engine build resolved by the frozen Playwright lockfile | WebKit project plus logged Playwright and engine versions | | Safari | exact stable Safari build recorded for the release candidate | real Safari smoke before GA; Playwright WebKit is a separate preflight | | Node.js | LTS 22 and 24 for package import, JavaScript tests, and custom drivers; no built-in persistent Node driver | Node CI matrix; browser-facing TypeScript declarations require the DOM lib | | React Native | 0.83.x with AsyncStorage 2.2.x release fixture | official Jest mock plus iOS Detox against the exact registry RC tarball |

This matrix is deliberately evidence-shaped: one Playwright project does not certify two browser majors, Firefox ESR, Microsoft Edge, or real Safari. Those targets require their own release jobs before LocalSpace can claim them as tested support.

Legacy prefixed IndexedDB APIs and WebSQL are not supported. WebSQL data must be migrated before adopting LocalSpace.

Performance and package boundaries

  • Prefer batch APIs when operations belong together.
  • Use capabilities() instead of assuming an atomicity guarantee.
  • Run pnpm test:benchmark for environment-specific results.
  • Run pnpm benchmark:compare:2.1 for the integrity-pinned published 2.1.0 comparison described in benchmarks/README.md.
  • Published source maps embed sourcesContent, so stack traces map back to TypeScript even though src/ and declaration maps are not shipped.

Documentation

| Document | Description | | -------------------------------------------- | -------------------------------------------------------------- | | API Reference | methods, types, capabilities, drivers, and configuration | | Plugin System | hook pipeline, built-in plugins, envelopes, and custom plugins | | Migration Guide | 2.1.x to 3.0 changes and data migration | | Real-World Examples | application patterns | | Changelog | release history |

License

MIT