mendoza
A TypeScript decoder for the Mendoza diff format, applying structured patches to JSON documents with optional incremental history tracking.
Repository Health
Technical Analysis
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
incrementalsubmodule wraps values in aValue<T>tree that carries per-nodestartMeta/endMetamarkers 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 -
rebaseValuecompares 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 samePatcherclass 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
rebaseValuewith 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.