@sanity/diff-patch



Generate Sanity patch mutations by comparing two documents or values. This library creates conflict-resistant patches designed for collaborative editing environments where multiple users may be editing the same document simultaneously.
Objectives
- Conflict-resistant patches: Generate operations that work well in 3-way merges and collaborative scenarios
- Performance: Optimized for real-time, per-keystroke patch generation
- Intent preservation: Capture the user's intended change rather than just the final state
- Reliability: Consistent, well-tested behavior across different data types and editing patterns
Used internally by the Sanity App SDK for its collaborative editing system.
Installation
npm install @sanity/diff-patch
API Reference
diffPatch(source, target, options?)
Generate patch mutations to transform a source document into a target document.
Parameters:
source: DocumentStub
- The original document
target: DocumentStub
- The desired document state
options?: PatchOptions
- Configuration options
Returns: SanityPatchMutation[]
- Array of patch mutations
Options:
interface PatchOptions {
id?: string
basePath?: Path
ifRevisionID?: string | true
}
Example:
import {diffPatch} from '@sanity/diff-patch'
const source = {
_id: 'movie-123',
_type: 'movie',
_rev: 'abc',
title: 'The Matrix',
year: 1999,
}
const target = {
_id: 'movie-123',
_type: 'movie',
title: 'The Matrix Reloaded',
year: 2003,
director: 'The Wachowskis',
}
const mutations = diffPatch(source, target, {ifRevisionID: true})
diffValue(source, target, basePath?)
Generate patch operations for values without document wrapper.
Parameters:
source: unknown
- The original value
target: unknown
- The desired value state
basePath?: Path
- Base path to prefix operations (default: [])
Returns: SanityPatchOperations[]
- Array of patch operations
Example:
import {diffValue} from '@sanity/diff-patch'
const source = {
name: 'John',
tags: ['developer'],
}
const target = {
name: 'John Doe',
tags: ['developer', 'typescript'],
active: true,
}
const operations = diffValue(source, target)
const operations = diffValue(source, target, ['user', 'profile'])
Collaborative Editing Example
The library generates patches that preserve user intent and minimize conflicts in collaborative scenarios:
const originalDoc = {
_id: 'blog-post-123',
_type: 'blogPost',
title: 'Getting Started with Sanity',
paragraphs: [
{
_key: 'intro',
_type: 'paragraph',
text: 'Sanity is a complete content operating system for modern applications.',
},
{
_key: 'benefits',
_type: 'paragraph',
text: 'It offers real-time collaboration and gives developers controll over the entire stack.',
},
{
_key: 'conclusion',
_type: 'paragraph',
text: 'Learning Sanity will help you take control of your content workflow.',
},
],
}
const userAChanges = {
...originalDoc,
paragraphs: [
{
_key: 'intro',
_type: 'paragraph',
text: 'Sanity is a complete content operating system for modern applications.',
},
{
_key: 'conclusion',
_type: 'paragraph',
text: 'Learning Sanity will help you take control of your content workflow.',
},
{
_key: 'benefits',
_type: 'paragraph',
text: 'It offers real-time collaboration and gives developers control over the entire stack.',
},
],
}
const userBChanges = {
...originalDoc,
paragraphs: [
{
_key: 'intro',
_type: 'paragraph',
text: 'Sanity is a complete content operating system that gives developers control over the entire stack.',
},
{
_key: 'benefits',
_type: 'paragraph',
text: 'It offers real-time collaboration and gives developers control over the entire stack.',
},
{
_key: 'conclusion',
_type: 'paragraph',
text: 'Learning Sanity will help you take control of your content workflow.',
},
],
}
const patchA = diffPatch(originalDoc, userAChanges)
const patchB = diffPatch(originalDoc, userBChanges)
const finalMergedResult = {
_id: 'blog-post-123',
_type: 'blogPost',
title: 'Getting Started with Sanity',
paragraphs: [
{
_key: 'intro',
_type: 'paragraph',
text: 'Sanity is a complete content operating system that gives developers control over the entire stack.',
},
{
_key: 'conclusion',
_type: 'paragraph',
text: 'Learning Sanity will help you take control of your content workflow.',
},
{
_key: 'benefits',
_type: 'paragraph',
text: 'It offers real-time collaboration and gives developers control over the entire stack.',
},
],
}
Technical Details
String Diffing with diff-match-patch
When comparing strings, the library attempts to use diff-match-patch to generate granular text patches instead of simple replacements. This preserves editing intent and enables better conflict resolution.
Automatic selection criteria:
- String size limit: Strings larger than 1MB use
set
operations
- Change ratio threshold: If >40% of text changes (determined by simple string length difference), uses
set
(indicates replacement vs. editing)
- Small text optimization: Strings <10KB will always use diff-match-patch
- System key protection: Properties starting with
_
(e.g. _type
, _key
) always use set
operations as these are not typically edited by users
Performance rationale:
These thresholds are based on performance testing of the underlying @sanity/diff-match-patch
library on an M2 MacBook Pro:
- Keystroke editing: 0ms for typical edits, sub-millisecond even on large strings
- Small insertions/pastes: 0-10ms for content <50KB
- Large insertions/deletions: 0-50ms for content >50KB
- Text replacements: Can be 70ms-2s+ due to algorithm complexity
The 40% change ratio threshold catches problematic replacement scenarios while allowing the algorithm to excel at insertions, deletions, and small edits.
Migration from v5:
Version 5 allowed configuring diff-match-patch behavior with lengthThresholdAbsolute
and lengthThresholdRelative
options. Version 6 removes these options in favor of tested defaults that provide consistent performance across real-world editing patterns. This allows us to change the behavior of this over time to better meet performance needs.
Array Handling
Keyed arrays: Arrays containing objects with _key
properties are diffed by key rather than index, producing more stable patches for collaborative editing.
Index-based arrays: Arrays without keys are diffed by index position.
Undefined values: When undefined
values are encountered in arrays, they are converted to null
. This follows the same behavior as JSON.stringify()
and ensures consistent serialization. To remove undefined values before diffing:
const cleanArray = array.filter((item) => typeof item !== 'undefined')
System Keys
The following keys are ignored at the root of the document when diffing a document as they are managed by Sanity:
_id
_type
_createdAt
_updatedAt
_rev
Error Handling
- Missing document ID: Throws error if
_id
differs between documents and no explicit id
option provided
- Immutable _type: Throws error if attempting to change
_type
at document root
- Multi-dimensional arrays: Not supported, throws
DiffError
- Invalid revision: Throws error if
ifRevisionID: true
but no _rev
in source document
License
MIT © Sanity.io
6.0.0 (2025-06-13)
⚠ BREAKING CHANGES
- API Rename and Visibility:**
- The
diffItem
function is no longer exported. Its functionality is now primarily internal.
- A new function
diffValue(source: unknown, target: unknown, basePath?: Path): SanityPatchOperations[]
is introduced and exported. This function generates an array of SanityPatchOperations
(which are plain objects like {set: {...}}
, {unset: [...]}
) based on the differences between source
and target
values. It does not wrap these operations in the SanityPatchMutation
structure.
- The
diffPatch
function (which diffs documents and returns SanityPatchMutation[]
) now internally calls diffItem
and then uses the refactored serializePatches
to construct the final mutations. The logic for adding id
and ifRevisionID
to the patch mutations now resides within diffPatch
.
- Patch Type Refinements:**
- Removed older, more generic patch types like
SetPatch
, InsertAfterPatch
, SanitySetPatch
, SanityUnsetPatch
, SanityInsertPatch
, and SanityDiffMatchPatch
from the public API (some were previously exported from patches.ts
).
- Introduced new, more specific types for patch operations:
SanitySetPatchOperation
({ set: Record<string, unknown> }
)
SanityUnsetPatchOperation
({ unset: string[] }
)
SanityInsertPatchOperation
({ insert: { before/after/replace: string, items: unknown[] } }
)
SanityDiffMatchPatchOperation
({ diffMatchPatch: Record<string, string> }
)
- The
SanityPatchOperations
type is now a Partial
union of these new operation types, reflecting that a single patch object from diffValue
will contain one or more of these operations.
- The
SanityPatch
type (used within SanityPatchMutation
) now extends SanityPatchOperations
and includes id
and optional ifRevisionID
.
- The internal
Patch
type (used by diffItem
) remains but is now an internal detail.
- Refactored
serializePatches
Function:
- The
serializePatches
function now takes an array of internal Patch
objects and returns an array of SanityPatchOperation[]
(the raw operation objects like {set: {...}}
).
- It no longer handles adding
id
or ifRevisionID
; this responsibility is moved to the diffPatch
function.
- The logic for grouping
set
, unset
, insert
, and diffMatchPatch
operations into distinct objects in the output array has been improved for clarity.
- Refactored
diffPatch
Function:
- Now calls the internal
diffItem
to get the raw patch list.
- Calls the refactored
serializePatches
to get SanityPatchOperations[]
.
- Maps over these operations to create
SanityPatchMutation[]
, adding the id
to each and ifRevisionID
only to the first patch mutation in the array.
- JSDoc Updates:
- Updated JSDoc for
diffValue
to clearly explain its purpose, parameters, and return type.
- Updated JSDoc for
diffPatch
and internal types to reflect the changes.
Rationale:
- Clearer Public API:
diffValue
provides a more intuitive name for diffing arbitrary JavaScript values and returning the raw operations, distinct from diffPatch
which is document-centric.
- Improved Type Safety & Granularity: The new
Sanity...Operation
types are more precise and make it easier to work with the different kinds of patch operations programmatically.
- Correct
ifRevisionID
Handling: Ensuring ifRevisionID
is only on the first patch of a transaction is crucial for correct optimistic locking in Sanity.
- Better Separation of Concerns:
diffItem
focuses on generating a flat list of diffs, serializePatches
(as used by diffValue
) groups them into operations, and diffPatch
handles the document-specific concerns like _id
and ifRevisionID
.
This refactor provides a cleaner and more robust API for generating patches, both for full documents and for arbitrary values.
- remove undefined-to-null conversion warnings and simplify internal APIs (#38)
* Removed the `diffMatchPatch` options (`enabled`, `lengthThresholdAbsolute`, `lengthThresholdRelative`) from `PatchOptions`.
* Removed the `DiffMatchPatchOptions` and `DiffOptions` (which included `diffMatchPatch`) interfaces from the public API.
* Removed the internal `mergeOptions` function and the DMP-specific parts of `defaultOptions`.
- New Performance-Based Heuristics for DMP:
- Introduced a new exported utility function
shouldUseDiffMatchPatch(source: string, target: string): boolean
. This function encapsulates the new logic for deciding whether to use DMP.
- The decision is now based on:
- Document Size Limit: Documents larger than 1MB (
DMP_MAX_DOCUMENT_SIZE
) will use set
operations.
- Change Ratio Threshold: If more than 40% (
DMP_MAX_CHANGE_RATIO
) of the text changes, set
is used (indicates replacement vs. editing).
- Small Document Optimization: Documents smaller than 10KB (
DMP_MIN_SIZE_FOR_RATIO_CHECK
) always use DMP, as performance is consistently high for these.
- System Key Protection: Properties starting with
_
(system keys) continue to use set
operations.
- Added extensive JSDoc to
shouldUseDiffMatchPatch
detailing the heuristic rationale, performance characteristics (based on testing @sanity/diff-match-patch
on an M2 MacBook Pro), algorithm details, and test methodology.
- Internal Simplification:
- The internal
getDiffMatchPatch
function now uses shouldUseDiffMatchPatch
to make its decision and no longer accepts DMP-related options.
- Simplified the call to the underlying
@sanity/diff-match-patch
library within getDiffMatchPatch
to use makePatches(source, target)
directly. This is more concise and leverages the internal optimizations of that library, with performance validated to be equivalent to the previous multi-step approach.
- Constants: Introduced
SYSTEM_KEYS
, DMP_MAX_DOCUMENT_SIZE
, DMP_MAX_CHANGE_RATIO
, and DMP_MIN_SIZE_FOR_RATIO_CHECK
to define these thresholds.
- Test Updates: Snapshots have been updated to reflect the new DMP behavior based on these heuristics.
Rationale for Change:
The previous configurable thresholds for DMP were somewhat arbitrary and could lead to suboptimal performance or overly verbose patches in certain scenarios. This change is based on empirical performance testing of the @sanity/diff-match-patch
library itself. The new heuristics are designed to:
- Optimize for common editing patterns: Ensure fast performance for keystrokes and small pastes, which are the most frequent operations.
- Prevent performance degradation: Avoid triggering complex and potentially slow DMP algorithm paths when users perform large text replacements (e.g., pasting entirely new content).
- Simplify the API: Remove the burden of configuration from the user, providing sensible defaults.
- Maintain conflict-resistance: Continue to leverage DMP's strengths for collaborative editing where appropriate.
By hardcoding these well-tested heuristics, we aim for a more robust and performant string diffing strategy by default.
Features
- add key-based reordering support for keyed object arrays (#41) (27dcdc2)
- remove undefined-to-null conversion warnings and simplify internal APIs (#38) (86cff6e)
- replace
diffItem
with diffValue
(#39) (b8ad36a)
- replace configurable DMP with perf-based heuristics (#36) (9577019)
Bug Fixes