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

@cero-base/cero

v1.0.1

Published

The ideal p2p API — everything is a handle, handles contain refs, refs contain rows.

Readme

cero

Define your data. Use it. cero handles storage, sync between devices, and sharing with others — no server.

Describe your data, compile it to a spec once, then open a cero against the built spec:

// schema.js
import { cero } from '@cero-base/cero'

export const schema = cero.schema({
  profile: cero.t.single({ name: cero.t.string }),
  room: { messages: cero.t.collection({ text: cero.t.string }) }
})
// build.js — run once; re-run when the schema changes
import { build } from '@cero-base/cero/build'
import { schema } from './schema.js'

await build('./spec', schema)
import { cero } from '@cero-base/cero'
import { spec } from './spec/index.js'

const me = await cero('./data', spec)
await cero.set(me.profile, { name: 'Alice' })

const room = await cero.open(me.room, { name: 'general' })
const invite = await room.invite()
// room.revoke(invite)  ← invalidate it before anyone joins

On another device — same built spec, its own identity and data dir:

const peer = await cero('./peer-data', spec)
const joined = await cero.open(peer.room, invite)
await cero.put(joined.messages, { text: 'hi' })

Ships with TypeScript declarations (.d.ts) generated from JSDoc.

cero(dir, spec, opts?)

| Opt | Meaning | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | bootstrap | Hyperswarm bootstrap nodes. | | name / isMobile | Stamped on the device's add-writer event. | | seed / phrase | Restore from explicit 16-/32-byte entropy or a BIP-39 mnemonic. Without either, a stored identity is loaded if present, else a fresh one is generated. | | key | Open against an existing bee key (multi-device flow). | | recovery | true triggers bootstrap({ recovering: true }) — for a second device opening with key + the same identity. Needed under autobee 1.0.3 so the second device swaps off the identity-keyed bootstrap-writer slot and starts applying remote appends. | | recoveryTimeout | Bound on the recovery wait. | | routes | Custom action handlers keyed by route name. | | encryptionKey | Override the per-identity encryption key. | | onerror | Called with background/async failures that would otherwise be swallowed (failed after-hooks, onApply callbacks, pairing candidate errors); the app decides how to log or report them. |

Reads me.id, me.device, me.identity for canonical metadata. me.identity.toPhrase() renders the seed phrase.

cero.peek(dir, spec)

const initialized = await cero.peek('./data', def) // true if an identity is stored

Cheap probe — opens the local store, reads the master row, closes. Use it before deciding whether to show a setup screen or jump straight in.

cero.restore(me, phrase)

Wipe the current data and re-open with a different identity from a BIP-39 mnemonic. Used when a user signs back in on a fresh device or recovers from a phrase backup.

const me = await cero('./data', def)
const recovered = await cero.restore(me, 'twelve words …')

Schema

| Shape | Meaning | | -------------------------- | ----------------------------------------------- | | cero.t.single({ … }) | one row (e.g. profile, settings) | | cero.t.collection({ … }) | many rows with auto ids (e.g. messages) | | { … } (plain object) | a child handle — its own scope, share by invite | | local: { … } | device-only — never leaves this device |

A collection can declare secondary indexes for fast exact-field lookups:

cero.t.collection(
  { text: cero.t.string },
  { indexes: { 'by-text': ['text'] } } // name → field(s); query via `cero.get(ref, { text: '…' })`
)

Operators

Every operator takes a ref (e.g. me.profile, room.messages) as the first arg.

cero.put(ref, row)

Append a row to a collection. Generates an id, createdAt, and updatedAt if you don't provide them.

const { data } = await cero.put(room.messages, { text: 'hi' })
data.id // → 'abc…'  (plus createdAt / updatedAt)
// `memberId` (the writer→member backlink) is stamped in the apply layer, so it
// surfaces on a later `cero.get`, not on the row this call returns.

cero.set(ref, row)

Insert-or-update. For a single ref it replaces; for a collection it upserts by id.

await cero.set(me.profile, { name: 'Alice' }) // single
await cero.set(room.messages, { id: 'abc', text: 'edited' }) // upsert by id

cero.get(ref, q?)

Read. The shape of q decides what comes back:

const { data } = await cero.get(me.profile) // single → the row (or null)
const { data } = await cero.get(room.messages, 'abc') // by id  → the row (or null)

// collection → paginated, with range filters
const { data, total, size } = await cero.get(room.messages, {
  gt: 'm-2025', // range: gt / gte / lt / lte
  limit: 20,
  reverse: true
})

// search → case-insensitive substring across string fields
const { data } = await cero.get(room.messages, { search: 'jo sm' })
// every space-separated term must match (AND); folds case + diacritics ('jose' ⇢ 'José').
// `fields` restricts which fields are searched (default: all string fields):
const { data } = await cero.get(room.guests, { search: 'jose', fields: ['name'] })

// exact field match → routed through a declared secondary index when the field has one
const { data } = await cero.get(room.messages, { text: 'alpha' })

total is the unfiltered row count; size is what came back after limit. search and exact-field filters compose with limit / reverse, and cero.count(ref, q) honors them too.

cero.del(ref, id)

Delete by id (collection) or clear (single).

await cero.del(room.messages, 'abc')
await cero.del(me.profile)

cero.count(ref, q?)

const { data: n } = await cero.count(room.messages)
const { data: n } = await cero.count(room.messages, { gt: 'm-2025' })

cero.watch(ref, q?, opts?)

A Readable stream that re-emits the latest snapshot on every change. Always emits an initial snapshot. The stream is tied to the ref's handle — closing the handle destroys it, so a watch never outlives its store and you don't track cleanup. Pass { signal } to bind it to a finer scope instead.

const stream = cero.watch(room.messages, { limit: 50, reverse: true })
for await (const { data, total, size } of stream) {
  /* render */
}
// stops on room.close(), on stream.destroy(), or on `signal` abort:
const ac = new AbortController()
cero.watch(room.messages, null, { signal: ac.signal }) // ac.abort() ⇒ destroyed

cero.call(actionRef, data)

Invoke an action defined in your schema with t.action({ … }). Custom write paths; the dispatcher handles encoding/decoding.

await cero.call(room.promote, { memberId: 'xyz', role: 'admin' })

cero.open(handleRef, arg)

The universal handle entry point. Dispatch is based on arg:

await cero.open(me.room, { name: 'general' }) // create — opts go through to _create
await cero.open(me.room, inviteString) // join — `arg` is the invite string
await cero.open(me.room, { invite: inviteString }) // join — explicit form
await cero.open(me.room, { id: existingRoomId }) // load an existing room by id

Returns the child handle, ready to operate on (cero.put(joined.messages, …) etc.).

cero.before(ref, fn, opts?) / cero.after(ref, fn, opts?)

Hook into writes to a ref. before runs in-path before the write commits — return false to cancel, or mutate ctx.row. after is a non-blocking event that fires once the write has committed. Both return an unsubscribe fn — call it to stop early, ignore it for a subscription that lives as long as the handle, or pass { signal } to unsubscribe when an AbortSignal fires (e.g. { signal: me.signal } to stop on close).

const off = cero.before(room.messages, (ctx) => {
  if (!ctx.row.text?.trim()) return false // reject empty messages
})

cero.after(me.profile, ({ row }) => {
  /* react to your profile changing */
})

ctx is { op, name, row } (plus result in after).

Files

Store a file (avatar, image, attachment) and get a ready-to-render URL. Bytes live in a per-handle blob core — replicated to members on demand, never inlined into the log — so rows carry only a small self-describing id.

Every handle has a builtin files collection. cero.put(handle.files, …) uploads bytes (a Buffer or a Readable) and returns a file with a .url:

const { data: file } = await cero.put(me.files, {
  data: bytes, // Buffer | Readable
  name: 'cat.jpg',
  type: 'image/jpeg'
})

img.src = file.url

Read them back — every row carries a fresh .url:

const { data: files } = await cero.get(me.files) // list all
const { data: one } = await cero.get(me.files, file.id) // one, by id
cero.watch(me.files).on('data', ({ data }) => render(data)) // live

The file() column type

For a file referenced from a row — an avatar, a room icon, a message attachment — declare the field cero.t.file(). Store the file's id; read it back already resolved to { id, name, type, size, url }, with no extra lookup:

// schema.js
profile: cero.t.single({ name: cero.t.string, avatar: cero.t.file() })

// save: upload, then store the id on the row
const { data: pic } = await cero.put(me.files, { data: bytes, type: 'image/png' })
await cero.set(me.profile, { avatar: pic.id })

// read: the avatar comes back url-ready
const { data: profile } = await cero.get(me.profile)
img.src = profile.avatar.url

cero.t.file({ embed: true }) also stores the file's name inline — handy for a list of named attachments, so rendering needs no per-item lookup.

URLs are local and ephemeral

A .url points at a localhost server this device runs, with a per-session token — it changes across restarts and is not shareable to other peers. Never persist a .url: store the id (cero does), and re-read to get a current one. Each member derives their own url from the same id, and the bytes download on demand when the url is first fetched.

Custom operators

Your app's business logic lives as custom operators — plain functions whose first arg is the handle they act on, composing the built-ins. Because the built-ins resolve through the handle (a real DB on the core, an RPC proxy on the client), custom operators are symmetric over RPC for free.

// guest.js — pure functions, one per export
import { put, del } from '@cero-base/cero'
import { ensureGuestId } from './ids.js'

export const create = (room, data) => put(room.guests, { ...data, id: ensureGuestId(data.id) })
export const remove = (room, id) => del(room.guests, id)

cero.define(map)

Register custom operators by scope, once, and cero puts them on every matching handle — the root and each child as it opens, on both the core and the client. A bare key binds on the root handle; a key that names a child-handle type binds on each handle of that type:

import * as user from './user.js'
import * as guest from './guest.js'
import * as station from './station.js'

cero.define({
  user, // bound on the root  → me.user.rename(…)
  room: { guest, station } // bound on each room → room.guest.create(…)
})

const room = await cero.open(me.room, { id })
await room.guest.create({ name: 'Bob' }) // no binding at the call site

cero reads the spec to tell root namespaces from child-handle types, so there is no root wrapper. Call cero.define(...) once at startup — in the build/server process and the client — the same as extensions.

cero.bind(handle, map)

The primitive define uses. Curry a handle onto a { ns: module } map yourself — for a handle you opened manually, or to bind operators you don't want globally registered:

import * as guest from './guest.js'

cero.bind(room, { guest })
await room.guest.create({ name: 'Bob' }) // → guest.create(room, …)

Non-functions in the map are skipped; the handle is returned.

Extensions

An extension bundles schema (build-time) with behavior (runtime). Register it with cero.use()build() folds in its schema and cero() runs its setup once the handle is ready. cero core stays schema-agnostic; apps opt into the behaviors they want.

import { profileSync } from '@cero-base/cero/extensions'

cero.use(profileSync()) // before build() (for the schema) and before cero() (for the behavior)
await build('./spec', schema)

An extension is two optional parts:

function myExtension() {
  return {
    schema: { members: cero.t.extend({ status: cero.t.string }) }, // merged into your schema
    setup(me) {
      const onHandle = (room, opts) => {
        /* opts carries the open args (e.g. opts.name on create); me.children is the set of open rooms */
      }
      me.on('handle', onHandle, { signal: me.signal }) // dropped on me.close()
    }
  }
}

For a behavior-only extension (no schema), pass a function directly — it's shorthand for { setup }:

cero.use((me) => {
  me.on('handle', (room) => {
    /* … */
  })
})

Build and the running app are separate processes, so cero.use() runs in both — your build script (for the schema) and once at app startup before cero() (for the behavior).

Cleanup

You rarely write teardown — a handle's subscriptions follow the same "dies with the node" model as DOM events:

  • cero.watch(ref) streams are destroyed automatically when the ref's handle closes.
  • me.on(...), before, after take { signal } to drop themselves when an AbortSignal fires.
  • me.signal is an AbortSignal that aborts on me.close(). Pass it to tie a subscription to the handle's life, or use your own AbortController for a finer scope (drop it before the handle closes):
setup(me) {
  me.on('handle', onHandle, { signal: me.signal }) // dropped when me closes
  cero.after(me.profile, onProfile, { signal: me.signal })
}

For anything the close cascade won't reach — a timer, an external connection, a stream you made yourself — hand it to me.own(resource) (destroyed on close) or return a disposer from setup:

setup(me) {
  const timer = setInterval(() => ping(me), 30_000)
  me.own({ destroy: () => clearInterval(timer) }) // tied to me.close()
  // …or: return () => clearInterval(timer)
}

profileSync (bundled)

Mirrors your profile onto your member row in every room, so others see your name and avatar. It declares its own profile single (name + the synced fields) and extends member with them — no app schema required. Defaults to syncing avatar:

cero.use(profileSync())
cero.use(profileSync({ fields: { status: cero.t.string } })) // sync extra fields

Want a richer profile (e.g. a bio that doesn't sync)? Declare your own profile single — the app schema wins, and profileSync still adds the member side.

handleSync (bundled)

Mirrors a child handle's profile (name + avatar) onto its row in the parent's handles list — so a handle/room list renders names and photos without opening each handle. Adds avatar (or your fields) to the handle builtin and reflects the handle's own profile (which your app owns) onto the row.

cero.use(handleSync())

// your handle type declares a `profile`; your app sets it:
const room = await cero.open(me.room)
await cero.set(room.profile, { name: 'general', avatar: 'pic.png' })
// → the me.handles row now carries name + avatar — render the room list, no opens

Requires your handle types to declare a profile single; handles without one are left untouched.

RPC

cero runs in one process; your UI runs in another. Connect them with any duplex stream (Bare IPC, Electron contextBridge, a worker port, even a TCP socket).

The client side gets the same operator API as a local cero — cero.put, cero.set, cero.open, etc. all work over the wire.

Server (the process that owns the data)

// workers/main.js — e.g. a Bare worker or Node background process
import { serve } from '@cero-base/cero/server'
import { spec } from './spec/index.js'

// serve builds and owns the root cero — pass storage + the built spec, not a handle.
// Returns a Server instance (call server.close() to shut down).
const server = await serve(Bare.IPC, { storage: './data', spec, seed: '…' })

Client (the UI process)

// renderer.js — runs in Electron renderer, React Native, etc.
import { cero } from '@cero-base/cero'
import { connect } from '@cero-base/cero/client'
import { spec } from './spec/index.js'

const me = await connect(ipcStream, spec)

await cero.set(me.profile, { name: 'Alice' })
const room = await cero.open(me.room, { name: 'general' })
const inviteStr = await room.invite()
cero.watch(room.messages).on('data', ({ data }) => render(data))

The same spec/index.js is imported on both sides. @cero-base/cero/build emits one file; its imports (hyperdb/runtime, hyperdispatch/runtime, hyperschema/runtime) are all pure JS — no native deps — so bundlers like vite handle the renderer bundle fine.

What you can do over RPC

Everything the local API offers, plus lifecycle methods on returned handles:

| Operator / method | Works over RPC | | -------------------------------------------------------------------------------------------------------------------- | -------------- | | cero.put / set / get / del / count / watch / call | ✓ | | cero.open(ref, …) — create, join, load by id | ✓ | | room.invite({ role, expiresIn, multiUse }) — mint an invite (multiUse: true keeps it alive after the first join) | ✓ | | room.revoke(invite) — invalidate an outstanding invite; true if it existed | ✓ | | room.close() — release the handle on the server | ✓ | | room.leave() — remove yourself from the room and from your handles list | ✓ |

cero.watch(ref) returns a Readable on both sides; snapshots flow as a server-streamed RPC.

Wiring up a stream

A duplex stream is anything with write(buf, cb) and that emits data. Some real cases:

// Bare worker → renderer
import { Duplex } from 'streamx'
const ipc = new Duplex({
  write(data, cb) {
    window.bridge.writeWorkerIPC('/main.js', data)
    cb()
  }
})
window.bridge.onWorkerIPC('/main.js', (data) => ipc.push(data))
const me = await connect(ipc, spec)

// Two ceros in the same process (tests)
function streamPair() {
  let a, b
  a = new Duplex({
    write(d, cb) {
      b.push(d)
      cb()
    }
  })
  b = new Duplex({
    write(d, cb) {
      a.push(d)
      cb()
    }
  })
  return [a, b]
}
const [s, c] = streamPair()
await serve(s, { storage: './data', spec })
const remote = await connect(c, spec)

Exports

| Path | What you get | | ---------------------------- | ------------------------------------------------------------------------ | | @cero-base/cero | factory + operators + schema DSL + define/bind | | @cero-base/cero/client | connect(ipc, spec) — talk to a cero running in another process | | @cero-base/cero/server | serve(ipc, { storage, spec }) — run + expose a cero over an IPC stream | | @cero-base/cero/build | build(specDir, schema) — generate the on-disk spec | | @cero-base/cero/extensions | bundled extensions (profileSync, …) |

Tests

npm test

Runs the test files in parallel via xargs -P2 -n1 brittle-node (brittle v4). npm run build:test regenerates the test fixture spec.

Types

npm run build:types

Emits .d.ts from JSDoc into types/. Runs automatically on npm publish via prepublishOnly.