{"name":"@playlive/react-query","dist-tags":{"latest":"0.4.4"},"versions":{"0.4.2":{"name":"@playlive/react-query","version":"0.4.2","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.5.1","@playlive/tiltify-core":"^0.4.17"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-FZ8x8GFIYHLkIpqua9hNqgGKYMsUVXkTfDLNSz/Ac1Ny4ePuC5R2HnuV+oXXMe86NUhAN4As258N4w7avRoc+A==","shasum":"1d722cbe70b30061bcb85896ab030fe637143a7c","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useCampaigns`        | `Array<TiltifyCampaign \\| … \\| null>` (per-row errors)                 | per-row: `id` nullish   |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n| `useCurrentEvents`    | `TiltifyFundraisingEvent[]` — cause-level list, season-filtered unless `currentOnly: false` | never (always enabled) |\n| `useTiltifyUserCampaigns` | `TiltifyPersonalCampaign[]`                                        | `userId` is nullish / `\"null\"` |\n| `useTiltifyUserAndTeamCampaigns` | `(TiltifyPersonalCampaign \\| TiltifyTeamCampaign)[]`            | `userId` is nullish / `\"null\"` |\n| `useScheduleBlockRaised` | `ScheduleBlockRaised`                                               | any of `campaignId` / `start` / `end` nullish |\n| `useLifetimeRaised`   | `number \\| null`                                                       | `username` is nullish   |\n| `usePreviousYearTotals` | `PreviousYearTotalItem[]`                                            | `slug` is nullish       |\n| `useDonorSpotlightOverview` | `DonorSpotlightSnapshot \\| null` — REST-only snapshot (see note) | `campaignId` is nullish |\n| `useLeaderboardExclusions` | `{ data, donorNames, addExclusion, removeExclusion, … }`          | `campaignID` is empty   |\n| `useLeaderboardWithExclusions` | `TiltifyLeaderboardEntry[]`                                    | `campaignID` is empty   |\n| `useTiltifyLeaderboard` | `{ entries, pages, fetchNextPage, hasNextPage, … }`                  | `campaignId` is empty   |\n| `useLeaderboard`      | `{ leaderboard, donations, currentTotal, exclusions, … }`              | `campaignId` is empty   |\n| `useDonationTrains`   | `DonationTrain[]`                                                      | `campaignID` is empty   |\n| `useDonationTrainHighRateDonors` | `DonationTrainHighRateDonor[]`                              | `campaignID` is empty   |\n| `useDonationTrainCommonTrains`   | `CommonDonationTrain[]`                                     | `campaignID` is empty   |\n| `useCampaignRulesets` | `DonationTrainRuleset[]`                                               | `campaignID` is empty   |\n| `useDonationTrainState` | `{ trains, updateTrainStatus, updateTrainVisibility, highRateDonors, commonTrains, isLoading }` | never |\n| `useCampaignRulesetsState` | `{ rulesets, updateRuleset, deleteRuleset, createRuleset }`      | never                   |\n\nDonation-train **mutation** hooks — same shape as TanStack Query's\n`useMutation`, projected into the package's `UseMutationResult`:\n`useUpdateTrainVisibility`, `useRefreshTrainStatus`,\n`useProcessDonationsForTrains`, `useCreateCampaignRuleset`,\n`useUpdateRuleset`, `useDeleteRuleset`. Each accepts a variables\nobject that matches the underlying fundraiser-data fetcher's params\n(minus the `signal`).\n\n`useTestDonations` is the one non-train mutation hook: it fires a\nsynthetic `TiltifyDonation` (or array) through the core REST API so\nalerts, donation trains, the subathon timer and every WebSocket\nsubscriber react as if Tiltify had delivered it. Pass `adminApiKey`\n**or** `tiltifyOAuthToken`; demo campaigns need neither.\n\n> **`useDonorSpotlightOverview` vs `useDonorSpotlight`** — this package\n> ships the plain-REST snapshot hook (`…Overview`), for dashboards and\n> editors. `@playlive/react-pipeline/fusion` ships `useDonorSpotlight`,\n> which fuses the same REST baseline with live WebSocket updates — use\n> that one on surfaces that already hold a pipeline connection.\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n`useFlattenedDonations` walks the whole donation history by default\n(100 pages × 100 rows). Surfaces that only need a recent slice should\ncap it — `useFlattenedDonations({ campaignId, count: 50, maxPages: 1 })`.\nBoth fields participate in the query key, so a capped consumer and a\nfull-history consumer don't share a cache entry.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood and now reads **both**\ncursor shapes so Twitch pagination works end-to-end.\n\n```tsx\nconst {\n  data,                    // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,           // () => Promise<void>\n  fetchPreviousPage,       // () => Promise<void>   — Tiltify only\n  hasNextPage,             // boolean\n  hasPreviousPage,         // boolean               — always false on Twitch path\n  isFetchingNextPage,      // boolean\n  isFetchingPreviousPage,  // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n**Cursor semantics:**\n\n- `getNextPageParam` reads `lastPage.metadata.after` (Tiltify) **and** `lastPage.metadata.nextPage` (Twitch) — previously only the Tiltify cursor was consulted, so Twitch pagination silently stopped after page 1.\n- `getPreviousPageParam` reads Tiltify's `lastPage.metadata.before`. The Twitch charity donations endpoint doesn't expose a reverse cursor at the wire level, so `hasPreviousPage` is permanently `false` on that path and `fetchPreviousPage` no-ops.\n\n**Query-key partitioning.** `queryKey` includes `config.completedBefore` / `config.completedAfter` (so two hooks watching the same campaign with different date-range filters don't share pages) and `options.cachingEnabled` (see the options table below).\n\nDisabled when `campaignId` is nullish.\n\n### Leaderboards\n\nFour hooks that back overlays consuming the Play Live leaderboard\nservice (UDP `pl-leaderboard-api`) plus Tiltify's donor-leaderboard\nendpoint. Each fills a distinct slot:\n\n| Hook | Use when… |\n| ---- | -------- |\n| `useLeaderboardExclusions`     | You need to read + mutate the donor-name exclusion list (admin dashboards, moderation UIs). |\n| `useLeaderboardWithExclusions` | You want the campaign's leaderboard with exclusions already applied server-side. |\n| `useTiltifyLeaderboard`        | You want the *unfiltered* Tiltify leaderboard, cursor-aware. |\n| `useLeaderboard`               | You want a donation-derived leaderboard composed from `useInfiniteDonations` + `useLeaderboardExclusions` (matches the historical overlay-vite behavior). |\n\n#### `useLeaderboardExclusions`\n\n```tsx\nconst {\n  data,             // LeaderboardExclusion[] | undefined\n  donorNames,       // string[] projection — handy for `.includes(name)` guards\n  isMutating,\n  addExclusion,     // (donorName: string) => Promise<LeaderboardExclusion>\n  removeExclusion,  // (donorName: string) => Promise<LeaderboardExclusion>\n  refetch,\n} = useLeaderboardExclusions(\n  { campaignID },\n  { adminApiKey: process.env.ADMIN_KEY },\n  //     └─ or { tiltifyOAuthToken: token } for campaign-owner clients\n);\n```\n\nRead is public (`GET /leaderboard-exclusions/{id}`); mutations\n(`POST` / `DELETE`) accept `adminApiKey` (sent as `x-api-key`) **or**\n`tiltifyOAuthToken` (sent as `Authorization: OAuth <token>`).\nSuccessful mutations invalidate the read so the UI picks up the new\nlist without polling. Auth fields are stripped off the merged\n`authAndOptions` bag before options forward to TanStack.\n\nAuto-disables when `campaignID` is empty.\n\n#### `useLeaderboardWithExclusions`\n\n```tsx\nconst { data, isLoading, refetch } = useLeaderboardWithExclusions({\n  charityType: \"tiltify\",\n  campaignID,\n  timeType: \"all\",       // or \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"ytd\"\n  count: 100,\n  // — or — supply an ad-hoc window (switches the service to a SQL-aggregation path)\n  // startDate: new Date(\"2025-01-01\"),\n  // endDate:   new Date(\"2025-12-31\"),\n});\n```\n\nTwitch path returns `[]`. `queryKey` partitions on every parameter so\nconsecutive window flips don't collide.\n\n#### `useTiltifyLeaderboard`\n\n```tsx\nconst {\n  entries,             // TiltifyLeaderboardEntry[] flattened across every fetched page\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useTiltifyLeaderboard({ campaignId, timeType: \"all\", limit: 50 });\n```\n\nSame surface shape as `useInfiniteDonations` plus the flat `entries`\nprojection. Use for the unfiltered Tiltify view — swap to\n`useLeaderboardWithExclusions` when the exclusion list should apply.\n\n#### `useLeaderboard`\n\n```tsx\nconst {\n  leaderboard,           // LeaderboardRow[], ranked + capped\n  donations,             // every donation aggregated (pre-limit)\n  currentTotal,          // sum of every donation's amount.value (pre-exclusion)\n  exclusions,            // string[] used for prefiltering\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useLeaderboard({\n  charityType: \"tiltify\",\n  campaignId,\n  limit: 10,             // 0 returns every donor\n  prefilterExclusions: true,\n  removeAnonymous: true,\n  eagerFetchPages: true, // walks the cursor via useEffect — default\n});\n```\n\nComposes `useInfiniteDonations` + `useLeaderboardExclusions`.\nAggregates `amount.value` per donor id, sorts desc, caps by `limit`.\n`eagerFetchPages` (default `true`) makes the leaderboard converge\nwithout the caller wiring `fetchNextPage`.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `false` (or `0`) in tests / for 404-legit hooks.      |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n| `initialData`     | —       | Seed the query with pre-fetched data (TanStack `initialData`). Route loaders should pass this so first paint shows real data instead of the loading state. Typed `unknown` — cast at the call site. For `useCampaigns` may be an **array** indexed 1:1 against `params` for per-row seeding, or a **scalar** shared across rows. |\n| `maxPages`        | —       | Retention cap for `useInfiniteQuery`. Only meaningful for `useInfiniteDonations` / `useTiltifyLeaderboard` — other hooks ignore it. TanStack v5 drops the oldest page when the limit is hit. |\n| `cachingEnabled`  | —       | Partition the `queryKey` by a boolean flag. Two hook instances that pass different values get separate cache slots — useful when the same campaign is fetched with and without cache-busting query params. Contributes to the key only; the fetcher is unchanged. |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  isFetchingPreviousPage: boolean;\n  hasNextPage: boolean;\n  hasPreviousPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  fetchPreviousPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.4.2.tgz","shasum":"1d722cbe70b30061bcb85896ab030fe637143a7c","integrity":"sha512-FZ8x8GFIYHLkIpqua9hNqgGKYMsUVXkTfDLNSz/Ac1Ny4ePuC5R2HnuV+oXXMe86NUhAN4As258N4w7avRoc+A=="}},"0.1.0":{"name":"@playlive/react-query","version":"0.1.0","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{"@playlive/fundraiser-data":"^0.1.0"},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.1.0","@playlive/tiltify-core":"^0.1.1"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-+X3J5Ki5ijnRAxkC8Dw3YumA3RrvRdHIW60lRgd1lvvZ7cSe4D+MYzpUkqnDjmLxG6qqaAXdFYOLcF2QtMfY/Q==","shasum":"94a680d987b9711e64982d4033e143d54dca7d6d","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioural difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useUser`, `useTeam`, `useFundraisingEvent`,\n`useCause`, `useEventCampaigns`) resolve to `[]` / `null` on the\nTwitch path rather than throwing — same lenient semantics as the\nunderlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood.\n\n```tsx\nconst {\n  data,                 // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,        // () => Promise<void>\n  hasNextPage,          // boolean\n  isFetchingNextPage,   // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n`getNextPageParam` reads `lastPage.metadata.after` for both Tiltify\nand Twitch paths. Disabled when `campaignId` is nullish.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `0` in tests that assert error surfacing.             |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  hasNextPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.1.0.tgz","shasum":"94a680d987b9711e64982d4033e143d54dca7d6d","integrity":"sha512-+X3J5Ki5ijnRAxkC8Dw3YumA3RrvRdHIW60lRgd1lvvZ7cSe4D+MYzpUkqnDjmLxG6qqaAXdFYOLcF2QtMfY/Q=="}},"0.1.2":{"name":"@playlive/react-query","version":"0.1.2","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.1.3","@playlive/tiltify-core":"^0.4.9"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-IfK8sDW5TGhbaxwOhuEUuyneO5vmFX1qErWQecGaKIoy4FnODRPaW43nynkyQCc8xxChhvc9tIIymrp6mPN09g==","shasum":"ff128157f88402f261c795d3c5c9913e644a81ba","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useUser`, `useTeam`, `useFundraisingEvent`,\n`useCause`, `useEventCampaigns`) resolve to `[]` / `null` on the\nTwitch path rather than throwing — same lenient semantics as the\nunderlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood.\n\n```tsx\nconst {\n  data,                 // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,        // () => Promise<void>\n  hasNextPage,          // boolean\n  isFetchingNextPage,   // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n`getNextPageParam` reads `lastPage.metadata.after` for both Tiltify\nand Twitch paths. Disabled when `campaignId` is nullish.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `0` in tests that assert error surfacing.             |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  hasNextPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.1.2.tgz","shasum":"ff128157f88402f261c795d3c5c9913e644a81ba","integrity":"sha512-IfK8sDW5TGhbaxwOhuEUuyneO5vmFX1qErWQecGaKIoy4FnODRPaW43nynkyQCc8xxChhvc9tIIymrp6mPN09g=="}},"0.1.3":{"name":"@playlive/react-query","version":"0.1.3","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.1.5","@playlive/tiltify-core":"^0.4.10"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-EqAJTK2Xbko1t1QStWIviBB8ks+6Q4SOEIH7mTrvkr3jpZ0sj5L8CM5d/2h/Xh3OeZxpuIovgMb1zRzbgUtwmA==","shasum":"0135a15ed64978032786a2eec5d005249823bd29","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood.\n\n```tsx\nconst {\n  data,                 // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,        // () => Promise<void>\n  hasNextPage,          // boolean\n  isFetchingNextPage,   // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n`getNextPageParam` reads `lastPage.metadata.after` for both Tiltify\nand Twitch paths. Disabled when `campaignId` is nullish.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `0` in tests that assert error surfacing.             |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  hasNextPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.1.3.tgz","shasum":"0135a15ed64978032786a2eec5d005249823bd29","integrity":"sha512-EqAJTK2Xbko1t1QStWIviBB8ks+6Q4SOEIH7mTrvkr3jpZ0sj5L8CM5d/2h/Xh3OeZxpuIovgMb1zRzbgUtwmA=="}},"0.2.0":{"name":"@playlive/react-query","version":"0.2.0","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.2.0","@playlive/tiltify-core":"^0.4.11"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-Ry2eWWkmzEUTv1RYeB5Mo41C4URXxxtKVNs8l0lrXZyWNB0E1igOL9j0hpOZF4AGCxdOwJx60/U7grjrXpr3kA==","shasum":"4a0bf33130900eb0c84dc18477c578aec1f0573f","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood.\n\n```tsx\nconst {\n  data,                 // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,        // () => Promise<void>\n  hasNextPage,          // boolean\n  isFetchingNextPage,   // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n`getNextPageParam` reads `lastPage.metadata.after` for both Tiltify\nand Twitch paths. Disabled when `campaignId` is nullish.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `0` in tests that assert error surfacing.             |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  hasNextPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.2.0.tgz","shasum":"4a0bf33130900eb0c84dc18477c578aec1f0573f","integrity":"sha512-Ry2eWWkmzEUTv1RYeB5Mo41C4URXxxtKVNs8l0lrXZyWNB0E1igOL9j0hpOZF4AGCxdOwJx60/U7grjrXpr3kA=="}},"0.2.1":{"name":"@playlive/react-query","version":"0.2.1","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.2.0","@playlive/tiltify-core":"^0.4.11"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-E3oxU3xbz1mPg2YbI/eLuK1Cqcz3FFyCSNZuH1ySWhicT/jsRnml0uzyDWIPWY0U11lEO9uPra+kw4Wr0rR3OA==","shasum":"2b938433ad10fb9af3addc512ecdbb5dd820cdd7","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood.\n\n```tsx\nconst {\n  data,                 // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,        // () => Promise<void>\n  hasNextPage,          // boolean\n  isFetchingNextPage,   // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n`getNextPageParam` reads `lastPage.metadata.after` for both Tiltify\nand Twitch paths. Disabled when `campaignId` is nullish.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `0` in tests that assert error surfacing.             |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  hasNextPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.2.1.tgz","shasum":"2b938433ad10fb9af3addc512ecdbb5dd820cdd7","integrity":"sha512-E3oxU3xbz1mPg2YbI/eLuK1Cqcz3FFyCSNZuH1ySWhicT/jsRnml0uzyDWIPWY0U11lEO9uPra+kw4Wr0rR3OA=="}},"0.2.2":{"name":"@playlive/react-query","version":"0.2.2","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.2.0","@playlive/tiltify-core":"^0.4.11"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-SbCeDCbzGjDf+IqR+MJhq6qrHRAtFG+O+JXEPijfbdKA8gHzzoiWpBdwYZVqY6g2M8qRb5z69NxziySUbYEiag==","shasum":"878b55fda703a8779f978435a4feda7285fdd14c","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood.\n\n```tsx\nconst {\n  data,                 // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,        // () => Promise<void>\n  hasNextPage,          // boolean\n  isFetchingNextPage,   // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n`getNextPageParam` reads `lastPage.metadata.after` for both Tiltify\nand Twitch paths. Disabled when `campaignId` is nullish.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `0` in tests that assert error surfacing.             |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  hasNextPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.2.2.tgz","shasum":"878b55fda703a8779f978435a4feda7285fdd14c","integrity":"sha512-SbCeDCbzGjDf+IqR+MJhq6qrHRAtFG+O+JXEPijfbdKA8gHzzoiWpBdwYZVqY6g2M8qRb5z69NxziySUbYEiag=="}},"0.2.3":{"name":"@playlive/react-query","version":"0.2.3","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.2.0","@playlive/tiltify-core":"^0.4.11"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-8cPj08jHX3Qyuvo11GCYfHwwZ/B3XcPkeSFJLbPuROuFIzKTmTiygAzYgT7aowvJI+wIcTByBHZZB+8AzHavOA==","shasum":"1273c0f987c78e9018dbf7c111b30cc5688e1020","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood.\n\n```tsx\nconst {\n  data,                 // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,        // () => Promise<void>\n  hasNextPage,          // boolean\n  isFetchingNextPage,   // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n`getNextPageParam` reads `lastPage.metadata.after` for both Tiltify\nand Twitch paths. Disabled when `campaignId` is nullish.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `0` in tests that assert error surfacing.             |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  hasNextPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.2.3.tgz","shasum":"1273c0f987c78e9018dbf7c111b30cc5688e1020","integrity":"sha512-8cPj08jHX3Qyuvo11GCYfHwwZ/B3XcPkeSFJLbPuROuFIzKTmTiygAzYgT7aowvJI+wIcTByBHZZB+8AzHavOA=="}},"0.2.4":{"name":"@playlive/react-query","version":"0.2.4","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.2.1","@playlive/tiltify-core":"^0.4.11"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-N1YkhDzwzQ1jiYkb0uwfmZQ0sTvfE0SPrtVZviVWJZCk1Mi3VEOAvd7oDLKFi0C3FMwiHCIDtF0dyz8hcJV6yg==","shasum":"b32417365eeba00cab2b5c9cc0aea2ae621b3d00","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useCampaigns`        | `Array<TiltifyCampaign \\| … \\| null>` (per-row errors)                 | per-row: `id` nullish   |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n| `useScheduleBlockRaised` | `ScheduleBlockRaised`                                               | any of `campaignId` / `start` / `end` nullish |\n| `useLifetimeRaised`   | `number \\| null`                                                       | `username` is nullish   |\n| `usePreviousYearTotals` | `PreviousYearTotalItem[]`                                            | `slug` is nullish       |\n| `useLeaderboardExclusions` | `{ data, donorNames, addExclusion, removeExclusion, … }`          | `campaignID` is empty   |\n| `useLeaderboardWithExclusions` | `TiltifyLeaderboardEntry[]`                                    | `campaignID` is empty   |\n| `useTiltifyLeaderboard` | `{ entries, pages, fetchNextPage, hasNextPage, … }`                  | `campaignId` is empty   |\n| `useLeaderboard`      | `{ leaderboard, donations, currentTotal, exclusions, … }`              | `campaignId` is empty   |\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood and now reads **both**\ncursor shapes so Twitch pagination works end-to-end.\n\n```tsx\nconst {\n  data,                    // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,           // () => Promise<void>\n  fetchPreviousPage,       // () => Promise<void>   — Tiltify only\n  hasNextPage,             // boolean\n  hasPreviousPage,         // boolean               — always false on Twitch path\n  isFetchingNextPage,      // boolean\n  isFetchingPreviousPage,  // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n**Cursor semantics:**\n\n- `getNextPageParam` reads `lastPage.metadata.after` (Tiltify) **and** `lastPage.metadata.nextPage` (Twitch) — previously only the Tiltify cursor was consulted, so Twitch pagination silently stopped after page 1.\n- `getPreviousPageParam` reads Tiltify's `lastPage.metadata.before`. The Twitch charity donations endpoint doesn't expose a reverse cursor at the wire level, so `hasPreviousPage` is permanently `false` on that path and `fetchPreviousPage` no-ops.\n\n**Query-key partitioning.** `queryKey` includes `config.completedBefore` / `config.completedAfter` (so two hooks watching the same campaign with different date-range filters don't share pages) and `options.cachingEnabled` (see the options table below).\n\nDisabled when `campaignId` is nullish.\n\n### Leaderboards\n\nFour hooks that back overlays consuming the Play Live leaderboard\nservice (UDP `pl-leaderboard-api`) plus Tiltify's donor-leaderboard\nendpoint. Each fills a distinct slot:\n\n| Hook | Use when… |\n| ---- | -------- |\n| `useLeaderboardExclusions`     | You need to read + mutate the donor-name exclusion list (admin dashboards, moderation UIs). |\n| `useLeaderboardWithExclusions` | You want the campaign's leaderboard with exclusions already applied server-side. |\n| `useTiltifyLeaderboard`        | You want the *unfiltered* Tiltify leaderboard, cursor-aware. |\n| `useLeaderboard`               | You want a donation-derived leaderboard composed from `useInfiniteDonations` + `useLeaderboardExclusions` (matches the historical overlay-vite behaviour). |\n\n#### `useLeaderboardExclusions`\n\n```tsx\nconst {\n  data,             // LeaderboardExclusion[] | undefined\n  donorNames,       // string[] projection — handy for `.includes(name)` guards\n  isMutating,\n  addExclusion,     // (donorName: string) => Promise<LeaderboardExclusion>\n  removeExclusion,  // (donorName: string) => Promise<LeaderboardExclusion>\n  refetch,\n} = useLeaderboardExclusions(\n  { campaignID },\n  { adminApiKey: process.env.ADMIN_KEY },\n  //     └─ or { tiltifyOAuthToken: token } for campaign-owner clients\n);\n```\n\nRead is public (`GET /leaderboard-exclusions/{id}`); mutations\n(`POST` / `DELETE`) accept `adminApiKey` (sent as `x-api-key`) **or**\n`tiltifyOAuthToken` (sent as `Authorization: OAuth <token>`).\nSuccessful mutations invalidate the read so the UI picks up the new\nlist without polling. Auth fields are stripped off the merged\n`authAndOptions` bag before options forward to TanStack.\n\nAuto-disables when `campaignID` is empty.\n\n#### `useLeaderboardWithExclusions`\n\n```tsx\nconst { data, isLoading, refetch } = useLeaderboardWithExclusions({\n  charityType: \"tiltify\",\n  campaignID,\n  timeType: \"all\",       // or \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"ytd\"\n  count: 100,\n  // — or — supply an ad-hoc window (switches the service to a SQL-aggregation path)\n  // startDate: new Date(\"2025-01-01\"),\n  // endDate:   new Date(\"2025-12-31\"),\n});\n```\n\nTwitch path returns `[]`. `queryKey` partitions on every parameter so\nconsecutive window flips don't collide.\n\n#### `useTiltifyLeaderboard`\n\n```tsx\nconst {\n  entries,             // TiltifyLeaderboardEntry[] flattened across every fetched page\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useTiltifyLeaderboard({ campaignId, timeType: \"all\", limit: 50 });\n```\n\nSame surface shape as `useInfiniteDonations` plus the flat `entries`\nprojection. Use for the unfiltered Tiltify view — swap to\n`useLeaderboardWithExclusions` when the exclusion list should apply.\n\n#### `useLeaderboard`\n\n```tsx\nconst {\n  leaderboard,           // LeaderboardRow[], ranked + capped\n  donations,             // every donation aggregated (pre-limit)\n  currentTotal,          // sum of every donation's amount.value (pre-exclusion)\n  exclusions,            // string[] used for prefiltering\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useLeaderboard({\n  charityType: \"tiltify\",\n  campaignId,\n  limit: 10,             // 0 returns every donor\n  prefilterExclusions: true,\n  removeAnonymous: true,\n  eagerFetchPages: true, // walks the cursor via useEffect — default\n});\n```\n\nComposes `useInfiniteDonations` + `useLeaderboardExclusions`.\nAggregates `amount.value` per donor id, sorts desc, caps by `limit`.\n`eagerFetchPages` (default `true`) makes the leaderboard converge\nwithout the caller wiring `fetchNextPage`.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `false` (or `0`) in tests / for 404-legit hooks.      |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n| `initialData`     | —       | Seed the query with pre-fetched data (TanStack `initialData`). Route loaders should pass this so first paint shows real data instead of the loading state. Typed `unknown` — cast at the call site. For `useCampaigns` may be an **array** indexed 1:1 against `params` for per-row seeding, or a **scalar** shared across rows. |\n| `maxPages`        | —       | Retention cap for `useInfiniteQuery`. Only meaningful for `useInfiniteDonations` / `useTiltifyLeaderboard` — other hooks ignore it. TanStack v5 drops the oldest page when the limit is hit. |\n| `cachingEnabled`  | —       | Partition the `queryKey` by a boolean flag. Two hook instances that pass different values get separate cache slots — useful when the same campaign is fetched with and without cache-busting query params. Contributes to the key only; the fetcher is unchanged. |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  isFetchingPreviousPage: boolean;\n  hasNextPage: boolean;\n  hasPreviousPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  fetchPreviousPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.2.4.tgz","shasum":"b32417365eeba00cab2b5c9cc0aea2ae621b3d00","integrity":"sha512-N1YkhDzwzQ1jiYkb0uwfmZQ0sTvfE0SPrtVZviVWJZCk1Mi3VEOAvd7oDLKFi0C3FMwiHCIDtF0dyz8hcJV6yg=="}},"0.2.5":{"name":"@playlive/react-query","version":"0.2.5","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.2.2","@playlive/tiltify-core":"^0.4.11"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-NvtvqmEOKRnw156QQReL1dm99MdKWLml2P4iKQBd0q9eZ8orQ/yihfddjfIxGpfSrKE0yKRxnl/IKQde/5JZMw==","shasum":"2b0cc2029b01e5e6e22deaf12b614d6d51277c7a","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useCampaigns`        | `Array<TiltifyCampaign \\| … \\| null>` (per-row errors)                 | per-row: `id` nullish   |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n| `useTiltifyUserCampaigns` | `TiltifyPersonalCampaign[]`                                        | `userId` is nullish / `\"null\"` |\n| `useTiltifyUserAndTeamCampaigns` | `(TiltifyPersonalCampaign \\| TiltifyTeamCampaign)[]`            | `userId` is nullish / `\"null\"` |\n| `useScheduleBlockRaised` | `ScheduleBlockRaised`                                               | any of `campaignId` / `start` / `end` nullish |\n| `useLifetimeRaised`   | `number \\| null`                                                       | `username` is nullish   |\n| `usePreviousYearTotals` | `PreviousYearTotalItem[]`                                            | `slug` is nullish       |\n| `useLeaderboardExclusions` | `{ data, donorNames, addExclusion, removeExclusion, … }`          | `campaignID` is empty   |\n| `useLeaderboardWithExclusions` | `TiltifyLeaderboardEntry[]`                                    | `campaignID` is empty   |\n| `useTiltifyLeaderboard` | `{ entries, pages, fetchNextPage, hasNextPage, … }`                  | `campaignId` is empty   |\n| `useLeaderboard`      | `{ leaderboard, donations, currentTotal, exclusions, … }`              | `campaignId` is empty   |\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood and now reads **both**\ncursor shapes so Twitch pagination works end-to-end.\n\n```tsx\nconst {\n  data,                    // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,           // () => Promise<void>\n  fetchPreviousPage,       // () => Promise<void>   — Tiltify only\n  hasNextPage,             // boolean\n  hasPreviousPage,         // boolean               — always false on Twitch path\n  isFetchingNextPage,      // boolean\n  isFetchingPreviousPage,  // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n**Cursor semantics:**\n\n- `getNextPageParam` reads `lastPage.metadata.after` (Tiltify) **and** `lastPage.metadata.nextPage` (Twitch) — previously only the Tiltify cursor was consulted, so Twitch pagination silently stopped after page 1.\n- `getPreviousPageParam` reads Tiltify's `lastPage.metadata.before`. The Twitch charity donations endpoint doesn't expose a reverse cursor at the wire level, so `hasPreviousPage` is permanently `false` on that path and `fetchPreviousPage` no-ops.\n\n**Query-key partitioning.** `queryKey` includes `config.completedBefore` / `config.completedAfter` (so two hooks watching the same campaign with different date-range filters don't share pages) and `options.cachingEnabled` (see the options table below).\n\nDisabled when `campaignId` is nullish.\n\n### Leaderboards\n\nFour hooks that back overlays consuming the Play Live leaderboard\nservice (UDP `pl-leaderboard-api`) plus Tiltify's donor-leaderboard\nendpoint. Each fills a distinct slot:\n\n| Hook | Use when… |\n| ---- | -------- |\n| `useLeaderboardExclusions`     | You need to read + mutate the donor-name exclusion list (admin dashboards, moderation UIs). |\n| `useLeaderboardWithExclusions` | You want the campaign's leaderboard with exclusions already applied server-side. |\n| `useTiltifyLeaderboard`        | You want the *unfiltered* Tiltify leaderboard, cursor-aware. |\n| `useLeaderboard`               | You want a donation-derived leaderboard composed from `useInfiniteDonations` + `useLeaderboardExclusions` (matches the historical overlay-vite behaviour). |\n\n#### `useLeaderboardExclusions`\n\n```tsx\nconst {\n  data,             // LeaderboardExclusion[] | undefined\n  donorNames,       // string[] projection — handy for `.includes(name)` guards\n  isMutating,\n  addExclusion,     // (donorName: string) => Promise<LeaderboardExclusion>\n  removeExclusion,  // (donorName: string) => Promise<LeaderboardExclusion>\n  refetch,\n} = useLeaderboardExclusions(\n  { campaignID },\n  { adminApiKey: process.env.ADMIN_KEY },\n  //     └─ or { tiltifyOAuthToken: token } for campaign-owner clients\n);\n```\n\nRead is public (`GET /leaderboard-exclusions/{id}`); mutations\n(`POST` / `DELETE`) accept `adminApiKey` (sent as `x-api-key`) **or**\n`tiltifyOAuthToken` (sent as `Authorization: OAuth <token>`).\nSuccessful mutations invalidate the read so the UI picks up the new\nlist without polling. Auth fields are stripped off the merged\n`authAndOptions` bag before options forward to TanStack.\n\nAuto-disables when `campaignID` is empty.\n\n#### `useLeaderboardWithExclusions`\n\n```tsx\nconst { data, isLoading, refetch } = useLeaderboardWithExclusions({\n  charityType: \"tiltify\",\n  campaignID,\n  timeType: \"all\",       // or \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"ytd\"\n  count: 100,\n  // — or — supply an ad-hoc window (switches the service to a SQL-aggregation path)\n  // startDate: new Date(\"2025-01-01\"),\n  // endDate:   new Date(\"2025-12-31\"),\n});\n```\n\nTwitch path returns `[]`. `queryKey` partitions on every parameter so\nconsecutive window flips don't collide.\n\n#### `useTiltifyLeaderboard`\n\n```tsx\nconst {\n  entries,             // TiltifyLeaderboardEntry[] flattened across every fetched page\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useTiltifyLeaderboard({ campaignId, timeType: \"all\", limit: 50 });\n```\n\nSame surface shape as `useInfiniteDonations` plus the flat `entries`\nprojection. Use for the unfiltered Tiltify view — swap to\n`useLeaderboardWithExclusions` when the exclusion list should apply.\n\n#### `useLeaderboard`\n\n```tsx\nconst {\n  leaderboard,           // LeaderboardRow[], ranked + capped\n  donations,             // every donation aggregated (pre-limit)\n  currentTotal,          // sum of every donation's amount.value (pre-exclusion)\n  exclusions,            // string[] used for prefiltering\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useLeaderboard({\n  charityType: \"tiltify\",\n  campaignId,\n  limit: 10,             // 0 returns every donor\n  prefilterExclusions: true,\n  removeAnonymous: true,\n  eagerFetchPages: true, // walks the cursor via useEffect — default\n});\n```\n\nComposes `useInfiniteDonations` + `useLeaderboardExclusions`.\nAggregates `amount.value` per donor id, sorts desc, caps by `limit`.\n`eagerFetchPages` (default `true`) makes the leaderboard converge\nwithout the caller wiring `fetchNextPage`.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `false` (or `0`) in tests / for 404-legit hooks.      |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n| `initialData`     | —       | Seed the query with pre-fetched data (TanStack `initialData`). Route loaders should pass this so first paint shows real data instead of the loading state. Typed `unknown` — cast at the call site. For `useCampaigns` may be an **array** indexed 1:1 against `params` for per-row seeding, or a **scalar** shared across rows. |\n| `maxPages`        | —       | Retention cap for `useInfiniteQuery`. Only meaningful for `useInfiniteDonations` / `useTiltifyLeaderboard` — other hooks ignore it. TanStack v5 drops the oldest page when the limit is hit. |\n| `cachingEnabled`  | —       | Partition the `queryKey` by a boolean flag. Two hook instances that pass different values get separate cache slots — useful when the same campaign is fetched with and without cache-busting query params. Contributes to the key only; the fetcher is unchanged. |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  isFetchingPreviousPage: boolean;\n  hasNextPage: boolean;\n  hasPreviousPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  fetchPreviousPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.2.5.tgz","shasum":"2b0cc2029b01e5e6e22deaf12b614d6d51277c7a","integrity":"sha512-NvtvqmEOKRnw156QQReL1dm99MdKWLml2P4iKQBd0q9eZ8orQ/yihfddjfIxGpfSrKE0yKRxnl/IKQde/5JZMw=="}},"0.2.6":{"name":"@playlive/react-query","version":"0.2.6","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.2.4","@playlive/tiltify-core":"^0.4.11"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-tkOu7TBnbvbInfUkhIn2R3O2wMmpmFranshp3lv/kz7inmnCqC/LGUDGdRTpZM1J8oTSTjOfw9TIy16gl/c7vQ==","shasum":"83bf005c28b199cfdbb1fdf006a128bf6141260b","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useCampaigns`        | `Array<TiltifyCampaign \\| … \\| null>` (per-row errors)                 | per-row: `id` nullish   |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n| `useTiltifyUserCampaigns` | `TiltifyPersonalCampaign[]`                                        | `userId` is nullish / `\"null\"` |\n| `useTiltifyUserAndTeamCampaigns` | `(TiltifyPersonalCampaign \\| TiltifyTeamCampaign)[]`            | `userId` is nullish / `\"null\"` |\n| `useScheduleBlockRaised` | `ScheduleBlockRaised`                                               | any of `campaignId` / `start` / `end` nullish |\n| `useLifetimeRaised`   | `number \\| null`                                                       | `username` is nullish   |\n| `usePreviousYearTotals` | `PreviousYearTotalItem[]`                                            | `slug` is nullish       |\n| `useLeaderboardExclusions` | `{ data, donorNames, addExclusion, removeExclusion, … }`          | `campaignID` is empty   |\n| `useLeaderboardWithExclusions` | `TiltifyLeaderboardEntry[]`                                    | `campaignID` is empty   |\n| `useTiltifyLeaderboard` | `{ entries, pages, fetchNextPage, hasNextPage, … }`                  | `campaignId` is empty   |\n| `useLeaderboard`      | `{ leaderboard, donations, currentTotal, exclusions, … }`              | `campaignId` is empty   |\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood and now reads **both**\ncursor shapes so Twitch pagination works end-to-end.\n\n```tsx\nconst {\n  data,                    // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,           // () => Promise<void>\n  fetchPreviousPage,       // () => Promise<void>   — Tiltify only\n  hasNextPage,             // boolean\n  hasPreviousPage,         // boolean               — always false on Twitch path\n  isFetchingNextPage,      // boolean\n  isFetchingPreviousPage,  // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n**Cursor semantics:**\n\n- `getNextPageParam` reads `lastPage.metadata.after` (Tiltify) **and** `lastPage.metadata.nextPage` (Twitch) — previously only the Tiltify cursor was consulted, so Twitch pagination silently stopped after page 1.\n- `getPreviousPageParam` reads Tiltify's `lastPage.metadata.before`. The Twitch charity donations endpoint doesn't expose a reverse cursor at the wire level, so `hasPreviousPage` is permanently `false` on that path and `fetchPreviousPage` no-ops.\n\n**Query-key partitioning.** `queryKey` includes `config.completedBefore` / `config.completedAfter` (so two hooks watching the same campaign with different date-range filters don't share pages) and `options.cachingEnabled` (see the options table below).\n\nDisabled when `campaignId` is nullish.\n\n### Leaderboards\n\nFour hooks that back overlays consuming the Play Live leaderboard\nservice (UDP `pl-leaderboard-api`) plus Tiltify's donor-leaderboard\nendpoint. Each fills a distinct slot:\n\n| Hook | Use when… |\n| ---- | -------- |\n| `useLeaderboardExclusions`     | You need to read + mutate the donor-name exclusion list (admin dashboards, moderation UIs). |\n| `useLeaderboardWithExclusions` | You want the campaign's leaderboard with exclusions already applied server-side. |\n| `useTiltifyLeaderboard`        | You want the *unfiltered* Tiltify leaderboard, cursor-aware. |\n| `useLeaderboard`               | You want a donation-derived leaderboard composed from `useInfiniteDonations` + `useLeaderboardExclusions` (matches the historical overlay-vite behaviour). |\n\n#### `useLeaderboardExclusions`\n\n```tsx\nconst {\n  data,             // LeaderboardExclusion[] | undefined\n  donorNames,       // string[] projection — handy for `.includes(name)` guards\n  isMutating,\n  addExclusion,     // (donorName: string) => Promise<LeaderboardExclusion>\n  removeExclusion,  // (donorName: string) => Promise<LeaderboardExclusion>\n  refetch,\n} = useLeaderboardExclusions(\n  { campaignID },\n  { adminApiKey: process.env.ADMIN_KEY },\n  //     └─ or { tiltifyOAuthToken: token } for campaign-owner clients\n);\n```\n\nRead is public (`GET /leaderboard-exclusions/{id}`); mutations\n(`POST` / `DELETE`) accept `adminApiKey` (sent as `x-api-key`) **or**\n`tiltifyOAuthToken` (sent as `Authorization: OAuth <token>`).\nSuccessful mutations invalidate the read so the UI picks up the new\nlist without polling. Auth fields are stripped off the merged\n`authAndOptions` bag before options forward to TanStack.\n\nAuto-disables when `campaignID` is empty.\n\n#### `useLeaderboardWithExclusions`\n\n```tsx\nconst { data, isLoading, refetch } = useLeaderboardWithExclusions({\n  charityType: \"tiltify\",\n  campaignID,\n  timeType: \"all\",       // or \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"ytd\"\n  count: 100,\n  // — or — supply an ad-hoc window (switches the service to a SQL-aggregation path)\n  // startDate: new Date(\"2025-01-01\"),\n  // endDate:   new Date(\"2025-12-31\"),\n});\n```\n\nTwitch path returns `[]`. `queryKey` partitions on every parameter so\nconsecutive window flips don't collide.\n\n#### `useTiltifyLeaderboard`\n\n```tsx\nconst {\n  entries,             // TiltifyLeaderboardEntry[] flattened across every fetched page\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useTiltifyLeaderboard({ campaignId, timeType: \"all\", limit: 50 });\n```\n\nSame surface shape as `useInfiniteDonations` plus the flat `entries`\nprojection. Use for the unfiltered Tiltify view — swap to\n`useLeaderboardWithExclusions` when the exclusion list should apply.\n\n#### `useLeaderboard`\n\n```tsx\nconst {\n  leaderboard,           // LeaderboardRow[], ranked + capped\n  donations,             // every donation aggregated (pre-limit)\n  currentTotal,          // sum of every donation's amount.value (pre-exclusion)\n  exclusions,            // string[] used for prefiltering\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useLeaderboard({\n  charityType: \"tiltify\",\n  campaignId,\n  limit: 10,             // 0 returns every donor\n  prefilterExclusions: true,\n  removeAnonymous: true,\n  eagerFetchPages: true, // walks the cursor via useEffect — default\n});\n```\n\nComposes `useInfiniteDonations` + `useLeaderboardExclusions`.\nAggregates `amount.value` per donor id, sorts desc, caps by `limit`.\n`eagerFetchPages` (default `true`) makes the leaderboard converge\nwithout the caller wiring `fetchNextPage`.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `false` (or `0`) in tests / for 404-legit hooks.      |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n| `initialData`     | —       | Seed the query with pre-fetched data (TanStack `initialData`). Route loaders should pass this so first paint shows real data instead of the loading state. Typed `unknown` — cast at the call site. For `useCampaigns` may be an **array** indexed 1:1 against `params` for per-row seeding, or a **scalar** shared across rows. |\n| `maxPages`        | —       | Retention cap for `useInfiniteQuery`. Only meaningful for `useInfiniteDonations` / `useTiltifyLeaderboard` — other hooks ignore it. TanStack v5 drops the oldest page when the limit is hit. |\n| `cachingEnabled`  | —       | Partition the `queryKey` by a boolean flag. Two hook instances that pass different values get separate cache slots — useful when the same campaign is fetched with and without cache-busting query params. Contributes to the key only; the fetcher is unchanged. |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  isFetchingPreviousPage: boolean;\n  hasNextPage: boolean;\n  hasPreviousPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  fetchPreviousPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.2.6.tgz","shasum":"83bf005c28b199cfdbb1fdf006a128bf6141260b","integrity":"sha512-tkOu7TBnbvbInfUkhIn2R3O2wMmpmFranshp3lv/kz7inmnCqC/LGUDGdRTpZM1J8oTSTjOfw9TIy16gl/c7vQ=="}},"0.2.7":{"name":"@playlive/react-query","version":"0.2.7","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.2.4","@playlive/tiltify-core":"^0.4.11"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-KBq/FIx5aHnLRmyLOWr4DVdVM2Yh5FD23v4byAAwALUBe6f3JVNtMTiFSFvxc20O9lAEuUepL+fHSR/wPLJFHw==","shasum":"243397138b6c56d4751645e7a8c6cc4485ba167f","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useCampaigns`        | `Array<TiltifyCampaign \\| … \\| null>` (per-row errors)                 | per-row: `id` nullish   |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n| `useTiltifyUserCampaigns` | `TiltifyPersonalCampaign[]`                                        | `userId` is nullish / `\"null\"` |\n| `useTiltifyUserAndTeamCampaigns` | `(TiltifyPersonalCampaign \\| TiltifyTeamCampaign)[]`            | `userId` is nullish / `\"null\"` |\n| `useScheduleBlockRaised` | `ScheduleBlockRaised`                                               | any of `campaignId` / `start` / `end` nullish |\n| `useLifetimeRaised`   | `number \\| null`                                                       | `username` is nullish   |\n| `usePreviousYearTotals` | `PreviousYearTotalItem[]`                                            | `slug` is nullish       |\n| `useLeaderboardExclusions` | `{ data, donorNames, addExclusion, removeExclusion, … }`          | `campaignID` is empty   |\n| `useLeaderboardWithExclusions` | `TiltifyLeaderboardEntry[]`                                    | `campaignID` is empty   |\n| `useTiltifyLeaderboard` | `{ entries, pages, fetchNextPage, hasNextPage, … }`                  | `campaignId` is empty   |\n| `useLeaderboard`      | `{ leaderboard, donations, currentTotal, exclusions, … }`              | `campaignId` is empty   |\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood and now reads **both**\ncursor shapes so Twitch pagination works end-to-end.\n\n```tsx\nconst {\n  data,                    // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,           // () => Promise<void>\n  fetchPreviousPage,       // () => Promise<void>   — Tiltify only\n  hasNextPage,             // boolean\n  hasPreviousPage,         // boolean               — always false on Twitch path\n  isFetchingNextPage,      // boolean\n  isFetchingPreviousPage,  // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n**Cursor semantics:**\n\n- `getNextPageParam` reads `lastPage.metadata.after` (Tiltify) **and** `lastPage.metadata.nextPage` (Twitch) — previously only the Tiltify cursor was consulted, so Twitch pagination silently stopped after page 1.\n- `getPreviousPageParam` reads Tiltify's `lastPage.metadata.before`. The Twitch charity donations endpoint doesn't expose a reverse cursor at the wire level, so `hasPreviousPage` is permanently `false` on that path and `fetchPreviousPage` no-ops.\n\n**Query-key partitioning.** `queryKey` includes `config.completedBefore` / `config.completedAfter` (so two hooks watching the same campaign with different date-range filters don't share pages) and `options.cachingEnabled` (see the options table below).\n\nDisabled when `campaignId` is nullish.\n\n### Leaderboards\n\nFour hooks that back overlays consuming the Play Live leaderboard\nservice (UDP `pl-leaderboard-api`) plus Tiltify's donor-leaderboard\nendpoint. Each fills a distinct slot:\n\n| Hook | Use when… |\n| ---- | -------- |\n| `useLeaderboardExclusions`     | You need to read + mutate the donor-name exclusion list (admin dashboards, moderation UIs). |\n| `useLeaderboardWithExclusions` | You want the campaign's leaderboard with exclusions already applied server-side. |\n| `useTiltifyLeaderboard`        | You want the *unfiltered* Tiltify leaderboard, cursor-aware. |\n| `useLeaderboard`               | You want a donation-derived leaderboard composed from `useInfiniteDonations` + `useLeaderboardExclusions` (matches the historical overlay-vite behaviour). |\n\n#### `useLeaderboardExclusions`\n\n```tsx\nconst {\n  data,             // LeaderboardExclusion[] | undefined\n  donorNames,       // string[] projection — handy for `.includes(name)` guards\n  isMutating,\n  addExclusion,     // (donorName: string) => Promise<LeaderboardExclusion>\n  removeExclusion,  // (donorName: string) => Promise<LeaderboardExclusion>\n  refetch,\n} = useLeaderboardExclusions(\n  { campaignID },\n  { adminApiKey: process.env.ADMIN_KEY },\n  //     └─ or { tiltifyOAuthToken: token } for campaign-owner clients\n);\n```\n\nRead is public (`GET /leaderboard-exclusions/{id}`); mutations\n(`POST` / `DELETE`) accept `adminApiKey` (sent as `x-api-key`) **or**\n`tiltifyOAuthToken` (sent as `Authorization: OAuth <token>`).\nSuccessful mutations invalidate the read so the UI picks up the new\nlist without polling. Auth fields are stripped off the merged\n`authAndOptions` bag before options forward to TanStack.\n\nAuto-disables when `campaignID` is empty.\n\n#### `useLeaderboardWithExclusions`\n\n```tsx\nconst { data, isLoading, refetch } = useLeaderboardWithExclusions({\n  charityType: \"tiltify\",\n  campaignID,\n  timeType: \"all\",       // or \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"ytd\"\n  count: 100,\n  // — or — supply an ad-hoc window (switches the service to a SQL-aggregation path)\n  // startDate: new Date(\"2025-01-01\"),\n  // endDate:   new Date(\"2025-12-31\"),\n});\n```\n\nTwitch path returns `[]`. `queryKey` partitions on every parameter so\nconsecutive window flips don't collide.\n\n#### `useTiltifyLeaderboard`\n\n```tsx\nconst {\n  entries,             // TiltifyLeaderboardEntry[] flattened across every fetched page\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useTiltifyLeaderboard({ campaignId, timeType: \"all\", limit: 50 });\n```\n\nSame surface shape as `useInfiniteDonations` plus the flat `entries`\nprojection. Use for the unfiltered Tiltify view — swap to\n`useLeaderboardWithExclusions` when the exclusion list should apply.\n\n#### `useLeaderboard`\n\n```tsx\nconst {\n  leaderboard,           // LeaderboardRow[], ranked + capped\n  donations,             // every donation aggregated (pre-limit)\n  currentTotal,          // sum of every donation's amount.value (pre-exclusion)\n  exclusions,            // string[] used for prefiltering\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useLeaderboard({\n  charityType: \"tiltify\",\n  campaignId,\n  limit: 10,             // 0 returns every donor\n  prefilterExclusions: true,\n  removeAnonymous: true,\n  eagerFetchPages: true, // walks the cursor via useEffect — default\n});\n```\n\nComposes `useInfiniteDonations` + `useLeaderboardExclusions`.\nAggregates `amount.value` per donor id, sorts desc, caps by `limit`.\n`eagerFetchPages` (default `true`) makes the leaderboard converge\nwithout the caller wiring `fetchNextPage`.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `false` (or `0`) in tests / for 404-legit hooks.      |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n| `initialData`     | —       | Seed the query with pre-fetched data (TanStack `initialData`). Route loaders should pass this so first paint shows real data instead of the loading state. Typed `unknown` — cast at the call site. For `useCampaigns` may be an **array** indexed 1:1 against `params` for per-row seeding, or a **scalar** shared across rows. |\n| `maxPages`        | —       | Retention cap for `useInfiniteQuery`. Only meaningful for `useInfiniteDonations` / `useTiltifyLeaderboard` — other hooks ignore it. TanStack v5 drops the oldest page when the limit is hit. |\n| `cachingEnabled`  | —       | Partition the `queryKey` by a boolean flag. Two hook instances that pass different values get separate cache slots — useful when the same campaign is fetched with and without cache-busting query params. Contributes to the key only; the fetcher is unchanged. |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  isFetchingPreviousPage: boolean;\n  hasNextPage: boolean;\n  hasPreviousPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  fetchPreviousPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.2.7.tgz","shasum":"243397138b6c56d4751645e7a8c6cc4485ba167f","integrity":"sha512-KBq/FIx5aHnLRmyLOWr4DVdVM2Yh5FD23v4byAAwALUBe6f3JVNtMTiFSFvxc20O9lAEuUepL+fHSR/wPLJFHw=="}},"0.3.0":{"name":"@playlive/react-query","version":"0.3.0","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.3.0","@playlive/tiltify-core":"^0.4.11"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-JyCYjIjBLnH5eBeQn6Mc7amhSARcXw6h0p1+Nz1CmJocTlYfFiOlhy+e2utU3mD399wbXtA7qGIAdMYxE33lZQ==","shasum":"a50b82a360bdef151d65d635d2ce1f3ebcc8a9af","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useCampaigns`        | `Array<TiltifyCampaign \\| … \\| null>` (per-row errors)                 | per-row: `id` nullish   |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n| `useTiltifyUserCampaigns` | `TiltifyPersonalCampaign[]`                                        | `userId` is nullish / `\"null\"` |\n| `useTiltifyUserAndTeamCampaigns` | `(TiltifyPersonalCampaign \\| TiltifyTeamCampaign)[]`            | `userId` is nullish / `\"null\"` |\n| `useScheduleBlockRaised` | `ScheduleBlockRaised`                                               | any of `campaignId` / `start` / `end` nullish |\n| `useLifetimeRaised`   | `number \\| null`                                                       | `username` is nullish   |\n| `usePreviousYearTotals` | `PreviousYearTotalItem[]`                                            | `slug` is nullish       |\n| `useLeaderboardExclusions` | `{ data, donorNames, addExclusion, removeExclusion, … }`          | `campaignID` is empty   |\n| `useLeaderboardWithExclusions` | `TiltifyLeaderboardEntry[]`                                    | `campaignID` is empty   |\n| `useTiltifyLeaderboard` | `{ entries, pages, fetchNextPage, hasNextPage, … }`                  | `campaignId` is empty   |\n| `useLeaderboard`      | `{ leaderboard, donations, currentTotal, exclusions, … }`              | `campaignId` is empty   |\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood and now reads **both**\ncursor shapes so Twitch pagination works end-to-end.\n\n```tsx\nconst {\n  data,                    // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,           // () => Promise<void>\n  fetchPreviousPage,       // () => Promise<void>   — Tiltify only\n  hasNextPage,             // boolean\n  hasPreviousPage,         // boolean               — always false on Twitch path\n  isFetchingNextPage,      // boolean\n  isFetchingPreviousPage,  // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n**Cursor semantics:**\n\n- `getNextPageParam` reads `lastPage.metadata.after` (Tiltify) **and** `lastPage.metadata.nextPage` (Twitch) — previously only the Tiltify cursor was consulted, so Twitch pagination silently stopped after page 1.\n- `getPreviousPageParam` reads Tiltify's `lastPage.metadata.before`. The Twitch charity donations endpoint doesn't expose a reverse cursor at the wire level, so `hasPreviousPage` is permanently `false` on that path and `fetchPreviousPage` no-ops.\n\n**Query-key partitioning.** `queryKey` includes `config.completedBefore` / `config.completedAfter` (so two hooks watching the same campaign with different date-range filters don't share pages) and `options.cachingEnabled` (see the options table below).\n\nDisabled when `campaignId` is nullish.\n\n### Leaderboards\n\nFour hooks that back overlays consuming the Play Live leaderboard\nservice (UDP `pl-leaderboard-api`) plus Tiltify's donor-leaderboard\nendpoint. Each fills a distinct slot:\n\n| Hook | Use when… |\n| ---- | -------- |\n| `useLeaderboardExclusions`     | You need to read + mutate the donor-name exclusion list (admin dashboards, moderation UIs). |\n| `useLeaderboardWithExclusions` | You want the campaign's leaderboard with exclusions already applied server-side. |\n| `useTiltifyLeaderboard`        | You want the *unfiltered* Tiltify leaderboard, cursor-aware. |\n| `useLeaderboard`               | You want a donation-derived leaderboard composed from `useInfiniteDonations` + `useLeaderboardExclusions` (matches the historical overlay-vite behaviour). |\n\n#### `useLeaderboardExclusions`\n\n```tsx\nconst {\n  data,             // LeaderboardExclusion[] | undefined\n  donorNames,       // string[] projection — handy for `.includes(name)` guards\n  isMutating,\n  addExclusion,     // (donorName: string) => Promise<LeaderboardExclusion>\n  removeExclusion,  // (donorName: string) => Promise<LeaderboardExclusion>\n  refetch,\n} = useLeaderboardExclusions(\n  { campaignID },\n  { adminApiKey: process.env.ADMIN_KEY },\n  //     └─ or { tiltifyOAuthToken: token } for campaign-owner clients\n);\n```\n\nRead is public (`GET /leaderboard-exclusions/{id}`); mutations\n(`POST` / `DELETE`) accept `adminApiKey` (sent as `x-api-key`) **or**\n`tiltifyOAuthToken` (sent as `Authorization: OAuth <token>`).\nSuccessful mutations invalidate the read so the UI picks up the new\nlist without polling. Auth fields are stripped off the merged\n`authAndOptions` bag before options forward to TanStack.\n\nAuto-disables when `campaignID` is empty.\n\n#### `useLeaderboardWithExclusions`\n\n```tsx\nconst { data, isLoading, refetch } = useLeaderboardWithExclusions({\n  charityType: \"tiltify\",\n  campaignID,\n  timeType: \"all\",       // or \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"ytd\"\n  count: 100,\n  // — or — supply an ad-hoc window (switches the service to a SQL-aggregation path)\n  // startDate: new Date(\"2025-01-01\"),\n  // endDate:   new Date(\"2025-12-31\"),\n});\n```\n\nTwitch path returns `[]`. `queryKey` partitions on every parameter so\nconsecutive window flips don't collide.\n\n#### `useTiltifyLeaderboard`\n\n```tsx\nconst {\n  entries,             // TiltifyLeaderboardEntry[] flattened across every fetched page\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useTiltifyLeaderboard({ campaignId, timeType: \"all\", limit: 50 });\n```\n\nSame surface shape as `useInfiniteDonations` plus the flat `entries`\nprojection. Use for the unfiltered Tiltify view — swap to\n`useLeaderboardWithExclusions` when the exclusion list should apply.\n\n#### `useLeaderboard`\n\n```tsx\nconst {\n  leaderboard,           // LeaderboardRow[], ranked + capped\n  donations,             // every donation aggregated (pre-limit)\n  currentTotal,          // sum of every donation's amount.value (pre-exclusion)\n  exclusions,            // string[] used for prefiltering\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useLeaderboard({\n  charityType: \"tiltify\",\n  campaignId,\n  limit: 10,             // 0 returns every donor\n  prefilterExclusions: true,\n  removeAnonymous: true,\n  eagerFetchPages: true, // walks the cursor via useEffect — default\n});\n```\n\nComposes `useInfiniteDonations` + `useLeaderboardExclusions`.\nAggregates `amount.value` per donor id, sorts desc, caps by `limit`.\n`eagerFetchPages` (default `true`) makes the leaderboard converge\nwithout the caller wiring `fetchNextPage`.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `false` (or `0`) in tests / for 404-legit hooks.      |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n| `initialData`     | —       | Seed the query with pre-fetched data (TanStack `initialData`). Route loaders should pass this so first paint shows real data instead of the loading state. Typed `unknown` — cast at the call site. For `useCampaigns` may be an **array** indexed 1:1 against `params` for per-row seeding, or a **scalar** shared across rows. |\n| `maxPages`        | —       | Retention cap for `useInfiniteQuery`. Only meaningful for `useInfiniteDonations` / `useTiltifyLeaderboard` — other hooks ignore it. TanStack v5 drops the oldest page when the limit is hit. |\n| `cachingEnabled`  | —       | Partition the `queryKey` by a boolean flag. Two hook instances that pass different values get separate cache slots — useful when the same campaign is fetched with and without cache-busting query params. Contributes to the key only; the fetcher is unchanged. |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  isFetchingPreviousPage: boolean;\n  hasNextPage: boolean;\n  hasPreviousPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  fetchPreviousPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.3.0.tgz","shasum":"a50b82a360bdef151d65d635d2ce1f3ebcc8a9af","integrity":"sha512-JyCYjIjBLnH5eBeQn6Mc7amhSARcXw6h0p1+Nz1CmJocTlYfFiOlhy+e2utU3mD399wbXtA7qGIAdMYxE33lZQ=="}},"0.3.1":{"name":"@playlive/react-query","version":"0.3.1","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.3.1","@playlive/tiltify-core":"^0.4.11"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-TjWyFc+1wm/v2nhCgMie98vmR9VAgvJ8YVAdV4p+c1usjtDpABs7XyGH/1y02VAlCiOxMy18raMcSkTsu8eJVw==","shasum":"7415190410e964abde29444f0bef4ca98fd1992c","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useCampaigns`        | `Array<TiltifyCampaign \\| … \\| null>` (per-row errors)                 | per-row: `id` nullish   |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n| `useTiltifyUserCampaigns` | `TiltifyPersonalCampaign[]`                                        | `userId` is nullish / `\"null\"` |\n| `useTiltifyUserAndTeamCampaigns` | `(TiltifyPersonalCampaign \\| TiltifyTeamCampaign)[]`            | `userId` is nullish / `\"null\"` |\n| `useScheduleBlockRaised` | `ScheduleBlockRaised`                                               | any of `campaignId` / `start` / `end` nullish |\n| `useLifetimeRaised`   | `number \\| null`                                                       | `username` is nullish   |\n| `usePreviousYearTotals` | `PreviousYearTotalItem[]`                                            | `slug` is nullish       |\n| `useLeaderboardExclusions` | `{ data, donorNames, addExclusion, removeExclusion, … }`          | `campaignID` is empty   |\n| `useLeaderboardWithExclusions` | `TiltifyLeaderboardEntry[]`                                    | `campaignID` is empty   |\n| `useTiltifyLeaderboard` | `{ entries, pages, fetchNextPage, hasNextPage, … }`                  | `campaignId` is empty   |\n| `useLeaderboard`      | `{ leaderboard, donations, currentTotal, exclusions, … }`              | `campaignId` is empty   |\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood and now reads **both**\ncursor shapes so Twitch pagination works end-to-end.\n\n```tsx\nconst {\n  data,                    // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,           // () => Promise<void>\n  fetchPreviousPage,       // () => Promise<void>   — Tiltify only\n  hasNextPage,             // boolean\n  hasPreviousPage,         // boolean               — always false on Twitch path\n  isFetchingNextPage,      // boolean\n  isFetchingPreviousPage,  // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n**Cursor semantics:**\n\n- `getNextPageParam` reads `lastPage.metadata.after` (Tiltify) **and** `lastPage.metadata.nextPage` (Twitch) — previously only the Tiltify cursor was consulted, so Twitch pagination silently stopped after page 1.\n- `getPreviousPageParam` reads Tiltify's `lastPage.metadata.before`. The Twitch charity donations endpoint doesn't expose a reverse cursor at the wire level, so `hasPreviousPage` is permanently `false` on that path and `fetchPreviousPage` no-ops.\n\n**Query-key partitioning.** `queryKey` includes `config.completedBefore` / `config.completedAfter` (so two hooks watching the same campaign with different date-range filters don't share pages) and `options.cachingEnabled` (see the options table below).\n\nDisabled when `campaignId` is nullish.\n\n### Leaderboards\n\nFour hooks that back overlays consuming the Play Live leaderboard\nservice (UDP `pl-leaderboard-api`) plus Tiltify's donor-leaderboard\nendpoint. Each fills a distinct slot:\n\n| Hook | Use when… |\n| ---- | -------- |\n| `useLeaderboardExclusions`     | You need to read + mutate the donor-name exclusion list (admin dashboards, moderation UIs). |\n| `useLeaderboardWithExclusions` | You want the campaign's leaderboard with exclusions already applied server-side. |\n| `useTiltifyLeaderboard`        | You want the *unfiltered* Tiltify leaderboard, cursor-aware. |\n| `useLeaderboard`               | You want a donation-derived leaderboard composed from `useInfiniteDonations` + `useLeaderboardExclusions` (matches the historical overlay-vite behaviour). |\n\n#### `useLeaderboardExclusions`\n\n```tsx\nconst {\n  data,             // LeaderboardExclusion[] | undefined\n  donorNames,       // string[] projection — handy for `.includes(name)` guards\n  isMutating,\n  addExclusion,     // (donorName: string) => Promise<LeaderboardExclusion>\n  removeExclusion,  // (donorName: string) => Promise<LeaderboardExclusion>\n  refetch,\n} = useLeaderboardExclusions(\n  { campaignID },\n  { adminApiKey: process.env.ADMIN_KEY },\n  //     └─ or { tiltifyOAuthToken: token } for campaign-owner clients\n);\n```\n\nRead is public (`GET /leaderboard-exclusions/{id}`); mutations\n(`POST` / `DELETE`) accept `adminApiKey` (sent as `x-api-key`) **or**\n`tiltifyOAuthToken` (sent as `Authorization: OAuth <token>`).\nSuccessful mutations invalidate the read so the UI picks up the new\nlist without polling. Auth fields are stripped off the merged\n`authAndOptions` bag before options forward to TanStack.\n\nAuto-disables when `campaignID` is empty.\n\n#### `useLeaderboardWithExclusions`\n\n```tsx\nconst { data, isLoading, refetch } = useLeaderboardWithExclusions({\n  charityType: \"tiltify\",\n  campaignID,\n  timeType: \"all\",       // or \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"ytd\"\n  count: 100,\n  // — or — supply an ad-hoc window (switches the service to a SQL-aggregation path)\n  // startDate: new Date(\"2025-01-01\"),\n  // endDate:   new Date(\"2025-12-31\"),\n});\n```\n\nTwitch path returns `[]`. `queryKey` partitions on every parameter so\nconsecutive window flips don't collide.\n\n#### `useTiltifyLeaderboard`\n\n```tsx\nconst {\n  entries,             // TiltifyLeaderboardEntry[] flattened across every fetched page\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useTiltifyLeaderboard({ campaignId, timeType: \"all\", limit: 50 });\n```\n\nSame surface shape as `useInfiniteDonations` plus the flat `entries`\nprojection. Use for the unfiltered Tiltify view — swap to\n`useLeaderboardWithExclusions` when the exclusion list should apply.\n\n#### `useLeaderboard`\n\n```tsx\nconst {\n  leaderboard,           // LeaderboardRow[], ranked + capped\n  donations,             // every donation aggregated (pre-limit)\n  currentTotal,          // sum of every donation's amount.value (pre-exclusion)\n  exclusions,            // string[] used for prefiltering\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useLeaderboard({\n  charityType: \"tiltify\",\n  campaignId,\n  limit: 10,             // 0 returns every donor\n  prefilterExclusions: true,\n  removeAnonymous: true,\n  eagerFetchPages: true, // walks the cursor via useEffect — default\n});\n```\n\nComposes `useInfiniteDonations` + `useLeaderboardExclusions`.\nAggregates `amount.value` per donor id, sorts desc, caps by `limit`.\n`eagerFetchPages` (default `true`) makes the leaderboard converge\nwithout the caller wiring `fetchNextPage`.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `false` (or `0`) in tests / for 404-legit hooks.      |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n| `initialData`     | —       | Seed the query with pre-fetched data (TanStack `initialData`). Route loaders should pass this so first paint shows real data instead of the loading state. Typed `unknown` — cast at the call site. For `useCampaigns` may be an **array** indexed 1:1 against `params` for per-row seeding, or a **scalar** shared across rows. |\n| `maxPages`        | —       | Retention cap for `useInfiniteQuery`. Only meaningful for `useInfiniteDonations` / `useTiltifyLeaderboard` — other hooks ignore it. TanStack v5 drops the oldest page when the limit is hit. |\n| `cachingEnabled`  | —       | Partition the `queryKey` by a boolean flag. Two hook instances that pass different values get separate cache slots — useful when the same campaign is fetched with and without cache-busting query params. Contributes to the key only; the fetcher is unchanged. |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  isFetchingPreviousPage: boolean;\n  hasNextPage: boolean;\n  hasPreviousPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  fetchPreviousPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.3.1.tgz","shasum":"7415190410e964abde29444f0bef4ca98fd1992c","integrity":"sha512-TjWyFc+1wm/v2nhCgMie98vmR9VAgvJ8YVAdV4p+c1usjtDpABs7XyGH/1y02VAlCiOxMy18raMcSkTsu8eJVw=="}},"0.3.2":{"name":"@playlive/react-query","version":"0.3.2","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.3.2","@playlive/tiltify-core":"^0.4.11"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-zPTExmGULkKraKqoR6yXuC0HlYdlAj+jaCKPQG3pf2wi+lRzUbCP4ISFlu9B95xwF7ZtPDE43Mp6lDiWJM5piA==","shasum":"bbfa900e7101245e69c33f61a520a4a6bc95ab6f","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useCampaigns`        | `Array<TiltifyCampaign \\| … \\| null>` (per-row errors)                 | per-row: `id` nullish   |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n| `useTiltifyUserCampaigns` | `TiltifyPersonalCampaign[]`                                        | `userId` is nullish / `\"null\"` |\n| `useTiltifyUserAndTeamCampaigns` | `(TiltifyPersonalCampaign \\| TiltifyTeamCampaign)[]`            | `userId` is nullish / `\"null\"` |\n| `useScheduleBlockRaised` | `ScheduleBlockRaised`                                               | any of `campaignId` / `start` / `end` nullish |\n| `useLifetimeRaised`   | `number \\| null`                                                       | `username` is nullish   |\n| `usePreviousYearTotals` | `PreviousYearTotalItem[]`                                            | `slug` is nullish       |\n| `useLeaderboardExclusions` | `{ data, donorNames, addExclusion, removeExclusion, … }`          | `campaignID` is empty   |\n| `useLeaderboardWithExclusions` | `TiltifyLeaderboardEntry[]`                                    | `campaignID` is empty   |\n| `useTiltifyLeaderboard` | `{ entries, pages, fetchNextPage, hasNextPage, … }`                  | `campaignId` is empty   |\n| `useLeaderboard`      | `{ leaderboard, donations, currentTotal, exclusions, … }`              | `campaignId` is empty   |\n| `useDonationTrains`   | `DonationTrain[]`                                                      | `campaignID` is empty   |\n| `useDonationTrainHighRateDonors` | `DonationTrainHighRateDonor[]`                              | `campaignID` is empty   |\n| `useDonationTrainCommonTrains`   | `CommonDonationTrain[]`                                     | `campaignID` is empty   |\n| `useCampaignRulesets` | `DonationTrainRuleset[]`                                               | `campaignID` is empty   |\n| `useDonationTrainState` | `{ trains, updateTrainStatus, updateTrainVisibility, highRateDonors, commonTrains, isLoading }` | never |\n| `useCampaignRulesetsState` | `{ rulesets, updateRuleset, deleteRuleset, createRuleset }`      | never                   |\n\nDonation-train **mutation** hooks — same shape as TanStack Query's\n`useMutation`, projected into the package's `UseMutationResult`:\n`useUpdateTrainVisibility`, `useRefreshTrainStatus`,\n`useProcessDonationsForTrains`, `useCreateCampaignRuleset`,\n`useUpdateRuleset`, `useDeleteRuleset`. Each accepts a variables\nobject that matches the underlying fundraiser-data fetcher's params\n(minus the `signal`).\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood and now reads **both**\ncursor shapes so Twitch pagination works end-to-end.\n\n```tsx\nconst {\n  data,                    // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,           // () => Promise<void>\n  fetchPreviousPage,       // () => Promise<void>   — Tiltify only\n  hasNextPage,             // boolean\n  hasPreviousPage,         // boolean               — always false on Twitch path\n  isFetchingNextPage,      // boolean\n  isFetchingPreviousPage,  // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n**Cursor semantics:**\n\n- `getNextPageParam` reads `lastPage.metadata.after` (Tiltify) **and** `lastPage.metadata.nextPage` (Twitch) — previously only the Tiltify cursor was consulted, so Twitch pagination silently stopped after page 1.\n- `getPreviousPageParam` reads Tiltify's `lastPage.metadata.before`. The Twitch charity donations endpoint doesn't expose a reverse cursor at the wire level, so `hasPreviousPage` is permanently `false` on that path and `fetchPreviousPage` no-ops.\n\n**Query-key partitioning.** `queryKey` includes `config.completedBefore` / `config.completedAfter` (so two hooks watching the same campaign with different date-range filters don't share pages) and `options.cachingEnabled` (see the options table below).\n\nDisabled when `campaignId` is nullish.\n\n### Leaderboards\n\nFour hooks that back overlays consuming the Play Live leaderboard\nservice (UDP `pl-leaderboard-api`) plus Tiltify's donor-leaderboard\nendpoint. Each fills a distinct slot:\n\n| Hook | Use when… |\n| ---- | -------- |\n| `useLeaderboardExclusions`     | You need to read + mutate the donor-name exclusion list (admin dashboards, moderation UIs). |\n| `useLeaderboardWithExclusions` | You want the campaign's leaderboard with exclusions already applied server-side. |\n| `useTiltifyLeaderboard`        | You want the *unfiltered* Tiltify leaderboard, cursor-aware. |\n| `useLeaderboard`               | You want a donation-derived leaderboard composed from `useInfiniteDonations` + `useLeaderboardExclusions` (matches the historical overlay-vite behaviour). |\n\n#### `useLeaderboardExclusions`\n\n```tsx\nconst {\n  data,             // LeaderboardExclusion[] | undefined\n  donorNames,       // string[] projection — handy for `.includes(name)` guards\n  isMutating,\n  addExclusion,     // (donorName: string) => Promise<LeaderboardExclusion>\n  removeExclusion,  // (donorName: string) => Promise<LeaderboardExclusion>\n  refetch,\n} = useLeaderboardExclusions(\n  { campaignID },\n  { adminApiKey: process.env.ADMIN_KEY },\n  //     └─ or { tiltifyOAuthToken: token } for campaign-owner clients\n);\n```\n\nRead is public (`GET /leaderboard-exclusions/{id}`); mutations\n(`POST` / `DELETE`) accept `adminApiKey` (sent as `x-api-key`) **or**\n`tiltifyOAuthToken` (sent as `Authorization: OAuth <token>`).\nSuccessful mutations invalidate the read so the UI picks up the new\nlist without polling. Auth fields are stripped off the merged\n`authAndOptions` bag before options forward to TanStack.\n\nAuto-disables when `campaignID` is empty.\n\n#### `useLeaderboardWithExclusions`\n\n```tsx\nconst { data, isLoading, refetch } = useLeaderboardWithExclusions({\n  charityType: \"tiltify\",\n  campaignID,\n  timeType: \"all\",       // or \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"ytd\"\n  count: 100,\n  // — or — supply an ad-hoc window (switches the service to a SQL-aggregation path)\n  // startDate: new Date(\"2025-01-01\"),\n  // endDate:   new Date(\"2025-12-31\"),\n});\n```\n\nTwitch path returns `[]`. `queryKey` partitions on every parameter so\nconsecutive window flips don't collide.\n\n#### `useTiltifyLeaderboard`\n\n```tsx\nconst {\n  entries,             // TiltifyLeaderboardEntry[] flattened across every fetched page\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useTiltifyLeaderboard({ campaignId, timeType: \"all\", limit: 50 });\n```\n\nSame surface shape as `useInfiniteDonations` plus the flat `entries`\nprojection. Use for the unfiltered Tiltify view — swap to\n`useLeaderboardWithExclusions` when the exclusion list should apply.\n\n#### `useLeaderboard`\n\n```tsx\nconst {\n  leaderboard,           // LeaderboardRow[], ranked + capped\n  donations,             // every donation aggregated (pre-limit)\n  currentTotal,          // sum of every donation's amount.value (pre-exclusion)\n  exclusions,            // string[] used for prefiltering\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useLeaderboard({\n  charityType: \"tiltify\",\n  campaignId,\n  limit: 10,             // 0 returns every donor\n  prefilterExclusions: true,\n  removeAnonymous: true,\n  eagerFetchPages: true, // walks the cursor via useEffect — default\n});\n```\n\nComposes `useInfiniteDonations` + `useLeaderboardExclusions`.\nAggregates `amount.value` per donor id, sorts desc, caps by `limit`.\n`eagerFetchPages` (default `true`) makes the leaderboard converge\nwithout the caller wiring `fetchNextPage`.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `false` (or `0`) in tests / for 404-legit hooks.      |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n| `initialData`     | —       | Seed the query with pre-fetched data (TanStack `initialData`). Route loaders should pass this so first paint shows real data instead of the loading state. Typed `unknown` — cast at the call site. For `useCampaigns` may be an **array** indexed 1:1 against `params` for per-row seeding, or a **scalar** shared across rows. |\n| `maxPages`        | —       | Retention cap for `useInfiniteQuery`. Only meaningful for `useInfiniteDonations` / `useTiltifyLeaderboard` — other hooks ignore it. TanStack v5 drops the oldest page when the limit is hit. |\n| `cachingEnabled`  | —       | Partition the `queryKey` by a boolean flag. Two hook instances that pass different values get separate cache slots — useful when the same campaign is fetched with and without cache-busting query params. Contributes to the key only; the fetcher is unchanged. |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  isFetchingPreviousPage: boolean;\n  hasNextPage: boolean;\n  hasPreviousPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  fetchPreviousPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.3.2.tgz","shasum":"bbfa900e7101245e69c33f61a520a4a6bc95ab6f","integrity":"sha512-zPTExmGULkKraKqoR6yXuC0HlYdlAj+jaCKPQG3pf2wi+lRzUbCP4ISFlu9B95xwF7ZtPDE43Mp6lDiWJM5piA=="}},"0.3.3":{"name":"@playlive/react-query","version":"0.3.3","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.3.3","@playlive/tiltify-core":"^0.4.11"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-6YD2hZc5+m9Tnvx01HZR/RPudt8UXKoVmXNTF/rN/D2FBjYaeIXR5UhbRCocUaLpxI98ISjFuR0QbPgrvIlrWw==","shasum":"f3693211f8042d01fa1016f052f9d70e03b5602a","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useCampaigns`        | `Array<TiltifyCampaign \\| … \\| null>` (per-row errors)                 | per-row: `id` nullish   |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n| `useTiltifyUserCampaigns` | `TiltifyPersonalCampaign[]`                                        | `userId` is nullish / `\"null\"` |\n| `useTiltifyUserAndTeamCampaigns` | `(TiltifyPersonalCampaign \\| TiltifyTeamCampaign)[]`            | `userId` is nullish / `\"null\"` |\n| `useScheduleBlockRaised` | `ScheduleBlockRaised`                                               | any of `campaignId` / `start` / `end` nullish |\n| `useLifetimeRaised`   | `number \\| null`                                                       | `username` is nullish   |\n| `usePreviousYearTotals` | `PreviousYearTotalItem[]`                                            | `slug` is nullish       |\n| `useLeaderboardExclusions` | `{ data, donorNames, addExclusion, removeExclusion, … }`          | `campaignID` is empty   |\n| `useLeaderboardWithExclusions` | `TiltifyLeaderboardEntry[]`                                    | `campaignID` is empty   |\n| `useTiltifyLeaderboard` | `{ entries, pages, fetchNextPage, hasNextPage, … }`                  | `campaignId` is empty   |\n| `useLeaderboard`      | `{ leaderboard, donations, currentTotal, exclusions, … }`              | `campaignId` is empty   |\n| `useDonationTrains`   | `DonationTrain[]`                                                      | `campaignID` is empty   |\n| `useDonationTrainHighRateDonors` | `DonationTrainHighRateDonor[]`                              | `campaignID` is empty   |\n| `useDonationTrainCommonTrains`   | `CommonDonationTrain[]`                                     | `campaignID` is empty   |\n| `useCampaignRulesets` | `DonationTrainRuleset[]`                                               | `campaignID` is empty   |\n| `useDonationTrainState` | `{ trains, updateTrainStatus, updateTrainVisibility, highRateDonors, commonTrains, isLoading }` | never |\n| `useCampaignRulesetsState` | `{ rulesets, updateRuleset, deleteRuleset, createRuleset }`      | never                   |\n\nDonation-train **mutation** hooks — same shape as TanStack Query's\n`useMutation`, projected into the package's `UseMutationResult`:\n`useUpdateTrainVisibility`, `useRefreshTrainStatus`,\n`useProcessDonationsForTrains`, `useCreateCampaignRuleset`,\n`useUpdateRuleset`, `useDeleteRuleset`. Each accepts a variables\nobject that matches the underlying fundraiser-data fetcher's params\n(minus the `signal`).\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood and now reads **both**\ncursor shapes so Twitch pagination works end-to-end.\n\n```tsx\nconst {\n  data,                    // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,           // () => Promise<void>\n  fetchPreviousPage,       // () => Promise<void>   — Tiltify only\n  hasNextPage,             // boolean\n  hasPreviousPage,         // boolean               — always false on Twitch path\n  isFetchingNextPage,      // boolean\n  isFetchingPreviousPage,  // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n**Cursor semantics:**\n\n- `getNextPageParam` reads `lastPage.metadata.after` (Tiltify) **and** `lastPage.metadata.nextPage` (Twitch) — previously only the Tiltify cursor was consulted, so Twitch pagination silently stopped after page 1.\n- `getPreviousPageParam` reads Tiltify's `lastPage.metadata.before`. The Twitch charity donations endpoint doesn't expose a reverse cursor at the wire level, so `hasPreviousPage` is permanently `false` on that path and `fetchPreviousPage` no-ops.\n\n**Query-key partitioning.** `queryKey` includes `config.completedBefore` / `config.completedAfter` (so two hooks watching the same campaign with different date-range filters don't share pages) and `options.cachingEnabled` (see the options table below).\n\nDisabled when `campaignId` is nullish.\n\n### Leaderboards\n\nFour hooks that back overlays consuming the Play Live leaderboard\nservice (UDP `pl-leaderboard-api`) plus Tiltify's donor-leaderboard\nendpoint. Each fills a distinct slot:\n\n| Hook | Use when… |\n| ---- | -------- |\n| `useLeaderboardExclusions`     | You need to read + mutate the donor-name exclusion list (admin dashboards, moderation UIs). |\n| `useLeaderboardWithExclusions` | You want the campaign's leaderboard with exclusions already applied server-side. |\n| `useTiltifyLeaderboard`        | You want the *unfiltered* Tiltify leaderboard, cursor-aware. |\n| `useLeaderboard`               | You want a donation-derived leaderboard composed from `useInfiniteDonations` + `useLeaderboardExclusions` (matches the historical overlay-vite behaviour). |\n\n#### `useLeaderboardExclusions`\n\n```tsx\nconst {\n  data,             // LeaderboardExclusion[] | undefined\n  donorNames,       // string[] projection — handy for `.includes(name)` guards\n  isMutating,\n  addExclusion,     // (donorName: string) => Promise<LeaderboardExclusion>\n  removeExclusion,  // (donorName: string) => Promise<LeaderboardExclusion>\n  refetch,\n} = useLeaderboardExclusions(\n  { campaignID },\n  { adminApiKey: process.env.ADMIN_KEY },\n  //     └─ or { tiltifyOAuthToken: token } for campaign-owner clients\n);\n```\n\nRead is public (`GET /leaderboard-exclusions/{id}`); mutations\n(`POST` / `DELETE`) accept `adminApiKey` (sent as `x-api-key`) **or**\n`tiltifyOAuthToken` (sent as `Authorization: OAuth <token>`).\nSuccessful mutations invalidate the read so the UI picks up the new\nlist without polling. Auth fields are stripped off the merged\n`authAndOptions` bag before options forward to TanStack.\n\nAuto-disables when `campaignID` is empty.\n\n#### `useLeaderboardWithExclusions`\n\n```tsx\nconst { data, isLoading, refetch } = useLeaderboardWithExclusions({\n  charityType: \"tiltify\",\n  campaignID,\n  timeType: \"all\",       // or \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"ytd\"\n  count: 100,\n  // — or — supply an ad-hoc window (switches the service to a SQL-aggregation path)\n  // startDate: new Date(\"2025-01-01\"),\n  // endDate:   new Date(\"2025-12-31\"),\n});\n```\n\nTwitch path returns `[]`. `queryKey` partitions on every parameter so\nconsecutive window flips don't collide.\n\n#### `useTiltifyLeaderboard`\n\n```tsx\nconst {\n  entries,             // TiltifyLeaderboardEntry[] flattened across every fetched page\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useTiltifyLeaderboard({ campaignId, timeType: \"all\", limit: 50 });\n```\n\nSame surface shape as `useInfiniteDonations` plus the flat `entries`\nprojection. Use for the unfiltered Tiltify view — swap to\n`useLeaderboardWithExclusions` when the exclusion list should apply.\n\n#### `useLeaderboard`\n\n```tsx\nconst {\n  leaderboard,           // LeaderboardRow[], ranked + capped\n  donations,             // every donation aggregated (pre-limit)\n  currentTotal,          // sum of every donation's amount.value (pre-exclusion)\n  exclusions,            // string[] used for prefiltering\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useLeaderboard({\n  charityType: \"tiltify\",\n  campaignId,\n  limit: 10,             // 0 returns every donor\n  prefilterExclusions: true,\n  removeAnonymous: true,\n  eagerFetchPages: true, // walks the cursor via useEffect — default\n});\n```\n\nComposes `useInfiniteDonations` + `useLeaderboardExclusions`.\nAggregates `amount.value` per donor id, sorts desc, caps by `limit`.\n`eagerFetchPages` (default `true`) makes the leaderboard converge\nwithout the caller wiring `fetchNextPage`.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `false` (or `0`) in tests / for 404-legit hooks.      |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n| `initialData`     | —       | Seed the query with pre-fetched data (TanStack `initialData`). Route loaders should pass this so first paint shows real data instead of the loading state. Typed `unknown` — cast at the call site. For `useCampaigns` may be an **array** indexed 1:1 against `params` for per-row seeding, or a **scalar** shared across rows. |\n| `maxPages`        | —       | Retention cap for `useInfiniteQuery`. Only meaningful for `useInfiniteDonations` / `useTiltifyLeaderboard` — other hooks ignore it. TanStack v5 drops the oldest page when the limit is hit. |\n| `cachingEnabled`  | —       | Partition the `queryKey` by a boolean flag. Two hook instances that pass different values get separate cache slots — useful when the same campaign is fetched with and without cache-busting query params. Contributes to the key only; the fetcher is unchanged. |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  isFetchingPreviousPage: boolean;\n  hasNextPage: boolean;\n  hasPreviousPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  fetchPreviousPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.3.3.tgz","shasum":"f3693211f8042d01fa1016f052f9d70e03b5602a","integrity":"sha512-6YD2hZc5+m9Tnvx01HZR/RPudt8UXKoVmXNTF/rN/D2FBjYaeIXR5UhbRCocUaLpxI98ISjFuR0QbPgrvIlrWw=="}},"0.3.4":{"name":"@playlive/react-query","version":"0.3.4","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.3.4","@playlive/tiltify-core":"^0.4.11"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-NYaPstwnNNH83l88OerLD4jRzQDG7hJpwPJugoxG2FLu5f22TXXUhAH7Ym3CTVFBtYg/FmtN0RjSHMFTKjdmwA==","shasum":"ee77dec4c9a1b334af896970d55f319d1b32e925","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useCampaigns`        | `Array<TiltifyCampaign \\| … \\| null>` (per-row errors)                 | per-row: `id` nullish   |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n| `useTiltifyUserCampaigns` | `TiltifyPersonalCampaign[]`                                        | `userId` is nullish / `\"null\"` |\n| `useTiltifyUserAndTeamCampaigns` | `(TiltifyPersonalCampaign \\| TiltifyTeamCampaign)[]`            | `userId` is nullish / `\"null\"` |\n| `useScheduleBlockRaised` | `ScheduleBlockRaised`                                               | any of `campaignId` / `start` / `end` nullish |\n| `useLifetimeRaised`   | `number \\| null`                                                       | `username` is nullish   |\n| `usePreviousYearTotals` | `PreviousYearTotalItem[]`                                            | `slug` is nullish       |\n| `useLeaderboardExclusions` | `{ data, donorNames, addExclusion, removeExclusion, … }`          | `campaignID` is empty   |\n| `useLeaderboardWithExclusions` | `TiltifyLeaderboardEntry[]`                                    | `campaignID` is empty   |\n| `useTiltifyLeaderboard` | `{ entries, pages, fetchNextPage, hasNextPage, … }`                  | `campaignId` is empty   |\n| `useLeaderboard`      | `{ leaderboard, donations, currentTotal, exclusions, … }`              | `campaignId` is empty   |\n| `useDonationTrains`   | `DonationTrain[]`                                                      | `campaignID` is empty   |\n| `useDonationTrainHighRateDonors` | `DonationTrainHighRateDonor[]`                              | `campaignID` is empty   |\n| `useDonationTrainCommonTrains`   | `CommonDonationTrain[]`                                     | `campaignID` is empty   |\n| `useCampaignRulesets` | `DonationTrainRuleset[]`                                               | `campaignID` is empty   |\n| `useDonationTrainState` | `{ trains, updateTrainStatus, updateTrainVisibility, highRateDonors, commonTrains, isLoading }` | never |\n| `useCampaignRulesetsState` | `{ rulesets, updateRuleset, deleteRuleset, createRuleset }`      | never                   |\n\nDonation-train **mutation** hooks — same shape as TanStack Query's\n`useMutation`, projected into the package's `UseMutationResult`:\n`useUpdateTrainVisibility`, `useRefreshTrainStatus`,\n`useProcessDonationsForTrains`, `useCreateCampaignRuleset`,\n`useUpdateRuleset`, `useDeleteRuleset`. Each accepts a variables\nobject that matches the underlying fundraiser-data fetcher's params\n(minus the `signal`).\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood and now reads **both**\ncursor shapes so Twitch pagination works end-to-end.\n\n```tsx\nconst {\n  data,                    // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,           // () => Promise<void>\n  fetchPreviousPage,       // () => Promise<void>   — Tiltify only\n  hasNextPage,             // boolean\n  hasPreviousPage,         // boolean               — always false on Twitch path\n  isFetchingNextPage,      // boolean\n  isFetchingPreviousPage,  // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n**Cursor semantics:**\n\n- `getNextPageParam` reads `lastPage.metadata.after` (Tiltify) **and** `lastPage.metadata.nextPage` (Twitch) — previously only the Tiltify cursor was consulted, so Twitch pagination silently stopped after page 1.\n- `getPreviousPageParam` reads Tiltify's `lastPage.metadata.before`. The Twitch charity donations endpoint doesn't expose a reverse cursor at the wire level, so `hasPreviousPage` is permanently `false` on that path and `fetchPreviousPage` no-ops.\n\n**Query-key partitioning.** `queryKey` includes `config.completedBefore` / `config.completedAfter` (so two hooks watching the same campaign with different date-range filters don't share pages) and `options.cachingEnabled` (see the options table below).\n\nDisabled when `campaignId` is nullish.\n\n### Leaderboards\n\nFour hooks that back overlays consuming the Play Live leaderboard\nservice (UDP `pl-leaderboard-api`) plus Tiltify's donor-leaderboard\nendpoint. Each fills a distinct slot:\n\n| Hook | Use when… |\n| ---- | -------- |\n| `useLeaderboardExclusions`     | You need to read + mutate the donor-name exclusion list (admin dashboards, moderation UIs). |\n| `useLeaderboardWithExclusions` | You want the campaign's leaderboard with exclusions already applied server-side. |\n| `useTiltifyLeaderboard`        | You want the *unfiltered* Tiltify leaderboard, cursor-aware. |\n| `useLeaderboard`               | You want a donation-derived leaderboard composed from `useInfiniteDonations` + `useLeaderboardExclusions` (matches the historical overlay-vite behaviour). |\n\n#### `useLeaderboardExclusions`\n\n```tsx\nconst {\n  data,             // LeaderboardExclusion[] | undefined\n  donorNames,       // string[] projection — handy for `.includes(name)` guards\n  isMutating,\n  addExclusion,     // (donorName: string) => Promise<LeaderboardExclusion>\n  removeExclusion,  // (donorName: string) => Promise<LeaderboardExclusion>\n  refetch,\n} = useLeaderboardExclusions(\n  { campaignID },\n  { adminApiKey: process.env.ADMIN_KEY },\n  //     └─ or { tiltifyOAuthToken: token } for campaign-owner clients\n);\n```\n\nRead is public (`GET /leaderboard-exclusions/{id}`); mutations\n(`POST` / `DELETE`) accept `adminApiKey` (sent as `x-api-key`) **or**\n`tiltifyOAuthToken` (sent as `Authorization: OAuth <token>`).\nSuccessful mutations invalidate the read so the UI picks up the new\nlist without polling. Auth fields are stripped off the merged\n`authAndOptions` bag before options forward to TanStack.\n\nAuto-disables when `campaignID` is empty.\n\n#### `useLeaderboardWithExclusions`\n\n```tsx\nconst { data, isLoading, refetch } = useLeaderboardWithExclusions({\n  charityType: \"tiltify\",\n  campaignID,\n  timeType: \"all\",       // or \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"ytd\"\n  count: 100,\n  // — or — supply an ad-hoc window (switches the service to a SQL-aggregation path)\n  // startDate: new Date(\"2025-01-01\"),\n  // endDate:   new Date(\"2025-12-31\"),\n});\n```\n\nTwitch path returns `[]`. `queryKey` partitions on every parameter so\nconsecutive window flips don't collide.\n\n#### `useTiltifyLeaderboard`\n\n```tsx\nconst {\n  entries,             // TiltifyLeaderboardEntry[] flattened across every fetched page\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useTiltifyLeaderboard({ campaignId, timeType: \"all\", limit: 50 });\n```\n\nSame surface shape as `useInfiniteDonations` plus the flat `entries`\nprojection. Use for the unfiltered Tiltify view — swap to\n`useLeaderboardWithExclusions` when the exclusion list should apply.\n\n#### `useLeaderboard`\n\n```tsx\nconst {\n  leaderboard,           // LeaderboardRow[], ranked + capped\n  donations,             // every donation aggregated (pre-limit)\n  currentTotal,          // sum of every donation's amount.value (pre-exclusion)\n  exclusions,            // string[] used for prefiltering\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useLeaderboard({\n  charityType: \"tiltify\",\n  campaignId,\n  limit: 10,             // 0 returns every donor\n  prefilterExclusions: true,\n  removeAnonymous: true,\n  eagerFetchPages: true, // walks the cursor via useEffect — default\n});\n```\n\nComposes `useInfiniteDonations` + `useLeaderboardExclusions`.\nAggregates `amount.value` per donor id, sorts desc, caps by `limit`.\n`eagerFetchPages` (default `true`) makes the leaderboard converge\nwithout the caller wiring `fetchNextPage`.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `false` (or `0`) in tests / for 404-legit hooks.      |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n| `initialData`     | —       | Seed the query with pre-fetched data (TanStack `initialData`). Route loaders should pass this so first paint shows real data instead of the loading state. Typed `unknown` — cast at the call site. For `useCampaigns` may be an **array** indexed 1:1 against `params` for per-row seeding, or a **scalar** shared across rows. |\n| `maxPages`        | —       | Retention cap for `useInfiniteQuery`. Only meaningful for `useInfiniteDonations` / `useTiltifyLeaderboard` — other hooks ignore it. TanStack v5 drops the oldest page when the limit is hit. |\n| `cachingEnabled`  | —       | Partition the `queryKey` by a boolean flag. Two hook instances that pass different values get separate cache slots — useful when the same campaign is fetched with and without cache-busting query params. Contributes to the key only; the fetcher is unchanged. |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  isFetchingPreviousPage: boolean;\n  hasNextPage: boolean;\n  hasPreviousPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  fetchPreviousPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.3.4.tgz","shasum":"ee77dec4c9a1b334af896970d55f319d1b32e925","integrity":"sha512-NYaPstwnNNH83l88OerLD4jRzQDG7hJpwPJugoxG2FLu5f22TXXUhAH7Ym3CTVFBtYg/FmtN0RjSHMFTKjdmwA=="}},"0.4.0":{"name":"@playlive/react-query","version":"0.4.0","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.5.0","@playlive/tiltify-core":"^0.4.13"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-syFDs4199FIuoy0suAaRBWUX/w1J7mqelPPDv3AaOirs0wB8c1icZ2pFD+P9M+hMJu4T8UsmIqrAKWdR4fiYRQ==","shasum":"95c211a3ec75abf38b84415e66ee1e4ab64bdf4b","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useCampaigns`        | `Array<TiltifyCampaign \\| … \\| null>` (per-row errors)                 | per-row: `id` nullish   |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n| `useCurrentEvents`    | `TiltifyFundraisingEvent[]` — cause-level list, season-filtered unless `currentOnly: false` | never (always enabled) |\n| `useTiltifyUserCampaigns` | `TiltifyPersonalCampaign[]`                                        | `userId` is nullish / `\"null\"` |\n| `useTiltifyUserAndTeamCampaigns` | `(TiltifyPersonalCampaign \\| TiltifyTeamCampaign)[]`            | `userId` is nullish / `\"null\"` |\n| `useScheduleBlockRaised` | `ScheduleBlockRaised`                                               | any of `campaignId` / `start` / `end` nullish |\n| `useLifetimeRaised`   | `number \\| null`                                                       | `username` is nullish   |\n| `usePreviousYearTotals` | `PreviousYearTotalItem[]`                                            | `slug` is nullish       |\n| `useDonorSpotlightOverview` | `DonorSpotlightSnapshot \\| null` — REST-only snapshot (see note) | `campaignId` is nullish |\n| `useLeaderboardExclusions` | `{ data, donorNames, addExclusion, removeExclusion, … }`          | `campaignID` is empty   |\n| `useLeaderboardWithExclusions` | `TiltifyLeaderboardEntry[]`                                    | `campaignID` is empty   |\n| `useTiltifyLeaderboard` | `{ entries, pages, fetchNextPage, hasNextPage, … }`                  | `campaignId` is empty   |\n| `useLeaderboard`      | `{ leaderboard, donations, currentTotal, exclusions, … }`              | `campaignId` is empty   |\n| `useDonationTrains`   | `DonationTrain[]`                                                      | `campaignID` is empty   |\n| `useDonationTrainHighRateDonors` | `DonationTrainHighRateDonor[]`                              | `campaignID` is empty   |\n| `useDonationTrainCommonTrains`   | `CommonDonationTrain[]`                                     | `campaignID` is empty   |\n| `useCampaignRulesets` | `DonationTrainRuleset[]`                                               | `campaignID` is empty   |\n| `useDonationTrainState` | `{ trains, updateTrainStatus, updateTrainVisibility, highRateDonors, commonTrains, isLoading }` | never |\n| `useCampaignRulesetsState` | `{ rulesets, updateRuleset, deleteRuleset, createRuleset }`      | never                   |\n\nDonation-train **mutation** hooks — same shape as TanStack Query's\n`useMutation`, projected into the package's `UseMutationResult`:\n`useUpdateTrainVisibility`, `useRefreshTrainStatus`,\n`useProcessDonationsForTrains`, `useCreateCampaignRuleset`,\n`useUpdateRuleset`, `useDeleteRuleset`. Each accepts a variables\nobject that matches the underlying fundraiser-data fetcher's params\n(minus the `signal`).\n\n`useTestDonations` is the one non-train mutation hook: it fires a\nsynthetic `TiltifyDonation` (or array) through the core REST API so\nalerts, donation trains, the subathon timer and every WebSocket\nsubscriber react as if Tiltify had delivered it. Pass `adminApiKey`\n**or** `tiltifyOAuthToken`; demo campaigns need neither.\n\n> **`useDonorSpotlightOverview` vs `useDonorSpotlight`** — this package\n> ships the plain-REST snapshot hook (`…Overview`), for dashboards and\n> editors. `@playlive/react-pipeline/fusion` ships `useDonorSpotlight`,\n> which fuses the same REST baseline with live WebSocket updates — use\n> that one on surfaces that already hold a pipeline connection.\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n`useFlattenedDonations` walks the whole donation history by default\n(100 pages × 100 rows). Surfaces that only need a recent slice should\ncap it — `useFlattenedDonations({ campaignId, count: 50, maxPages: 1 })`.\nBoth fields participate in the query key, so a capped consumer and a\nfull-history consumer don't share a cache entry.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood and now reads **both**\ncursor shapes so Twitch pagination works end-to-end.\n\n```tsx\nconst {\n  data,                    // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,           // () => Promise<void>\n  fetchPreviousPage,       // () => Promise<void>   — Tiltify only\n  hasNextPage,             // boolean\n  hasPreviousPage,         // boolean               — always false on Twitch path\n  isFetchingNextPage,      // boolean\n  isFetchingPreviousPage,  // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n**Cursor semantics:**\n\n- `getNextPageParam` reads `lastPage.metadata.after` (Tiltify) **and** `lastPage.metadata.nextPage` (Twitch) — previously only the Tiltify cursor was consulted, so Twitch pagination silently stopped after page 1.\n- `getPreviousPageParam` reads Tiltify's `lastPage.metadata.before`. The Twitch charity donations endpoint doesn't expose a reverse cursor at the wire level, so `hasPreviousPage` is permanently `false` on that path and `fetchPreviousPage` no-ops.\n\n**Query-key partitioning.** `queryKey` includes `config.completedBefore` / `config.completedAfter` (so two hooks watching the same campaign with different date-range filters don't share pages) and `options.cachingEnabled` (see the options table below).\n\nDisabled when `campaignId` is nullish.\n\n### Leaderboards\n\nFour hooks that back overlays consuming the Play Live leaderboard\nservice (UDP `pl-leaderboard-api`) plus Tiltify's donor-leaderboard\nendpoint. Each fills a distinct slot:\n\n| Hook | Use when… |\n| ---- | -------- |\n| `useLeaderboardExclusions`     | You need to read + mutate the donor-name exclusion list (admin dashboards, moderation UIs). |\n| `useLeaderboardWithExclusions` | You want the campaign's leaderboard with exclusions already applied server-side. |\n| `useTiltifyLeaderboard`        | You want the *unfiltered* Tiltify leaderboard, cursor-aware. |\n| `useLeaderboard`               | You want a donation-derived leaderboard composed from `useInfiniteDonations` + `useLeaderboardExclusions` (matches the historical overlay-vite behaviour). |\n\n#### `useLeaderboardExclusions`\n\n```tsx\nconst {\n  data,             // LeaderboardExclusion[] | undefined\n  donorNames,       // string[] projection — handy for `.includes(name)` guards\n  isMutating,\n  addExclusion,     // (donorName: string) => Promise<LeaderboardExclusion>\n  removeExclusion,  // (donorName: string) => Promise<LeaderboardExclusion>\n  refetch,\n} = useLeaderboardExclusions(\n  { campaignID },\n  { adminApiKey: process.env.ADMIN_KEY },\n  //     └─ or { tiltifyOAuthToken: token } for campaign-owner clients\n);\n```\n\nRead is public (`GET /leaderboard-exclusions/{id}`); mutations\n(`POST` / `DELETE`) accept `adminApiKey` (sent as `x-api-key`) **or**\n`tiltifyOAuthToken` (sent as `Authorization: OAuth <token>`).\nSuccessful mutations invalidate the read so the UI picks up the new\nlist without polling. Auth fields are stripped off the merged\n`authAndOptions` bag before options forward to TanStack.\n\nAuto-disables when `campaignID` is empty.\n\n#### `useLeaderboardWithExclusions`\n\n```tsx\nconst { data, isLoading, refetch } = useLeaderboardWithExclusions({\n  charityType: \"tiltify\",\n  campaignID,\n  timeType: \"all\",       // or \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"ytd\"\n  count: 100,\n  // — or — supply an ad-hoc window (switches the service to a SQL-aggregation path)\n  // startDate: new Date(\"2025-01-01\"),\n  // endDate:   new Date(\"2025-12-31\"),\n});\n```\n\nTwitch path returns `[]`. `queryKey` partitions on every parameter so\nconsecutive window flips don't collide.\n\n#### `useTiltifyLeaderboard`\n\n```tsx\nconst {\n  entries,             // TiltifyLeaderboardEntry[] flattened across every fetched page\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useTiltifyLeaderboard({ campaignId, timeType: \"all\", limit: 50 });\n```\n\nSame surface shape as `useInfiniteDonations` plus the flat `entries`\nprojection. Use for the unfiltered Tiltify view — swap to\n`useLeaderboardWithExclusions` when the exclusion list should apply.\n\n#### `useLeaderboard`\n\n```tsx\nconst {\n  leaderboard,           // LeaderboardRow[], ranked + capped\n  donations,             // every donation aggregated (pre-limit)\n  currentTotal,          // sum of every donation's amount.value (pre-exclusion)\n  exclusions,            // string[] used for prefiltering\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useLeaderboard({\n  charityType: \"tiltify\",\n  campaignId,\n  limit: 10,             // 0 returns every donor\n  prefilterExclusions: true,\n  removeAnonymous: true,\n  eagerFetchPages: true, // walks the cursor via useEffect — default\n});\n```\n\nComposes `useInfiniteDonations` + `useLeaderboardExclusions`.\nAggregates `amount.value` per donor id, sorts desc, caps by `limit`.\n`eagerFetchPages` (default `true`) makes the leaderboard converge\nwithout the caller wiring `fetchNextPage`.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `false` (or `0`) in tests / for 404-legit hooks.      |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n| `initialData`     | —       | Seed the query with pre-fetched data (TanStack `initialData`). Route loaders should pass this so first paint shows real data instead of the loading state. Typed `unknown` — cast at the call site. For `useCampaigns` may be an **array** indexed 1:1 against `params` for per-row seeding, or a **scalar** shared across rows. |\n| `maxPages`        | —       | Retention cap for `useInfiniteQuery`. Only meaningful for `useInfiniteDonations` / `useTiltifyLeaderboard` — other hooks ignore it. TanStack v5 drops the oldest page when the limit is hit. |\n| `cachingEnabled`  | —       | Partition the `queryKey` by a boolean flag. Two hook instances that pass different values get separate cache slots — useful when the same campaign is fetched with and without cache-busting query params. Contributes to the key only; the fetcher is unchanged. |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  isFetchingPreviousPage: boolean;\n  hasNextPage: boolean;\n  hasPreviousPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  fetchPreviousPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.4.0.tgz","shasum":"95c211a3ec75abf38b84415e66ee1e4ab64bdf4b","integrity":"sha512-syFDs4199FIuoy0suAaRBWUX/w1J7mqelPPDv3AaOirs0wB8c1icZ2pFD+P9M+hMJu4T8UsmIqrAKWdR4fiYRQ=="}},"0.4.1":{"name":"@playlive/react-query","version":"0.4.1","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.5.1","@playlive/tiltify-core":"^0.4.17"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-sWo7UvFQ1WaSKqOhUebC1u3CDVaSl2CLZIOioVKzwzHm+uwOHQm9iZgxto84E0b0F52sxOcXncV65UZMm2YGOw==","shasum":"387aa3d8463013ad142f7cfef1684b0c64aac125","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/).\n**Drop-in compatible with [`@playlive/react-data`](../react-data/)** — same\nhook names, same parameter shapes, same `{ data, error, isLoading,\nisFetching, refetch }` return — plus cache sharing, request dedup,\nbackground refetch, and a `useInfiniteDonations` tier-exclusive hook\nbuilt on `useInfiniteQuery`.\n\nUse this tier when you want everything TanStack gives you for free.\nFor the no-cache, no-dedup variant see `@playlive/react-data`.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @tanstack/react-query\nbun add -d react\n```\n\n`react`, `@tanstack/react-query`, and `@playlive/fundraiser-data` are\n**peer dependencies** (jose-style — consumer brings their own).\n`@playlive/tiltify-core` is also listed as a peer because the hook\ntypes reference Tiltify domain types; the value imports are stripped\nat compile time so nothing of it ships in this package's bundle.\n\nNo `react-dom` — these hooks render nothing.\n\n## Quick start\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data/config\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useFlattenedDonations,\n  useMilestones,\n} from \"@playlive/react-query\";\n\n// Configure fundraiser-data once at app boot.\nconfigure({ tiltifyProxyUrl: \"https://proxy.example\" });\n\n// Build a QueryClient pre-seeded with the workspace defaults\n// (5.1 s staleTime, 5 s polling, 10 retries — see table below).\nconst qc = makeQueryClient();\n\nfunction App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <Overlay id=\"abc-123\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ id }: { id: string }) {\n  const campaign = useCampaign(\n    { charityType: \"tiltify\", id },\n    { refetchInterval: 5_000 },\n  );\n  const donations = useFlattenedDonations({ campaignId: id });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId: id });\n\n  if (campaign.isLoading) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n\n  return (\n    <pre>\n      {JSON.stringify(\n        { campaign: campaign.data, donations: donations.data, milestones: milestones.data },\n        null,\n        2,\n      )}\n    </pre>\n  );\n}\n```\n\n### Swapping out for the no-TanStack tier\n\nEvery hook in this package has an API-compatible counterpart in\n`@playlive/react-data`. Migration is a single import rewrite:\n\n```diff\n-import { useCampaign } from \"@playlive/react-query\";\n+import { useCampaign } from \"@playlive/react-data\";\n```\n\nSame params. Same return shape. Drop the `<QueryClientProvider>` if\nnothing else in your tree needs it. The behavioral difference is\nthat you lose cache sharing across components, request dedup, and\nbackground refetch-on-focus — gain a smaller bundle and zero\nTanStack dep.\n\n`useInfiniteDonations` is **tier-exclusive to `@playlive/react-query`**\n— `@playlive/react-data` only ships the one-shot\n`useFlattenedDonations` (walks the cursor internally with a `maxPages`\nguard). Migration path for infinite scroll: keep `@playlive/react-query`.\n\n## Subpath exports\n\n| Subpath                          | Description                                                                            |\n| -------------------------------- | -------------------------------------------------------------------------------------- |\n| `@playlive/react-query`          | Default barrel — every hook + `makeQueryClient` + `DEFAULT_QUERY_OPTIONS` + types.     |\n| `@playlive/react-query/config`   | `DEFAULT_QUERY_OPTIONS` + `makeQueryClient` factory only.                              |\n| `@playlive/react-query/types`    | `UseFetchResult` + `UseFetchOptions` + `UseInfiniteDonationsResult`.                   |\n\nEach subpath ships an ESM bundle, a Bun source condition, and `.d.ts`\ndeclarations. Tree-shaking removes unused exports from the consumer's\nbundle.\n\n## API reference\n\n### Standard hooks\n\n| Hook                  | Returns                                                                | Disabled when           |\n| --------------------- | ---------------------------------------------------------------------- | ----------------------- |\n| `useCampaign`         | `Tiltify(Campaign \\| PersonalCampaign \\| TeamCampaign) \\| null`        | never (always enabled)  |\n| `useCampaigns`        | `Array<TiltifyCampaign \\| … \\| null>` (per-row errors)                 | per-row: `id` nullish   |\n| `useFlattenedDonations` | `TiltifyDonation[]`                                                  | `campaignId` is nullish |\n| `useMilestones`       | `TiltifyMilestone[]`                                                   | `campaignId` is nullish |\n| `useRewards`          | `TiltifyReward[]`                                                      | `campaignId` is nullish |\n| `usePolls`            | `TiltifyPoll[]`                                                        | `campaignId` is nullish |\n| `useTargets`          | `TiltifyTarget[]`                                                      | `campaignId` is nullish |\n| `useSchedule`         | `TiltifySchedule[]`                                                    | `campaignId` is nullish |\n| `useUser`             | `TiltifyUser \\| null`                                                  | `userSlug` is empty     |\n| `useTeam`             | `TiltifyTeam \\| null`                                                  | `teamSlug` is empty     |\n| `useFundraisingEvent` | `TiltifyFundraisingEvent \\| null`                                      | `eventId` is nullish    |\n| `useCause`            | `TiltifyCause \\| null`                                                 | `causeId` is nullish    |\n| `useEventCampaigns`   | `TiltifyCampaign[]`                                                    | `eventId` is nullish    |\n| `useCurrentEvents`    | `TiltifyFundraisingEvent[]` — cause-level list, season-filtered unless `currentOnly: false` | never (always enabled) |\n| `useTiltifyUserCampaigns` | `TiltifyPersonalCampaign[]`                                        | `userId` is nullish / `\"null\"` |\n| `useTiltifyUserAndTeamCampaigns` | `(TiltifyPersonalCampaign \\| TiltifyTeamCampaign)[]`            | `userId` is nullish / `\"null\"` |\n| `useScheduleBlockRaised` | `ScheduleBlockRaised`                                               | any of `campaignId` / `start` / `end` nullish |\n| `useLifetimeRaised`   | `number \\| null`                                                       | `username` is nullish   |\n| `usePreviousYearTotals` | `PreviousYearTotalItem[]`                                            | `slug` is nullish       |\n| `useDonorSpotlightOverview` | `DonorSpotlightSnapshot \\| null` — REST-only snapshot (see note) | `campaignId` is nullish |\n| `useLeaderboardExclusions` | `{ data, donorNames, addExclusion, removeExclusion, … }`          | `campaignID` is empty   |\n| `useLeaderboardWithExclusions` | `TiltifyLeaderboardEntry[]`                                    | `campaignID` is empty   |\n| `useTiltifyLeaderboard` | `{ entries, pages, fetchNextPage, hasNextPage, … }`                  | `campaignId` is empty   |\n| `useLeaderboard`      | `{ leaderboard, donations, currentTotal, exclusions, … }`              | `campaignId` is empty   |\n| `useDonationTrains`   | `DonationTrain[]`                                                      | `campaignID` is empty   |\n| `useDonationTrainHighRateDonors` | `DonationTrainHighRateDonor[]`                              | `campaignID` is empty   |\n| `useDonationTrainCommonTrains`   | `CommonDonationTrain[]`                                     | `campaignID` is empty   |\n| `useCampaignRulesets` | `DonationTrainRuleset[]`                                               | `campaignID` is empty   |\n| `useDonationTrainState` | `{ trains, updateTrainStatus, updateTrainVisibility, highRateDonors, commonTrains, isLoading }` | never |\n| `useCampaignRulesetsState` | `{ rulesets, updateRuleset, deleteRuleset, createRuleset }`      | never                   |\n\nDonation-train **mutation** hooks — same shape as TanStack Query's\n`useMutation`, projected into the package's `UseMutationResult`:\n`useUpdateTrainVisibility`, `useRefreshTrainStatus`,\n`useProcessDonationsForTrains`, `useCreateCampaignRuleset`,\n`useUpdateRuleset`, `useDeleteRuleset`. Each accepts a variables\nobject that matches the underlying fundraiser-data fetcher's params\n(minus the `signal`).\n\n`useTestDonations` is the one non-train mutation hook: it fires a\nsynthetic `TiltifyDonation` (or array) through the core REST API so\nalerts, donation trains, the subathon timer and every WebSocket\nsubscriber react as if Tiltify had delivered it. Pass `adminApiKey`\n**or** `tiltifyOAuthToken`; demo campaigns need neither.\n\n> **`useDonorSpotlightOverview` vs `useDonorSpotlight`** — this package\n> ships the plain-REST snapshot hook (`…Overview`), for dashboards and\n> editors. `@playlive/react-pipeline/fusion` ships `useDonorSpotlight`,\n> which fuses the same REST baseline with live WebSocket updates — use\n> that one on surfaces that already hold a pipeline connection.\n\nAll Twitch-only-unsupported entities (`useMilestones`, `useRewards`,\n`usePolls`, `useTargets`, `useSchedule`, `useUser`, `useTeam`,\n`useFundraisingEvent`, `useCause`, `useEventCampaigns`) resolve to `[]`\n/ `null` on the Twitch path rather than throwing — same lenient\nsemantics as the underlying fetchers.\n\n`useFlattenedDonations` walks the whole donation history by default\n(100 pages × 100 rows). Surfaces that only need a recent slice should\ncap it — `useFlattenedDonations({ campaignId, count: 50, maxPages: 1 })`.\nBoth fields participate in the query key, so a capped consumer and a\nfull-history consumer don't share a cache entry.\n\n### Tier-exclusive: `useInfiniteDonations`\n\nCursor-aware paginated donations hook over TanStack's\n`useInfiniteQuery`. Tiltify uses an opaque string cursor; Twitch uses\na numeric page index — the hook dispatches to the matching\nplatform-specific fetcher under the hood and now reads **both**\ncursor shapes so Twitch pagination works end-to-end.\n\n```tsx\nconst {\n  data,                    // { pages: PaginatedResponse<…>[]; pageParams: […] } | undefined\n  fetchNextPage,           // () => Promise<void>\n  fetchPreviousPage,       // () => Promise<void>   — Tiltify only\n  hasNextPage,             // boolean\n  hasPreviousPage,         // boolean               — always false on Twitch path\n  isFetchingNextPage,      // boolean\n  isFetchingPreviousPage,  // boolean\n  isLoading,\n  isFetching,\n  error,\n  refetch,\n} = useInfiniteDonations({\n  charityType: \"tiltify\",\n  campaignId,\n});\n\nuseEffect(() => {\n  if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n}, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n```\n\n**Cursor semantics:**\n\n- `getNextPageParam` reads `lastPage.metadata.after` (Tiltify) **and** `lastPage.metadata.nextPage` (Twitch) — previously only the Tiltify cursor was consulted, so Twitch pagination silently stopped after page 1.\n- `getPreviousPageParam` reads Tiltify's `lastPage.metadata.before`. The Twitch charity donations endpoint doesn't expose a reverse cursor at the wire level, so `hasPreviousPage` is permanently `false` on that path and `fetchPreviousPage` no-ops.\n\n**Query-key partitioning.** `queryKey` includes `config.completedBefore` / `config.completedAfter` (so two hooks watching the same campaign with different date-range filters don't share pages) and `options.cachingEnabled` (see the options table below).\n\nDisabled when `campaignId` is nullish.\n\n### Leaderboards\n\nFour hooks that back overlays consuming the Play Live leaderboard\nservice (UDP `pl-leaderboard-api`) plus Tiltify's donor-leaderboard\nendpoint. Each fills a distinct slot:\n\n| Hook | Use when… |\n| ---- | -------- |\n| `useLeaderboardExclusions`     | You need to read + mutate the donor-name exclusion list (admin dashboards, moderation UIs). |\n| `useLeaderboardWithExclusions` | You want the campaign's leaderboard with exclusions already applied server-side. |\n| `useTiltifyLeaderboard`        | You want the *unfiltered* Tiltify leaderboard, cursor-aware. |\n| `useLeaderboard`               | You want a donation-derived leaderboard composed from `useInfiniteDonations` + `useLeaderboardExclusions` (matches the historical overlay-vite behavior). |\n\n#### `useLeaderboardExclusions`\n\n```tsx\nconst {\n  data,             // LeaderboardExclusion[] | undefined\n  donorNames,       // string[] projection — handy for `.includes(name)` guards\n  isMutating,\n  addExclusion,     // (donorName: string) => Promise<LeaderboardExclusion>\n  removeExclusion,  // (donorName: string) => Promise<LeaderboardExclusion>\n  refetch,\n} = useLeaderboardExclusions(\n  { campaignID },\n  { adminApiKey: process.env.ADMIN_KEY },\n  //     └─ or { tiltifyOAuthToken: token } for campaign-owner clients\n);\n```\n\nRead is public (`GET /leaderboard-exclusions/{id}`); mutations\n(`POST` / `DELETE`) accept `adminApiKey` (sent as `x-api-key`) **or**\n`tiltifyOAuthToken` (sent as `Authorization: OAuth <token>`).\nSuccessful mutations invalidate the read so the UI picks up the new\nlist without polling. Auth fields are stripped off the merged\n`authAndOptions` bag before options forward to TanStack.\n\nAuto-disables when `campaignID` is empty.\n\n#### `useLeaderboardWithExclusions`\n\n```tsx\nconst { data, isLoading, refetch } = useLeaderboardWithExclusions({\n  charityType: \"tiltify\",\n  campaignID,\n  timeType: \"all\",       // or \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"ytd\"\n  count: 100,\n  // — or — supply an ad-hoc window (switches the service to a SQL-aggregation path)\n  // startDate: new Date(\"2025-01-01\"),\n  // endDate:   new Date(\"2025-12-31\"),\n});\n```\n\nTwitch path returns `[]`. `queryKey` partitions on every parameter so\nconsecutive window flips don't collide.\n\n#### `useTiltifyLeaderboard`\n\n```tsx\nconst {\n  entries,             // TiltifyLeaderboardEntry[] flattened across every fetched page\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useTiltifyLeaderboard({ campaignId, timeType: \"all\", limit: 50 });\n```\n\nSame surface shape as `useInfiniteDonations` plus the flat `entries`\nprojection. Use for the unfiltered Tiltify view — swap to\n`useLeaderboardWithExclusions` when the exclusion list should apply.\n\n#### `useLeaderboard`\n\n```tsx\nconst {\n  leaderboard,           // LeaderboardRow[], ranked + capped\n  donations,             // every donation aggregated (pre-limit)\n  currentTotal,          // sum of every donation's amount.value (pre-exclusion)\n  exclusions,            // string[] used for prefiltering\n  hasNextPage,\n  isFetchingNextPage,\n  …,\n} = useLeaderboard({\n  charityType: \"tiltify\",\n  campaignId,\n  limit: 10,             // 0 returns every donor\n  prefilterExclusions: true,\n  removeAnonymous: true,\n  eagerFetchPages: true, // walks the cursor via useEffect — default\n});\n```\n\nComposes `useInfiniteDonations` + `useLeaderboardExclusions`.\nAggregates `amount.value` per donor id, sorts desc, caps by `limit`.\n`eagerFetchPages` (default `true`) makes the leaderboard converge\nwithout the caller wiring `fetchNextPage`.\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                  |\n| ----------------- | ------- | ---------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. Toggling flips state.                            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                            |\n| `retry`           | `10`    | Retries on error. Pass `false` (or `0`) in tests / for 404-legit hooks.      |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff.                       |\n| `staleTime`       | `5_100` | Workspace default; pass `0` to disable freshness windows entirely.           |\n| `initialData`     | —       | Seed the query with pre-fetched data (TanStack `initialData`). Route loaders should pass this so first paint shows real data instead of the loading state. Typed `unknown` — cast at the call site. For `useCampaigns` may be an **array** indexed 1:1 against `params` for per-row seeding, or a **scalar** shared across rows. |\n| `maxPages`        | —       | Retention cap for `useInfiniteQuery`. Only meaningful for `useInfiniteDonations` / `useTiltifyLeaderboard` — other hooks ignore it. TanStack v5 drops the oldest page when the limit is hit. |\n| `cachingEnabled`  | —       | Partition the `queryKey` by a boolean flag. Two hook instances that pass different values get separate cache slots — useful when the same campaign is fetched with and without cache-busting query params. Contributes to the key only; the fetcher is unchanged. |\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins).\nExported from `@playlive/react-query/config` for direct reuse.\n\n| Option                 | Value   | Rationale                                                       |\n| ---------------------- | ------- | --------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.         |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                                 |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.         |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.               |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.            |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right semantic for live.   |\n\n### Result (`UseFetchResult<T>`)\n\n```ts\n{\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isFetching: boolean;\n  refetch: () => Promise<void>;\n}\n```\n\n### Result (`UseInfiniteDonationsResult<TPage>`)\n\n```ts\n{\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  isFetchingPreviousPage: boolean;\n  hasNextPage: boolean;\n  hasPreviousPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  fetchPreviousPage: () => Promise<void>;\n  refetch: () => Promise<void>;\n}\n```\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc\nsite at `dist/docs/`.\n\n## Upstream spec\n\nNo external API surface. Every endpoint is reached transitively\nthrough `@playlive/fundraiser-data`'s `configure()` — see that\npackage's README for the proxy + Twitch-service URL knobs.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this\npackage can fetch. **It is empty.** This package doesn't hardcode any\nproduction hosts — every endpoint is reached transitively through\n`@playlive/fundraiser-data`. Add that package's URLs (plus your own\n`tiltifyProxyUrl` + `twitchServiceUrl` overrides) to your Extension\nsubmission's URL disclosure list.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\nconsole.log(KNOWN_URLS); // []\n```\n\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\nLive overlays consuming these hooks live under `apps/*` once they're\nscaffolded (phase 10). Until then, see the Quick-start snippet above.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook\nthat mirrors a fundraiser-data fetcher, run the `add-react-hook`\nagent skill (shared with `@playlive/react-data` — pick the\ntarget package via prompt).\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live\nCodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.4.1.tgz","shasum":"387aa3d8463013ad142f7cfef1684b0c64aac125","integrity":"sha512-sWo7UvFQ1WaSKqOhUebC1u3CDVaSl2CLZIOioVKzwzHm+uwOHQm9iZgxto84E0b0F52sxOcXncV65UZMm2YGOw=="}},"0.4.3":{"name":"@playlive/react-query","version":"0.4.3","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.5.3","@playlive/tiltify-core":"^0.4.18"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-iFXaHELyOfSztll4VKVhCc0LHGq5fLiRQ32uYZFrGajkH4E2MKBS766KApgMIADq7yQYShRDOkIeCN7V+vgzXw==","shasum":"afc1f660f691c07444be6088977873a91c57472e","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/) — campaigns,\ndonations, milestones, leaderboards, donation trains, and the Play Live first-party\nservices, each wrapped in a `useQuery` / `useInfiniteQuery` / `useMutation` with\nworkspace-tuned defaults. The 19 hooks that also exist in\n[`@playlive/react-data`](../react-data/) keep identical names, parameter shapes, and\nreturn shapes, so those call sites swap tiers with a single import rewrite.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @playlive/tiltify-core @tanstack/react-query react\n```\n\nAll four are **peer dependencies** (jose-style — the consumer brings their own). None\nare optional; `package.json` declares no `peerDependenciesMeta`:\n\n| Peer                       | Range          | Notes                                                                                               |\n| -------------------------- | -------------- | --------------------------------------------------------------------------------------------------- |\n| `react`                    | `^19.0.0`      | Hooks only — no `react-dom`, nothing here renders.                                                    |\n| `@tanstack/react-query`    | `^5.0.0`       | v5 API (`isPending`, `initialPageParam`, object-form `useQuery`). v4 will not work.                   |\n| `@playlive/fundraiser-data`| `workspace:*`  | Every fetcher this package wraps. Must be `configure()`d at boot.                                     |\n| `@playlive/tiltify-core`   | `workspace:*`  | Type-only at runtime — the hooks module imports it under `import type`, so nothing of it ships in the emitted bundle. Still a required peer because the emitted `.d.ts` references `TiltifyCampaign`, `TiltifyDonation`, etc. |\n\n`dependencies` is empty. `sideEffects: false`, so unused hooks tree-shake out.\n\n## Quick start\n\nThree steps: `configure()` the data layer, build a `QueryClient`, mount the provider.\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data\";\nimport { makeQueryClient, useCampaign, useCampaignAmounts } from \"@playlive/react-query\";\n\n// 1. Point the data layer at the Tiltify proxy (once, at module scope).\nconfigure({ tiltifyProxyUrl: \"https://api.experience.stjude.org/tiltify\" });\n\n// 2. A QueryClient pre-seeded with the workspace defaults\n//    (5.1 s staleTime, 5 s polling, 10 retries — see the table below).\nconst qc = makeQueryClient();\n\n// 3. Provide it above every component that calls a hook.\nexport function App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <ProgressBar campaignId=\"demo-campaign-a\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction ProgressBar({ campaignId }: { campaignId: string }) {\n  const campaign = useCampaign({ charityType: \"tiltify\", id: campaignId });\n  const { totalAmount, goalAmount } = useCampaignAmounts(campaign.data);\n\n  if (campaign.isPending) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n  if (!campaign.data) return <p>No campaign.</p>;\n\n  const percent = goalAmount > 0 ? (totalAmount / goalAmount) * 100 : 0;\n  return (\n    <figure>\n      <figcaption>{campaign.data.name}</figcaption>\n      <progress value={totalAmount} max={goalAmount} />\n      <span>{percent.toFixed(1)}%</span>\n    </figure>\n  );\n}\n```\n\n`configure()` is re-exported from both the `@playlive/fundraiser-data` barrel and its\n`/config` subpath. `tiltifyProxyUrl` is the only required field; `twitchServiceUrl`,\n`causeId`, `scheduleApiUrl`, `lifetimeApiUrl`, `leaderboardApiUrl`,\n`donorSpotlightApiUrl`, and `donationTrainApiUrl` are optional and gate the hooks that\ntalk to those services.\n\n### `isPending` vs `isLoading`\n\nBoth are passed straight through from TanStack Query v5, and they differ in exactly one\ncase that matters here: a hook whose id is nullish is **disabled**, which leaves\n`isPending: true` forever while `isLoading` is `false`. Branch on `isPending` when you\nwant \"we have neither data nor an error yet\"; branch on `isLoading` when you want \"a\nfirst fetch is actually in flight\".\n\n### How this differs from `@playlive/react-data`\n\n`@playlive/react-data` implements its hooks on a single `useFetch` primitive built from\n`useState` / `useRef` — the state lives inside the calling component. Two components\nasking for the same campaign issue two independent requests and hold two copies. This\npackage hands the same work to a `QueryClient`, which buys:\n\n- **Cache sharing + dedupe** — hooks that build the same `queryKey` share one cache\n  entry and one in-flight request. `useCampaigns` rows de-dupe against sibling\n  `useCampaign` consumers for free.\n- **Cursor pagination** — `useInfiniteDonations` / `useTiltifyLeaderboard` sit on\n  `useInfiniteQuery`; the dependency-free tier has no equivalent.\n- **Mutations with invalidation** — `useLeaderboardExclusions` invalidates its own read\n  on a successful add/remove, so the list updates without waiting for a poll.\n- **Devtools** — every query is a plain TanStack query, so\n  `@tanstack/react-query-devtools` can inspect them. This package does not bundle or\n  re-export devtools; add them yourself if you want them.\n- **26 extra hooks** — leaderboards, donation trains, alerts, and the Play Live\n  first-party services only exist in this tier (see the API reference).\n\nThe cost is the `@tanstack/react-query` peer in your bundle. If you only need the 19\nshared hooks and want the smaller graph, use `@playlive/react-data`.\n\n## Subpath exports\n\n| Subpath                       | Contents                                                                                          |\n| ----------------------------- | --------------------------------------------------------------------------------------------------- |\n| `@playlive/react-query`       | Default barrel — re-exports `/config`, every hook, `/types`, plus `PACKAGE_NAME` and `KNOWN_URLS`.  |\n| `@playlive/react-query/config`| `DEFAULT_QUERY_OPTIONS` + `makeQueryClient`.                                                        |\n| `@playlive/react-query/types` | `UseFetchOptions`, `UseFetchResult`, `UseInfiniteDonationsResult`, `HookRefetchOptions`.            |\n\nThat is the complete `exports` map — there is no `/hooks` subpath; import hooks from the\nbarrel. Each entry ships an ESM bundle, a `bun` source condition pointing at `src/`, and\n`.d.ts` declarations.\n\n## API reference\n\n### Query hooks — standard `UseFetchResult<T>`\n\nEvery row takes `(params, options?: UseFetchOptions)` and returns\n`UseFetchResult<T>` (`{ data, error, isLoading, isPending, isFetching, refetch }`).\n\"Auto-disabled\" lists the guard that flips `enabled` to `false`.\n\n| Hook                             | `data`                                                          | Auto-disabled when                              |\n| -------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------- |\n| `useCampaign`                    | `TiltifyCampaign \\| TiltifyPersonalCampaign \\| TiltifyTeamCampaign \\| null` | never                             |\n| `useFlattenedDonations`          | `TiltifyDonation[]`                                               | `campaignId` nullish                              |\n| `useMilestones`                  | `TiltifyMilestone[]`                                              | `campaignId` nullish                              |\n| `useRewards`                     | `TiltifyReward[]`                                                 | `campaignId` nullish                              |\n| `usePolls`                       | `TiltifyPoll[]`                                                   | `campaignId` nullish                              |\n| `useTargets`                     | `TiltifyTarget[]`                                                 | `campaignId` nullish                              |\n| `useSchedule`                    | `TiltifySchedule[]`                                               | `campaignId` nullish                              |\n| `useUser`                        | `TiltifyUser \\| null`                                             | `userSlug` empty                                  |\n| `useTeam`                        | `TiltifyTeam \\| null`                                             | `teamSlug` empty                                  |\n| `useFundraisingEvent`            | `TiltifyFundraisingEvent \\| null`                                 | `eventId` nullish                                 |\n| `useFundraisingEventMilestones`  | `TiltifyFactMilestone[]`                                          | `eventId` nullish                                 |\n| `useCause`                       | `TiltifyCause \\| null`                                            | `causeId` nullish                                 |\n| `useEventCampaigns`              | `TiltifyCampaign[]`                                               | `eventId` nullish                                 |\n| `useCurrentEvents`               | `TiltifyFundraisingEvent[]`                                       | never                                             |\n| `useTiltifyUserCampaigns`        | `TiltifyPersonalCampaign[]`                                       | `userId` nullish, empty, or the string `\"null\"`    |\n| `useTiltifyUserAndTeamCampaigns` | `(TiltifyPersonalCampaign \\| TiltifyTeamCampaign)[]`              | `userId` nullish, empty, or the string `\"null\"`    |\n| `useScheduleBlockRaised`         | `ScheduleBlockRaised \\| undefined`                                | any of `campaignId` / `start` / `end` nullish     |\n| `useLifetimeRaised`              | `number \\| null`                                                  | `username` nullish or empty                       |\n| `usePreviousYearTotals`          | `PreviousYearTotalItem[]`                                         | `slug` nullish or empty                           |\n| `useGiftsThatGiveMilestones`     | `GiftsThatGiveMilestone[]`                                        | `goal` nullish                                    |\n| `useDonorSpotlightOverview`      | `DonorSpotlightSnapshot \\| null`                                  | `campaignId` nullish or empty                     |\n| `useLeaderboardWithExclusions`   | `MonetaryLeaderboardEntry[]`                                      | `campaignID` nullish or empty                     |\n| `useDonationTrains`              | `DonationTrain[]`                                                 | `campaignID` nullish or empty                     |\n| `useDonationTrainHighRateDonors` | `DonationTrainHighRateDonor[]`                                    | `campaignID` nullish or empty                     |\n| `useCampaignRulesets`            | `DonationTrainRuleset[]`                                          | `campaignID` nullish or empty                     |\n| `useDonationTrainCommonTrains`   | `CommonDonationTrain[]`                                           | `campaignID` nullish or empty                     |\n\n`useRewards` and `useTargets` additionally accept `sort?: boolean` in `params`.\n`useFundraisingEventMilestones` takes an optional `charityType` that defaults to\n`\"tiltify\"`. The donation-train and leaderboard-exclusion hooks take\n`(params, authAndOptions?)` where the second argument is\n`DonationTrainAuthOptions & UseFetchOptions` (or `LeaderboardAuthOptions & UseFetchOptions`) —\n`adminApiKey` / `tiltifyOAuthToken` are split out and never reach TanStack.\n\nTwitch-unsupported entities (`useMilestones`, `useRewards`, `usePolls`, `useTargets`,\n`useSchedule`, `useUser`, `useTeam`, `useFundraisingEvent`, `useFundraisingEventMilestones`,\n`useCause`, `useEventCampaigns`, `useLeaderboardWithExclusions`) resolve to `[]` / `null`\non the `charityType: \"twitch\"` path rather than throwing — same lenient semantics as the\nunderlying fetchers.\n\n**Per-hook cadence overrides.** Some hooks override `DEFAULT_QUERY_OPTIONS` because their\nupstream data doesn't change every five seconds. Pass `options.refetchInterval` /\n`options.staleTime` to win back control:\n\n| Hook                                                          | `staleTime` | `refetchInterval` |\n| ------------------------------------------------------------- | ----------- | ----------------- |\n| `useTiltifyUserCampaigns`, `useTiltifyUserAndTeamCampaigns`    | `Infinity`  | `false`           |\n| `useScheduleBlockRaised` (also `refetchOnReconnect: false`)    | `Infinity`  | `false`           |\n| `useLifetimeRaised`                                           | `5_100`     | `60_000`          |\n| `usePreviousYearTotals`                                        | `5_100`     | `5 * 60_000`      |\n| `useDonationTrains`, `useCampaignRulesets`                     | `5_100` / `5_000` | `15_000`    |\n| `useDonationTrainHighRateDonors`, `useDonationTrainCommonTrains` | `5_100`   | `25_000`          |\n\n### Query hooks with a custom return shape\n\n| Hook                                | Returns                                                                                                    |\n| ----------------------------------- | ------------------------------------------------------------------------------------------------------------ |\n| `useCampaigns`                      | `UseCampaignsResult` — `{ data[], errors[], isLoading, isPending, isFetching, isError, refetch }`, ordered 1-for-1 with the `params` array. Built on `useQueries` + `combine`. Rows are **never** auto-disabled; a row with no id still fires. |\n| `useInfiniteDonations`              | `UseInfiniteDonationsResult<PaginatedResponse<TiltifyDonation, TiltifyPaginationMetadata \\| TwitchPaginationMetadata>>` |\n| `useTiltifyLeaderboard`             | `UseTiltifyLeaderboardResult` — `{ entries, data, error, isLoading, isPending, isFetching, isFetchingNextPage, hasNextPage, fetchNextPage, refetch }`. Forward-only; no `fetchPreviousPage`. |\n| `useLeaderboardExclusions`          | `UseLeaderboardExclusionsResult` — `{ data, donorNames, error, isLoading, isPending, isFetching, isMutating, addExclusion, removeExclusion, refetch }` |\n| `useLeaderboard`                    | `UseLeaderboardResult` — `{ leaderboard, donations, currentTotal, exclusions, isLoadingExclusions, isLoadingDonations, isFetching, isFetchingNextPage, hasNextPage, error }`. Note: no `isLoading`, no `refetch`. |\n| `useCampaignGiftsThatGiveMilestones`| `{ giftMilestones: GiftsThatGiveMilestoneItem[]; isLoading: boolean }`                                       |\n| `useDonationTrainState`             | `UseDonationTrainStateResult` — `{ trains, updateTrainStatus, updateTrainVisibility, highRateDonors, commonTrains, isLoading }` |\n| `useCampaignRulesetsState`          | `UseCampaignRulesetsStateResult` — `{ rulesets, updateRuleset, deleteRuleset, createRuleset }`                |\n\n### Non-query hooks (pure client-side)\n\nThese issue no requests — they're `useMemo` / `useState` / `setTimeout` helpers that\nhappen to live here so overlays can import one package.\n\n| Hook                                  | Signature                                                                                              |\n| ------------------------------------- | -------------------------------------------------------------------------------------------------------- |\n| `useCampaignAmounts`                  | `(campaignOrFundraiser: CampaignLike \\| null \\| undefined) => CampaignAmounts` — reference-stable `{ totalAmount, currentAmount, goalAmount, originalGoalAmount, supportingAmount }`. |\n| `useCampaignFundraisingEventAmounts`  | `(campaign, fundraisingEvent, options?: { forceCampaignGoal?: boolean }) => CampaignFundraisingEventAmounts` |\n| `useDonationsReducer`                 | `(donationsData: DonationPagesInput \\| null \\| undefined, flipSorting = false, onlyReturnNew = false) => TiltifyDonation[]` — flattens a `{ pages, pageParams }` envelope; sticky-accumulates by default so rows survive `maxPages` trimming. |\n| `useAlertsQueue`                      | `(params: UseAlertsQueueParams, options?: UseAlertsQueueOptions) => { donation: TiltifyDonation \\| null }` — display-timing loop over an external donation queue. |\n\n### Mutation hooks\n\nEach returns `UseMutationResult<TData, TVariables>` —\n`{ mutate, mutateAsync, data, error, isPending, reset }`, a projection of TanStack's\n`useMutation` so consumers don't need TanStack's types.\n\n| Hook                           | Argument               | `TVariables`                                              | `TData`                        |\n| ------------------------------ | ---------------------- | ----------------------------------------------------------- | ------------------------------ |\n| `useUpdateTrainVisibility`     | `auth?`                | `{ trainID: string; trainVisible: boolean }`                | `DonationTrain \\| null`        |\n| `useRefreshTrainStatus`        | —                      | `{ trainID: string }`                                       | `DonationTrain \\| null`        |\n| `useProcessDonationsForTrains` | —                      | `{ donations: TiltifyDonation[] }`                          | `DonationTrain[]`              |\n| `useCreateCampaignRuleset`     | `auth?`                | `{ campaignID: string; ruleset: PartialDonationTrainRuleset }` | `DonationTrainRuleset \\| null` |\n| `useUpdateRuleset`             | `auth?`                | `{ ruleset: DonationTrainRuleset; campaignID?: string }`    | `DonationTrainRuleset`         |\n| `useDeleteRuleset`             | `auth?`                | `{ rulesetID: string; campaignID?: string }`                | `{ id: string } \\| null`       |\n| `useTestDonations`             | `auth?`                | `TiltifyDonation \\| TiltifyDonation[]`                      | `void`                         |\n\n`auth` is `DonationTrainAuthOptions` (`TestDonationAuthOptions` for `useTestDonations`) —\npass `adminApiKey` **or** `tiltifyOAuthToken`. `useUpdateRuleset` / `useDeleteRuleset`\npick their route from the presence of `campaignID`: with it, the campaign-scoped route\n(admin key or OAuth); without it, the admin-only legacy route.\n\n`useTestDonations` fires a synthetic donation through the core REST API\n(`POST /donations/tiltify/test`), which replays it down the same webhook path a real\ndonation takes — alerts, donation trains, the subathon timer, and every WebSocket\nsubscriber react as if Tiltify had delivered it. Demo campaigns skip auth entirely.\n\n### Constants\n\n| Export                       | Kind    | Value                                                                        |\n| ---------------------------- | ------- | ------------------------------------------------------------------------------ |\n| `PACKAGE_NAME`               | `const` | `\"@playlive/react-query\"` — for runtime version-pinning checks.                |\n| `KNOWN_URLS`                 | `const` | `readonly string[]`, frozen and **empty**. See the URL-disclosure section.      |\n| `DEFAULT_QUERY_OPTIONS`      | `const` | Workspace query defaults (table below). Also on `/config`.                     |\n| `DEFAULT_ALERT_DURATIONS_MS` | `const` | `Readonly<Record<DonorLevel, number>>` — `platinum` 9500, `gold` 8500, `silver` 7500, `bronze` 6500, `grey` 5500. Merged under `useAlertsQueue`'s `durations` override. |\n\n### Functions\n\n| Export             | Kind       | Signature                                                     |\n| ------------------ | ---------- | --------------------------------------------------------------- |\n| `makeQueryClient`  | `function` | `(overrides?: QueryClientConfig) => QueryClient` — builds a client whose `defaultOptions.queries` is `DEFAULT_QUERY_OPTIONS` with `overrides.defaultOptions.queries` merged on top. |\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                   |\n| ----------------- | ------- | ------------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. ANDed with the hook's own nullish guard.            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                              |\n| `retry`           | `10`    | Retries on error. Pass `false` (or `0`) in tests, or for hooks whose id may legitimately 404. |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff on top.                   |\n| `staleTime`       | `5_100` | Freshness window. Pass `0` to make every read refetch.                          |\n| `initialData`     | —       | Seed the query (TanStack `initialData`) so a route loader's payload paints immediately. Typed `unknown`; cast at the call site. For `useCampaigns` an **array** seeds rows 1:1 against `params`, a scalar seeds every row. |\n| `maxPages`        | —       | Retained-page cap. Honoured only by `useInfiniteDonations` and `useTiltifyLeaderboard`; every other hook ignores it. TanStack v5 drops the oldest page at the limit. |\n| `cachingEnabled`  | —       | Boolean cache partition. Contributes to the `queryKey` of `useInfiniteDonations` / `useTiltifyLeaderboard` only — the fetcher is unchanged. |\n\n`initialData`, `maxPages`, and `cachingEnabled` are stripped from the merged options bag\nbefore it reaches TanStack and re-applied per hook with a narrow cast; without that,\n`initialData?: unknown` would collapse `TQueryFnData` inference to `unknown` everywhere.\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins), and\n`makeQueryClient()` installs the same object as the client's `defaultOptions.queries`.\n\n| Option                 | Value   | Rationale                                                   |\n| ---------------------- | ------- | ------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.       |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                               |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.       |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.             |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.          |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right live semantic.     |\n\n### Result types\n\nExported from the barrel and from `@playlive/react-query/types`:\n\n```ts\ninterface UseFetchResult<T> {\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  refetch: (options?: HookRefetchOptions) => Promise<void>;\n}\n\ninterface UseInfiniteDonationsResult<TPage> {\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  isFetchingPreviousPage: boolean;\n  hasNextPage: boolean;\n  hasPreviousPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  fetchPreviousPage: () => Promise<void>;\n  refetch: (options?: HookRefetchOptions) => Promise<void>;\n}\n\n/** Structural subset of TanStack v5's `RefetchOptions`. */\ninterface HookRefetchOptions {\n  cancelRefetch?: boolean; // default true — cancels an in-flight fetch first\n}\n```\n\n`refetch` is wrapped in a `useCallback` keyed on TanStack's own `refetch`, so its\nidentity is stable across renders — safe to list in a `useEffect` dep array without\nlooping.\n\nEvery hook also exports its `Use…Params` / `Use…Result` interface (`UseCampaignParams`,\n`UseInfiniteDonationsParams`, `UseLeaderboardParams`, `LeaderboardRow`,\n`GiftsThatGiveMilestoneItem`, `UseMutationResult`, …) from the barrel. Domain payload\ntypes (`TiltifyDonation`, `DonationTrain`, `MonetaryLeaderboardEntry`,\n`ScheduleBlockRaised`, …) are **not** re-exported — import them from\n[`@playlive/tiltify-core`](../tiltify/core/) and [`@playlive/fundraiser-data`](../fundraiser-data/).\n\nRun `bun run docs:build` inside this package to emit the full TypeDoc site at\n`dist/docs/`.\n\n## Upstream spec\n\nNo external API surface of its own. Every endpoint is reached transitively through\n`@playlive/fundraiser-data`'s `configure()` — see [that package's README](../fundraiser-data/)\nfor the Tiltify proxy, Twitch service, and Play Live service URL knobs, and for the\nupstream Tiltify / Twitch Helix spec coverage.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this package can fetch.\n**It is empty** — this package hardcodes no production hosts; every request goes through\na `@playlive/fundraiser-data` fetcher pointed at a URL you supplied to `configure()`.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\n\nconsole.log(KNOWN_URLS); // []\n```\n\nFor an Extension submission, disclose `@playlive/fundraiser-data`'s URLs plus whatever\nyou passed for `tiltifyProxyUrl`, `twitchServiceUrl`, and the Play Live service URLs.\nSee [docs/twitch-extension-checklist.md](../../docs/twitch-extension-checklist.md).\n\n## Examples\n\n### Full overlay — provider, config, three hooks, all three states\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useCampaignAmounts,\n  useMilestones,\n  useLeaderboard,\n} from \"@playlive/react-query\";\n\nconfigure({\n  tiltifyProxyUrl: \"https://api.experience.stjude.org/tiltify\",\n  leaderboardApiUrl: \"https://api.experience.stjude.org/leaderboard\",\n});\n\nconst queryClient = makeQueryClient();\n\nexport function OverlayRoot({ campaignId }: { campaignId: string }) {\n  return (\n    <QueryClientProvider client={queryClient}>\n      <Overlay campaignId={campaignId} />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ campaignId }: { campaignId: string }) {\n  const campaign = useCampaign({ charityType: \"tiltify\", id: campaignId });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId });\n\n  // Composes useInfiniteDonations + useLeaderboardExclusions; `eagerFetchPages`\n  // (default true) walks the cursor so the ranking sees every donation.\n  const { leaderboard, currentTotal, isLoadingDonations } = useLeaderboard({\n    charityType: \"tiltify\",\n    campaignId,\n    limit: 5,\n    removeAnonymous: true,\n  });\n\n  const { totalAmount, goalAmount } = useCampaignAmounts(campaign.data);\n\n  if (campaign.isPending) return <p>Connecting…</p>;\n  if (campaign.error) return <p role=\"alert\">Campaign failed: {campaign.error.message}</p>;\n  if (!campaign.data) return <p>Campaign not found.</p>;\n\n  return (\n    <section>\n      <h1>{campaign.data.name}</h1>\n      <progress value={totalAmount} max={goalAmount} />\n\n      <ul>\n        {(milestones.data ?? [])\n          .filter((m) => m.active)\n          .map((m) => (\n            <li key={m.id}>\n              {m.name} — ${m.amount.value}\n            </li>\n          ))}\n      </ul>\n\n      <h2>Top donors (${currentTotal.toFixed(2)} raised)</h2>\n      {isLoadingDonations ? (\n        <p>Tallying…</p>\n      ) : (\n        <ol>\n          {leaderboard.map((row) => (\n            <li key={row.id}>\n              {row.donor_name} — ${row.amount.toFixed(2)}\n            </li>\n          ))}\n        </ol>\n      )}\n    </section>\n  );\n}\n```\n\n### Dependent query + polling override\n\n`cause_id` lives on every campaign shape, so the cause lookup can chain off the campaign\nresult. Passing `undefined` for `causeId` keeps `useCause` disabled (and therefore\n`isPending`) until the first campaign resolves. The cause record is static reference\ndata, so polling is switched off for it.\n\n```tsx\nimport { useCampaign, useCause } from \"@playlive/react-query\";\n\nfunction CauseBadge({ campaignId }: { campaignId: string }) {\n  const campaign = useCampaign({ charityType: \"tiltify\", id: campaignId });\n\n  const cause = useCause(\n    { charityType: \"tiltify\", causeId: campaign.data?.cause_id },\n    { refetchInterval: false, staleTime: Number.POSITIVE_INFINITY },\n  );\n\n  if (!cause.data) return null;\n  return <img src={cause.data.avatar?.src} alt={cause.data.name} />;\n}\n```\n\n### Infinite donation feed\n\n`useInfiniteDonations` is the tier-exclusive cursor hook. Tiltify pages on an opaque\nstring cursor (`metadata.after`), Twitch on a numeric page index (`metadata.nextPage`);\n`getNextPageParam` reads both, so `charityType` can be a runtime value. Only Tiltify\nexposes a reverse cursor (`metadata.before`), so `hasPreviousPage` is permanently `false`\non the Twitch path and `fetchPreviousPage` no-ops there.\n\n`useDonationsReducer` flattens the `{ pages, pageParams }` envelope into a sorted array\nand — with `onlyReturnNew` left at `false` — keeps rows that `maxPages` has already\ntrimmed out of the cache, which is what a scrolling column wants.\n\n```tsx\nimport { useEffect } from \"react\";\nimport { useDonationsReducer, useInfiniteDonations } from \"@playlive/react-query\";\n\nfunction DonationFeed({ campaignId }: { campaignId: string }) {\n  const {\n    data,\n    fetchNextPage,\n    hasNextPage,\n    isFetchingNextPage,\n    isPending,\n    error,\n  } = useInfiniteDonations(\n    { charityType: \"tiltify\", campaignId, count: 50 },\n    { maxPages: 5 },\n  );\n\n  const donations = useDonationsReducer(data);\n\n  // Walk the cursor to the end once, then let the 5 s poll pick up new rows.\n  useEffect(() => {\n    if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n  }, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n\n  if (isPending) return <p>Loading donations…</p>;\n  if (error) return <p role=\"alert\">{error.message}</p>;\n\n  return (\n    <ul>\n      {donations.map((d) => (\n        <li key={d.id}>\n          {d.donor_name || \"Anonymous\"} — ${d.amount.value}\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n### Moderating the exclusion list\n\nThe read (`GET /leaderboard-exclusions/{id}`) is public; the `POST` / `DELETE` behind\n`addExclusion` / `removeExclusion` need `adminApiKey` (sent as `x-api-key`) or\n`tiltifyOAuthToken` (sent as `Authorization: OAuth <token>`). Auth keys are split out of\nthe second argument before the rest forwards to TanStack, and a successful mutation\ninvalidates the read so the list refreshes without waiting for a poll.\n\n```tsx\nimport { useLeaderboardExclusions } from \"@playlive/react-query\";\n\nfunction ExclusionEditor({ campaignID, token }: { campaignID: string; token: string }) {\n  const { donorNames, isMutating, addExclusion, removeExclusion } = useLeaderboardExclusions(\n    { campaignID },\n    { tiltifyOAuthToken: token, refetchInterval: 30_000 },\n  );\n\n  return (\n    <ul>\n      {donorNames.map((name) => (\n        <li key={name}>\n          {name}\n          <button type=\"button\" disabled={isMutating} onClick={() => void removeExclusion(name)}>\n            Remove\n          </button>\n        </li>\n      ))}\n      <li>\n        <button type=\"button\" disabled={isMutating} onClick={() => void addExclusion(\"Bob\")}>\n          Exclude Bob\n        </button>\n      </li>\n    </ul>\n  );\n}\n```\n\n### Swapping tiers\n\nThe 19 hooks below exist in both packages with identical params and identical\n`UseFetchResult` returns, so migrating those call sites is one import rewrite:\n\n```diff\n-import { useCampaign, useMilestones } from \"@playlive/react-query\";\n+import { useCampaign, useMilestones } from \"@playlive/react-data\";\n```\n\n<sub>`useCampaign`, `useCampaigns`, `useCause`, `useEventCampaigns`, `useFlattenedDonations`,\n`useFundraisingEvent`, `useGiftsThatGiveMilestones`, `useLifetimeRaised`, `useMilestones`,\n`usePolls`, `usePreviousYearTotals`, `useRewards`, `useSchedule`, `useScheduleBlockRaised`,\n`useTargets`, `useTeam`, `useTiltifyUserAndTeamCampaigns`, `useTiltifyUserCampaigns`, `useUser`.</sub>\n\nDrop `<QueryClientProvider>` if nothing else in the tree needs it. Everything outside\nthat list — infinite pagination, leaderboards, donation trains, mutations, alerts, and\nthe Play Live first-party services — is react-query-only; those call sites have to stay\non this tier. See [docs/overlay-data-layer-migration.md](../../docs/overlay-data-layer-migration.md)\nfor the wider migration story, and [`@playlive/react-pipeline`](../react-pipeline/) for\nthe fusion hooks that layer live WebSocket updates over these REST baselines.\n\n## Contributing\n\nSee [CONTRIBUTING.md](../../CONTRIBUTING.md). To scaffold a new hook that mirrors a\n`@playlive/fundraiser-data` fetcher, run the `add-react-hook` agent skill (shared with\n`@playlive/react-data` — pick the target package via prompt). Run `bun test` in this\npackage; `tests/render-hook.ts` mounts each hook inside a fresh `QueryClientProvider`\nwith retries and polling disabled.\n\n## License\n\nMIT — see [LICENSE](../../LICENSE). Distributed via Play Live CodeArtifact (PRD §6).\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.4.3.tgz","shasum":"afc1f660f691c07444be6088977873a91c57472e","integrity":"sha512-iFXaHELyOfSztll4VKVhCc0LHGq5fLiRQ32uYZFrGajkH4E2MKBS766KApgMIADq7yQYShRDOkIeCN7V+vgzXw=="}},"0.4.4":{"name":"@playlive/react-query","version":"0.4.4","description":"TanStack Query hooks over @playlive/fundraiser-data — API-compatible with @playlive/react-data. Twitch-Extension safe.","type":"module","sideEffects":false,"main":"./index.js","types":"./index.d.ts","exports":{".":{"import":"./index.js","types":"./index.d.ts"},"./config":{"import":"./config/index.js","types":"./config/index.d.ts"},"./types":{"import":"./types/index.js","types":"./types/index.d.ts"}},"dependencies":{},"peerDependencies":{"react":"^19.0.0","@tanstack/react-query":"^5.0.0","@playlive/fundraiser-data":"^0.5.4","@playlive/tiltify-core":"^0.4.19"},"playlive":{"target":"browser","frontendEligible":true,"coverageFloor":85},"publishConfig":{"access":"restricted","registry":"https://playlive-767397689694.d.codeartifact.us-east-1.amazonaws.com/npm/playlive/"},"integrity":"sha512-RML72Ou/fx8rPx0GwPualA+xAY//8Jmwgqc7lT5LKhnS2MhGbdt3VLH7J5T2k0oI4NlOFmG8zrOHp/BbU+RKXw==","shasum":"419d0254d44618f2ad5078e210e184af9dbea9ba","readme":"# @playlive/react-query\n\nTanStack Query hooks over [`@playlive/fundraiser-data`](../fundraiser-data/) — campaigns,\ndonations, milestones, leaderboards, donation trains, and the Play Live first-party\nservices, each wrapped in a `useQuery` / `useInfiniteQuery` / `useMutation` with\nworkspace-tuned defaults. The 19 hooks that also exist in\n[`@playlive/react-data`](../react-data/) keep identical names, parameter shapes, and\nreturn shapes, so those call sites swap tiers with a single import rewrite.\n\n![Coverage](./coverage-badge.svg)\n\n## Install\n\n```bash\nbun add @playlive/react-query @playlive/fundraiser-data @playlive/tiltify-core @tanstack/react-query react\n```\n\nAll four are **peer dependencies** (jose-style — the consumer brings their own). None\nare optional; `package.json` declares no `peerDependenciesMeta`:\n\n| Peer                       | Range          | Notes                                                                                               |\n| -------------------------- | -------------- | --------------------------------------------------------------------------------------------------- |\n| `react`                    | `^19.0.0`      | Hooks only — no `react-dom`, nothing here renders.                                                    |\n| `@tanstack/react-query`    | `^5.0.0`       | v5 API (`isPending`, `initialPageParam`, object-form `useQuery`). v4 will not work.                   |\n| `@playlive/fundraiser-data`| `workspace:*`  | Every fetcher this package wraps. Must be `configure()`d at boot.                                     |\n| `@playlive/tiltify-core`   | `workspace:*`  | Type-only at runtime — the hooks module imports it under `import type`, so nothing of it ships in the emitted bundle. Still a required peer because the emitted `.d.ts` references `TiltifyCampaign`, `TiltifyDonation`, etc. |\n\n`dependencies` is empty. `sideEffects: false`, so unused hooks tree-shake out.\n\n## Quick start\n\nThree steps: `configure()` the data layer, build a `QueryClient`, mount the provider.\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data\";\nimport { makeQueryClient, useCampaign, useCampaignAmounts } from \"@playlive/react-query\";\n\n// 1. Point the data layer at the Tiltify proxy (once, at module scope).\nconfigure({ tiltifyProxyUrl: \"https://api.experience.stjude.org/tiltify\" });\n\n// 2. A QueryClient pre-seeded with the workspace defaults\n//    (5.1 s staleTime, 5 s polling, 10 retries — see the table below).\nconst qc = makeQueryClient();\n\n// 3. Provide it above every component that calls a hook.\nexport function App() {\n  return (\n    <QueryClientProvider client={qc}>\n      <ProgressBar campaignId=\"demo-campaign-a\" />\n    </QueryClientProvider>\n  );\n}\n\nfunction ProgressBar({ campaignId }: { campaignId: string }) {\n  const campaign = useCampaign({ charityType: \"tiltify\", id: campaignId });\n  const { totalAmount, goalAmount } = useCampaignAmounts(campaign.data);\n\n  if (campaign.isPending) return <p>Loading…</p>;\n  if (campaign.error) return <p>Error: {campaign.error.message}</p>;\n  if (!campaign.data) return <p>No campaign.</p>;\n\n  const percent = goalAmount > 0 ? (totalAmount / goalAmount) * 100 : 0;\n  return (\n    <figure>\n      <figcaption>{campaign.data.name}</figcaption>\n      <progress value={totalAmount} max={goalAmount} />\n      <span>{percent.toFixed(1)}%</span>\n    </figure>\n  );\n}\n```\n\n`configure()` is re-exported from both the `@playlive/fundraiser-data` barrel and its\n`/config` subpath. `tiltifyProxyUrl` is the only required field; `twitchServiceUrl`,\n`causeId`, `scheduleApiUrl`, `lifetimeApiUrl`, `leaderboardApiUrl`,\n`donorSpotlightApiUrl`, and `donationTrainApiUrl` are optional and gate the hooks that\ntalk to those services.\n\n### `isPending` vs `isLoading`\n\nBoth are passed straight through from TanStack Query v5, and they differ in exactly one\ncase that matters here: a hook whose id is nullish is **disabled**, which leaves\n`isPending: true` forever while `isLoading` is `false`. Branch on `isPending` when you\nwant \"we have neither data nor an error yet\"; branch on `isLoading` when you want \"a\nfirst fetch is actually in flight\".\n\n### How this differs from `@playlive/react-data`\n\n`@playlive/react-data` implements its hooks on a single `useFetch` primitive built from\n`useState` / `useRef` — the state lives inside the calling component. Two components\nasking for the same campaign issue two independent requests and hold two copies. This\npackage hands the same work to a `QueryClient`, which buys:\n\n- **Cache sharing + dedupe** — hooks that build the same `queryKey` share one cache\n  entry and one in-flight request. `useCampaigns` rows de-dupe against sibling\n  `useCampaign` consumers for free.\n- **Cursor pagination** — `useInfiniteDonations` / `useTiltifyLeaderboard` sit on\n  `useInfiniteQuery`; the dependency-free tier has no equivalent.\n- **Mutations with invalidation** — `useLeaderboardExclusions` invalidates its own read\n  on a successful add/remove, so the list updates without waiting for a poll.\n- **Devtools** — every query is a plain TanStack query, so\n  `@tanstack/react-query-devtools` can inspect them. This package does not bundle or\n  re-export devtools; add them yourself if you want them.\n- **26 extra hooks** — leaderboards, donation trains, alerts, and the Play Live\n  first-party services only exist in this tier (see the API reference).\n\nThe cost is the `@tanstack/react-query` peer in your bundle. If you only need the 19\nshared hooks and want the smaller graph, use `@playlive/react-data`.\n\n## Subpath exports\n\n| Subpath                       | Contents                                                                                          |\n| ----------------------------- | --------------------------------------------------------------------------------------------------- |\n| `@playlive/react-query`       | Default barrel — re-exports `/config`, every hook, `/types`, plus `PACKAGE_NAME` and `KNOWN_URLS`.  |\n| `@playlive/react-query/config`| `DEFAULT_QUERY_OPTIONS` + `makeQueryClient`.                                                        |\n| `@playlive/react-query/types` | `UseFetchOptions`, `UseFetchResult`, `UseInfiniteDonationsResult`, `HookRefetchOptions`.            |\n\nThat is the complete `exports` map — there is no `/hooks` subpath; import hooks from the\nbarrel. Each entry ships an ESM bundle, a `bun` source condition pointing at `src/`, and\n`.d.ts` declarations.\n\n## API reference\n\n### Query hooks — standard `UseFetchResult<T>`\n\nEvery row takes `(params, options?: UseFetchOptions)` and returns\n`UseFetchResult<T>` (`{ data, error, isLoading, isPending, isFetching, refetch }`).\n\"Auto-disabled\" lists the guard that flips `enabled` to `false`.\n\n| Hook                             | `data`                                                          | Auto-disabled when                              |\n| -------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------- |\n| `useCampaign`                    | `TiltifyCampaign \\| TiltifyPersonalCampaign \\| TiltifyTeamCampaign \\| null` | never                             |\n| `useFlattenedDonations`          | `TiltifyDonation[]`                                               | `campaignId` nullish                              |\n| `useMilestones`                  | `TiltifyMilestone[]`                                              | `campaignId` nullish                              |\n| `useRewards`                     | `TiltifyReward[]`                                                 | `campaignId` nullish                              |\n| `usePolls`                       | `TiltifyPoll[]`                                                   | `campaignId` nullish                              |\n| `useTargets`                     | `TiltifyTarget[]`                                                 | `campaignId` nullish                              |\n| `useSchedule`                    | `TiltifySchedule[]`                                               | `campaignId` nullish                              |\n| `useUser`                        | `TiltifyUser \\| null`                                             | `userSlug` empty                                  |\n| `useTeam`                        | `TiltifyTeam \\| null`                                             | `teamSlug` empty                                  |\n| `useFundraisingEvent`            | `TiltifyFundraisingEvent \\| null`                                 | `eventId` nullish                                 |\n| `useFundraisingEventMilestones`  | `TiltifyFactMilestone[]`                                          | `eventId` nullish                                 |\n| `useCause`                       | `TiltifyCause \\| null`                                            | `causeId` nullish                                 |\n| `useEventCampaigns`              | `TiltifyCampaign[]`                                               | `eventId` nullish                                 |\n| `useCurrentEvents`               | `TiltifyFundraisingEvent[]`                                       | never                                             |\n| `useTiltifyUserCampaigns`        | `TiltifyPersonalCampaign[]`                                       | `userId` nullish, empty, or the string `\"null\"`    |\n| `useTiltifyUserAndTeamCampaigns` | `(TiltifyPersonalCampaign \\| TiltifyTeamCampaign)[]`              | `userId` nullish, empty, or the string `\"null\"`    |\n| `useScheduleBlockRaised`         | `ScheduleBlockRaised \\| undefined`                                | any of `campaignId` / `start` / `end` nullish     |\n| `useLifetimeRaised`              | `number \\| null`                                                  | `username` nullish or empty                       |\n| `usePreviousYearTotals`          | `PreviousYearTotalItem[]`                                         | `slug` nullish or empty                           |\n| `useGiftsThatGiveMilestones`     | `GiftsThatGiveMilestone[]`                                        | `goal` nullish                                    |\n| `useDonorSpotlightOverview`      | `DonorSpotlightSnapshot \\| null`                                  | `campaignId` nullish or empty                     |\n| `useLeaderboardWithExclusions`   | `MonetaryLeaderboardEntry[]`                                      | `campaignID` nullish or empty                     |\n| `useDonationTrains`              | `DonationTrain[]`                                                 | `campaignID` nullish or empty                     |\n| `useDonationTrainHighRateDonors` | `DonationTrainHighRateDonor[]`                                    | `campaignID` nullish or empty                     |\n| `useCampaignRulesets`            | `DonationTrainRuleset[]`                                          | `campaignID` nullish or empty                     |\n| `useDonationTrainCommonTrains`   | `CommonDonationTrain[]`                                           | `campaignID` nullish or empty                     |\n\n`useRewards` and `useTargets` additionally accept `sort?: boolean` in `params`.\n`useFundraisingEventMilestones` takes an optional `charityType` that defaults to\n`\"tiltify\"`. The donation-train and leaderboard-exclusion hooks take\n`(params, authAndOptions?)` where the second argument is\n`DonationTrainAuthOptions & UseFetchOptions` (or `LeaderboardAuthOptions & UseFetchOptions`) —\n`adminApiKey` / `tiltifyOAuthToken` are split out and never reach TanStack.\n\nTwitch-unsupported entities (`useMilestones`, `useRewards`, `usePolls`, `useTargets`,\n`useSchedule`, `useUser`, `useTeam`, `useFundraisingEvent`, `useFundraisingEventMilestones`,\n`useCause`, `useEventCampaigns`, `useLeaderboardWithExclusions`) resolve to `[]` / `null`\non the `charityType: \"twitch\"` path rather than throwing — same lenient semantics as the\nunderlying fetchers.\n\n**Per-hook cadence overrides.** Some hooks override `DEFAULT_QUERY_OPTIONS` because their\nupstream data doesn't change every five seconds. Pass `options.refetchInterval` /\n`options.staleTime` to win back control:\n\n| Hook                                                          | `staleTime` | `refetchInterval` |\n| ------------------------------------------------------------- | ----------- | ----------------- |\n| `useTiltifyUserCampaigns`, `useTiltifyUserAndTeamCampaigns`    | `Infinity`  | `false`           |\n| `useScheduleBlockRaised` (also `refetchOnReconnect: false`)    | `Infinity`  | `false`           |\n| `useLifetimeRaised`                                           | `5_100`     | `60_000`          |\n| `usePreviousYearTotals`                                        | `5_100`     | `5 * 60_000`      |\n| `useDonationTrains`, `useCampaignRulesets`                     | `5_100` / `5_000` | `15_000`    |\n| `useDonationTrainHighRateDonors`, `useDonationTrainCommonTrains` | `5_100`   | `25_000`          |\n\n### Query hooks with a custom return shape\n\n| Hook                                | Returns                                                                                                    |\n| ----------------------------------- | ------------------------------------------------------------------------------------------------------------ |\n| `useCampaigns`                      | `UseCampaignsResult` — `{ data[], errors[], isLoading, isPending, isFetching, isError, refetch }`, ordered 1-for-1 with the `params` array. Built on `useQueries` + `combine`. Rows are **never** auto-disabled; a row with no id still fires. |\n| `useInfiniteDonations`              | `UseInfiniteDonationsResult<PaginatedResponse<TiltifyDonation, TiltifyPaginationMetadata \\| TwitchPaginationMetadata>>` |\n| `useTiltifyLeaderboard`             | `UseTiltifyLeaderboardResult` — `{ entries, data, error, isLoading, isPending, isFetching, isFetchingNextPage, hasNextPage, fetchNextPage, refetch }`. Forward-only; no `fetchPreviousPage`. |\n| `useLeaderboardExclusions`          | `UseLeaderboardExclusionsResult` — `{ data, donorNames, error, isLoading, isPending, isFetching, isMutating, addExclusion, removeExclusion, refetch }` |\n| `useLeaderboard`                    | `UseLeaderboardResult` — `{ leaderboard, donations, currentTotal, exclusions, isLoadingExclusions, isLoadingDonations, isFetching, isFetchingNextPage, hasNextPage, error }`. Note: no `isLoading`, no `refetch`. |\n| `useCampaignGiftsThatGiveMilestones`| `{ giftMilestones: GiftsThatGiveMilestoneItem[]; isLoading: boolean }`                                       |\n| `useDonationTrainState`             | `UseDonationTrainStateResult` — `{ trains, updateTrainStatus, updateTrainVisibility, highRateDonors, commonTrains, isLoading }` |\n| `useCampaignRulesetsState`          | `UseCampaignRulesetsStateResult` — `{ rulesets, updateRuleset, deleteRuleset, createRuleset }`                |\n\n### Non-query hooks (pure client-side)\n\nThese issue no requests — they're `useMemo` / `useState` / `setTimeout` helpers that\nhappen to live here so overlays can import one package.\n\n| Hook                                  | Signature                                                                                              |\n| ------------------------------------- | -------------------------------------------------------------------------------------------------------- |\n| `useCampaignAmounts`                  | `(campaignOrFundraiser: CampaignLike \\| null \\| undefined) => CampaignAmounts` — reference-stable `{ totalAmount, currentAmount, goalAmount, originalGoalAmount, supportingAmount }`. |\n| `useCampaignFundraisingEventAmounts`  | `(campaign, fundraisingEvent, options?: { forceCampaignGoal?: boolean }) => CampaignFundraisingEventAmounts` |\n| `useDonationsReducer`                 | `(donationsData: DonationPagesInput \\| null \\| undefined, flipSorting = false, onlyReturnNew = false) => TiltifyDonation[]` — flattens a `{ pages, pageParams }` envelope; sticky-accumulates by default so rows survive `maxPages` trimming. |\n| `useAlertsQueue`                      | `(params: UseAlertsQueueParams, options?: UseAlertsQueueOptions) => { donation: TiltifyDonation \\| null }` — display-timing loop over an external donation queue. |\n\n### Mutation hooks\n\nEach returns `UseMutationResult<TData, TVariables>` —\n`{ mutate, mutateAsync, data, error, isPending, reset }`, a projection of TanStack's\n`useMutation` so consumers don't need TanStack's types.\n\n| Hook                           | Argument               | `TVariables`                                              | `TData`                        |\n| ------------------------------ | ---------------------- | ----------------------------------------------------------- | ------------------------------ |\n| `useUpdateTrainVisibility`     | `auth?`                | `{ trainID: string; trainVisible: boolean }`                | `DonationTrain \\| null`        |\n| `useRefreshTrainStatus`        | —                      | `{ trainID: string }`                                       | `DonationTrain \\| null`        |\n| `useProcessDonationsForTrains` | —                      | `{ donations: TiltifyDonation[] }`                          | `DonationTrain[]`              |\n| `useCreateCampaignRuleset`     | `auth?`                | `{ campaignID: string; ruleset: PartialDonationTrainRuleset }` | `DonationTrainRuleset \\| null` |\n| `useUpdateRuleset`             | `auth?`                | `{ ruleset: DonationTrainRuleset; campaignID?: string }`    | `DonationTrainRuleset`         |\n| `useDeleteRuleset`             | `auth?`                | `{ rulesetID: string; campaignID?: string }`                | `{ id: string } \\| null`       |\n| `useTestDonations`             | `auth?`                | `TiltifyDonation \\| TiltifyDonation[]`                      | `void`                         |\n\n`auth` is `DonationTrainAuthOptions` (`TestDonationAuthOptions` for `useTestDonations`) —\npass `adminApiKey` **or** `tiltifyOAuthToken`. `useUpdateRuleset` / `useDeleteRuleset`\npick their route from the presence of `campaignID`: with it, the campaign-scoped route\n(admin key or OAuth); without it, the admin-only legacy route.\n\n`useTestDonations` fires a synthetic donation through the core REST API\n(`POST /donations/tiltify/test`), which replays it down the same webhook path a real\ndonation takes — alerts, donation trains, the subathon timer, and every WebSocket\nsubscriber react as if Tiltify had delivered it. Demo campaigns skip auth entirely.\n\n### Constants\n\n| Export                       | Kind    | Value                                                                        |\n| ---------------------------- | ------- | ------------------------------------------------------------------------------ |\n| `PACKAGE_NAME`               | `const` | `\"@playlive/react-query\"` — for runtime version-pinning checks.                |\n| `KNOWN_URLS`                 | `const` | `readonly string[]`, frozen and **empty**. See the URL-disclosure section.      |\n| `DEFAULT_QUERY_OPTIONS`      | `const` | Workspace query defaults (table below). Also on `/config`.                     |\n| `DEFAULT_ALERT_DURATIONS_MS` | `const` | `Readonly<Record<DonorLevel, number>>` — `platinum` 9500, `gold` 8500, `silver` 7500, `bronze` 6500, `grey` 5500. Merged under `useAlertsQueue`'s `durations` override. |\n\n### Functions\n\n| Export             | Kind       | Signature                                                     |\n| ------------------ | ---------- | --------------------------------------------------------------- |\n| `makeQueryClient`  | `function` | `(overrides?: QueryClientConfig) => QueryClient` — builds a client whose `defaultOptions.queries` is `DEFAULT_QUERY_OPTIONS` with `overrides.defaultOptions.queries` merged on top. |\n\n### Common options (`UseFetchOptions`)\n\n| Option            | Default | Description                                                                   |\n| ----------------- | ------- | ------------------------------------------------------------------------------- |\n| `enabled`         | `true`  | Skip fetching when `false`. ANDed with the hook's own nullish guard.            |\n| `refetchInterval` | `5_000` | Poll every N ms. Pass `false` to disable polling.                              |\n| `retry`           | `10`    | Retries on error. Pass `false` (or `0`) in tests, or for hooks whose id may legitimately 404. |\n| `retryDelay`      | `1_000` | Base delay (ms); TanStack applies exponential backoff on top.                   |\n| `staleTime`       | `5_100` | Freshness window. Pass `0` to make every read refetch.                          |\n| `initialData`     | —       | Seed the query (TanStack `initialData`) so a route loader's payload paints immediately. Typed `unknown`; cast at the call site. For `useCampaigns` an **array** seeds rows 1:1 against `params`, a scalar seeds every row. |\n| `maxPages`        | —       | Retained-page cap. Honoured only by `useInfiniteDonations` and `useTiltifyLeaderboard`; every other hook ignores it. TanStack v5 drops the oldest page at the limit. |\n| `cachingEnabled`  | —       | Boolean cache partition. Contributes to the `queryKey` of `useInfiniteDonations` / `useTiltifyLeaderboard` only — the fetcher is unchanged. |\n\n`initialData`, `maxPages`, and `cachingEnabled` are stripped from the merged options bag\nbefore it reaches TanStack and re-applied per hook with a narrow cast; without that,\n`initialData?: unknown` would collapse `TQueryFnData` inference to `unknown` everywhere.\n\n### Workspace defaults (`DEFAULT_QUERY_OPTIONS`)\n\nEvery hook merges these in as base defaults (caller `options` wins), and\n`makeQueryClient()` installs the same object as the client's `defaultOptions.queries`.\n\n| Option                 | Value   | Rationale                                                   |\n| ---------------------- | ------- | ------------------------------------------------------------- |\n| `staleTime`            | `5_100` | Matches overlay-data-layer's existing freshness window.       |\n| `refetchInterval`      | `5_000` | Sane polling for live overlays.                               |\n| `retry`                | `10`    | Flaky stream-conf networks; backoff protects the proxy.       |\n| `retryDelay`           | `1_000` | Base for TanStack's exponential backoff schedule.             |\n| `refetchOnWindowFocus` | `false` | OBS browser sources have no meaningful focus events.          |\n| `refetchOnReconnect`   | `true`  | Recovery after a network blip is the right live semantic.     |\n\n### Result types\n\nExported from the barrel and from `@playlive/react-query/types`:\n\n```ts\ninterface UseFetchResult<T> {\n  data: T | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  refetch: (options?: HookRefetchOptions) => Promise<void>;\n}\n\ninterface UseInfiniteDonationsResult<TPage> {\n  data: { pages: TPage[]; pageParams: Array<string | number | null | undefined> } | undefined;\n  error: Error | null;\n  isLoading: boolean;\n  isPending: boolean;\n  isFetching: boolean;\n  isFetchingNextPage: boolean;\n  isFetchingPreviousPage: boolean;\n  hasNextPage: boolean;\n  hasPreviousPage: boolean;\n  fetchNextPage: () => Promise<void>;\n  fetchPreviousPage: () => Promise<void>;\n  refetch: (options?: HookRefetchOptions) => Promise<void>;\n}\n\n/** Structural subset of TanStack v5's `RefetchOptions`. */\ninterface HookRefetchOptions {\n  cancelRefetch?: boolean; // default true — cancels an in-flight fetch first\n}\n```\n\n`refetch` is wrapped in a `useCallback` keyed on TanStack's own `refetch`, so its\nidentity is stable across renders — safe to list in a `useEffect` dep array without\nlooping.\n\nEvery hook also exports its `Use…Params` / `Use…Result` interface (`UseCampaignParams`,\n`UseInfiniteDonationsParams`, `UseLeaderboardParams`, `LeaderboardRow`,\n`GiftsThatGiveMilestoneItem`, `UseMutationResult`, …) from the barrel. Domain payload\ntypes (`TiltifyDonation`, `DonationTrain`, `MonetaryLeaderboardEntry`,\n`ScheduleBlockRaised`, …) are **not** re-exported — import them from\n[`@playlive/tiltify-core`](../tiltify/core/) and [`@playlive/fundraiser-data`](../fundraiser-data/).\n\nFull generated API documentation:\n<https://packages.playlive.experience.stjude.org/p/@playlive/react-query/docs/>\n\n## Upstream spec\n\nNo external API surface of its own. Every hook wraps a fetcher from\n[`@playlive/fundraiser-data`](../fundraiser-data/), so the endpoints, authentication,\nand URL knobs are entirely that package's — `configure()` it once at boot and these\nhooks inherit whatever you pointed it at (the Tiltify proxy, the Twitch charity\nservice, and the Play Live schedule / lifetime-raised / leaderboard / donor-spotlight /\ndonation-train services). See [that package's README](../fundraiser-data/) for the full\nlist, and Tiltify's public developer documentation at <https://developers.tiltify.com>\nfor the upstream resource shapes.\n\n## Twitch Extension URL disclosure\n\nThe `KNOWN_URLS` export enumerates every absolute URL or host this package can fetch.\n**It is empty** — this package hardcodes no production hosts; every request goes through\na `@playlive/fundraiser-data` fetcher pointed at a URL you supplied to `configure()`.\n\n```ts\nimport { KNOWN_URLS } from \"@playlive/react-query\";\n\nconsole.log(KNOWN_URLS); // []\n```\n\nFor an Extension submission, disclose `@playlive/fundraiser-data`'s URLs plus whatever\nyou passed for `tiltifyProxyUrl`, `twitchServiceUrl`, and the Play Live service URLs.\n\n## Examples\n\n### Full overlay — provider, config, three hooks, all three states\n\n```tsx\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { configure } from \"@playlive/fundraiser-data\";\nimport {\n  makeQueryClient,\n  useCampaign,\n  useCampaignAmounts,\n  useMilestones,\n  useLeaderboard,\n} from \"@playlive/react-query\";\n\nconfigure({\n  tiltifyProxyUrl: \"https://api.experience.stjude.org/tiltify\",\n  leaderboardApiUrl: \"https://api.experience.stjude.org/leaderboard\",\n});\n\nconst queryClient = makeQueryClient();\n\nexport function OverlayRoot({ campaignId }: { campaignId: string }) {\n  return (\n    <QueryClientProvider client={queryClient}>\n      <Overlay campaignId={campaignId} />\n    </QueryClientProvider>\n  );\n}\n\nfunction Overlay({ campaignId }: { campaignId: string }) {\n  const campaign = useCampaign({ charityType: \"tiltify\", id: campaignId });\n  const milestones = useMilestones({ charityType: \"tiltify\", campaignId });\n\n  // Composes useInfiniteDonations + useLeaderboardExclusions; `eagerFetchPages`\n  // (default true) walks the cursor so the ranking sees every donation.\n  const { leaderboard, currentTotal, isLoadingDonations } = useLeaderboard({\n    charityType: \"tiltify\",\n    campaignId,\n    limit: 5,\n    removeAnonymous: true,\n  });\n\n  const { totalAmount, goalAmount } = useCampaignAmounts(campaign.data);\n\n  if (campaign.isPending) return <p>Connecting…</p>;\n  if (campaign.error) return <p role=\"alert\">Campaign failed: {campaign.error.message}</p>;\n  if (!campaign.data) return <p>Campaign not found.</p>;\n\n  return (\n    <section>\n      <h1>{campaign.data.name}</h1>\n      <progress value={totalAmount} max={goalAmount} />\n\n      <ul>\n        {(milestones.data ?? [])\n          .filter((m) => m.active)\n          .map((m) => (\n            <li key={m.id}>\n              {m.name} — ${m.amount.value}\n            </li>\n          ))}\n      </ul>\n\n      <h2>Top donors (${currentTotal.toFixed(2)} raised)</h2>\n      {isLoadingDonations ? (\n        <p>Tallying…</p>\n      ) : (\n        <ol>\n          {leaderboard.map((row) => (\n            <li key={row.id}>\n              {row.donor_name} — ${row.amount.toFixed(2)}\n            </li>\n          ))}\n        </ol>\n      )}\n    </section>\n  );\n}\n```\n\n### Dependent query + polling override\n\n`cause_id` lives on every campaign shape, so the cause lookup can chain off the campaign\nresult. Passing `undefined` for `causeId` keeps `useCause` disabled (and therefore\n`isPending`) until the first campaign resolves. The cause record is static reference\ndata, so polling is switched off for it.\n\n```tsx\nimport { useCampaign, useCause } from \"@playlive/react-query\";\n\nfunction CauseBadge({ campaignId }: { campaignId: string }) {\n  const campaign = useCampaign({ charityType: \"tiltify\", id: campaignId });\n\n  const cause = useCause(\n    { charityType: \"tiltify\", causeId: campaign.data?.cause_id },\n    { refetchInterval: false, staleTime: Number.POSITIVE_INFINITY },\n  );\n\n  if (!cause.data) return null;\n  return <img src={cause.data.avatar?.src} alt={cause.data.name} />;\n}\n```\n\n### Infinite donation feed\n\n`useInfiniteDonations` is the tier-exclusive cursor hook. Tiltify pages on an opaque\nstring cursor (`metadata.after`), Twitch on a numeric page index (`metadata.nextPage`);\n`getNextPageParam` reads both, so `charityType` can be a runtime value. Only Tiltify\nexposes a reverse cursor (`metadata.before`), so `hasPreviousPage` is permanently `false`\non the Twitch path and `fetchPreviousPage` no-ops there.\n\n`useDonationsReducer` flattens the `{ pages, pageParams }` envelope into a sorted array\nand — with `onlyReturnNew` left at `false` — keeps rows that `maxPages` has already\ntrimmed out of the cache, which is what a scrolling column wants.\n\n```tsx\nimport { useEffect } from \"react\";\nimport { useDonationsReducer, useInfiniteDonations } from \"@playlive/react-query\";\n\nfunction DonationFeed({ campaignId }: { campaignId: string }) {\n  const {\n    data,\n    fetchNextPage,\n    hasNextPage,\n    isFetchingNextPage,\n    isPending,\n    error,\n  } = useInfiniteDonations(\n    { charityType: \"tiltify\", campaignId, count: 50 },\n    { maxPages: 5 },\n  );\n\n  const donations = useDonationsReducer(data);\n\n  // Walk the cursor to the end once, then let the 5 s poll pick up new rows.\n  useEffect(() => {\n    if (hasNextPage && !isFetchingNextPage) void fetchNextPage();\n  }, [hasNextPage, isFetchingNextPage, fetchNextPage]);\n\n  if (isPending) return <p>Loading donations…</p>;\n  if (error) return <p role=\"alert\">{error.message}</p>;\n\n  return (\n    <ul>\n      {donations.map((d) => (\n        <li key={d.id}>\n          {d.donor_name || \"Anonymous\"} — ${d.amount.value}\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n### Moderating the exclusion list\n\nThe read (`GET /leaderboard-exclusions/{id}`) is public; the `POST` / `DELETE` behind\n`addExclusion` / `removeExclusion` need `adminApiKey` (sent as `x-api-key`) or\n`tiltifyOAuthToken` (sent as `Authorization: OAuth <token>`). Auth keys are split out of\nthe second argument before the rest forwards to TanStack, and a successful mutation\ninvalidates the read so the list refreshes without waiting for a poll.\n\n```tsx\nimport { useLeaderboardExclusions } from \"@playlive/react-query\";\n\nfunction ExclusionEditor({ campaignID, token }: { campaignID: string; token: string }) {\n  const { donorNames, isMutating, addExclusion, removeExclusion } = useLeaderboardExclusions(\n    { campaignID },\n    { tiltifyOAuthToken: token, refetchInterval: 30_000 },\n  );\n\n  return (\n    <ul>\n      {donorNames.map((name) => (\n        <li key={name}>\n          {name}\n          <button type=\"button\" disabled={isMutating} onClick={() => void removeExclusion(name)}>\n            Remove\n          </button>\n        </li>\n      ))}\n      <li>\n        <button type=\"button\" disabled={isMutating} onClick={() => void addExclusion(\"Bob\")}>\n          Exclude Bob\n        </button>\n      </li>\n    </ul>\n  );\n}\n```\n\n### Swapping tiers\n\nThe 19 hooks below exist in both packages with identical params and identical\n`UseFetchResult` returns, so migrating those call sites is one import rewrite:\n\n```diff\n-import { useCampaign, useMilestones } from \"@playlive/react-query\";\n+import { useCampaign, useMilestones } from \"@playlive/react-data\";\n```\n\n<sub>`useCampaign`, `useCampaigns`, `useCause`, `useEventCampaigns`, `useFlattenedDonations`,\n`useFundraisingEvent`, `useGiftsThatGiveMilestones`, `useLifetimeRaised`, `useMilestones`,\n`usePolls`, `usePreviousYearTotals`, `useRewards`, `useSchedule`, `useScheduleBlockRaised`,\n`useTargets`, `useTeam`, `useTiltifyUserAndTeamCampaigns`, `useTiltifyUserCampaigns`, `useUser`.</sub>\n\nDrop `<QueryClientProvider>` if nothing else in the tree needs it. Everything outside\nthat list — infinite pagination, leaderboards, donation trains, mutations, alerts, and\nthe Play Live first-party services — is react-query-only; those call sites have to stay\non this tier. See [`@playlive/react-pipeline`](../react-pipeline/) for the fusion hooks\nthat layer live WebSocket updates over these REST baselines.\n\n## License\n\nMIT © St. Jude Children's Research Hospital\n","readmeFilename":"README.md","dist":{"tarball":"https://packages.playlive.experience.stjude.org/@playlive/react-query/-/react-query-0.4.4.tgz","shasum":"419d0254d44618f2ad5078e210e184af9dbea9ba","integrity":"sha512-RML72Ou/fx8rPx0GwPualA+xAY//8Jmwgqc7lT5LKhnS2MhGbdt3VLH7J5T2k0oI4NlOFmG8zrOHp/BbU+RKXw=="}}},"time":{"0.4.2":"2026-08-26T18:10:04.311Z","modified":"2026-08-26T20:09:43.020Z","0.1.0":"2026-08-26T18:15:30.441Z","0.1.2":"2026-08-26T18:15:30.991Z","0.1.3":"2026-08-26T18:15:31.508Z","0.2.0":"2026-08-26T18:15:32.058Z","0.2.1":"2026-08-26T18:15:32.545Z","0.2.2":"2026-08-26T18:15:33.065Z","0.2.3":"2026-08-26T18:15:33.598Z","0.2.4":"2026-08-26T18:15:34.110Z","0.2.5":"2026-08-26T18:15:34.662Z","0.2.6":"2026-08-26T18:15:35.242Z","0.2.7":"2026-08-26T18:15:35.739Z","0.3.0":"2026-08-26T18:15:36.267Z","0.3.1":"2026-08-26T18:15:36.861Z","0.3.2":"2026-08-26T18:15:37.456Z","0.3.3":"2026-08-26T18:15:38.012Z","0.3.4":"2026-08-26T18:15:38.545Z","0.4.0":"2026-08-26T18:15:39.157Z","0.4.1":"2026-08-26T18:15:39.660Z","0.4.3":"2026-08-26T19:45:50.144Z","0.4.4":"2026-08-26T20:09:43.020Z"}}