包详细信息

@sanity/diff-patch

sanity-io711.5kMIT6.0.0

Generates a set of Sanity patches needed to change an item (usually a document) from one shape to another

sanity, patch, diff, mutation

自述文件

@sanity/diff-patch

npm versionnpm bundle sizenpm weekly downloads

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 // Document ID (extracted from _id if not provided)
  basePath?: Path // Base path for patches (default: [])
  ifRevisionID?: string | true // Revision lock for optimistic updates
}

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})
// [
//   {
//     patch: {
//       id: 'movie-123',
//       ifRevisionID: 'abc',
//       set: {
//         title: 'The Matrix Reloaded',
//         year: 2003,
//         director: 'The Wachowskis'
//       }
//     }
//   }
// ]

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)
// [
//   {
//     set: {
//       name: 'John Doe',
//       'tags[1]': 'typescript',
//       active: true
//     }
//   }
// ]

// With base path
const operations = diffValue(source, target, ['user', 'profile'])
// [
//   {
//     set: {
//       'user.profile.name': 'John Doe',
//       'user.profile.tags[1]': 'typescript',
//       'user.profile.active': true
//     }
//   }
// ]

Collaborative Editing Example

The library generates patches that preserve user intent and minimize conflicts in collaborative scenarios:

// Starting document
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.',
    },
  ],
}

// User A reorders paragraphs AND fixes a typo
const userAChanges = {
  ...originalDoc,
  paragraphs: [
    {
      _key: 'intro',
      _type: 'paragraph',
      text: 'Sanity is a complete content operating system for modern applications.',
    },
    {
      _key: 'conclusion', // Moved conclusion before benefits
      _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.', // Fixed typo: "controll" → "control"
    },
  ],
}

// User B simultaneously improves the intro text
const userBChanges = {
  ...originalDoc,
  paragraphs: [
    {
      _key: 'intro',
      _type: 'paragraph',
      text: 'Sanity is a complete content operating system that gives developers control over the entire stack.', // Added more specific language about developer control
    },
    {
      _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.',
    },
  ],
}

// Generate patches that capture each user's intent
const patchA = diffPatch(originalDoc, userAChanges)
const patchB = diffPatch(originalDoc, userBChanges)

// Apply both patches - they merge successfully because they target different aspects
// User A's reordering and typo fix + User B's content improvement both apply
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.', // ✅ User B's improvement
    },
    {
      _key: 'conclusion', // ✅ User A's reordering
      _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.', // ✅ User A's typo fix
    },
  ],
}

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

更新日志

📓 Changelog

All notable changes to this project will be documented in this file. See Conventional Commits for commit guidelines.

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

5.0.0 (2025-02-05)

⚠ BREAKING CHANGES

  • Module name is now @sanity/diff-patch (from previous sanity-diff-patch). Update imports accordingly!

Features

  • rename module to @sanity/diff-patch (#33) (891241f)

4.0.0 (2024-10-15)

⚠ BREAKING CHANGES

  • We now require node 18 or higher to run this module

Bug Fixes

  • apply unset operations first (692f5d6)
  • require node 18.2 or higher (dc2437b)

3.0.4 (2024-10-15)

Bug Fixes

  • use correct escaping for unsafe property names in paths (53f84f8)

3.0.3 (2024-10-15)

Bug Fixes

  • allow (non-leading) dashes in properties (bce4d2f), closes #28

3.0.2 (2023-04-28)

Bug Fixes

  • upgrade diff-match-patch dependency (166f5e6)

3.0.1 (2023-04-25)

Bug Fixes

3.0.0 (2023-04-25)

⚠ BREAKING CHANGES

  • validateDocument() has been removed
  • ifRevisionId option must be written as ifRevisionID

Features

  • remove internal APIs, modernize tooling (474d6ff)