@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