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

@kafitra/lynx-async-storage

v0.1.0

Published

Async key-value storage for the Lynx runtime — mirrors React Native AsyncStorage

Readme

@kafitra/lynx-async-storage

Async key-value storage for the Lynx runtime — mirrors the React Native AsyncStorage API.

Features

  • Drop-in API – same method signatures as React Native AsyncStorage
  • Pure JS – no native Android/iOS modules, no prebuild step
  • Thin async wrapper – microtask-scheduled Promises over the synchronous Lynx runtime storage
  • JSON merge – shallow object merge via mergeItem / multiMerge
  • Pluggable backend – swap the storage adapter at runtime or in tests
  • Tree-shakeable"sideEffects": false, ESM + CJS dual output
  • Strict TypeScript – full type safety, no implicit any

Installation

pnpm add @kafitra/lynx-async-storage
# or
npm install @kafitra/lynx-async-storage

Quick Start

import AsyncStorage from "@kafitra/lynx-async-storage";

// Store a value
await AsyncStorage.setItem("token", "abc123");

// Retrieve a value (null if absent)
const token = await AsyncStorage.getItem("token");
console.log(token); // 'abc123'

// Remove a value
await AsyncStorage.removeItem("token");

// Clear everything
await AsyncStorage.clear();

API Reference

Single-key methods

| Method | Signature | Description | | ------------ | ----------------------------------------------- | ------------------------------------------ | | getItem | (key: string) => Promise<string \| null> | Returns the value, or null if absent | | setItem | (key: string, value: string) => Promise<void> | Stores a value | | removeItem | (key: string) => Promise<void> | Removes a single key | | clear | () => Promise<void> | Removes all keys | | getAllKeys | () => Promise<string[]> | Returns all keys, sorted lexicographically |

Batch methods

| Method | Signature | Description | | ------------- | --------------------------------------------------------- | ----------------------------------------- | | multiGet | (keys: string[]) => Promise<[string, string \| null][]> | Fetches multiple keys; order is preserved | | multiSet | (pairs: [string, string][]) => Promise<void> | Stores multiple pairs atomically | | multiRemove | (keys: string[]) => Promise<void> | Removes multiple keys |

Merge methods

| Method | Signature | Description | | ------------ | ----------------------------------------------- | -------------------------------------------------- | | mergeItem | (key: string, value: string) => Promise<void> | Shallow-merges a JSON object into the stored value | | multiMerge | (pairs: [string, string][]) => Promise<void> | Applies mergeItem for each pair |


Usage Examples

Storing structured data

import AsyncStorage from "@kafitra/lynx-async-storage";

const user = { id: 1, name: "Alice", role: "admin" };
await AsyncStorage.setItem("user", JSON.stringify(user));

const raw = await AsyncStorage.getItem("user");
const parsed = raw ? JSON.parse(raw) : null;

Batch read / write

await AsyncStorage.multiSet([
  ["firstName", "Alice"],
  ["lastName", "Smith"],
  ["city", "Oslo"],
]);

const results = await AsyncStorage.multiGet(["firstName", "city"]);
// [['firstName', 'Alice'], ['city', 'Oslo']]

JSON merge

// Initial state
await AsyncStorage.setItem(
  "prefs",
  JSON.stringify({ theme: "light", lang: "en" }),
);

// Partial update – only theme changes; lang is preserved
await AsyncStorage.mergeItem("prefs", JSON.stringify({ theme: "dark" }));

const prefs = JSON.parse((await AsyncStorage.getItem("prefs")) ?? "{}");
// { theme: 'dark', lang: 'en' }

List all stored keys

await AsyncStorage.multiSet([
  ["b", "2"],
  ["a", "1"],
  ["c", "3"],
]);
const keys = await AsyncStorage.getAllKeys();
// ['a', 'b', 'c']  ← always sorted

Custom / Test Backend

Inject any object that satisfies StorageBackend for testing or custom adapters:

import { AsyncStorage, MemoryBackend } from "@kafitra/lynx-async-storage";

const storage = new AsyncStorage(new MemoryBackend());

// Or swap the singleton's backend:
import instance from "@kafitra/lynx-async-storage";
import { MemoryBackend } from "@kafitra/lynx-async-storage";

instance.useBackend(new MemoryBackend());

Implement StorageBackend yourself:

import type { StorageBackend } from "@kafitra/lynx-async-storage";

class MyCustomBackend implements StorageBackend {
  getItem(key: string): string | null {
    /* … */
  }
  setItem(key: string, value: string): void {
    /* … */
  }
  removeItem(key: string): void {
    /* … */
  }
  clear(): void {
    /* … */
  }
  getAllKeys(): string[] {
    /* … */
  }
}

Error Handling

All methods return Promises. Errors are always delivered via rejection – no method throws synchronously.

import {
  InvalidKeyError,
  InvalidValueError,
  InvalidJsonError,
} from "@kafitra/lynx-async-storage";

try {
  await AsyncStorage.setItem("key", "value");
} catch (err) {
  if (err instanceof InvalidKeyError) {
    /* bad key type */
  }
  if (err instanceof InvalidValueError) {
    /* bad value type */
  }
  if (err instanceof InvalidJsonError) {
    /* merge JSON error */
  }
}

Error types

| Class | Code | Cause | | ------------------- | ----------------------------------- | ---------------------------- | | InvalidKeyError | ERR_ASYNC_STORAGE_INVALID_KEY | Key is not a string | | InvalidValueError | ERR_ASYNC_STORAGE_INVALID_VALUE | Value is not a string | | InvalidJsonError | ERR_ASYNC_STORAGE_INVALID_JSON | Non-JSON-object during merge | | BackendError | ERR_ASYNC_STORAGE_BACKEND_FAILURE | Underlying storage threw |


Runtime Backend Detection

At startup createDefaultBackend() probes the runtime in priority order:

1. NativeModules.LynxStorage available?  → NativeStorageBackend  (disk, persists across restarts)
2. globalThis.localStorage available?    → LocalStorageBackend   (browser / WebView)
3. fallback                              → MemoryBackend         (in-memory, lost on reload)

For disk-persistent storage in a Lynx app, install @kafitra/lynx-storage alongside:

pnpm add @kafitra/lynx-storage @kafitra/lynx-async-storage

No extra configuration needed — NativeModules.LynxStorage is auto-detected at runtime.


Design Decisions

Why microtask scheduling?

Promise.resolve().then(work) schedules work on the microtask queue. This guarantees the public API is always async – callers can safely await without relying on synchronous completion, even when the backend is synchronous.

Why shallow merge?

React Native AsyncStorage specifies a shallow merge for mergeItem. Deep merge would require a third-party library and introduces ambiguity around array handling. Consumers who need deep merge can read-modify-write using getItem / setItem.

Why sorted getAllKeys?

Sorted output is deterministic and consistent across calls regardless of the order in which keys were inserted – essential for reproducible application behaviour.

Why fail-fast validation in batch methods?

Batch operations (multiSet, multiGet, etc.) validate all inputs before any backend write begins. This provides best-effort atomicity: either all writes happen or none do, minimising partial-state corruption.


Development

# Install dependencies
pnpm install

# Build (ESM + CJS + types)
pnpm build

# Run tests
pnpm test

# Run tests with coverage
pnpm test:coverage

License

MIT © Kafitra