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

umkehr

v0.1.0

Published

Typed JSON patch updates with undo/redo history and optional React bindings.

Readme

Umkehr

The no-nonsense undo/redo library for json state.

Pronounced "oom-care". from the german for "a turning back", "changing one's ways", or "repentance".

What does it do? It allows you to make surgical edits to a large state document, handles undo/redo and arbitrary jumps around the history tree, with extremely handy type-safe update builders.

Inspired by the JSON Patch standard and CRDTs, though different from both.

Install

npm install umkehr
pnpm add umkehr
bun add umkehr

Entry Points

| Import | Use | | --- | --- | | umkehr | Core patch builders, patch application, and history helpers | | umkehr/react | React contexts, hooks, and updater types |

React is an optional peer dependency. Non-React users should import from umkehr.

Examples

Small runnable examples live in examples:

| Example | Shows | | --- | --- | | examples/basic | Draft patches, realized changes, applying and inverting patches | | examples/history | Dispatch, undo, redo, branching, and jump | | examples/react | React context setup, useValue, preview updates, undo, and redo | | examples/tagged-union | $variant with direct and callback forms |

Quick Start

import {createPatchBuilder, resolveAndApply} from 'umkehr';

type State = {
    title: string;
    tags: string[];
};

const state: State = {
    title: 'Draft',
    tags: ['local'],
};

const $ = createPatchBuilder<State>();

const {current, changes} = resolveAndApply(
    state,
    [$.title('Published'), $.tags.$push('featured')],
    undefined,
    'type',
    Object.is,
);

current.title; // "Published"
current.tags; // ["local", "featured"]
changes; // realized, invertible patch operations

Core Terms

DraftPatch

DraftPatch<T> is the authoring form of a patch operation. It records what the caller wants to do, but it may omit data that can only be known by reading the current state.

For example:

| Draft operation | State-dependent realization | | --- | --- | | replace | Adds the previous value so the patch can be inverted | | remove | Adds the removed value so the patch can be inverted | | push | Resolves to an add at the current array length | | add, move, reorder | Already contain enough information to apply |

Patch

Patch<T> is the realized, invertible form of a patch operation. These are the operations to store in history because they can be applied and inverted later.

Most callers should use resolveAndApply, dispatch, createStateContext, or createHistoryContext, which realize drafts for you.

Path

Path is an array of structured path segments:

[
    {type: 'key', key: 'items'},
    {type: 'key', key: 0},
];

Umkehr patch objects are inspired by JSON Patch, but they are not JSON Patch compatible. Paths are structured arrays, not JSON Pointer strings like "/items/0", and tagged-union path segments are Umkehr-specific.

Building Draft Operations

Use createPatchBuilder<T>() to create typed draft operations without applying them:

import {createPatchBuilder, type DraftPatch} from 'umkehr';

type State = {
    title: string;
    tags: string[];
    settings?: {
        archived: boolean;
    };
};

const $ = createPatchBuilder<State>();

const rename: DraftPatch<State> = $.title('New title');
const addTag: DraftPatch<State> = $.tags.$push('featured');
const removeSettings: DraftPatch<State> = $.settings.$remove();

Every property access extends the path. Calling a node is shorthand for replacing that path:

$.title('New title');
$.settings.archived(true);

When you pass a function instead of a value, Umkehr treats it as a nested update. The function gets the current value at that path and an up helper rooted at the same path:

$.settings((settings, up) => up.archived(!settings?.archived));

up looks like the normal patch builder, but it only creates draft operations. It does not dispatch or apply them by itself. Return one draft or an array of drafts, and Umkehr will rebase them onto the outer path and apply them together as a single update (so that they "undo" and "redo" together).

Use createPatchBuilder('kind') when your tagged unions use a discriminant other than 'type'.

Use createPatchBuilderWithContext when nested $update callbacks need caller-provided context:

import {createPatchBuilderWithContext} from 'umkehr';

const $ = createPatchBuilderWithContext<State, {source: string}>('type', {source: 'example'});

Use createPatchDispatcher when you want the same builder API to immediately call an application function:

import {createPatchDispatcher} from 'umkehr';

const $ = createPatchDispatcher<State, undefined, 'type'>(
    (draft, timing) => dispatch(draft, timing),
    undefined,
    'type',
);

Builder Methods

| Method | Available on | Result | | --- | --- | --- | | some.path.$replace(value) | Any path | Draft replace | | some.path(value) | Any path | Alias for .$replace | | some.path.$update((value, up) => draft | draft[]) | Any path | Nested draft update based on current value. Can be used to combine multiple changes into a single "history item" | | some.path((value, up) => draft) | Any path | Alias for .$update | | some.path.$add(value) | Any path | Draft add | | some.path.$remove() | Any path | Draft remove | | some.path.$push(value) | Arrays | Draft push, realized as an add at the current array length | | some.path.$move(from, to) | Arrays and objects | Draft move within the current path | | some.path.$reorder(indices) | Arrays | Realized reorder using old indices in their new order | | some.path.$variant(tag) | Tagged unions | Refines the updater to one union arm | | some.path.$variant(value, handlers) | Tagged unions | Runs the handler for the active union arm |

$reorder([2, 0, 1]) changes ['a', 'b', 'c'] into ['c', 'a', 'b'].

Applying Drafts

resolveAndApply realizes one or more draft operations, applies them in order, and returns the new state plus realized patch operations:

import {createPatchBuilder, resolveAndApply} from 'umkehr';

const $ = createPatchBuilder<State>();

const {current, changes} = resolveAndApply(
    state,
    [$.title('New title'), $.tags.$push('featured')],
    undefined,
    'type',
    Object.is,
);

History

Use blankHistory(initialState) to create a history tree:

import {blankHistory, createPatchBuilder, dispatch} from 'umkehr';

const $ = createPatchBuilder<State>();
const history = blankHistory(initialState);
const nextHistory = dispatch(history, [$.title('New title')]);
const undone = dispatch(nextHistory, {op: 'undo'});
const redone = dispatch(undone, {op: 'redo'});

The simple dispatch overload uses the default 'type' discriminant, no builder context, and fast-deep-equal. The lower-level overload accepts a context value, tag key, equality function, and ID generator.

History is a tree. If you undo and then dispatch a new change, the new node becomes another child of the current history node rather than deleting the old branch.

React Quick Start

import {blankHistory} from 'umkehr';
import {createHistoryContext, useValue} from 'umkehr/react';

type State = {
    title: string;
};

const [ProvideState, useStateContext] = createHistoryContext<State, never>('type');

export function App() {
    return (
        <ProvideState initial={blankHistory<State>({title: 'Draft'})}>
            <TitleEditor />
        </ProvideState>
    );
}

function TitleEditor() {
    const ctx = useStateContext();
    const title = useValue(ctx.$.title);

    return (
        <>
            <input value={title} onChange={(event) => ctx.$.title(event.target.value)} />
            <button onClick={() => ctx.undo()} disabled={!ctx.canUndo()}>
                Undo
            </button>
            <button onClick={() => ctx.redo()} disabled={!ctx.canRedo()}>
                Redo
            </button>
        </>
    );
}

The history context exposes:

| API | Use | | --- | --- | | ctx.$ | Root patch builder for the current state | | ctx.latest() | Current state value | | ctx.undo() / ctx.redo() | History navigation | | ctx.canUndo() / ctx.canRedo() | History availability | | ctx.previewJump(id) | Temporarily previews the state at another history node | | ctx.clearPreview() | Clears temporary preview state without committing it | | ctx.useHistory() | React hook for subscribing to history changes | | ctx.dispatch(...) | Lower-level dispatch for draft ops or history commands |

Use useValue(ctx.$.path) to read and subscribe to a specific path. Components re-render when that path, an ancestor, or a descendant is notified:

const title = useValue(ctx.$.title);
const firstTag = useValue(ctx.$.tags[0]);

useValue also accepts a selector and equality function for derived values:

const parity = useValue(
    ctx.$.count,
    (count) => ({parity: count % 2}),
    true,
    (a, b) => a.parity === b.parity,
);

The default selector returns the path value itself, and the default equality function is fast-deep-equal.

For state without undo/redo, use createStateContext:

import {createStateContext, useValue} from 'umkehr/react';

const [ProvideState, useStateContext] = createStateContext<State>('type');

The non-history context exposes ctx.$, ctx.latest(), ctx.clearPreview(), and ctx.dispatch(...).

Preview Updates

Most updater methods accept an optional timing argument:

ctx.$.title('Preview title', 'preview');
ctx.$.title('Committed title');

Preview changes are applied to temporary state and notify path subscribers, but they are cleared before the next committed update.

This is to enable interactions such as "scrubbing through a color picker" where you want the update the UI with the currently-hovered-value, but you don't want to spam history with these temporary updates or persist them. The next "non-preview" update is based on the state before any preview updates were processed, and clears all preview updates.

Note that preview updates are queued via requestAnimationFrame, whereas non-preview updates are processed immediately.

Tagged Unions

Pass the discriminant key to createPatchBuilder, createStateContext, or createHistoryContext. The default is 'type'.

type Item = {type: 'shape'; radius: number} | {type: 'text'; text: string};

ctx.$.item.$variant('shape').radius(10);

There is also a callback form for code that has the current value:

ctx.$.item.$variant(item, {
    shape: (value, up) => up.radius(value.radius + 1),
    text: (value, up) => up.text(`${value.text}!`),
});

Supported Data Model

Umkehr is intended for plain JSON-like data:

| Area | Support | | --- | --- | | Objects and arrays | Supported; changed ancestors are cloned | | Primitive values | Supported as leaves and root values | | undefined | Treated as absence by draft realization for add/remove decisions | | Equality | Defaults to fast-deep-equal in history and React helpers; lower-level APIs accept a custom equality function | | Paths | Structured PathSegment[]; no JSON Pointer strings | | Tagged unions | Supported through Umkehr-specific tag path segments | | CRDT behavior | Not supported | | Arbitrary object diffing | Not supported |

Limitations

  • Umkehr patches are not JSON Patch compatible.
  • copy is not part of the public patch operation set.
  • Preview updates are temporary React-context state; they are cleared before the next committed update.
  • Array paths use numeric indices. Realized array operations are tied to the array state they were realized against.
  • Persisted patch history assumes compatible application state shape. If your schema changes, you need to migrate stored history or start a new history root.