mendoza

A TypeScript decoder for the Mendoza diff format, applying structured patches to JSON documents with optional incremental history tracking.

Library
npm
v3.0.8
46stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
40/100Fair
Development Activity48
Maintenance20
Community20
Maturity60
Momentum12

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
58/100Fair
Architecture78
Code Quality58
Innovation62
Learning Curve35

Mendoza is a compact, stack-based patch format for diffing and applying changes to structured JSON-like documents, built by Sanity.io to synchronize documents in real time without shipping the whole payload on every edit. This package is the TypeScript decoder: it interprets a Mendoza patch (an array of opcodes and operands) against an existing document and produces the patched result, using a bytecode-style virtual machine (Patcher) that walks separate input and output stacks in lockstep via push/pop/copy/return operations.

Beyond the simple applyPatch entry point for plain JavaScript objects, the package ships an incremental module that wraps documents in a Value<T> tree carrying per-node metadata (e.g. revision/origin markers), letting consumers apply a stream of patches while retaining node identity across versions, split and merge string fragments at the UTF-8 level, and rebaseValue a new document against a previous one to reuse unchanged subtrees. These are the building blocks Sanity’s own tooling uses for real-time collaborative editing, undo history, and diff visualization on top of its content lake.

What You Get

  • Simple patcher - applyPatch(left, patch) applies a raw Mendoza patch array directly to a plain JavaScript object or array and returns the resulting value.
  • Incremental patcher - The incremental submodule wraps values in a Value<T> tree that carries per-node startMeta/endMeta markers so you can track when each field or array element last changed.
  • String-level diffing - UTF-8-aware slice/split logic reconstructs strings from byte-offset patch operations, correctly handling multi-byte characters and surrogate pairs.
  • Value rebasing - rebaseValue compares two incremental values and reuses identical objects, array elements, and string parts from the old value, minimizing allocations and preserving reference identity for unchanged data.
  • Pluggable object model - The ObjectModel<V,S,O,A> interface decouples the patch-interpreting virtual machine from any specific value representation, so the same Patcher class powers both the plain-object and incremental patchers.

Common Use Cases

  • Real-time collaborative editing - Apply incoming Mendoza patches from a live connection to keep a local document in sync with concurrent edits from other users.
  • Content lake sync clients - Sanity Studio and custom clients decode mutation events delivered as Mendoza patches instead of full document snapshots to reduce bandwidth.
  • Undo/redo and audit history - Use the incremental patcher’s per-node metadata to know exactly when a field, array element, or string fragment was last touched.
  • Diff visualization tooling - Combine rebaseValue with the incremental model to compute which parts of two document versions are identical versus changed, powering diff UIs.

Under The Hood

Architecture The core is a small stack-based virtual machine, Patcher in internal-patcher.ts, which reads a flat opcode array (OPS) and dispatches to one process<OpName> method per opcode, maintaining separate inputStack/outputStack arrays as it walks the source document and builds the target document in lockstep (push/pop/copy/return-into-object/return-into-array). The VM itself is generic over an ObjectModel<V,S,O,A> interface (object-model.ts) that abstracts wrapping, copying, and mutating objects/arrays/strings, so two concrete models — SimpleModel in simple-patcher.ts (operates on plain JS values) and IncrementalModel<T> in incremental-patcher.ts (operates on a Value<T> tree with cached content and per-node metadata) — reuse the exact same interpreter. Changing the opcode set or stack protocol would ripple through both models, but the ObjectModel boundary keeps that blast radius contained and well-defined.

Tech Stack Written in strict TypeScript (5.7), built and packaged via @sanity/pkg-utils (emits dual ESM/CJS output plus bundled .d.ts types), tested with Vitest 2.x plus @vitest/coverage-v8 and a GitHub Actions-aware reporter, linted with ESLint 8 (@typescript-eslint, simple-import-sort, eslint-config-prettier) and formatted via @sanity/prettier-config. Releases are fully automated with semantic-release and @sanity/semantic-release-preset. Notably the package has zero runtime dependencies — everything above is dev-only tooling.

Code Quality Typing is thorough: the VM, object model, and incremental value tree are all expressed with generics (ObjectModel<V,S,O,A>, Value<T>, Content<T>) rather than loosely-typed objects, and failure modes (unknown opcode, out-of-bounds string split, mismatched string-part bookkeeping) throw descriptive errors instead of failing silently. Naming is consistent and CI runs tsc --noEmit, ESLint, and a coverage-tracked test suite across three OSes and multiple Node versions. That said, the test suite visible in the repo is thin for the amount of logic present — a single test file exercises applyPatch with one representative patch, with no dedicated tests for the incremental patcher, rebasing, or the UTF-8 string-splitting edge cases directly in the repo.

API Design The public surface is intentionally small: applyPatch covers the common case with near-zero setup (import {applyPatch} from 'mendoza'), while the more advanced incremental namespace is exposed as a single grouped import (Value, rebaseValue, wrap, unwrap, getType, applyPatch). The tradeoff is that the incremental API’s semantics (start/end metadata, rebase targets, origin tracking) are only explained through one annotated README example rather than dedicated docs or JSDoc, so using it correctly requires reading the source.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search