Repository navigation
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — Adds localized, backwards-compatible SSR helpers for hashing, hydration, and streamed-query snapshots. The stream behavior is opt-in through a plugin, with existing paths unchanged and focused tests covering server and browser modes. You can add or adjust custom eligibility rules. Learn more. |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
orpc | 3774105 | Commit Preview URL Branch Preview URL |
Oct 21 2026, 02:20 PM |
Merging this PR will not alter performance1 warning
Comparing Footnotes
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — a refactor that promotes TanStack Query SSR boilerplate into three public exports plus a plugin in packages/tanstack-query, with tests, docs, and playground switched over.
createQueryKeyHashFn(src/key.ts) — oRPC-type-aware query key hashing (hashKey([json, meta?.map(stringifyJSON).sort()])), defaulting tonew RPCJsonSerializer().stringifyJSONresolves toJSON.stringify, so this matches the inline form it replaces.createHydrationOptions(src/ssr.ts) — returnsdehydrate/hydratedefaults forQueryClientdefaultOptions;serializeDataintentionally dropsmaps/blobs.cancelStreamsOnSuccess(src/ssr.ts) — same success+fetching guard, now returns the cache-subscription unsubscribe fn and uses aWeakSet<Query>instead ofSet<string>.StreamQuerySSRUtilsPlugin(src/ssr.ts) — appliesrefetchOnMount: 'always'to streamed/live options andinitialData: []to streamed, viamergeProcedureUtilsOptions; this correctly composes with function modifiers, which the old object-spread plugin could not.- Tests —
ssr.test.ts/ssr.test-d.tscover serialization round-tripping, custom serializers, cancellation, unsubscribe, and plugin override precedence. - Docs / playground —
tanstack-query.mdx, the SSR blog post, andplaygrounds/next/src/lib/{orpc,query-client}.tsnow use the public API.
I ran the new unit tests (17 passed), the root tsc -p tsconfig.json --noEmit (clean, covers *.test-d.ts), and scripts/verify-docs-jsdoc.ts (0 errors — all new @see anchors resolve to real docs sections).
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Dismissing prior approval to re-evaluate bd321f8
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — this run re-reviewed the single commit pushed since pullfrog's prior review (476239f), a targeted fix to cancelStreamsOnSuccess.
cancelStreamsOnSuccessnow waits for an active retryer — the cache subscriber only cancels whenquery.promise !== undefined, and the dedupWeakSet<Query>was removed.Query#fetchdispatches thefetchaction (which notifies listeners) before it installs#retryer, so for a query seeded withinitialData: []the old code calledcancel()too early — a no-op — and then theWeakSetblocked any retry, hanging SSR on an endless stream. Sincequery.cancel()is a no-op on a settled retryer and#retryeris cleared infetch'sfinally, the dedup is genuinely unnecessary.- Regression test added —
cancels streams of queries that already had data before the fetch started, which times out on the previous implementation.
The fix matches the resolved macroscope thread at src/ssr.ts:53, and the query.promise getter it relies on is present in @tanstack/query-core as far back as 5.89.0, so the >=5.89.0 peer range is safe. I ran pnpm vitest run packages/tanstack-query/src/ssr.test.ts — 10/10 passing.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — this run re-reviewed the one commit pushed since pullfrog's prior review (bd321f8), a refactor that replaces the server-side stream-cancel subscriber with settling inside the plugin, plus the docs and playground that follow it.
- Removed
cancelStreamsOnSuccess— the query-cache subscriber is gone (never released), so the package no longer exports a helper that silently cancels regular background refetches or leavesfetchStatusatfetchingon the server client. - Added
snapshotStreamstoProcedureUtilsOptions(@internal) — the plugin now setssnapshotStreams: this.isServer, and the built-in streamed/livequeryFns wrap the client iterator withtakeChunks(output, 0 | 1)so the server settles as a normalsuccess/idlequery instead of relying on query-core notification timing. - Added the
takeChunksgenerator (procedure-utils.ts:693) — readscountchunks then closes the iterator in afinally, so the underlying request is aborted even when zero chunks are read.AsyncIteratorClass.return()runs cleanup (reader.cancel()) even whennext()was never called, so this does not leak the server-side stream. StreamQuerySSRUtilsPlugingained{ isServer }— defaults to!('window' in globalThis), replacing the call-siteenvironmentManager.isServer()guards in the playground and blog snippets.- Tests rewritten —
ssr.test.tsnow drives the plugin end-to-end throughcreateRouterUtils(server settle + cleanup for streamed/live, browser following,windowdetection, override precedence);key.ts/key.test.tsare unchanged since the prior review.
I ran pnpm vitest run packages/tanstack-query/src/ssr.test.ts packages/tanstack-query/src/key.test.ts (17 passed), pnpm --filter @orpc/tanstack-query type:check, and eslint over the changed files — all clean. No lingering references to the removed symbol, and the @see anchors resolve to existing docs sections.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
084979f to
3774105
Compare
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — this run re-reviewed the single commit pushed since pullfrog's prior review (ae5804b), a test-only addition to packages/tanstack-query/src/ssr.test.ts.
- Added a server-mode regression test —
settles live queries whose stream ends before the first event on the server(src/ssr.test.ts:104) drives an empty stream throughcreateRouterUtilswithStreamQuerySSRUtilsPlugin({ isServer: true })and asserts it rejects withliveQuery'sdid not yield any dataerror rather than hanging. This complements the existing "settles on the first event" coverage.
I ran pnpm vitest run packages/tanstack-query/src/ssr.test.ts packages/tanstack-query/src/key.test.ts (18 passed: key 8 + ssr 10), pnpm --filter @orpc/tanstack-query type:check, and eslint over the changed files — all clean.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…amed queries TanStack Query SSR setup was copied by hand into the docs, the Next.js blog post, the Next.js playground and a test. Export it instead: - `createQueryKeyHashFn(serializer?)` (`key.ts`): a `queryKeyHashFn` that hashes keys through an RPC JSON serializer, so `bigint` doesn't throw and `Date`, `Set`, `Map`, ... stay distinct from their JSON form. - `createHydrationOptions(serializer?)` (`ssr.ts`): `dehydrate.serializeData` and `hydrate.deserializeData` defaults that preserve oRPC types. - `StreamQuerySSRUtilsPlugin` (`ssr.ts`): SSR support for streamed and live queries. On the server (no global `window`, or `isServer: true`), it sets the internal `snapshotStreams` procedure option: streamed queries settle once they connect and live queries on their first event, then the stream is closed. In the browser, streamed and live queries refetch on mount so streams reopen after hydration, and streamed queries start from `initialData: []`. Utils and per-call options override its defaults. Both serializer helpers take `Public<RPCJsonSerializer>` (an `RPCSerializer` works too) and default to `new RPCJsonSerializer()`. The docs, blog post and playground now use these exports, and the Custom Serializers section now says its `serializer` is your RPC serializer; since 81698ab the snippet used it without explanation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CTKKtCpRNTor3JCoqar7BR
b9e3e17 to
05ab337
Compare
There was a problem hiding this comment.
ℹ️ No critical issues — one documentation snippet nit inline.
Reviewed changes — this run re-reviewed the single commit pushed since pullfrog's prior review (3774105a), rebased and squashed as 05ab337. Only the TanStack Query integration docs changed.
- Reworded the Custom Serializers prose (
tanstack-query.mdx) — now says to pass "your RPC Serializer" and links to/docs/rpc/serializer#custom-serializers. - Removed the snippet's serializer setup — the
import { RPCJsonSerializer } from '@orpc/client'andconst serializer = new RPCJsonSerializer({ handlers: {...} })lines are gone, while the snippet still callscreateQueryKeyHashFn(serializer)andcreateHydrationOptions(serializer). - Condensed the Streamed and Live Queries section — prose-only; the plugin example and the
isServeroverride note remain.
Every other file is byte-identical to the previously reviewed revision (verified by git ls-tree blob hashes), so the code, tests, blog, and playground are unchanged.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏


Summary
TanStack Query SSR setup was copied by hand. Users wrote
queryKeyHashFn,dehydrate.serializeData,hydrate.deserializeData, a stream-cancel subscriber and arefetchOnMountplugin themselves, and the same code lived in the docs, the Next.js blog post, the Next.js playground and a test. This PR replaces all of it with exports from@orpc/tanstack-query.Changes
createQueryKeyHashFn(serializer?)(key.ts): aqueryKeyHashFnthat hashes keys through an RPC JSON serializer. UnlikehashKey, it doesn't throw onbigint, and it keepsDate,Set,Map, ... distinct from their JSON form (aDateand its ISO string hash differently). Meta entries are sorted, so object key order doesn't change the hash. It lives inkey.tsbecause it is useful without SSR too.createHydrationOptions(serializer?)(ssr.ts): returns{ dehydrate: { serializeData }, hydrate: { deserializeData } }to spread intodefaultOptions. It dropsmaps/blobs, which don't survive JSON.StreamQuerySSRUtilsPlugin(ssr.ts): SSR support for streamed and live queries, in one router utils plugin.success/idlequery.refetchOnMount: 'always', so a new stream opens after hydration. Streamed queries also start frominitialData: [].windowas the server;new StreamQuerySSRUtilsPlugin({ isServer })overrides that.mergeProcedureUtilsOptions:scopedand per-call options override them, and function-form options are kept. The hand-written plugin's spread dropped those.snapshotStreams(procedure-utils.ts,@internal): the procedure utils option the plugin sets on the server. When it's set, the default streamed query function reads no chunks and the live query function reads one, then both close the iterator. Queries with a customqueryFnare left as is.Public<RPCJsonSerializer>, so anRPCSerializerworks too, and default tonew RPCJsonSerializer().key.test.tsusescreateQueryKeyHashFninstead of its own hasher.Why not a query cache subscriber
The docs previously cancelled streams on the server with a query cache subscriber that silently cancelled any query in
success+fetchingstate. Doing it in the plugin is better:fetchStatusatfetchingon the server client.initialData: []from the documented plugin), the cancel did nothing and SSR hung on the endless stream.Fixes
docs/integrations/tanstack-query.mdxusedserializerwithout saying where it comes from. The section now says it is your RPC serializer.initialDatanever settled. See above.Testing
ssr.test.ts:[]once connected, and the iterator is closedsuccess/idleliveQuery's usual error instead of hangingwindow, therefetchOnMount/initialDatadefaults, and overrides.key.test.ts: hash edge cases (bigint,Datevs ISO string,Setvs array, key order, custom serializer).ssr.test-d.ts: the helpers accept anRPCSerializer, and the plugin infers the client type.tscfor@orpc/tanstack-query, the root project and the Next playgroundpnpm docs:validate(JSDoc backlinks and Blume links)prerenderToNodeStream+useSuspenseQuery) and Vue (renderToString+onServerPrefetch), against a real oRPC router overRPCLinkand an in-process router client. The HTML contains the first live event, the server stream closes, and after hydration the browser reopens the stream.🤖 Generated with Claude Code
https://claude.ai/code/session_01CTKKtCpRNTor3JCoqar7BR
Note
Add SSR helpers
createQueryKeyHashFn,createHydrationOptions, andStreamQuerySSRUtilsPluginto tanstack-querycreateQueryKeyHashFnhashes query keys with an oRPC JSON serializer, so types likebigint,Date,Set, andMaphash deterministically instead of relying on JSON-unsafe native hashing. Both the factory andcreateHydrationOptionsaccept a custom serializer.createHydrationOptionsreturns dehydration/hydration defaults that preserve oRPC values across server-to-browser transport via serialized JSON and metadata.StreamQuerySSRUtilsPlugin(a router-utils plugin) detects the server viaisServer(default:typeof window === 'undefined') and setssnapshotStreams. On the server, streamed queries take a finite snapshot and live queries take their first event; in the browser, streamed/live queries refetch on mount and streamed queries default to empty initial data. Utility and per-call options can override plugin defaults.snapshotStreams(viatakeChunksin procedure-utils.ts) now close their iterators early instead of consuming the full stream on the server.Macroscope summarized 05ab337.