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

@p9v/core

v0.4.0

Published

TanStack Query prefetch integrity with query contracts, exact-key enforcement, streaming, and CI waterfall budgets.

Downloads

779

Readme

p9v — TanStack Query Prefetch Integrity

English | 한국어

TanStack Query knows how to prefetch. p9v makes sure you didn't forget.

p9v is a correctness layer for TanStack Query applications. It colocates query requirements with components, verifies that routes start those exact queries, and turns accidental browser cache misses into actionable development errors and CI failures. TanStack Query remains responsible for fetching, caching, Suspense, dehydration, and hydration.

  • Use ordinary queryOptions, useQuery, and useSuspenseQuery
  • Catch missing prefetches by contract name at compile time and exact key at runtime
  • Start blocking and streaming queries together in Next.js App Router
  • Separate prefetched, intentionally deferred, and unexpected requests in Devtools
  • Enforce route budgets with p9v analyze
  • Keep the v0 Resource and fragment APIs when they fit better

Why p9v?

Prefetching has two owners that can drift apart

With component-level TanStack Query, the component owns what data it consumes, while the route owns what data starts early. TanStack Query provides excellent prefetch primitives, but those two declarations are not connected by default.

Consider a page that correctly prefetches userQuery. Later, UserCard starts rendering teamQuery, or changes userQuery("u1") to userQuery("u2"):

route                        component tree
prefetch user:u1             UserCard
                               ├─ read user:u1
                               └─ read team:t1   ← added during a refactor

The page still works because TanStack Query safely fetches the missing data in the browser. Functional tests can keep passing while the page quietly gains another network round trip. The correctness fallback hides the performance regression.

p9v makes that silent regression fail loudly

p9v connects the route declaration to its consumers and checks the relationship at three points:

  1. Type checking: a declared component query missing from the route contract fails TypeScript.
  2. Development runtime: a wrong or missing exact query key throws P9vWaterfallError before becoming a hidden browser waterfall.
  3. CI: recorded unexpected-waterfall, depth, and critical-path budgets can fail a pull request before deployment.

| After a child query changes | Manual TanStack prefetch | p9v contract | | --- | --- | --- | | UI still renders | Yes | Yes in production | | Missing exact key is visible during development | Only through manual inspection | Immediate error | | Route/component coverage is type-checked | No | Yes | | Regression can fail CI | Custom tooling required | Built in |

p9v protects speed; it does not invent a faster cache

p9v runs TanStack Query's own primitives. A correct manual prefetch and p9v therefore have equivalent runtime performance:

naive nested fetching       1,202 ms
manual TanStack prefetch      401 ms
p9v prefetch                  401 ms

The value is keeping the last line at 401 ms as components move and queries change. p9v is most useful for shared component libraries, large route trees, multi-team applications, and codebases with enforced performance budgets.

For a small application with one or two obvious queries per page, manual TanStack prefetching is usually simpler and p9v may not be necessary.

Install

npm install @p9v/core @tanstack/react-query

React 18 or 19 and TanStack Query 5 are supported.

Quick start

1. Add identity to ordinary TanStack options

import { queryOptions } from "@tanstack/react-query";
import { defineQueryContract } from "@p9v/core";

export const userQuery = defineQueryContract({
  name: "user",
  options: (id: string) =>
    queryOptions({
      queryKey: ["user", id] as const,
      queryFn: () => api.get<User>(`/users/${id}`),
    }),
});

defineQueryContract preserves the TanStack options type. It also supports infiniteQueryOptions; p9v selects prefetchInfiniteQuery automatically.

2. Colocate the requirement with its consumer

"use client";

import { useSuspenseQuery } from "@tanstack/react-query";
import { withQueryRequirements } from "@p9v/core";

export const UserCard = withQueryRequirements(
  [userQuery],
  function UserCard({ userId }: { userId: string }) {
    const { data } = useSuspenseQuery(userQuery(userId));
    return <span>{data.name}</span>;
  },
);

The consumer still uses TanStack Query directly. If userQuery(userId) is not in the hydrated cache, development strict mode throws P9vWaterfallError with the exact key instead of silently starting a browser waterfall.

3. Define and execute the route contract

import { defineRouteContract } from "@p9v/core";
import { Prefetch } from "@p9v/core/server";

export const userPage = defineRouteContract({
  name: "user-page",
  load: ({ id }: { id: string }) => [
    { query: userQuery(id), policy: "blocking" },
    { query: statsQuery(id), policy: "streaming" },
  ],
  includes: [UserCard, StatsPanel],
});

export default async function Page({ params }) {
  const { id } = await params;
  return (
    <Prefetch contract={userPage} params={{ id }}>
      <UserCard userId={id} />
      <StatsPanel userId={id} />
    </Prefetch>
  );
}

All entries start before p9v waits. blocking entries delay the server boundary; streaming entries are dehydrated with their pending Promise for an RSC-capable framework such as Next.js App Router. A component requirement missing from load fails TypeScript and development route validation.

If the route loads userQuery("u1") while the component reads userQuery("u2"), the exact-key cache miss is still caught at runtime.

Intentional client queries

Strict contract queries do not begin unexpectedly in the development browser. Opt in at the contract or call site when a client fetch is intentional:

const searchQuery = defineQueryContract({
  name: "search",
  defer: true,
  options: (term: string) => queryOptions({ /* ... */ }),
});

useSuspenseQuery(userQuery(id, { defer: true }));

Production keeps the safe TanStack fetch fallback. Deferred requests are marked intentional-deferred; missing strict contracts are marked unexpected-waterfall; server route entries are marked prefetched.

Devtools and CI budgets

Mount the panel inside QueryClientProvider:

import { P9vDevtools } from "@p9v/core/devtools/react";

<QueryClientProvider client={queryClient}>
  <P9vDevtools />
  {children}
</QueryClientProvider>;

The panel separates contract-backed classifications from timing-based suspected waterfalls. Save WaterfallRecorder.toJSON() to p9v.record.json, then add an optional p9v.config.json:

{
  "maxUnexpectedWaterfalls": 0,
  "maxDepth": 1,
  "maxCriticalPathMs": 500,
  "routes": {
    "dashboard": { "maxCriticalPathMs": 400 }
  }
}
npx p9v analyze
npx p9v analyze artifacts/profile.json --config config/p9v.json

The command exits non-zero when a configured global or per-route budget is exceeded. Without a config it retains the legacy behavior and fails on an inferred waterfall deeper than one request.

Existing Resource and fragment APIs

The 0.3 APIs remain supported in 0.4:

const userResource = defineResource({
  name: "user",
  key: (id: string) => ["user", id] as const,
  fetch: (id) => fetchUser(id),
});

<Prefetch resources={[userResource(id)]} mode="streaming">
  <UserCard userId={id} />
</Prefetch>;

Use defineResource/useResource for the smallest p9v-owned API, and fragment/useFragment when field masking is valuable. See the 0.4 adoption guide; no 0.3 call site has to migrate.

API and package entry points

| Import | Main APIs | | --- | --- | | @p9v/core | defineQueryContract, withQueryRequirements, defineRouteContract, legacy Resource/fragment APIs | | @p9v/core/react | RouteContractProvider, P9vProvider, legacy read hooks | | @p9v/core/server | Next.js/RSC <Prefetch>, getServerQueryClient | | @p9v/core/devtools | recorder, timing analysis, evaluateBudgets | | @p9v/core/devtools/react | browser P9vDevtools panel |

Development

pnpm install
pnpm typecheck
pnpm test
pnpm build
pnpm test:package

The included Next.js example demonstrates blocking, streaming, and browser waterfall diagnostics.

License

MIT