fix: keep nested clients and utils referentially stable - #1757
Conversation
…ts and utils referentially stable Every property access on a recursive proxy used to build a brand new child client/utils object, so `client.planet.list !== client.planet.list`. Each proxy now memoizes its children in a Map keyed by the accessed property. Covers createORPCClient, createSafeClient, createRouterClient, and createRouterUtils (tanstack-query, pinia-colada, swr). createRouterClient also skips the repeated getRouter lookup on cached keys. Nested proxies now read through Reflect.get instead of getOrBind — binding per access is unnecessary now that children are stable — and the eslint rule banning Reflect.get is dropped.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/bun
@orpc/client
@orpc/cloudflare
@orpc/contract
@orpc/experimental-effect
@orpc/evlog
@orpc/json-schema
@orpc/nest
@orpc/next
@orpc/openapi
@orpc/opentelemetry
@orpc/pinia-colada
@orpc/pino
@orpc/publisher
@orpc/ratelimit
@orpc/server
@orpc/shared
@orpc/swr
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/zod
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — 17 files across 6 packages adding per-instance Map memoization to all recursive proxy handlers so nested clients and utils are referentially stable.
createORPCClient/createSafeClient/createRouterClient: each proxy now holds aMap<string, ...>cache; repeated property access returns the same object, and thegetOrBindcall is replaced withReflect.getsince the target values don't need runtime binding.createRouterUtils(pinia-colada, tanstack-query, swr): same memoization pattern applied — the inner function-wrapper proxy (which delegates property access to the child utils proxy) is now also cached soutils.key.pong === utils.key.pong.- ESLint: the
Reflect.getban is dropped; every previous call site was a no-binding context or is replaced with the cached lookup. - Tests: referential-equality
.toBe()tests added to all 6 packages; call-count assertions verify nested procedure clients/utils are constructed exactly once (e.g. 4createProcedureClientcalls when walkingping,nested.pong,lazy, andlazy.peng). - Scoped options:
get(options.scoped, [prop])replaced with optional chaining in tanstack-query/pinia-colada — functionally equivalent sinceoptions.scopedis always a plain object orundefined.
All 42 tests across the 6 affected test suites pass cleanly.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
bindMethods gains an `unbound` option listing methods that should be copied as-is instead of bound to the source object, and now resolves values through getOrBind. Procedure utils in tanstack-query, pinia-colada, and swr use it for `call` so the property keeps the client's own identity.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — one new commit (a61f71b) extending bindMethods with an unbound option and applying it to ProcedureUtils.call in the three createRouterUtils variants.
bindMethodsgainsunboundoption: methods listed in theunboundarray are returned as-is instead of being bound to the instance. Tests cover string keys, symbol keys, and prototype-chain methods.ProcedureUtils.callmarked unbound in tanstack-query, pinia-colada, and swr: thecallproperty stores the client function and should remain the raw function, not a bound copy.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Accessing a nested procedure on a recursive proxy used to build a brand new object every time, so
client.planet.list !== client.planet.list. Each proxy now memoizes its children in aMapkeyed by the accessed property, making repeated access strictly equal.Fixes
createORPCClient,createSafeClient,createRouterClient, andcreateRouterUtils(tanstack-query, pinia-colada, swr) all return the same child on repeated access.useMemo.Performance
createRouterClientno longer repeats itsgetRouterlookup (and lazy-router traversal) on every access.Reflect.getinstead ofgetOrBind; per-access binding is unnecessary now that children are stable, and the eslint rule banningReflect.getis dropped.Caches are per-proxy-instance, so two separate
createORPCClientcalls stay distinct. Symbols, unwrap keys (then,bind,toString, …), and keys with no matching router still fall through untouched and uncached.Testing
Each affected package gained a repeated-access equality test; the tanstack-query and pinia-colada suites also assert each procedure client/utils is constructed exactly once. Full suite,
type:check, andlintpass.