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

@async/json

v0.5.0

Published

Use a JSON file or folder like a small database, with sidecar state, queries, history, and Redis-backed scale paths.

Readme

@async/json

Use a JSON file or a folder of JSON files like a small database.

pnpm add @async/json

The package is ESM-only and supports Node.js 24 and newer.

import json from '@async/json';

const users = await json('./users.json');
await users.create({ name: 'Ada Lovelace', role: 'admin' });
await users.find({ where: { role: 'admin' } });

const db = await json('./db');
await db.users.find({ where: { role: 'admin' } });
await db.settings.get();
await db.collection('users').find({ where: { role: 'admin' } });
await db.document('settings').get();

By default the visible JSON file is seed data and writes go to sidecar state under .async-json/state. Use writes: 'source' only when the JSON file itself should be rewritten.

Folder database handles expose resources as properties. Callable controls stay callable, so resources named collection, document, resourceNames, or close can still use property access while the control call keeps working:

await db.collection('users').all();
await db.collection.find(); // resource named "collection"
await db.resourceNames();
await db.resourceNames.find(); // resource named "resourceNames"
await db._.collection('_').all(); // explicit escape hatch

@async/json owns standalone JSON database semantics: collection/document runtime APIs, scalar and compound identity, append-only collections, encoded payload validation, sidecar state, local indexes, RedisJSON storage, stable stringify, and JSON5-compatible parsing. Use @async/db when you also need readers, schemas, generated types, REST/GraphQL, operations, lifecycle, and store graduation.

Nested Folders And Runtime-Created Resources

Folder databases stay flat by default. Two opt-in options widen them:

const db = await json('./data', { recursive: true, create: true });

// data/messages/general.json → resource "messages/general"
await db.document('messages/general').get();

// unknown names become sidecar-backed resources on first use
await db.document('messages/new-channel').set('', [{ id: 'm1', text: 'hi' }]);

recursive: true discovers nested folders as path-named resources (messages/general). Dot-prefixed entries, including the sidecar state folder, are always skipped.

create: true lets collection()/document() grow resources for names that don't exist yet. No visible seed file is created — state lives in the sidecar and survives reopens — so a folder of seed JSON can gain runtime data without the source tree changing. Resource names are validated: no empty, dot-prefixed, ./.., or backslash segments.

Stable JSON Helpers

stableStringify() produces deterministic JSON by sorting object keys lexically. Array order is preserved.

import { parseJson, registerJson, stableJson, stableStringify } from '@async/json';

stableStringify({ b: 1, a: 2 });
// {"a":2,"b":1}

stableJson.stringify({ b: 1, a: 2 }, true);
// {
//   "a": 2,
//   "b": 1
// }

Pass pretty: true for two-space output, or use space with the same number clamping and string truncation rules as JSON.stringify():

stableStringify({ b: 1, a: 2 }, { pretty: true });
stableStringify({ b: 1, a: 2 }, { space: '\t' });

Sorting defaults to true. Disable it to preserve insertion order, or provide a custom comparator:

stableStringify({ b: 1, a: 2 }, { sort: false });

stableStringify({ id: 1, metadata: {}, name: 'Ada' }, {
  sort: (left, right) => left.length - right.length || left.localeCompare(right),
});

The second argument can be a replacer function. Replacers receive path and cycle context, so recursive structures can be rendered as JSON references:

const root = { id: 'root' };
root.self = root;

stableStringify(root, (key, value, context) =>
  context.circular
    ? { $ref: `#/${context.refPath?.join('/') ?? ''}` }
    : value,
);

parseJson() accepts JSON5-compatible input, including comments, trailing commas, unquoted keys, and single-quoted strings. $ref objects are not resolved automatically; revivers can resolve them in one place:

const graph = parseJson(`{
  users: { u_1: { name: 'Ada' } },
  owner: { $ref: '#/users/u_1' },
}`, (key, value, context) =>
  context.isRef ? context.resolvePointer(context.ref ?? '#') : value,
);

registerJson() installs an opt-in JSON shim on globalThis or a provided target. The shim keeps native JSON.stringify(value, replacer, space) and JSON.parse(text, reviver) argument shapes while using @async/json parsing and stable stringify behavior. The returned function restores the previous JSON object.

const restore = registerJson();
try {
  JSON.stringify({ b: 1, a: 2 });
  JSON.parse('{a: 1}');
} finally {
  restore();
}

File Helpers

Use file.patch() to update JSON files like package.json without sorting or otherwise reordering existing object keys. It preserves the file's indentation and trailing newline, writes atomically, and returns false when the patch does not change the file text.

import { file } from '@async/json';

await file.patch('./package.json', {
  scripts: {
    test: 'node --test',
  },
  devDependencies: {
    typescript: '^6.0.0',
  },
});

Object patches deep-merge plain objects, replace arrays and scalar values, and append new keys after existing keys. Callback patches can mutate the parsed object directly or return a replacement value:

await file.patch('./package.json', (pkg) => {
  pkg.scripts ??= {};
  pkg.scripts.lint = 'eslint .';
});

Identity, Logs, And Encoded Payloads

Single-field resources keep using id by default:

const users = await json('./users.json');
await users.get('u_1');

Compound identity uses object keys instead of delimiter-encoded ids:

const memberships = await json('./memberships.json', {
  identity: { fields: ['orgId', 'userId'] },
  indexes: ['role'],
});

await memberships.get({ orgId: 'o_1', userId: 'u_1' });
await memberships.patch({ orgId: 'o_1', userId: 'u_1' }, { role: 'admin' });

Append-only collections allow append() and block create/update/delete/replace APIs:

const events = await json('./events.json', {
  writePolicy: 'append-only',
});

await events.append({ id: 'e_1', type: 'created' });

Bytes fields validate JSON-safe encoded strings while keeping payloads opaque:

const updates = await json('./updates.json', {
  fields: {
    id: { type: 'string', required: true },
    update: { type: 'bytes', encoding: 'base64url', required: true },
  },
});

await updates.append({ id: 'u_1', update: 'YWJjZA' });

Redis JSON

import json from '@async/json';
import { redisJson } from '@async/json/redis';

const db = await json('./db', {
  store: redisJson({ client, prefix: 'app:' }),
  indexes: {
    users: [{ fields: ['email'] }],
  },
});

Redis mode stores collection records as per-record Redis JSON keys. Declared indexes can be mapped to Redis Search; indexes are not created implicitly from observed queries.

Local Development

pnpm install
pnpm run build
pnpm run test
pnpm run release:check

release:check builds the package, runs tests, verifies the API surface ledger, and runs a package dry-run.

Generated files:

  • dist/ is build output and is not committed.
  • .async/pages/ is generated GitHub Pages output and is not committed.
  • .async/, .async-json/, node_modules/, and *.tgz are local runtime, cache, dependency, and package output.
  • .github/workflows/async-pipeline.yml is generated from pipeline.ts.

Website

The project site is published through GitHub Pages at https://async.github.io/json/.

Release Status

@async/json publishes as a public npm package. Public API changes should update api-contract.json, regenerate API_SURFACE.md, update this README, and add tests in the same change.