voryn-core
v0.1.3
Published
A change engine for JavaScript and TypeScript objects — diff, apply, reverse, merge, and undo/redo for structured data.
Maintainers
Readme
Voryn
Git for JavaScript objects.
Turn changes between JavaScript/TypeScript objects into deterministic, serializable, reversible changesets — then use the same changeset to apply, reverse, store, transmit, or merge that change.
diff · apply · reverse · merge · historynpm install voryn-coreWorks in Node.js (≥18), browsers, Bun, Deno, and Cloudflare Workers, with ESM and CommonJS builds plus TypeScript declarations.
⚠️ Early release (v0.1.x). Published on npm — this isn't a placeholder or a preview, it's a real, working package you can install today. The API and specification are still stabilizing based on real-world feedback before a 1.0 commitment — see Status below.
30 seconds
import { diff, apply, reverse } from "voryn-core";
const before = { name: "John", age: 30 };
const after = { name: "John", age: 31 };
const changes = diff(before, after);
// { ops: [{ operation: "replace", path: ["age"], oldValue: 30, newValue: 31 }] }
const updated = apply(before, changes); // { name: "John", age: 31 }
const original = apply(updated, reverse(changes)); // back to { name: "John", age: 30 }before after
────────────── ──────────────
{ {
name: "John", name: "John",
age: 30 age: 31
} }
↓
voryn changeset
replace ["age"]
30 → 31diff()
↓
Changeset
↓
apply() ──────→ new state
reverse() ──────→ original state
merge() ──────→ combined state (or a reported conflict)That's the whole product. Everything below is detail.
Why
Most applications end up rebuilding the same handful of things, usually under different names: undo/redo, audit logs, version history, synchronization, merge-conflict handling, and — more recently — reviewing what an AI agent is proposing to change before letting it happen. All of these are really the same underlying question, asked repeatedly: what changed?
Voryn answers that once, as a plain, storable value, instead of every feature re-deriving it independently:
- Store changes — object → changeset → database column
- Send changes — client → changeset → server
- Reverse changes — changeset →
reverse()→ the previous state - Combine changes — two changesets from a common source →
merge()
Concrete use cases this shows up in: undo/redo, audit logs, AI agent action review, collaborative editing, version history, event sourcing, offline sync, and database change tracking.
Voryn isn't trying to replace every diff/patch/state-management library — it's built specifically around one thing: a portable, reversible changeset model, rather than just a list of mutations. If you only need a boolean "did these differ," a general deep-equal function is simpler. If you need to reverse, store, merge, or transmit a change, that's what Voryn is for. See Voryn vs. JSON Patch for a detailed, RFC-verified comparison against the closest existing standard.
What Voryn actually is
Voryn is an open changeset format — a specification for
representing a change between two structured values.
voryn-core is its TypeScript reference implementation.
That distinction matters because the specification is
language-independent by design: nothing about the format requires
TypeScript, so a Rust, Go, or Python implementation could exist
someday, following the same spec, producing changesets that mean the
same thing regardless of which implementation made them. See
spec/changeset.md for the formal contract,
and docs/philosophy.md for the reasoning
behind it.
The core idea: a Changeset
A changeset describes how one value becomes another, as a flat, ordered, serializable array of operations:
{
ops: [
{ operation: "replace", path: ["age"], oldValue: 30, newValue: 31 }
]
}There are exactly three operation kinds — add, remove, replace
— deliberately, not four or five. See
docs/philosophy.md
for why there's no move. A changeset is plain, inspectable data: log
it, store it in a database column, send it over the wire, diff it
against another changeset.
Usage
diff() — compare two values
import { diff } from "voryn-core";
const changes = diff(
{ user: { name: "Ada", tags: ["math"] } },
{ user: { name: "Ada", tags: ["math", "computing"] } },
);
// {
// ops: [
// { operation: "add", path: ["user", "tags", 1], newValue: "computing" }
// ]
// }Changes are scoped to exactly what changed — a small edit deep inside a large object produces a small changeset, not a wholesale replace of the whole tree.
apply() — apply a changeset
import { apply } from "voryn-core";
const updated = apply(before, changes);apply() never mutates its input; it returns a new value, reusing
untouched parts of the original structure by reference.
reverse() — invert a changeset
import { diff, apply, reverse } from "voryn-core";
const changes = diff(before, after);
const undone = apply(after, reverse(changes));
// undone deep-equals `before`This is the basis of undo/redo — see createHistory() below, which is
built entirely on top of apply() and reverse().
merge() — combine changesets from a common source
import { diff, merge, apply } from "voryn-core";
const source = { profile: { name: "Old", avatar: "old.png" } };
const fromUserA = diff(source, { profile: { name: "New", avatar: "old.png" } });
const fromUserB = diff(source, { profile: { name: "Old", avatar: "new.png" } });
const combined = merge(fromUserA, fromUserB);
apply(source, combined);
// { profile: { name: "New", avatar: "new.png" } }If both changesets touch the same path with genuinely different
results, merge() throws VorynMergeConflictError rather than
silently picking a side — resolving a real conflict is application
logic, not something Voryn decides for you. See
spec §9 for the exact conflict-detection rules.
createHistory() — undo/redo
import { createHistory } from "voryn-core";
const history = createHistory({ count: 0 });
history.update({ count: 1 });
history.update({ count: 2 });
history.undo(); // state is now { count: 1 }
history.undo(); // state is now { count: 0 }
history.redo(); // state is now { count: 1 }
history.canUndo; // boolean
history.canRedo; // booleanupdate() diffs against the current state internally — you never
construct changesets by hand for ordinary undo/redo. history also
exposes apply(changeset) directly for cases where you already have a
changeset (e.g. the output of merge()).
Reviewing AI agent changes before they land
A changeset is also how you let an AI agent propose an edit without
giving it direct write access — diff() the current record against
whatever the agent proposes, inspect the resulting changeset, and only
apply() it once approved:
import { diff, apply } from "voryn-core";
const proposedByAgent = { ...ticket, status: "in_progress", priority: "urgent" };
const changes = diff(ticket, proposedByAgent);
// inspect changes.ops — a human or a policy decides here
if (humanApproves(changes)) {
const updated = apply(ticket, changes);
}Full example: examples/ai-agent-review.ts
and docs/guide/ai-agent-review.md.
More runnable examples: examples/ — undo/redo,
audit logging, collaborative merging, and AI agent review, each
paired with a guide in docs/guide/. There's also a
browser playground you can open directly, no build step required:
examples/playground.html.
Design principles
- Small, orthogonal vocabulary. Three operations. No
move, nocopy, no custom comparators, no configuration sprawl. - Deterministic and canonical. The same inputs always produce the same changeset — a requirement for changesets to work as a real interchange format, not just an implementation convenience.
- Secure by default, with no opt-out. Prototype-pollution
guarding, circular-reference detection, and resource limits are
built in and cannot be disabled — see
docs/guide/error-handling.mdandSECURITY.md. - Zero runtime dependencies.
- The specification is the product. See "What Voryn actually is"
above and
docs/philosophy.md.
Status
Voryn is at 0.1.x — early, but real: the specification and all five
verbs (diff, apply, reverse, merge, history) are implemented,
invariant-tested (124 conformance tests, 300 fuzz cases), and
benchmarked against jsondiffpatch and microdiff (see
benchmarks/README.md). What that doesn't
mean yet: the API and the specification itself may still change before
a 1.0 release. If you adopt it now, pin your version and expect to
read release notes before upgrading — see CHANGELOG.md
and docs/roadmap.md for what's shipped versus
still planned.
Documentation
docs/guide/— task-oriented walkthroughs (undo/redo, audit logging, collaborative merging, AI agent review, error handling)docs/comparisons/voryn-vs-json-patch.md— an RFC-verified comparison against the closest existing standardspec/changeset.md— the formal, implementation-neutral Changeset Specificationdocs/philosophy.md— why Voryn is built the way it is; read this before proposing a featuredocs/architecture.md— how this TypeScript implementation maps onto the specificationdocs/roadmap.md— what's shipped, what's next, what's under considerationbenchmarks/README.md— methodology and results vs. jsondiffpatch/microdiffSECURITY.md— what's protected, and how to report a vulnerabilityCHANGELOG.md— release history
Contributing
See CONTRIBUTING.md for development setup,
commit conventions, and what a good pull request looks like here.
This project follows the Code of Conduct.
License
MIT — see LICENSE.
