Skip to content

fix: keep nested clients and utils referentially stable - #1757

Merged
dinwwwh merged 2 commits into
middleapi:mainfrom
dinwwwh:claude/createorpcclient-consistency-4d579b
Jul 31, 2026
Merged

fix: keep nested clients and utils referentially stable#1757
dinwwwh merged 2 commits into
middleapi:mainfrom
dinwwwh:claude/createorpcclient-consistency-4d579b

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Jul 31, 2026

Copy link
Copy Markdown
Member

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 a Map keyed by the accessed property, making repeated access strictly equal.

Fixes

  • createORPCClient, createSafeClient, createRouterClient, and createRouterUtils (tanstack-query, pinia-colada, swr) all return the same child on repeated access.
  • Utils and clients are now safe to pass straight into dependency arrays, memo hooks, and identity comparisons without wrapping them in a useMemo.

Performance

  • No proxy, options object, or path array allocated per property access — only on the first one.
  • createRouterClient no longer repeats its getRouter lookup (and lazy-router traversal) on every access.
  • Nested proxies read through Reflect.get instead of getOrBind; per-access binding is unnecessary now that children are stable, and the eslint rule banning Reflect.get is dropped.

Caches are per-proxy-instance, so two separate createORPCClient calls 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, and lint pass.

…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.
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orpc Ready Ready Preview Jul 31, 2026 3:11am

@dinwwwh dinwwwh changed the title fix(client,server,tanstack-query,pinia-colada,swr): keep nested clients and utils referentially stable fix: keep nested clients and utils referentially stable Jul 31, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jul 31, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@1757

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@1757

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@1757

@orpc/client

npm i https://pkg.pr.new/@orpc/client@1757

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@1757

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@1757

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@1757

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@1757

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@1757

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@1757

@orpc/next

npm i https://pkg.pr.new/@orpc/next@1757

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@1757

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@1757

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@1757

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@1757

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@1757

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@1757

@orpc/server

npm i https://pkg.pr.new/@orpc/server@1757

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@1757

@orpc/swr

npm i https://pkg.pr.new/@orpc/swr@1757

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@1757

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@1757

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@1757

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@1757

commit: a61f71b

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 25 untouched benchmarks


Comparing dinwwwh:claude/createorpcclient-consistency-4d579b (a61f71b) with main (07b4a21)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (1402783) during the generation of this report, so 07b4a21 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 a Map<string, ...> cache; repeated property access returns the same object, and the getOrBind call is replaced with Reflect.get since 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 so utils.key.pong === utils.key.pong.
  • ESLint: the Reflect.get ban 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. 4 createProcedureClient calls when walking ping, nested.pong, lazy, and lazy.peng).
  • Scoped options: get(options.scoped, [prop]) replaced with optional chaining in tanstack-query/pinia-colada — functionally equivalent since options.scoped is always a plain object or undefined.

All 42 tests across the 6 affected test suites pass cleanly.

Pullfrog  | View workflow run | Using 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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

  • bindMethods gains unbound option: methods listed in the unbound array are returned as-is instead of being bound to the instance. Tests cover string keys, symbol keys, and prototype-chain methods.
  • ProcedureUtils.call marked unbound in tanstack-query, pinia-colada, and swr: the call property stores the client function and should remain the raw function, not a bound copy.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@dinwwwh
dinwwwh merged commit 46bd033 into middleapi:main Jul 31, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant