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

simple-object-patcher

v0.1.0

Published

Simple RFC6902 object patcher, with options

Readme

simple-object-patcher

RFC6902 (JSON Patch) apply and diff, with the options a live-state application tends to need: replacing the document at its root, patching destructively or into a clone, creating the parents an add needs, and an error that says which part of the patch failed and whether anything was written before it did.

It is a thin layer over rfc6902: that package does the patching, this one wraps it — and keeps its array diff from falling over on long arrays.

Installation

npm install simple-object-patcher

ESM only, no build step.

Usage

import { applyPatch, createPatch, PatchError } from "simple-object-patcher";

const state = { headline: "old" };

applyPatch(state, [{ op: "replace", path: "/headline", value: "new" }]);
// state is now { headline: "new" }

createPatch({ a: 1 }, { a: 2 });
// [{ op: "replace", path: "/a", value: 2 }]

applyPatch(document, patch, options?)

Applies patch (an array of RFC6902 operations) and returns the patched document. By default it mutates document in place and returns that same object.

| Option | Default | Effect | | --- | --- | --- | | nest | true | Allows the patch to replace the document at its root, with path: "" | | immutable | false | Clones the document and patches the clone, leaving the original alone | | create | true | Creates the missing parent objects an add needs |

Parts are applied one at a time, in order, and the first failure throws. Parts before the failure have already been applied; parts after it are not attempted.

nest

RFC6902 cannot replace a document at its root, because there is no parent to assign to. With nest the document is put inside a wrapper, the patch paths are rewritten against it, and the wrapper's contents are returned — so a whole-document replace works:

applyPatch({ a: 1 }, [{ op: "replace", path: "", value: { b: 2 } }]);
// { b: 2 }

A root replace necessarily returns a different object than the one passed in, since it is a new value rather than a mutation. Every other operation still mutates in place and returns the original reference. Set nest: false to patch the document directly and reject root-level operations.

immutable

const before = { a: 1 };
const after = applyPatch(before, [{ op: "add", path: "/b", value: 2 }], { immutable: true });
// before is still { a: 1 }, after is { a: 1, b: 2 }

A null or undefined document is treated as {}, so a state that does not exist yet can be patched into being rather than throwing:

applyPatch(undefined, [{ op: "add", path: "/a", value: 1 }], { immutable: true });
// { a: 1 }

create

RFC6902 requires the parent of an add to exist. With create, the parents are built first, so a nested value can be set without walking the object into place by hand:

applyPatch({}, [{ op: "add", path: "/a/b/c", value: 1 }]);
// { a: { b: { c: 1 } } }

applyPatch({}, [{ op: "add", path: "/a/b/c", value: 1 }], { create: false });
// throws PatchError

createPatch(document1, document2, options?)

Returns the patch that turns document1 into document2, or [] if they are already equal.

const patch = createPatch(before, after);
applyPatch(before, patch, { immutable: true }); // deep-equals after

| Option | Default | Effect | | --- | --- | --- | | positionalArrayLength | 100 | Array length above which elements are matched by position rather than by edit distance |

positionalArrayLength

rfc6902 diffs arrays with an edit-distance search, which finds the shortest patch: an element inserted at the front is described as the one insertion it was, rather than as a change at every position after it. That search compares every element against every other, though, and recurses as it goes — so on a long array it takes time and memory quadratic in the length, and dies outright somewhere past a couple of thousand elements, by call stack or by heap depending on how big the elements are.

Above positionalArrayLength, elements are instead matched up by position — element 0 against element 0, and so on, with a shorter side made up by appending or trimming the tail. That is a single pass, so cost stays proportional to the data:

| Elements | Edit distance | By position | | --- | --- | --- | | 500 | 3.4 s | 4 ms | | 1,000 | 21.6 s | 4 ms | | 2,000 | out of memory | 6 ms | | 100,000 | out of memory | 155 ms |

The trade is that position matching does not always give the shortest patch. Editing a cell in a 50,000-row table still gives one operation, but inserting a row at the front gives an operation per row after it. If you are choosing between sending a patch and sending the whole value, compare the two sizes and send the smaller.

Both sides are measured, so a 100-element array growing to 101 crosses a threshold of 100. Set positionalArrayLength: Infinity to always use the edit-distance search, whatever the length.

createPatch(before, after, { positionalArrayLength: Infinity });

PatchError

Thrown when a part of the patch cannot be applied. Beyond message, it carries:

  • patch — the single operation that failed. With nest on (the default) its path is the rewritten one, prefixed with the wrapper key.
  • mutated — whether a non-test operation had been attempted before the throw, meaning the document may already have been changed. A patch that fails on a leading test reports false.
try {
    applyPatch(state, patch);
}
catch(error) {
    if(error instanceof PatchError && error.mutated)
        // the document is in a partly-patched state; reload it rather than trusting it
}

mutated is deliberately pessimistic: it reports that a write was attempted, not that it succeeded.

Tests

npm test

License

MIT