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

@sharma/undoable

v0.1.1

Published

A runtime for optimistic, undoable mutations. One primitive, no UI.

Readme

undoable

npm bundle size dependencies types license

A runtime that owns the mechanism of optimistic, undoable mutations. Your app owns the data and the presentation. One primitive, no UI, no dependencies.

Works with React, Vue, Angular and Svelte — see docs/frameworks.md. See docs/spec.md for the design contract and docs/plan.md for how it was built.


Install

npm install @sharma/undoable
import { defineAction, runAction, flushPending, configure } from '@sharma/undoable';

Or skip the build step entirely — one script tag is a complete integration, because binding is delegated from document and needs no init call:

<script src="https://cdn.jsdelivr.net/npm/@sharma/[email protected]/dist/undoable.global.js"></script>
<script>
  undoable.defineAction('archiveItem', { /* ... */ });
</script>

The /dist/undoable.global.js path is required — the bare package URL resolves to main, which is the CommonJS build and throws module is not defined in a browser.

The package is scoped because the unscoped undoable name is held on npm by an abandoned 2018 placeholder. The import binding and the <script> global are both still undoable.

Use

Define the action once. apply mutates local state synchronously and returns its inverse; commit persists it.

undoable.defineAction('archiveItem', {
  apply: (id) => {
    const index = items.findIndex((i) => i.id === id);
    const [item] = items.splice(index, 1);
    render();
    return () => {
      items.splice(index, 0, item);
      render();
    };
  },
  commit: (id) => fetch(`/items/${id}/archive`, { method: 'POST' }).then(assertOk),
});

Bind it in markup. Binding is delegated from document, so dynamically inserted rows work with no registration step:

<li data-undoable="archiveItem"
    data-undoable-arg="42"
    data-undoable-label="Item archived">
  <button data-undoable-trigger>Archive</button>
</li>

data-undoable-arg is passed through as a raw string — never parsed or coerced. For structured arguments, use the programmatic path:

undoable.runAction('reorderItem', { id, from, to }, { trigger: dragHandle });

The one thing you must do

SPA route changes are invisible to the runtime. Call flushPending() in your router's navigation hook, or a pending change will be silently dropped when the view unmounts:

router.beforeEach(() => undoable.flushPending());

pagehide and tab-hide are already handled.

Showing an undo affordance

The runtime ships no UI. Everything visible is a listener:

document.addEventListener('undoable:pending', (e) => {
  const { label, undo, expiresAt } = e.detail;
  showToast(label, undo, expiresAt);
});

If nothing listens, actions still work — they are just silent visually. The aria-live announcement still fires.

| Event | detail | |---|---| | undoable:pending | { name, arg, label, undo(), expiresAt } | | undoable:committed | { name, arg } | | undoable:reverted | { name, arg } | | undoable:failed | { name, arg, error, reverted } | | undoable:desync | { name, arg, error } |

undoable:desync is not an error to swallow

At most one action is pending at a time; starting a new one flushes the previous into committing. A flushed action's revert is stale — calling it would discard the newer change — so if its commit rejects, the runtime emits desync instead of reverting. Handle it by refetching. A sustained desync rate means the flush model is wrong for your app.

API

defineAction<A>(name: string, def: { apply: (a: A) => Revert; commit: (a: A) => Promise<void> }): void
runAction<A>(name: string, arg: A, opts?: { trigger?: Element }): void
flushPending(): void
configure(opts: { window?: number }): void   // default 5000ms, global
getMetrics(): Metrics

ActionDef accepts exactly apply and commit; anything else throws. configure has exactly one option. Both constraints are deliberate — when a new requirement appears, the answer is an event listener or a second named action, never a new key.

Try it

npm install
npm run example        # harness at http://localhost:5173
npm test               # the 21-row acceptance suite (jsdom)
npm run probe          # drives the harness headlessly (jsdom)
npm run probe:browser  # drives it through real Chromium (Playwright)

probe:browser needs a one-off npx playwright install chromium. It exists because jsdom cannot answer questions about focus and the accessibility tree — it was what established that Chromium blurs an element when it is disabled, which is the defect behind F3.

The harness runs all three action shapes with adjustable commit latency, failure rate, undo window, and render strategy.

examples/FINDINGS.md is worth reading before changing anything: it records five places where the original spec produced the wrong behaviour — three of them focus defects that the first test matrix passed straight through — what each fix was, and the one cost that could not be avoided.