Skip to content

Repository files navigation

@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.

importjsonfrom'@async/json';constusers=awaitjson('./users.json');awaitusers.create({name: 'Ada Lovelace',role: 'admin'});awaitusers.find({where: {role: 'admin'}});constdb=awaitjson('./db');awaitdb.users.find({where: {role: 'admin'}});awaitdb.settings.get();awaitdb.collection('users').find({where: {role: 'admin'}});awaitdb.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:

awaitdb.collection('users').all();awaitdb.collection.find();// resource named "collection"awaitdb.resourceNames();awaitdb.resourceNames.find();// resource named "resourceNames"awaitdb._.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:

constdb=awaitjson('./data',{recursive: true,create: true});// data/messages/general.json → resource "messages/general"awaitdb.document('messages/general').get();// unknown names become sidecar-backed resources on first useawaitdb.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:

constroot={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:

constgraph=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.

constrestore=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';awaitfile.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:

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

Identity, Logs, And Encoded Payloads

Single-field resources keep using id by default:

constusers=awaitjson('./users.json');awaitusers.get('u_1');

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

constmemberships=awaitjson('./memberships.json',{identity: {fields: ['orgId','userId']},indexes: ['role'],});awaitmemberships.get({orgId: 'o_1',userId: 'u_1'});awaitmemberships.patch({orgId: 'o_1',userId: 'u_1'},{role: 'admin'});

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

constevents=awaitjson('./events.json',{writePolicy: 'append-only',});awaitevents.append({id: 'e_1',type: 'created'});

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

constupdates=awaitjson('./updates.json',{fields: {id: {type: 'string',required: true},update: {type: 'bytes',encoding: 'base64url',required: true},},});awaitupdates.append({id: 'u_1',update: 'YWJjZA'});

Redis JSON

importjsonfrom'@async/json';import{redisJson}from'@async/json/redis';constdb=awaitjson('./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.

About

JSON file and folder database engine with sidecar state, queries, version history, and optional RedisJSON storage.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages