Files
suaveplan/packages/foundation/data/diff/src/patch/patch.md
2026-05-27 23:47:53 +01:00

4.7 KiB

patch

(no submodules)

Purpose

patch.ts is the RFC 6902 patch application engine for @suaveplan/diff. It applies a sequence of operations to a document, producing a PatchResult with the new document state, a count of applied operations, and any error messages encountered. The engine supports both fail-fast (default) and lenient (collect-errors) modes, atomic rollback, and emits OpenTelemetry spans for observability.

The revertPatch helper provides a convenient undo primitive: given the post-patch document, the original patch, and the pre-patch snapshot, it computes the inverse patch and applies it.

Features

  • applyPatch — applies all six RFC 6902 operation types to a document copy
  • Atomic mode (default) — rolls back to the original on any failure
  • Fail-fast mode (default) — throws DiffError at the first error
  • Lenient mode — failFast: false collects errors without throwing
  • OTel integration — emits diff.apply span with outcome, duration_ms, applied_count attributes
  • revertPatch — undo a patch using the pre-patch document snapshot
  • All mutations operate on a deep clone; the original document is never modified

Usage: Basic Example

import { applyPatch } from "@suaveplan/diff/patch";
import type { JsonValue } from "@suaveplan/diff/utils";

const doc: JsonValue = { name: "Alice", age: 30 };

const { doc: updated, applied, errors } = applyPatch(doc, [
  { op: "replace", path: ["age"], value: 31 },
  { op: "add",     path: ["city"], value: "NYC" },
]);
// updated: { name: "Alice", age: 31, city: "NYC" }
// applied: 2, errors: []

Usage: Advanced Example

import { applyPatch, revertPatch } from "@suaveplan/diff/patch";
import { diff } from "@suaveplan/diff/diff";
import type { JsonValue } from "@suaveplan/diff/utils";

const before: JsonValue = { items: [1, 2, 3], meta: { v: 1 } };
const after:  JsonValue = { items: [1, 3],    meta: { v: 2 } };

// Compute and apply patch
const patch = diff(before, after);
const { doc: patched } = applyPatch(before, patch);

// Undo — requires the original doc
const reverted = revertPatch(patched, patch, before);
// reverted deepEquals before ✓

// Test operation — throws DiffError on mismatch (atomic rollback)
try {
  applyPatch(before, [
    { op: "test", path: ["meta", "v"], value: 999 }, // wrong value
    { op: "replace", path: ["meta", "v"], value: 2 },
  ]);
} catch (e) {
  // e.code === "DIFF.TEST_FAILED"
  // before is unchanged (applyPatch never mutates its input)
}

// Lenient mode — collect all errors, apply partial patch
const result = applyPatch(before, [
  { op: "remove", path: ["missing"] },
  { op: "replace", path: ["meta", "v"], value: 5 },
], { failFast: false, atomic: false });
// result.errors contains the remove error; replace still applied

API Reference

PatchResult

interface PatchResult {
  doc: JsonValue;    // Document state after applying the patch
  applied: number;   // Count of successfully applied operations
  errors: string[];  // Error messages (empty on full success)
}

PatchOptions

interface PatchOptions {
  failFast?: boolean; // default true — throw on first error
  atomic?: boolean;   // default true — rollback on failure
}

applyPatch(doc, patch, options?): PatchResult

Applies patch to doc. Never mutates doc. With atomic: true (default), any failure rolls back to the original clone. With failFast: true (default), throws a DiffError on the first failure. Returns PatchResult.

Throws DiffError with codes:

  • DIFF.TEST_FAILED — a test operation value mismatch
  • DIFF.APPLY_FAILED — move/copy source path not found, or generic apply failure

revertPatch(doc, patch, originalDoc?): JsonValue

Inverts patch (using originalDoc as the pre-patch snapshot) and applies the inverse to doc. Returns the reverted document. If originalDoc is omitted, falls back to doc (only correct for add-only patches).

Implementation Notes

  • applyOperation mutates its doc argument in-place for performance, but applyPatch always passes a deep clone so the caller's original is never affected.
  • OTel integration uses a lazy require so packages that do not configure an OTel provider pay zero cost. The tracer is memoised in _tracer after first resolution.
  • The _tracer module-level variable is reset to undefined on module load, which is correct for test isolation — each test process gets a fresh module.
  • The revertPatch function delegates to invertPatch from operations.ts, which steps through intermediate document states to produce accurate inverses for remove and replace operations.