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

@directive-run/optimistic

v0.1.0

Published

Resolver-scope optimistic update + automatic rollback for Directive.

Readme

@directive-run/optimistic

Resolver-scope optimistic update + automatic rollback for Directive.

npm install @directive-run/optimistic

What it solves

The "snapshot before, restore on catch, rethrow" pattern that recurred ~3 times during the Minglingo migration. Manual version:

submit: async ({ payload, facts }) => {
  const previousValues = [...facts.values];
  facts.values = optimisticGuess(payload);
  try {
    facts.values = await deps.submit(payload);
  } catch (err) {
    facts.values = previousValues;
    throw err;
  }
}

With this package:

import { withOptimistic } from '@directive-run/optimistic';

interface FormFacts { values: FormValues; /* ... */ }

submit: withOptimistic<FormFacts>(['values'])(async ({ payload, facts }) => {
  facts.values = optimisticGuess(payload);
  facts.values = await deps.submit(payload);
}),

The single-arg outer call (withOptimistic<F>(keys)) is what makes the keys array type-check against keyof F — a typo like ['valuess'] becomes a compile error. The inner call accepts your mutator handler unchanged.

If the inner handler throws, facts.values snaps back to its pre-handler value and the throw propagates upward.

Scope: deliberately tight

This package operates within a single resolver invocation. It is:

  • ✅ A "try / restore on catch" macro
  • ❌ NOT a system-wide transaction
  • ❌ NOT a cross-module rollback
  • ❌ NOT a replay-undo

If you need cross-module rollback, you're describing a distributed transaction — not what this is. The MIGRATION_FEEDBACK item this addresses (#19) is explicitly resolver-scope.

API

createSnapshot(facts, keys) → restore

Capture the current values of selected keys; return a restore function. Use inside a try/catch:

const restore = createSnapshot(facts, ['values', 'lastSavedAt']);
try {
  facts.values = optimisticGuess(payload);
  facts.values = await deps.submit(payload);
  facts.lastSavedAt = Date.now();
} catch (err) {
  restore();
  throw err;
}

restore() can be called multiple times — each call writes the captured snapshot back. Useful if your handler has multiple mid-execution decision points.

withOptimistic<F>(keys)(handler) → wrappedHandler

Higher-order helper that wraps a handler with snapshot + automatic rollback. The two-call signature lets TypeScript infer the keys array against keyof F — typos are compile errors. Composes with @directive-run/mutator:

import { defineMutator } from '@directive-run/mutator';
import { withOptimistic } from '@directive-run/optimistic';

const mut = defineMutator<FormMutations, FormFacts>({
  submit: withOptimistic<FormFacts>(['values'])(
    async ({ payload, facts }) => {
      facts.values = optimisticGuess(payload);
      facts.values = await deps.submit(payload);
    },
  ),
  cancel: ({ facts }) => { facts.values = []; },
});

The wrapper:

  1. Snapshots facts.values at handler entry.
  2. Runs the inner handler.
  3. On uncaught throw: restores facts.values, then rethrows.
  4. On success: leaves the new values in place.

Cloning semantics

Snapshots are deep-cloned via structuredClone (Node 17+, modern browsers — Directive's documented engine baseline). There is no JSON-roundtrip fallback: that path silently dropped functions, symbols, undefined values, and was the exact silent-corruption hole optimistic rollback exists to prevent. structuredClone covers what the JSON-roundtrip-fact contract allows, so falling back to JSON adds zero recoverable cases and one corruption surface.

If a fact contains a function, DOM node, non-cloneable instance, or some other shape structuredClone rejects, the snapshot throws an OptimisticCloneError with the offending key — making the violation loud rather than silently corrupting the rolled-back state. Convert at the boundary (e.g. Date → number, BigInt → string) before assigning to facts.

import { OptimisticCloneError } from '@directive-run/optimistic';

try {
  const restore = createSnapshot(facts, ['weirdField']);
  // ...
} catch (err) {
  if (err instanceof OptimisticCloneError) {
    // err.key is the fact key that couldn't be cloned
  }
}

Composition with mutator

When using @directive-run/mutator and a handler throws, the mutator captures the error on pendingMutation.error and stops the constraint from re-firing. With withOptimistic, the rollback runs before the mutator captures the error — so by the time the UI renders the error, the facts are already back to their pre-mutation state.

This is the right ordering for optimistic UI:

  • Optimistic write happens immediately (good UX)
  • Rollback happens before the error surfaces (no torn state)
  • Error message is preserved on pendingMutation.error (UI can show)

When to skip the helper

  • Synchronous handlers. No async work means no in-flight state to protect from. Just write the value.
  • Single fact mutation that's idempotent. If the only thing the handler writes is the result of an awaited call (facts.x = await fn()) and there's no optimistic guess, there's nothing to roll back.
  • Multi-fact reads/writes that aren't related. Snapshot only the facts you actually optimistically wrote.

See also

License

MIT OR Apache-2.0