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

@iodev/patch-and-resolve

v1.0.9

Published

[![npm version](https://badge.fury.io/js/@iodev%2Fpatch-and-resolve.svg)](https://www.npmjs.com/package/@iodev/patch-and-resolve) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)

Readme

Patch and Resolve

npm version License: GPL v3

A library for merging conflicting JSON patches with automatic conflict detection and resolution UI.

Overview

This library helps resolve conflicts when two patches modify the same document. Common use case: a user has a document open on multiple devices (desktop and mobile), makes different changes on each, and both try to save at the same time.

Key Features:

  • Automatically merges non-conflicting patches
  • Detects and reports conflicts when patches modify the same fields
  • Full support for nested objects with field-level conflict detection
  • JSON Patch (RFC 6902) compatible - works with fast-json-patch and similar libraries
  • Provides a modal UI for manual conflict resolution
  • Completely agnostic to your storage/API layer
  • TypeScript support with full type definitions

Demo

The demo application shows how to merge multiple remote patches into your local changes:

Step 1: Start with local and remote patches

Patch Merge Demo

Step 2: When conflicts are detected, navigate through them one at a time

Patch Conflict Resolution

Step 3: Select a value to enable the Next button

Patch Conflict Resolution Selected

Step 4: After resolving all conflicts, see the merged result

Patch Merge Demo Merged

Installation

npm install @iodev/patch-and-resolve

For development:

git clone https://github.com/isaac76/patchAndResolve.git
cd patchAndResolve
npm install

Usage

Basic Example

import { ConflictResolver } from 'patch-and-resolve';

const resolver = new ConflictResolver();

// Two patches from different sources
const desktopPatch = { message: 'Updated on desktop', x: 100 };
const mobilePatch = { imageId: 'img123', y: 200 };

// Try to merge them
const result = resolver.mergePatches(desktopPatch, mobilePatch);

if (result.success) {
  // No conflicts - patches modified different fields
  console.log('Merged:', result.merged);
  // { message: 'Updated on desktop', x: 100, imageId: 'img123', y: 200 }
} else {
  // Conflicts detected - show modal to user
  console.log('Conflicts:', result.conflicts);
  // Manually resolve
  const resolved = resolver.resolveConflict('use-first', desktopPatch, mobilePatch);
}

React Hook

import { usePatchMerger } from 'patch-and-resolve';

function MyComponent() {
  const { mergePatches, resolveConflict, mergeResult, hasConflict } = usePatchMerger({
    onMergeSuccess: (merged) => {
      // Save the merged patch
      saveToDB(merged);
    },
    onConflict: (result) => {
      // Show conflict UI
      setShowConflictModal(true);
    }
  });

  // Merge two patches
  mergePatches(patch1, patch2);
}

React Component

The library includes a complete demo component:

import { PatchManager } from 'patch-and-resolve';

<PatchManager 
  onMergeComplete={(merged) => {
    // Your app handles saving
    await myApiClient.savePatch(merged);
  }}
/>

Advanced Features

Multiple Remote Patches

The library now supports merging an array of remote patches into your local changes. This is useful when you need to catch up with multiple versions from a server:

import { ConflictResolver } from 'patch-and-resolve';

const resolver = new ConflictResolver();

// Desktop at version 15, server has versions 16-20
const localPatch = { message: 'Local changes', version: 15 };
const remotePatches = [
  { message: 'Server v16', version: 16 },
  { message: 'Server v17', version: 17 },
  { x: 100, version: 18 },
  { y: 200, version: 19 },
  { color: 'blue', version: 20 }
];

const result = resolver.mergePatches(localPatch, remotePatches);

Remote patches are merged sequentially, and later patches can overwrite earlier ones without conflict. Conflicts only occur between your local changes and the remote patches.

Nested Object Support

The library supports merging patches with nested object structures. This method handles complex nested data while following simple conflict rules:

Conflict Rules:

  • ✅ Conflicts occur only when the exact same path is modified by both patches
  • ✅ Different paths merge automatically, even under the same parent object
  • ✅ Arrays are treated as single values (conflict if entire array differs)
import { ConflictResolver } from 'patch-and-resolve';

const resolver = new ConflictResolver();

const localPatch = {
  user: {
    name: 'Alice',
    email: '[email protected]',
  },
  title: 'My Project',
};

const remotePatch = {
  user: {
    phone: '555-1234',      // Different path: user.phone
    address: {              // Different path: user.address.city
      city: 'NYC',
    },
  },
  description: 'Updated',   // Different path: description
};

const result = resolver.mergePatches(localPatch, [remotePatch]);

// Result: Success! All paths are different, so no conflicts
// Merged: {
//   user: {
//     name: 'Alice',          // from local
//     email: 'alice@...',     // from local
//     phone: '555-1234',      // from remote
//     address: { city: 'NYC' } // from remote
//   },
//   title: 'My Project',      // from local
//   description: 'Updated'    // from remote
// }

Conflict Example:

const localPatch = {
  user: {
    name: 'Alice',  // This path: user.name
  },
};

const remotePatch = {
  user: {
    name: 'Bob',    // Same path: user.name → CONFLICT!
  },
};

const result = resolver.mergePatches(localPatch, [remotePatch]);

// Result: Conflict detected
// result.conflicts[0] = {
//   path: 'user.name',
//   localValue: 'Alice',
//   remoteValue: 'Bob',
//   remotePatchIndex: 0
// }

Resolving Conflicts:

// After user makes choices in the UI
const resolutions = [
  { conflictIndex: 0, strategy: 'use-local' },  // Keep 'Alice'
];

const resolved = resolver.applyResolutions(localPatch, [remotePatch], resolutions);
// resolved.resolved contains the final merged patch

Path Format:

  • Paths use dot notation: "user.name", "user.address.city"
  • Arrays are identified by their parent path: "tags", "pages"
  • Version fields are automatically handled and excluded from conflict detection

Conflict Navigation

When multiple conflicts are detected, the UI provides Previous/Next/Finish buttons to step through them one at a time:

import { usePatchManager } from 'patch-and-resolve';

const { mergePatches, applyResolutions } = usePatchManager({
  onMergeSuccess: (merged) => saveToDB(merged)
});

// Merge and get conflicts
mergePatches(localPatch, remotePatches);

// Resolve conflicts one by one
const resolutions = [
  { conflictIndex: 0, strategy: 'use-local' },
  { conflictIndex: 1, strategy: 'use-remote' },
  { conflictIndex: 2, strategy: 'use-local' }
];

applyResolutions(resolutions);

Custom Conflict Visualization

Use the renderConflictValue prop to customize how conflict values are displayed. This is powerful for showing semantic previews instead of raw JSON:

import { ConflictModal, ConflictValueRenderContext } from 'patch-and-resolve';

<ConflictModal
  conflicts={conflicts}
  onResolve={handleResolve}
  onCancel={handleCancel}
  renderConflictValue={(value, context: ConflictValueRenderContext) => {
    const { conflict, side, isSelected } = context;
    
    // Show a visual preview for your specific data structure
    if (value.x !== undefined && value.y !== undefined) {
      return (
        <div>
          <div 
            style={{
              position: 'relative',
              width: 200,
              height: 150,
              border: '1px solid #ccc',
              margin: '8px 0'
            }}
          >
            <div style={{
              position: 'absolute',
              left: value.x,
              top: value.y,
              padding: '4px 8px',
              background: isSelected ? '#4CAF50' : '#2196F3',
              color: 'white',
              borderRadius: '4px'
            }}>
              {value.message}
            </div>
          </div>
          
          {/* Also show the raw JSON */}
          <pre>{JSON.stringify(value, null, 2)}</pre>
        </div>
      );
    }
    
    // Default rendering for other types
    return <pre>{JSON.stringify(value, null, 2)}</pre>;
  }}
/>

The ConflictValueRenderContext provides:

  • conflict: The full conflict object with path, localValue, remoteValue
  • side: Either 'local' or 'remote'
  • isSelected: Boolean indicating if this value is currently selected

This allows you to create rich, context-aware visualizations that help users make informed decisions about which value to keep.

Integration with JSON Patch (RFC 6902)

If your application uses JSON Patch format (RFC 6902) with libraries like fast-json-patch, you can convert between JSON Patch operations and the simple diff format used by this library:

import { jsonPatchToDiff, diffToJsonPatch, ConflictResolver } from 'patch-and-resolve';

// Backend sends JSON Patch operations
const localOps = [
  { op: "replace", path: "/message", value: "Local edit" },
  { op: "add", path: "/x", value: 100 }
];

const remoteOps = [
  { op: "replace", path: "/message", value: "Remote edit" },
  { op: "add", path: "/y", value: 200 }
];

// Convert to diffs for conflict resolution
const localDiff = jsonPatchToDiff(localOps);   // { message: "Local edit", x: 100 }
const remoteDiff = jsonPatchToDiff(remoteOps); // { message: "Remote edit", y: 200 }

// Merge with conflict detection
const resolver = new ConflictResolver();
const result = resolver.mergePatches(localDiff, [remoteDiff]);

if (result.success) {
  // Convert back to JSON Patch if needed
  const patchOps = diffToJsonPatch(result.merged);
  // Result: [
  //   { op: "replace", path: "/message", value: "Local edit" },
  //   { op: "replace", path: "/x", value: 100 },
  //   { op: "replace", path: "/y", value: 200 }
  // ]
  
  // Apply to your document with fast-json-patch
  applyPatch(document, patchOps);
} else {
  // Show conflict UI to user
  // After user resolves conflicts:
  const resolved = resolver.applyResolutions(localDiff, [remoteDiff], resolutions);
  
  // Convert resolved patch back to JSON Patch format
  const resolvedOps = diffToJsonPatch(resolved.resolved);
  // Send back to server or apply locally
  applyPatch(document, resolvedOps);
}

Supported JSON Patch Operations:

  • add and replace - Converted to field updates (supports nested paths like /user/name)
  • remove, move, copy, test - Ignored (don't map to simple diffs)
  • ✅ Nested paths fully supported (e.g., /user/address/city)

This makes the library compatible with standard JSON Patch workflows while providing an intuitive UI for conflict resolution.

Development

npm run dev      # Start demo app
npm test         # Run unit tests
npm run test:e2e # Run end-to-end tests in headless Chromium
npm run test:all # Run all tests (unit + e2e)
npm run build    # Build library

End-to-End Testing

The project includes comprehensive Playwright tests that verify the conflict modal works correctly in a real browser environment:

npm run test:e2e           # Run e2e tests (headless)
npm run test:e2e:headed    # Run e2e tests with visible browser
npm run test:e2e:ui        # Run e2e tests with Playwright UI
npx playwright show-report # View last test report

The e2e tests cover:

  • ✓ Merging non-conflicting patches
  • ✓ Detecting conflicts
  • ✓ Displaying the conflict modal
  • ✓ Resolving conflicts with user choice (use patch 1 vs patch 2)
  • ✓ Version number handling (automatically uses higher version)
  • ✓ Canceling conflict resolution

Commit Convention

This project uses Conventional Commits:

  • feat: - New feature (triggers minor version bump)
  • fix: - Bug fix (triggers patch version bump)
  • feat!: or fix!: - Breaking change (triggers major version bump)
  • chore:, docs:, style:, refactor:, test: - No version bump

Versioning

Versions are automatically managed by semantic-release based on commit messages.