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

@agentvibes/kit

v0.2.0

Published

House runtime primitives for MobX apps: Resource, Query, usePageStore, QueryView, runtime diagnostics, cn().

Readme

@agentvibes/kit

House runtime primitives for MobX applications. Four independent entry points, no runtime dependencies of its own — everything is a peer.

pnpm add @agentvibes/kit

| Entry point | What it gives you | Peers it needs | | --- | --- | --- | | @agentvibes/kit/resource | Resource<T, E>, Query<T, E>, QueryFamily<K, T>, Mutation<E>, PagedQuery<T, C, E> | mobx, ts-pattern | | @agentvibes/kit/react | usePageStore, QueryView, ShowWhen | react, mobx-react-lite, ts-pattern | | @agentvibes/kit/diagnostics | warnDegraded, warnNotImplemented, warnUnexpected | none | | @agentvibes/kit/cn | cn() | clsx, tailwind-merge |

ESM only. Every peer except mobx and ts-pattern is optional — install what the entry points you import actually need.

/resource

Resource<T, E>

The union every consumer is forced to render completely.

import { Resource } from "@agentvibes/kit/resource"

type Resource<T, E = Error> =
  | { status: "idle" }
  | { status: "loading"; progress?: number }  // progress absent = indeterminate
  | { status: "ready"; value: T }
  | { status: "error"; error: E }

store.exportState = Resource.loading(0.4)

Use it for process-shaped work — renders, exports, file sync — where there is progress and no such thing as revalidation. For fetch-shaped work, use Query, which projects onto Resource through .resource.

Query<T, E>

Stale-while-revalidate as one observable cell: throttling, in-flight dedup, invalidate(), focus refetch and interval polling, so a store never hand-rolls a loading boolean and a TTL guard again.

import { Query, createQuery } from "@agentvibes/kit/resource"

class BillingStore {
  quota = createQuery(() => api.fetchQuota(), {
    staleTimeMs: 30_000,
    refetchIntervalMs: 60_000,
    enabled: () => this.root.auth.isSignedIn,   // stays `idle` until it is
  })

  constructor(private root: RootStore) {
    makeAutoObservable(this)
    this.quota.enableWindowFocusRefetch()
  }

  dispose() {
    this.quota.dispose()
  }
}

The state has two independent axes, because one axis cannot express stale-while-revalidate:

type QueryState<T, E = Error> =
  | { status: "idle" }
  | { status: "pending";     fetchActivity: "idle" | "fetching" }
  | { status: "success";     data: T; dataUpdatedAt: number; fetchActivity: … }
  | { status: "error";       error: E; fetchActivity: … }
  | { status: "stale-error"; data: T; dataUpdatedAt: number; error: E; fetchActivity: … }

success + fetching is a revalidation; stale-error is "the refetch failed but the old data is still worth showing".

Readables: data, hasData, error, isLoading (first paint only), isFetching (any request in flight), isStale, enabled.

Projections, both derived from the same mapping so they cannot drift:

  • .viewState — the four render arms (idle / loading / error / ready), what QueryView matches on.
  • .resource — a Resource<T, E>. pending reads as loading; stale-error reads as ready with the error still on .error.

Options: fetcher, throttleMs (default 2000), staleTimeMs (default 30000), refetchIntervalMs, enabled, normalizeError, debugLabel. normalizeError is required only when E is not Error; the type enforces it.

Subscriptions are opt-in and each returns a disposer: enableWindowFocusRefetch(), enableRefetchInterval(). dispose() releases both.

fetch() is throttled; invalidate() skips the throttle. Both deduplicate — concurrent callers share one request and one promise — and both respect enabled. Neither ever rejects: a failed fetcher lands in the state.

QueryFamily<K, T, E>

One factory, many keys, memoized.

const months = new QueryFamily<string, Day[]>((month) =>
  createQuery(() => api.fetchMonth(month)),
)

months.get("2026-08").fetch()
months.dispose()   // disposes every entry

The cache is deliberately not observable — it is memoization, not state — so family.get(key) is safe to call during render. Also: peek, has, keys, size, remove, invalidateAll.

Mutation<E>

The write half of the four-state rule: one instance per store, one observable state per key, so two controls never share a spinner.

import { createMutation, takeOut, putBack } from "@agentvibes/kit/resource"

class GalleryStore {
  mutations = createMutation()

  deleteMedia(id: string) {
    return this.mutations.run(
      `deleteMedia:${id}`,
      () => api.deleteMedia(id),
      {
        optimistic: () => {
          const taken = takeOut(this.media, (m) => m.id, id)
          this.media = taken.list
          return () => {
            this.media = putBack(this.media, taken.removal, (m) => m.id).list
          }
        },
      },
    )
  }
}

Keys are the caller's and should be stable: "createGallery", `deleteMedia:${id}`. Read one with mutations.get(key), which returns {status:"idle"|"saving"|"ok"|"error"} — keys never run read as idle.

run() returns a three-armed result:

type MutationResult<T, E = Error> =
  | { status: "ok"; data: T }
  | { status: "error"; error: E }
  | { status: "busy" }     // already in flight for this key; nothing was sent

Concurrency: a second run() on a key already in flight sends nothing and returns busy. That is the conservative default — a double-submitted write is usually a bug, not a queue. Queueing was the alternative; it belongs in the caller, where the ordering rules are actually known.

optimistic applies an edit immediately and returns its undo. The rollback runs if and only if the request fails — never on success, and never on busy, where nothing was applied.

reset(key) returns a key to idle, backing the "clear" affordance (dismiss an error, drop a lingering checkmark) and freeing accumulated per-item keys. It clears the badge only: it does not cancel a request, and the key stays guarded until that request lands, so a reset cannot open a double-submit hole. isInFlight(key) reads the real thing.

takeOut / putBack

Optimistic list edits, with the two guards that were bugs before they were guards. takeOut returns a Removal<T> — a closed union, because "the item was not in the list" is a state, not a missing value. putBack clamps the remembered index (the list can have shrunk while the request was in flight) and refuses to insert a key the list already contains (a route transition can replace the list with one the server still reports the item in; splicing on top mounted the same id twice). Callers that track a count read the returned inserted rather than assuming the restore happened.

PagedQuery<T, C, E>

A cursor-paged list that accumulates.

import { createPagedQuery } from "@agentvibes/kit/resource"

const activity = createPagedQuery<ActivityEvent, string>({
  fetchPage: (cursor) => api.fetchActivity({ afterCursor: cursor }),
  keyOf: (event) => event.id,
})

await activity.load()       // first page
await activity.loadMore()   // append the next one
await activity.revalidate() // refresh the pages already loaded

A page is reported as { items, nextCursor: C | null, total?: number }. nextCursor: null is the single termination signal, and synthesising it is the adapter's job — the three real shapes in the park each report the end differently:

| Server shape | Adapter writes | | --- | --- | | opaque keyset cursor | nextCursor already arrives as string \| null | | page number plus total | page * limit >= total ? null : page + 1 | | offset plus hasMore | hasMore ? nextOffset : null |

Three independent axes, because a UI renders them in different places: state (idle/loading/ready/error — does the list paint at all), pageLoad (idle/loading/error — is the scroll sentinel working), refresh (same three — is the pull-to-refresh spinner turning). Plus items, hasMore, isEmpty, total, pageCount.

loadMore() no-ops unless the list is ready, no page is in flight, and a next cursor exists, so a scroll sentinel can call it on every intersection. A failed page leaves the cursor untouched, so a retry re-requests the page that failed rather than skipping it. Appends drop keys already loaded — an overlapping page must never reach React as two children with the same key.

revalidate() re-walks the pages already loaded, following the cursors the server hands back, and swaps the whole list in at the end. The walk builds into a scratch list and commits only if every page lands: a failure part-way leaves the current items exactly as they were and reports the error on refresh, the same choice Query makes with stale-error. Note that with a keyset cursor, rows inserted since the first walk shift page boundaries, so a re-walk can return slightly different membership — inherent to keyset pagination, not a defect.

/react

usePageStore(factory, deps)

Owns a page-scoped store for as long as the screen is mounted. Context-free by design: the factory closes over what the store needs, and the store travels downwards through props.

function GalleryScreen({ slug }: { slug: string }) {
  const store = usePageStore(() => new GalleryPageStore(rootStore, slug), [slug])
  return <GalleryGrid store={store} />
}
  • The factory runs once per distinct deps — held in a ref, not useMemo, because React's double render calls a useMemo factory twice and leaks the second store.
  • store.dispose() is called on unmount, if the store has one.
  • Disposal is ref-counted and deferred to a microtask, which is what makes it StrictMode-safe: mount → unmount → mount happens inside one task, so the store the component is holding is never disposed underneath it.
  • Changing deps disposes the old store and builds a new one.

QueryView

The canonical implementation of the four-state rule: every async state gets a distinct rendering, and the compiler will not let you skip one.

<QueryView
  query={store.quota}
  idle={() => <SignInPrompt />}                       // optional; defaults to loading
  loading={() => <Spinner />}
  error={({ error, retry }) => <ErrorBanner message={error.message} onRetry={retry} />}
  ready={({ value, revalidating, stale, error }) => (
    <QuotaPanel quota={value} shimmer={revalidating} warning={error?.message} />
  )}
/>

The body runs inside <Observer>, so a state change re-renders this view alone and never the parent.

ShowWhen

<ShowWhen when={store.hasUnsaved} fallback={<Saved />}>
  <UnsavedBadge />
</ShowWhen>

Both branches are named, and a numeric zero cannot leak into the output the way {count && <X/>} lets it. Not a reactive boundary — when is evaluated by the caller.

/diagnostics

The replacement for // TODO. A TODO never runs; these fire the first time the path is actually hit, dedup'd per category, greppable, and routable to a debug panel.

import { warnDegraded, warnNotImplemented, setDiagnosticHook } from "@agentvibes/kit/diagnostics"

warnDegraded("Chart.scale", "linear interpolation; cubic deferred")
warnNotImplemented("Export.pdf", "exists upstream, not ported yet")

setDiagnosticHook((event) => debugPanel.push(event))

warnDegraded and warnNotImplemented fire once per category; warnUnexpected(category, detail, data?) always fires, because a contradicted assumption matters every time. resetDiagnostics() clears the dedup set for tests.

/cn

import { cn } from "@agentvibes/kit/cn"

cn("p-2", isActive && "bg-blue-500", className)

clsx plus tailwind-merge, so the last conflicting utility wins and a caller's className can actually override.

Development

pnpm check     # typecheck + biome + vitest + build + publint + attw

CI runs the same command on every push and pull request.

Releasing

Every release goes through changesets — pnpm changeset to describe a change, then pnpm changeset:version to apply the accumulated changesets to the version and changelog, then pnpm release to publish.

The first publish goes through changeset version too; the version in package.json before that is pre-release scaffolding, not a version that was ever published. So the first published version is the one changeset version computes, and no version is skipped by starting above 0.0.0.

License

MIT