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

@kyneta/schema

v1.8.0

Published

Schema interpreter algebra — pure structure, pluggable interpretations

Readme

@kyneta/schema

Define a schema. Get a live, reactive, syncable document with full TypeScript type safety.

@kyneta/schema is a mathematically rigorous but beautiful and ergonomic building block for representing structured data as it changes over time. You can use plain JS, or bring your own CRDT library (e.g. Loro, Yjs).

import { Schema, createDoc, change, subscribe } from "@kyneta/schema/basic"

const TaskDoc = Schema.struct({
  title: Schema.text(),
  count: Schema.counter(),
  games: Schema.list(
    Schema.struct({
      type: Schema.string("uno", "catan"),
      players: Schema.number(2, 3, 4)
    })
  ),
  done:  Schema.boolean(),
})

const doc = createDoc(TaskDoc)

change(doc, d => {
  d.title.insert(0, "Ship it")
  d.done.set(true)
  d.games.push({ type: "catan", players: 3 })
})

doc()                    // { "title": "Ship it", "count": 0, ... }
doc.title()              // "Ship it"
doc.title.insert(7, "!") // surgical text edit
doc.count.increment()    // counter delta
doc.games.push({         // makes structural doc.games.at(1) available
  type: "uno",
  players: 4
})

subscribe(doc, (changeset) => {
  // fires for any change anywhere in the document
})

Zero runtime dependencies.

What you get from one schema

| Capability | How | |---|---| | Typed reads | doc.title() returns string, doc() returns the full plain snapshot | | Typed writes | .set(), .insert(), .increment(), .push(), .delete() — each ref knows its mutation surface | | Transactions | change(doc, d => { ... })Op[] — atomic batching, returns captured ops | | Sync | applyChanges(docB, ops) — apply ops from another doc, network, or undo stack | | Observation | subscribe(doc, cb) for tree-level, subscribeNode(ref, cb) for leaf-level | | Self-removal | remove(ref) — a child ref removes itself from its parent container | | Version tracking | version(doc), delta(doc, fromVersion), exportSnapshot(doc) | | Validation | validate(schema, data) — same schema, no separate Zod/Yup definition | | Template coercion | `Count: ${doc.count}` works via toPrimitive — no .() needed |

The sync story in 5 lines

// Capture mutations on docA
const ops = change(docA, d => {
  d.title.insert(0, "✨ ")
  d.count.increment(10)
})

// Apply to docB (could be on another machine)
applyChanges(docB, ops, { origin: "sync" })

// docA() deep-equals docB()

Schema types

// Scalars
Schema.string()                      // also Schema.string("a", "b") for constrained values
Schema.number()
Schema.boolean()

// CRDT kinds
Schema.text()                        // collaborative text
Schema.counter()                     // increment/decrement counter
Schema.set(itemSchema)               // add-wins set (value-addressed: .add(v), .has(v), .delete(v))
Schema.tree(itemSchema)              // tree with move semantics
Schema.movableList(itemSchema)       // ordered list with move
Schema.richText({ bold: { expand: "after" } })  // collaborative rich text with marks

// Composites
Schema.struct({ ... })               // fixed-key product
Schema.list(itemSchema)              // ordered sequence
Schema.record(valueSchema)           // dynamic-key map

// Unions
Schema.discriminatedUnion("type", [  // native TS narrowing
  Schema.struct({ type: Schema.string("text"), body: Schema.text() }),
  Schema.struct({ type: Schema.string("image"), url: Schema.string() }),
])
Schema.string().nullable()           // null | string (fluent method on all plain schema types)

// Root
Schema.struct({ ... })                  // document root (annotated product)

Collections

// Lists
doc.tasks.at(0)?.title()   // navigate to child ref
doc.tasks.get(0)           // read plain value directly
doc.tasks.length           // current length
doc.tasks.push({ ... })    // append
doc.tasks.insert(0, item)  // insert at index
doc.tasks.delete(1, 2)     // delete range
remove(doc.tasks.at(0))    // item removes itself from parent
for (const task of doc.tasks) { ... }  // iterate refs
doc.tasks()                // convert tasks to plain JSON

// Records
doc.labels.at("bug")?.()   // navigate + read
doc.labels.get("bug")      // read plain value
doc.labels.set("bug", "red")
doc.labels.delete("bug")
doc.labels.keys()           // string[]
doc.labels.has("bug")       // boolean
doc.labels()                // convert labels to plain JSON

// Sets — value-addressed (no .at(value); members are not addressable)
doc.tags.add("javascript")          // idempotent for an existing member
doc.tags.has("javascript")           // boolean (structural content equality)
doc.tags.delete("typescript")        // returns boolean (was present)
doc.tags.clear()
doc.tags.size                        // number
for (const tag of doc.tags) { ... }  // iterate plain values
doc.tags()                           // → string[]

Observation

// Tree-level — fires for any change in the subtree
const unsub = subscribe(doc, (changeset) => {
  for (const event of changeset.changes) {
    console.log(event.path, event.change.type)
  }
})

// `subscribe` works on leaves too — a leaf is a tree of size 1, so the
// delivered `Op.path` is the empty relative path.
subscribe(doc.count, (changeset) => {
  console.log(changeset.changes[0].path.segments) // []
})

// Node-level — explicit shallow opt-in; delivers Changeset<ChangeBase>
// without paths, for when you don't need the tree shape.
subscribeNode(doc.count, (changeset) => {
  console.log(changeset.origin) // "sync", "undo", etc.
})

Subscribers receive batched Changeset objects — never partially-applied state. Origin provenance ({ origin: "sync" }) flows through from change() and applyChanges().

Data readiness

// Every ref starts unpopulated — no data has arrived yet
doc.title.isPopulated()     // false

change(doc, d => d.title.insert(0, "Hello"))

doc.title.isPopulated()     // true (monotonic — never reverts)
doc.isPopulated()           // true (parent flips when any child does)
doc.count.isPopulated()     // false (untouched siblings stay false)

isPopulated is a reactive boolean on every ref. It starts false and flips to true when any mutation — local or remote — touches that ref or a descendant. Once true, it never reverts. Each isPopulated carries its own [CHANGEFEED], so the compiler can emit conditional rendering regions that activate when data arrives.

Validation

// Throws on first error
const data = validate(MySchema, unknownInput)
// data is now Plain<typeof MySchema> — fully narrowed

// Collect all errors
const result = tryValidate(MySchema, unknownInput)
if (!result.ok) {
  for (const err of result.errors) {
    console.log(err.path, err.expected, err.actual)
    // "tasks[0].priority"  "one of 1 | 2 | 3"  99
  }
}

Two import paths

| Path | Audience | What you get | |---|---|---| | @kyneta/schema/basic | App developers | createDoc, change, subscribe, validate, sync primitives — batteries included | | @kyneta/schema | Library authors | The full composable interpreter toolkit — build custom document systems |

Most projects only need @kyneta/schema/basic.

The /basic API is built on a composable interpreter algebra with six stackable layers (navigation, reading, addressing, caching, writing, observation). If you need custom stacks — read-only documents, write-only mutation dispatchers, or your own substrate — import from @kyneta/schema directly. See example/advanced/ for details.

Examples

# Getting started (basic API)
bun run example/basic/main.ts

# Under the hood (interpreter algebra)
bun run example/advanced/main.ts

Design (Math Nerd Corner)

Under the hood:

  • the schema is a recursive functor (Scalar | Product | Sequence | Map | Sum | Annotated)
  • interpret() is a catamorphism
    • each capability (reading, addressing, writing, caching, observation) is an F-algebra composed via interpreter transformers
  • subscribe is a coalgebra (Moore machine)
  • the step(state, change) → state functions are pure
  • the change → applyChanges round-trip is verified to be extensionally equal
  • the change vocabulary is open

This means the reactive system, the sync protocol, and the validation layer are all derived from the same structure — not parallel implementations that drift apart. It also means this representation of schemas is rigorous, and you can depend on it.

See theory/interpreter-algebra.md for the full treatment, or TECHNICAL.md for the implementation map.

License

MIT