npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fishan/cdiff

v2.1.1

Published

A robust library for creating, applying, and inverting compact, single-coordinate diff patches. Features advanced options including char-level operations, patch compression, and configurable safety levels ('unsafe' mode).

Readme

@fishan/cdiff

NPM Version License

A robust library for creating, applying, and inverting compact, single-coordinate diff patches. Features advanced options including char-level operations, patch compression, and configurable safety levels ('unsafe' mode).

This tool is designed for maximum efficiency, generating highly optimized, invertible (or one-way) patches suitable for any text or binary data.


Table of Contents


Key Features

  • Compact Single-Coordinate Patches: Generates easy-to-parse patches (A, D) that don't rely on complex headers or hunk ranges.
  • Character-Level Precision: Automatically optimizes line changes into fine-grained character operations (a, d, e) and groups them for maximum efficiency (a*, d*).
  • Built-in Patch Compression: A hybrid (String/Char Deduplication + Seed/Extend/Mask) algorithm can be enabled (compress: true) to drastically reduce patch size, replacing common fragments with @variables.
  • "Unsafe" Deletions: Supports deletionStrategy: 'unsafe' to generate X/x commands (which omit deleted content), creating minimal-sized "one-way" patches perfect for software updates.
  • Fully Invertible: "Safe" patches (the default) are 100% invertible, allowing you to move both forward (A -> B) and backward (B -> A).
  • Pluggable Core Engine: Leverages @fishan/myers-core-diff, allowing you to change the underlying diff algorithm (e.g., patienceDiff, preserveStructure) via options.
  • Configurable Granularity: Choose between mixed (char-level optimized) or strict line-level patches via the granularity option.
  • Optimal Compression: Use optimal: true to automatically skip compression if it adds overhead to small patches.
  • Configurable Validation: Built-in validation (validationLevel) to ensure patch integrity during creation.
  • Binary Mode: Capable of diffing binary content (mode: 'binary') by treating files as base64-encoded character streams.

Core Engine: @fishan/myers-core-diff

cdiff v2.0.0 is powered by the @fishan/myers-core-diff engine.

This core engine is responsible for the high performance and precision of cdiff. It tokenizes sequences into integers and uses highly optimized algorithms (like Myers O(ND), Patience, etc.) to find the diff.

cdiff acts as the high-level orchestrator that:

  1. Passes text lines to the core engine.
  2. Receives a list of changes.
  3. Optimizes those changes into A+, D+, a*, d*, etc.
  4. Applies compression and unsafe strategies.
  5. Handles patch application and inversion.

You can control which algorithm the core uses by passing the diffStrategyName option to createPatch.


Benchmarks

The key metric is Patch Size (B), where cdiff v2 (especially with compression and unsafe settings) consistently produces the smallest patches (🥇).

The benchmarks also highlight the stability of v2: the legacy cdiff v1 (which used jsdiff as its core) failed (❌ FAILED) on complex scenarios like Block Move, Whitespace Change, Binary Data, and Swapped Blocks. cdiff v2 (powered by @fishan/myers-core-diff) handles all scenarios correctly.

cdiff v2 (Unsafe+Compress) represents the "Software Update" use-case, generating the smallest possible one-way patch.

Standard Benchmarks

Realistic change in small (package source code) | Library | Patch Size (B) | Create (ms) | Apply (ms) | Total (ms) | Correctness | | :--- | :--- | :--- | :--- | :--- | :--- | | 'cdiff v1 (Legacy)' | 63 | '3.17' | '1.42' | '4.59' | '✅ OK' | | 'cdiff v2 (Safe+Compress) 🥇' | 50 | '7.76' | '3.56' | '11.32' | '✅ OK' | | 'cdiff v2 (Unsafe+Compress) 🥇' | 42 | '1.22' | '0.20' | '1.42 🥇' | '✅ OK' | | 'jsdiff (unified)' | 259 | '1.81' | '1.81' | '3.62' | '✅ OK' | | 'diff-match-patch' | 62 | '3.56' | '1.16' | '4.71' | '✅ OK' |

Realistic change in medium (source code) | Library | Patch Size (B) | Create (ms) | Apply (ms) | Total (ms) | Correctness | | :--- | :--- | :--- | :--- | :--- | :--- | | 'cdiff v1 (Legacy) 🥇' | 20 | '6.70' | '0.47' | '7.18' | '✅ OK' | | 'cdiff v2 (Safe+Compress) 🥇' | 26 | '0.94' | '0.25' | '1.19' | '✅ OK' | | 'cdiff v2 (Unsafe+Compress)' | 26 | '1.77' | '0.49' | '2.26' | '✅ OK' | | 'jsdiff (unified)' | 401 | '4.52' | '0.62' | '5.14' | '✅ OK' | | 'diff-match-patch' | 54 | '0.32' | '0.03' | '0.35 🥇' | '✅ OK' |

Realistic change in large (source code) | Library | Patch Size (B) | Create (ms) | Apply (ms) | Total (ms) | Correctness | | :--- | :--- | :--- | :--- | :--- | :--- | | 'cdiff v1 (Legacy) 🥇' | 33 | '229.35' | '5.01' | '234.36' | '✅ OK' | | 'cdiff v2 (Safe+Compress) 🥇' | 41 | '18.46' | '4.37' | '22.83' | '✅ OK' | | 'cdiff v2 (Unsafe+Compress)' | 41 | '13.73' | '6.62' | '20.35' | '✅ OK' | | 'jsdiff (unified)' | 197 | '164.27' | '2.61' | '166.88' | '✅ OK' | | 'diff-match-patch' | 59 | '0.43' | '0.35' | '0.78 🥇' | '✅ OK' |

Advanced Scenarios

Multiple Small Changes (large file) | Library | Patch Size (B) | Create (ms) | Apply (ms) | Total (ms) | Correctness | | :--- | :--- | :--- | :--- | :--- | :--- | | 'cdiff v1 (Legacy)' | 1258 | '196.21' | '5.78' | '201.99' | '✅ OK' | | 'cdiff v2 (Safe+Compress) 🥇' | 855 | '70.14' | '4.36' | '74.49' | '✅ OK' | | 'cdiff v2 (Unsafe+Compress) 🥇' | 833 | '33.67' | '7.29' | '40.95 🥇' | '✅ OK' | | 'jsdiff (unified)' | 16942 | '168.95' | '6.91' | '175.86' | '✅ OK' | | 'diff-match-patch' | 3473 | '63.30' | '9.52' | '72.82' | '✅ OK' |

Block Move (structural shift in large.js) | Library | Patch Size (B) | Create (ms) | Apply (ms) | Total (ms) | Correctness | | :--- | :--- | :--- | :--- | :--- | :--- | | 'cdiff v1 (Legacy)' | 2848 | '198.68' | '9.42' | '208.10' | '❌ FAILED' | | 'cdiff v2 (Safe+Compress) 🥇' | 1830 | '35.59' | '5.35' | '40.94' | '✅ OK' | | 'cdiff v2 (Unsafe+Compress) 🥇' | 1366 | '50.85' | '4.26' | '55.11' | '✅ OK' | | 'jsdiff (unified)' | 2938 | '174.30' | '2.85' | '177.15' | '✅ OK' | | 'diff-match-patch' | 3229 | '2.18' | '0.42' | '2.59 🥇' | '✅ OK' |

Whitespace Change (indentation in medium.js) | Library | Patch Size (B) | Create (ms) | Apply (ms) | Total (ms) | Correctness | | :--- | :--- | :--- | :--- | :--- | :--- | | 'cdiff v1 (Legacy)' | 10038 | '14.88' | '0.71' | '15.59 🥇' | '❌ FAILED' | | 'cdiff v2 (Safe+Compress) 🥇 🥇' | 989 | '8.22' | '10.38' | '18.60' | '✅ OK' | | 'cdiff v2 (Unsafe+Compress) 🥇' | 989 | '16.40' | '2.16' | '18.56' | '✅ OK' | | 'jsdiff (unified)' | 10834 | '14.12' | '3.10' | '17.22' | '✅ OK' | | 'diff-match-patch' | 7500 | '75.77' | '1.40' | '77.17' | '✅ OK' |

Inversion Benchmarks (Refactoring Scenario)

Invert Patch from Refactoring | Library | Invert+Apply (ms) | Correctness | | :--- | :--- | :--- | | 'cdiff v1 (Legacy)' | '5.79' | '✅ OK' | | 'cdiff v2 (Safe+Compress) 🥇 🥇' | '5.31' | '✅ OK' | | 'jsdiff (unified)' | '170.03' | '✅ OK' | | 'diff-match-patch' | '52.29' | '✅ OK' |

Core Strength Benchmarks

Huge File (50k lines) | Library | Patch Size (B) | Create (ms) | Apply (ms) | Total (ms) | Correctness | | :--- | :--- | :--- | :--- | :--- | :--- | | 'cdiff v1 (Legacy)' | 357 | '5513.01' | '287.15' | '5800.16' | '✅ OK' | | 'cdiff v2 (Safe+Compress) 🥇' | 281 | '137.03' | '31.51' | '168.55' | '✅ OK' | | 'cdiff v2 (Unsafe+Compress) 🥇' | 122 | '163.22' | '30.86' | '194.08' | '✅ OK' | | 'jsdiff (unified)' | 2222 | '2375.74' | '9.58' | '2385.32' | '✅ OK' | | 'diff-match-patch' | 470 | '26.30' | '13.74' | '40.04 🥇' | '✅ OK' |

Binary Data (1KB) | Library | Patch Size (B) | Create (ms) | Apply (ms) | Total (ms) | Correctness | | :--- | :--- | :--- | :--- | :--- | :--- | | 'cdiff v1 (Legacy)' | 55 | '0.81' | '0.13' | '0.94' | '❌ FAILED' | | 'cdiff v2 (Safe+Compress) 🥇' | 57 | '0.80' | '0.75' | '1.55' | '✅ OK' | | 'cdiff v2 (Unsafe+Compress) 🥇' | 47 | '0.99' | '0.34' | '1.33' | '✅ OK' | | 'jsdiff (unified)' | 1672 | '1.12' | '1.10' | '2.22' | '✅ OK' | | 'diff-match-patch' | 296 | '0.23' | '0.05' | '0.29 🥇' | '✅ OK' |

"Dirty" Data (Large common prefix/suffix) | Library | Patch Size (B) | Create (ms) | Apply (ms) | Total (ms) | Correctness | | :--- | :--- | :--- | :--- | :--- | :--- | | 'cdiff v1 (Legacy)' | 41 | '66.67' | '0.21' | '66.87' | '✅ OK' | | 'cdiff v2 (Safe+Compress) 🥇' | 47 | '0.49' | '0.23' | '0.72' | '✅ OK' | | 'cdiff v2 (Unsafe+Compress) 🥇' | 38 | '0.41' | '0.18' | '0.59' | '✅ OK' | | 'jsdiff (unified)' | 100206 | '66.00' | '34.02' | '100.02' | '✅ OK' | | 'diff-match-patch' | 58 | '0.15' | '0.03' | '0.18 🥇' | '✅ OK' |

Edge Case & Stress Test Scenarios

Low Entropy (Repeating Data) | Library | Patch Size (B) | Create (ms) | Apply (ms) | Total (ms) | Correctness | | :--- | :--- | :--- | :--- | :--- | :--- | | 'cdiff v1 (Legacy)' | 358 | '311.99' | '2.81' | '314.80' | '✅ OK' | | 'cdiff v2 (Safe+Compress) 🥇' | 105 | '14.98' | '1.61' | '16.59' | '✅ OK' | | 'cdiff v2 (Unsafe+Compress) 🥇' | 92 | '12.19' | '1.62' | '13.81 🥇' | '✅ OK' | | 'jsdiff (unified)' | 1972 | '279.14' | '1.67' | '280.81' | '✅ OK' | | 'diff-match-patch' | 330 | '68.85' | '1.23' | '70.08' | '✅ OK' |

Single Line Changes (Minified JS) | Library | Patch Size (B) | Create (ms) | Apply (ms) | Total (ms) | Correctness | | :--- | :--- | :--- | :--- | :--- | :--- | | 'cdiff v1 (Legacy)' | 907 | '147.11' | '23.71' | '170.82' | '✅ OK' | | 'cdiff v2 (Safe+Compress) 🥇' | 474 | '180.14' | '26.24' | '206.38' | '✅ OK' | | 'cdiff v2 (Unsafe+Compress) 🥇' | 454 | '213.40' | '10.78' | '224.18' | '✅ OK' | | 'jsdiff (unified)' | 336055 | '86.33' | '83.61' | '169.94' | '✅ OK' | | 'diff-match-patch' | 3331 | '58.27' | '6.36' | '64.64 🥇' | '✅ OK' |

Complete Replacement (Low Similarity) | Library | Patch Size (B) | Create (ms) | Apply (ms) | Total (ms) | Correctness | | :--- | :--- | :--- | :--- | :--- | :--- | | 'cdiff v1 (Legacy)' | 339379 | '279.65' | '2.62' | '282.27' | '❌ FAILED' | | 'cdiff v2 (Safe+Compress) 🥇' | 299375 | '1305.26' | '30.92' | '1336.18' | '✅ OK' | | 'cdiff v2 (Unsafe+Compress) 🥇' | 5870 | '37.87' | '10.09' | '47.96 🥇' | '✅ OK' | | 'jsdiff (unified)' | 301830 | '264.96' | '87.62' | '352.58' | '✅ OK' | | 'diff-match-patch' | 379704 | '1037.44' | '0.38' | '1037.82' | '✅ OK' |

Complete Replacement Invert (Low Similarity) | Library | Invert+Apply (ms) | Correctness | | :--- | :--- | :--- | | 'cdiff v1 (Legacy) 🥇' | '1.48' | '❌ FAILED' | | 'cdiff v2 (Safe+Compress) 🥇' | '18.38' | '✅ OK' | | 'jsdiff (unified)' | '1051.41' | '✅ OK' | | 'diff-match-patch' | '1001.34' | '✅ OK' |

Swapped Blocks | Library | Patch Size (B) | Create (ms) | Apply (ms) | Total (ms) | Correctness | | :--- | :--- | :--- | :--- | :--- | :--- | | 'cdiff v1 (Legacy)' | 6791 | '182.92' | '3.09' | '186.01' | '❌ FAILED' | | 'cdiff v2 (Safe+Compress) 🥇' | 4487 | '26.09' | '4.46' | '30.55' | '✅ OK' | | 'cdiff v2 (Unsafe+Compress) 🥇' | 3273 | '14.66' | '3.62' | '18.28 🥇' | '✅ OK' | | 'jsdiff (unified)' | 6346 | '172.66' | '4.99' | '177.66' | '✅ OK' | | 'diff-match-patch' | 7552 | '273.08' | '26.76' | '299.84' | '✅ OK' |


Installation

npm install @fishan/cdiff

Note: cdiff v2 requires @fishan/myers-core-diff as a peer dependency, which will be installed automatically.


Basic Usage

The API is straightforward: createPatch, applyPatch, and invertPatch.

import { CdiffService } from '@fishan/cdiff';

const oldContent = 'line 1\nold line 2\nline 3';
const newContent = 'line 1\nnew line 2\nline 3';

// 1. Create a "safe" (default) patch
// This patch is invertible
const patch = CdiffService.createPatch(oldContent, newContent);

console.log(patch);
// Output: [ '2 d 4 3 old', '2 a 4 3 new' ]

// 2. Apply the patch
const patchedContent = CdiffService.applyPatch(oldContent, patch, { strictMode: true });
console.log(patchedContent === newContent); // true

// 3. Invert the patch
const invertedPatch = CdiffService.invertPatch(patch);
console.log(invertedPatch);
// Output: [ '2 a 4 3 old', '2 d 4 3 new' ]

// 4. Apply the inverted patch to the new content to get the old one back
const restoredContent = CdiffService.applyInvertedPatch(newContent, invertedPatch, { strictMode: true });
console.log(restoredContent === oldContent); // true

Advanced Usage & Examples

Example 1: Patch Compression

Enable compression to significantly reduce patch size. This is ideal for network transfer.

import { CdiffService } from '@fishan/cdiff';

const oldContent = 'const val = "Hello World";\nconsole.log("Hello World");';
const newContent = 'const val = "Hello User";\nconsole.log("Hello User");';

// Enable compression
const patch = CdiffService.createPatch(oldContent, newContent, {
  compress: true
});

console.log(patch);
// Output: [ '~', '$', '2-3 d* L 5 World', '2-3 a* L 4 User' ]
//
// The patch is 100% self-contained and decompressed automatically
// by applyPatch.
const patchedContent = CdiffService.applyPatch(oldContent, patch);
console.log(patchedContent === newContent); // true

Example 2: "Unsafe" Deletions (One-Way Patch)

Create a minimal-sized patch for software updates where inversion is not needed. This generates X/X+ commands that do not store the deleted content.

import { CdiffService } from '@fishan/cdiff';

// Old content has a block of 4 lines
const oldContent = 'line 1\n/* START BLOCK */\nline 2\nline 3\n/* END BLOCK */\nline 4';
// New content has that block removed
const newContent = 'line 1\nline 4';

// Use 'unsafe' strategy
const patch = CdiffService.createPatch(oldContent, newContent, {
  deletionStrategy: 'unsafe'
});

console.log(patch);
// Output: [ '2 X+ 4' ]
//
// Note: It generated 'X+' (unsafe block delete) instead of 'D+'
// and 4 content lines, saving space. This patch is not invertible.

// This patch is NOT invertible
try {
  CdiffService.invertPatch(patch);
} catch (e) {
  console.log(e.message); // "Cannot invert patch: It contains 'X'/'x' (unsafe delete) commands."
}

Example 3: Binary Mode

cdiff can handle binary files (like images or executables) by treating them as latin1 strings and base64-encoding their content within the patch.

import { CdiffService } from '@fishan/cdiff';
import * as fs from 'fs';

// Load raw buffers
const oldBuffer = fs.readFileSync('old_file.bin');
const newBuffer = fs.readFileSync('new_file.bin');

// Convert buffers to 'latin1' strings for diffing
const oldContent = oldBuffer.toString('latin1');
const newContent = newBuffer.toString('latin1');

const patch = CdiffService.createPatch(oldContent, newContent, {
  mode: 'binary',
  compress: true // Compression is highly recommended for binary
});

// `applyPatch` also needs the 'binary' flag
const patchedContent = CdiffService.applyPatch(oldContent, patch, { mode: 'binary' });

// Convert back to buffer to verify
const patchedBuffer = Buffer.from(patchedContent, 'latin1');
console.log(Buffer.compare(newBuffer, patchedBuffer) === 0); // true

Example 4: Using a Different Core Strategy

You can change the diff algorithm by specifying diffStrategyName. This requires installing and registering the strategy from @fishan/myers-core-diff.

// --- In your main setup file (e.g., index.ts) ---
import { MyersCoreDiff, registerPatienceDiffStrategy } from '@fishan/myers-core-diff';
// Register the 'patienceDiff' plugin globally
registerPatienceDiffStrategy(MyersCoreDiff);


// --- In your application code ---
import { CdiffService } from '@fishan/cdiff';

const oldCode = 'if (a) {\n  b();\n}\nc();';
const newCode = 'c();\nif (a) {\n  b();\n}'; // Block move

// Use 'patienceDiff' which is good at detecting block moves
const patch = CdiffService.createPatch(oldCode, newCode, {
  diffStrategyName: 'patienceDiff'
});

console.log(patch);
// Output: [ '1 A c();', '4 D c();' ]

Example 5: Granularity Control

You can control whether cdiff generates character-level optimizations or strict line-level changes.


import { CdiffService } from '@fishan/cdiff';

const oldCode = `const config = {
  port: 8080,
  timeout: 5000,
  debug: false
};`;

const newCode = `const config = {
  port: 3000, // changed
  timeout: 5000,
  debug: true // changed
};`;

console.log('--- Granularity: Mixed (Default) ---');
// 'mixed' (default): Generates optimized char-level diffs ('d', 'a') inside lines
const mixedPatch = CdiffService.createPatch(oldCode, newCode, { granularity: 'mixed' });
console.log (mixedPatch);
/* Output: 
--- Granularity: Mixed (Default) ---
[
  '2 d 8 5 8080,',
  '2 a 8 16 3000, // changed',
  '4 d 9 5 false',
  '4 a 9 15 true // changed'
]
*/

console.log('--- Granularity: Lines ---');
// 'lines': Forces standard A/D line replacement
const linesPatch = CdiffService.createPatch(oldCode, newCode, { granularity: 'lines' });
console.log (linesPatch);
/* Output: 
--- Granularity: Lines ---
[
  '2 D   port: 8080,',
  '2 A   port: 3000, // changed',
  '4 D   debug: false',
  '4 A   debug: true // changed'
]
*/

Example 6: Optimal Compression

Use optimal: true to ensure that enabling compression never results in a larger patch than the uncompressed version (useful for small changes where compression headers add overhead).


import { CdiffService } from '@fishan/cdiff';

const oldText = "This is a simple text.";
const newText = "This is a simple test.";

console.log('--- Normal Compress ---');
// Might be larger due to headers (~, $) on such a tiny text
const normal = CdiffService.createPatch(oldText, newText, { compress: true, optimal: false });
console.log (normal);
/* Output: 
--- Normal Compress ---
[ '~', '$', '2 d L 1 x', '2 a L 1 s' ]
*/

console.log('--- Optimal Compress ---');
// Falls back to uncompressed because it's smaller
const optimal = CdiffService.createPatch(oldText, newText, { compress: true, optimal: true });
console.log (optimal);
/* Output: 
--- Optimal Compress ---
[ '1 d 19 1 x', '1 a 19 1 s' ]
*/

Patch Format

A cdiff patch is an array of string commands.

Line-Level Commands (A/D/X/E)

  • {NewLineNum} A {Content}: Add line.
  • {OldLineNum} D {Content}: Delete line (Safe).
  • {OldLineNum} X: Delete line (Unsafe, content omitted).
  • {LineNum} A+ {Count}: Add Block. The next {Count} lines are content to be added.
  • {LineNum} D+ {Count}: Delete Block (Safe). The next {Count} lines are the content to be deleted.
  • {LineNum} X+ {Count}: Delete Block (Unsafe). No content lines follow.
  • {NewLineNum} E+ {Count}: Equal Block (Context). The next {Count} lines are context lines.
    • Generated when includeEqualMode is set to 'inline' or 'context'.

Character-Level Commands (a/d/x/e)

  • {LineNum} a {pos} {len} {content} ...: Add char(s) at {pos}.
  • {LineNum} d {pos} {len} {content} ...: Delete char(s) at {pos} (Safe).
  • {LineNum} x {pos} {len} ...: Delete char(s) at {pos} (Unsafe).
  • {LineNum} e {pos} {len} {content} ...: Equal char(s) at {pos} (Context/Validation).
    • Generated when includeCharEquals: true (or implicitly by includeEqualMode: 'context').
  • {Range} a* {pos} {len} {content}: Group Add. Apply this a operation to all lines in {Range} (e.g., 1,3-5).
  • {Range} d* {pos} {len} {content}: Group Delete (Safe).
  • {Range} x* {pos} {len}: Group Delete (Unsafe).

Compressed Format (~)

If a patch is compressed, it will have the following structure:

  1. ~: The compression flag (always the first line).
  2. @variable {content}: Zero or more variable definitions.
  3. $: The definitions separator.
  4. (Compressed Commands): The patch commands (A, D, a, d, etc.) where content has been replaced by @variables and line/char numbers are Base58-encoded.

API Reference

CdiffService.createPatch(...)

public static createPatch(
  oldContent: string | undefined,
  newContent: string | undefined,
  options?: CdiffOptions
): string[]

Compares two strings and generates a compact cdiff patch.

CdiffService.applyPatch(...)

public static applyPatch(
  originalContent: string,
  cdiff: string[],
  options?: ApplyOptions
): string

Applies a cdiff patch to the originalContent to produce the newContent. Automatically handles decompression if the ~ flag is present.

CdiffService.invertPatch(...)

public static invertPatch(
  cdiff: string[],
  options?: CdiffOptions
): string[]

Inverts a "safe" patch (A<->D, a<->d). Throws an error if the patch contains "unsafe" X/x commands. Automatically handles decompression.

CdiffService.applyInvertedPatch(...)

public static applyInvertedPatch(
  newContent: string,
  invertedCdiff: string[],
  options?: Omit<ApplyOptions, 'inverting'>
): string

A convenience wrapper that applies an invertedCdiff to the newContent to restore the originalContent.


Options Reference

CdiffOptions

Options for CdiffService.createPatch.

| Option | Type | Default | Description | | :--- | :--- | :--- | :--- | | mode | 'text' | 'binary' | 'text' | Treats content as lines (text) or a single block (binary). | | debug | boolean | false | Enables verbose console logging during patch creation. | | granularity | 'mixed' | 'lines' | 'mixed' | Controls the granularity of patch generation. | compress | boolean | false | If true, enables the built-in hybrid compression. | | optimal | boolean | false | If true (and compress: true), discards compression if the result is larger than uncompressed. | | diffStrategyName | string | 'commonSES' | Name of the registered strategy from @fishan/myers-core-diff (e.g., patienceDiff). | | includeEqualMode | 'none' | 'inline' | 'separate' | 'context' | 'none' | Strategy for including EQUAL (context) blocks. | | includeCharEquals | boolean | false | If true, generates e (char-level equal) commands for validation. | | includeContextLines | number | 0 | Number of context lines to include (used with includeEqualMode: 'context'). | | deletionStrategy | 'safe' | 'unsafe' | function | 'safe' | How to handle deleted content. 'unsafe' creates X/x commands. | | validationLevel | 'none' | 'raw' | 'final' | ... | 'none' | Enables validation steps during patch creation. debug: true defaults this to 'all-invert'. |

ApplyOptions

Options for CdiffService.applyPatch and CdiffService.applyInvertedPatch.

| Option | Type | Default | Description | | :--- | :--- | :--- | :--- | | strictMode | boolean | false | If true, throws an error on content mismatch (e.g., 'D' command fails). If false, logs a warning. | | onWarning | (message: string) => void | undefined | Callback to receive warnings when strictMode: false. | | debug | boolean | false | Enables verbose console logging during patch application. | | mode | 'text' | 'binary' | 'text' | Must match the mode used during patch creation. | | includeCharEquals | boolean | false | If true, validates e (char-level equal) commands. | | inverting | boolean | false | @internal Used by applyInvertedPatch to change apply logic. |


Test Suite

cdiff is validated by a comprehensive suite of 182 tests covering patch creation, application, inversion, compression, and edge cases for both text and binary modes.

How to Run Tests

  1. Development Mode (Fastest): Runs tests directly on .ts files using ts-node.

    npm run test:dev
  2. Build Mode (Uncompressed JS): Compiles to JavaScript, then tests the uncompressed dist/src/cdiff.js.

    npm run test:build
  3. Production Mode (Minified JS): Builds, minifies, and tests the final cdiff.min.js file to ensure production integrity.

    npm run test:prod
  CdiffService.createPatch - mode: 'binary' (Unit Tests)
    ✅ should create a 'd' and 'a' patch (replacement)
    ✅ should create a 'd' patch (deletion)
    ✅ should create an 'a' patch (addition)
    ✅ should create an 'x' patch (unsafe deletion)
    ✅ should include 'e' commands (includeCharEquals: true)

  CdiffService.createPatch - mode: 'binary' (E2E Lifecycle)
    ✅ should correctly patch when compress=true
    ✅ should correctly patch with includeEqualMode='context'
    ✅ should correctly patch with includeEqualMode='inline'
    ✅ should correctly patch with includeEqualMode='separate'
    ✅ should pass 'all-invert' validation (for a 'safe' patch)
    ✅ should pass 'all-invert' validation (for an 'unsafe' patch)

  CdiffService.invertPatch - mode: 'binary'
    ✅ should successfully invert a 'safe' binary patch
    ✅ should FAIL to invert an 'unsafe' binary patch

  CdiffCharService: Character-level Patching (Comprehensive)
    createPatch: Generation Logic
      ✅ should return an empty array for identical strings
      ✅ should generate a simple addition patch
      ✅ should generate a simple deletion patch
      ✅ should generate a patch that correctly transforms the string
    createPatch: Grouping Logic
      ✅ should NOT merge distant changes separated by a long EQUAL block
      ✅ should merge close changes separated by a short EQUAL block (<= 4 chars)
      ✅ should handle a single continuous change correctly
      ✅ should merge changes where separators are single chars
    Whitespace Handling
      ✅ should correctly handle leading/trailing whitespace in content
      ✅ should handle changes involving only whitespace (E2E check)
      ✅ should handle patches for whitespace-only strings
    Robustness and Edge Cases
      ✅ should handle multiple non-contiguous modifications
      ✅ should handle a very long string with a small change
      ✅ should apply patch regardless of command order in array
    End-to-End Lifecycle
      ✅ should handle simple modification
      ✅ should handle additions at the beginning
      ✅ should handle deletions from the end
      ✅ should handle creating a string from empty
      ✅ should handle deleting the entire string
      ✅ should handle complete rewrite (line-level patch)
      ✅ should handle multiple changes (line-level patch)
      ✅ should handle changes with special characters
      ✅ should handle multiple non-contiguous modifications
      ✅ E2E: should correctly patch and invert MERGED changes
      ✅ E2E: should correctly patch and invert SEPARATE changes
      ✅ E2E: should correctly handle complex real-world case with merging
    Direct Test: CdiffCharService.groupChanges
      ✅ should group a simple replacement
      ✅ should merge changes around a short EQUAL block
      ✅ should NOT merge changes around a long EQUAL block
      ✅ should correctly calculate indices with multiple groups

  CdiffCompressService: End-to-End Compression Cycle (v17.3)
    ✅ [E2E] should return an empty array for an empty patch
    ✅ [E2E] should not create definitions for a patch with no profitable repetitions
    String Commands (v16+ Logic)
      ✅ [String v16 E2E] should compress and decompress repeated A+ blocks
      ✅ [String v16 E2E] should compress and decompress repeated D+ blocks
      ✅ [String v16 E2E] should compress and decompress repeated single A/D lines
      ✅ [String v16 E2E] should handle parametric (d #...@...#...) decompression with gaps
      ✅ [String v16 E2E] should handle simple (a @...) decompression (v7.2 format)
    Char Commands (v2 Logic)
      ✅ [Char v2 E2E] should compress and decompress repeated char insertions (a)
      ✅ [Char v2 E2E] should compress and decompress repeated char deletions (d)
      ✅ [Char v2 E2E] should correctly decompress (a index_B58@var) format
      ✅ [Char v2 E2E] should correctly decompress (d index_B58@var) format
      ✅ [Char v2 E2E] should compress and decompress a* or d* commands (v11.0 logic)
    Hybrid Compression (v5 + v2)
      ✅ [Hybrid E2E] should correctly compress/decompress mixed commands
      ✅ [Hybrid E2E] should correctly prioritize and merge templates
    Regression Tests (v11.8+)
      ✅ [Regression v11.8.1] should handle JSDoc @param bug
      ✅ [Regression v11.8.1] should handle literal @ and # in content
      ✅ [JSDoc Fail v11.8.15] should reproduce @return bug correctly now
      ✅ [JSDoc Fail v11.8.15] should reproduce @param bug correctly now
      ✅ [BugFix E2E v12+] should correctly handle D+ block with empty string
      ✅ [BugFix E2E v12+] should correctly handle A+ block with empty string
    Decompressor Standalone (v11.8+)
      ✅ [isCompressed v11.8.0] should return true only for patches with COMPRESSION_FLAG
      ✅ [isCompressed v11.8.0] should return false for uncompressed or empty patches
    CdiffService.createPatch: Hybrid Option Interactions (v18.0)

--- [START RAW (D/d) VALIDATION] ---
Patch application (RAW (D/d)) matches new content (Forward): true
Patch application (RAW (D/d)) matches old content (Backward): true
--- [END RAW (D/d) VALIDATION] ---


--- [START FINAL (X/x) VALIDATION] ---
Patch application (FINAL (X/x)) matches new content (Forward): true
[FINAL (X/x) VALIDATION] Backward (Invert) check skipped for 'unsafe' patch (expected behavior).
--- [END FINAL (X/x) VALIDATION] ---


--- [START COMPRESSED VALIDATION] ---
Patch application (COMPRESSED) matches new content (Forward): true
--- [END COMPRESSED VALIDATION] ---

      ✅ [Hybrid E2E] compress + unsafe + inline + validation

--- [START RAW (D/d) VALIDATION] ---
Patch application (RAW (D/d)) matches new content (Forward): true
Patch application (RAW (D/d)) matches old content (Backward): true
--- [END RAW (D/d) VALIDATION] ---


--- [START FINAL (D/d) VALIDATION] ---
Patch application (FINAL (D/d)) matches new content (Forward): true
Patch application (FINAL (D/d)) matches old content (Backward): true
--- [END FINAL (D/d) VALIDATION] ---


--- [START COMPRESSED VALIDATION] ---
Patch application (COMPRESSED) matches new content (Forward): true
Patch application (COMPRESSED) matches old content (Backward): true
--- [END COMPRESSED VALIDATION] ---

      ✅ [Hybrid E2E] compress + safe + context + validation
      ✅ [Hybrid E2E] compress + unsafe + separate

  CdiffService.createPatch - deletionStrategy
    ✅ should use 'safe' (D) deletion by default
    ✅ should use 'safe' (D) deletion when specified
    ✅ should use 'unsafe' (X) deletion when specified
    ✅ should use 'unsafe' (x) for char-level deletion
    ✅ should use functional deletion strategy

  CdiffService.createPatch - includeEqualMode
    ✅ should use 'none' (default) - no E+ blocks
    ✅ should use 'inline' - E+ blocks with new coordinates
    ✅ should use 'separate' - E+ blocks with dual coordinates
    ✅ should use 'context' (includeContextLines: 1)

  CdiffService.createPatch - includeCharEquals
    ✅ should NOT include 'e' commands by default
    ✅ should include 'e' commands when includeCharEquals=true
    ✅ should include 'e' commands when includeEqualMode='context'

  CdiffService: Uni-Coordinate Lifecycle
    ✅ [Apply] should add a single line
    ✅ [Apply] should delete a single line
    ✅ [Apply] should handle file creation from empty
    ✅ [Apply] should handle deleting all content
    ✅ [Create] should generate an empty cdiff for identical files
    ✅ [Create] should generate correct A command for addition
    ✅ [Create] should generate correct D command for deletion
    ✅ [Create] should generate correct d and a commands for modification
    ✅ [E2E] should correctly apply a patch it just created
    ✅ [Create+Apply] should handle multiple additions in different positions
    ✅ [Create+Apply] should handle multiple deletions in different positions
    ✅ [Create+Apply] should handle complex modifications (delete, add, replace)
    ✅ [Create+Apply] should handle adding lines at the end
    ✅ [Create+Apply] should handle deleting lines from the beginning
    ✅ [Create+Apply] should handle empty lines correctly
    ✅ [Create+Apply] should handle single-line file modification
    ✅ [Create+Apply] should handle complete deletion of multiple lines
    ✅ [Create+Apply] should handle adding empty lines
    ✅ [Create+Apply] should handle line moves (delete and re-add)
    ✅ [Apply] should handle multiple additions in the middle
    ✅ [Apply] should handle multiple deletions in the middle
    ✅ [Apply] should handle additions at the end
    ✅ [Create] should generate patch for replacement with empty line
    ✅ [Create] should generate patch for moving a line (delete + add elsewhere)
    ✅ [Invert] should correctly invert a complex patch with multiple changes
    ✅ [Create] should generate intra-line patches for aligned multi-line blocks
    ✅ [E2E-Invert] should handle multiple separate blocks of changes
    ✅ [E2E-Invert] should handle changes at the very beginning of the file
    ✅ [E2E-Invert] should handle changes at the very end of the file
    ✅ [E2E-Invert] should handle complete replacement of a block
    ✅ [E2E-Invert] should handle a completely rewritten file
    ✅ [E2E-Invert] should handle deletion of all content
    ✅ [E2E-Invert] should handle creation of a file from empty
    ✅ [E2E-Invert] should correctly handle empty lines in changes
    ✅ [E2E-Invert] should handle aligned multi-line block changes

  CdiffService: Additional Edge Cases and Robustness
[CdiffService] Invalid line number 4 for unsafe deletion (file has 3 lines). Deletion ignored.
    ✅ [Apply] should ignore invalid patch commands
    ✅ [Apply] should handle multiple additions at the same position
    ✅ [Apply] should ignore duplicate deletions at the same position
    ✅ [Create+Apply] should handle large file with multiple changes
    ✅ [Create+Apply] should handle only additions
    ✅ [Create+Apply] should handle only deletions
    ✅ [Create+Apply] should handle lines with spaces and special characters
    ✅ [Apply] should return original content for empty patch
    ✅ [Create+Apply] should handle multiple consecutive replacements
[CdiffService] Invalid line number 999 for deletion (file has 3 lines). Deletion ignored.
    ✅ [E2E-Invert] should handle patch with out-of-bounds positions
    ✅ [E2E-Invert] should handle trailing newlines
    ✅ [E2E-Invert] should handle single-line file with changes

  CdiffService: Extended Robustness Tests
    ✅ [Apply] should handle chaotic patch command order
    ✅ [Apply] should handle multiple changes at the same line
    ✅ [E2E-Invert] should handle multiple consecutive empty lines
[CdiffService] Deletion mismatch for line 2: expected 'wrong content', but actual is 'line 2'. Deletion ignored.
    ✅ [Apply] should ignore deletion with incorrect content
    ✅ [E2E-Invert] should handle very large file with multiple changes

  CdiffService: Whitespace and Special Characters
    ✅ [Create+Apply] should handle exact whitespace in deletions
    ✅ [E2E-Invert] should handle multiple spaces and tabs
[CdiffService] Deletion mismatch for line 2: expected 'spaces', but actual is '  spaces  '. Deletion ignored.
    ✅ [Apply] should ignore whitespace mismatch in non-strict mode
    ✅ [Apply] should throw on whitespace mismatch in strict mode

  CdiffService: Advanced Whitespace and Obfuscation
    ✅ [Create+Apply] should handle obfuscated whitespace
    ✅ [E2E-Invert] should handle empty line with mixed whitespace
    ✅ [Create+Apply] should handle whitespace-only line
    ✅ [Apply] should handle additions after intermediate content is exhausted
    ✅ [Apply] should insert additions at exact positions

  MyersCoreDiff - Swapped Blocks Edge Cases
    ✅ 1. Raw diff from MyersCoreDiff must be valid and reversible
    ✅ 2. _findMiddleSnake must not return invalid snake on swapped blocks
    ✅ 3. calculateDiff must produce valid diff when _recursiveDiff fails
    ✅ 4. Full CdiffService patch must apply correctly on swapped blocks
    ✅ 5. Character-level patch must be valid and reversible

  Direct Test: CdiffCharService Logic
    ✅ applyPatch should handle a simple addition
    ✅ applyPatch should handle a simple deletion
    ✅ DEBUG: should handle multiple non-contiguous modifications
    ✅ applyPatch should handle combined add and delete
    ✅ applyPatch should process multi-part commands correctly
    ✅ invertPatch should correctly swap a and d commands

  Direct Test: CdiffService Logic
    ✅ applyPatch should handle single line addition (A)
    ✅ applyPatch should handle single line deletion (D)
    ✅ applyPatch should handle block addition (A+)
    ✅ applyPatch should handle block deletion (D+)
    ✅ applyPatch should handle grouped character addition (a*)
    ✅ applyPatch should handle grouped character deletion (d*)
    ✅ applyPatch should correctly process a mix of commands
    ✅ invertPatch should correctly swap A/D, A+/D+, and a*/d* commands

  Direct Test: CdiffService Create & E2E Lifecycle
    ✅ createPatch should generate block commands (A+) for large additions
    ✅ createPatch should generate block commands (D+) for large deletions
    ✅ createPatch should choose char-level diff for efficient changes
    ✅ createPatch should generate grouped commands (a*) for indentation changes
    ✅ E2E Lifecycle: createPatch -> applyPatch should work for a simple change
    ✅ E2E Lifecycle: createPatch -> applyPatch should work for complex changes
    ✅ E2E Inversion Lifecycle: create -> invert -> applyInverted should restore original

  Direct Test: CdiffService Internal Helpers
    compressLineNumbers
      ✅ should handle an empty array
      ✅ should handle a single number
      ✅ should handle consecutive numbers
      ✅ should handle non-consecutive numbers
      ✅ should handle a mix of consecutive and non-consecutive numbers
      ✅ should handle unsorted input
    deconstructCharCommand
      ✅ should deconstruct a single-op command
      ✅ should deconstruct a multi-op command
      ✅ should handle an empty command
      ✅ should handle content with spaces

    ✅ E2E Inversion Lifecycle: should correctly handle a simple block swap
    ✅ should generate a correct patch for a simple block swap
    ✅ should generate a correct patch for a simple chars swap
    ✅ should generate a correct patch for a block move
    ✅ should handle a mix of replacements and pure additions/deletions

  [CdiffService] Granularity and Optimal Options
    ✅ granularity: 'lines' should force line-level commands (ignore char optimizations)
    ✅ granularity: 'mixed' (default) should choose optimal representation
    ✅ granularity: 'chars' should throw not implemented error
    ✅ granularity: invalid option should throw error
    ✅ granularity: 'lines' works correctly with includeEqualMode='inline'
    ✅ optimal: true should prevent compressed patch from being larger (Anti-bloat)
    ✅ optimal: true should keep compressed version if it is indeed smaller
    ✅ Scenario: 'lines' granularity with 'unsafe' deletionStrategy
    ✅ Scenario: Validation works with granularity='lines'

License

The cdiff library is licensed under the MIT License.

Dependency License

@fishan/myers-core-diff is distributed under the MIT license.

MIT License

Copyright (c) 2025 Aleks Fishan

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.