Skip to content

feat(effect): add createEffectClient and hidden ORPCError-aware catch utilities - #1965

Merged
dinwwwh merged 6 commits into
middleapi:mainfrom
dinwwwh:claude/effect-safe-client-util-b4d912
Aug 29, 2026
Merged

feat(effect): add createEffectClient and hidden ORPCError-aware catch utilities#1965
dinwwwh merged 6 commits into
middleapi:mainfrom
dinwwwh:claude/effect-safe-client-util-b4d912

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Aug 28, 2026

Copy link
Copy Markdown
Member

Adds createEffectClient, which wraps an oRPC client so every procedure returns a lazy Effect you can yield* inside Effect generators. Also makes the catchORPCError* utilities honest about errors the type system cannot see: failure types like Error can hold ORPCError instances at runtime, so handlers now account for them instead of pretending typed claims are the only possibility.

Client calls

  • client.planet.find({ id }) becomes yield* effectClient.planet.find({ id }): outputs land in the success channel, errors in the error channel with their original types preserved.
  • Effects are lazy, so Effect.retry re-invokes the client per attempt; interrupting the effect aborts the request, and a user-provided signal is merged rather than replaced.
  • Nested clients are proxied recursively with cached, referentially stable accessors (same pattern as createSafeClient).

Catch utilities soundness

  • On channels containing non-ORPCError failure types, handlers also receive a hidden ORPCError with unknown code and data, data on code-matched handlers widens to unknown, and code/key restrictions lift (undeclared codes become catchable).
  • Pure ORPCError channels keep exact typed data and restricted code suggestions, so server handlers that only fail with ORPCErrors lose nothing.
  • On the client, Effect.catchIf(isInferableError, ...) remains the fully typed path, since its runtime check justifies the narrowing. Documented in the Effect integration guide.
  • Runtime behavior is unchanged; this is purely type-level.

Testing

  • Runtime tests cover success, option forwarding and signal merging, error channel capture, laziness/retry, interruption abort, and proxy identity.
  • Type tests pin the hidden-error widening (including TypeError, string, and unknown channels), the preserved typed behavior on pure channels, and the client-side unknown data + isInferableError narrowing.
  • pnpm type:check, all effect package tests, lint, and pnpm docs:validate pass.

… utilities

createEffectClient wraps a client so every procedure returns a lazy
Effect: outputs land in the success channel, typed errors in the error
channel, retries re-invoke the client, and interruption aborts the call.

The catch utilities no longer trust Extract alone: failure types that
are not ORPCError (like Error) can still hold ORPCError instances at
runtime, so handlers now also receive those hidden errors with unknown
code and data, and code restrictions lift accordingly. Fully typed data
remains available on pure ORPCError channels and via isInferableError.
@pkg-pr-new

pkg-pr-new Bot commented Aug 28, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

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

@orpc/arktype

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

@orpc/bun

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

@orpc/client

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

@orpc/cloudflare

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

@orpc/contract

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

@orpc/experimental-effect

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

@orpc/evlog

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

@orpc/hibernation

npm i https://pkg.pr.new/@orpc/hibernation@1965

@orpc/json-schema

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

@orpc/experimental-msw

npm i https://pkg.pr.new/@orpc/experimental-msw@1965

@orpc/nest

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

@orpc/next

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

@orpc/node

npm i https://pkg.pr.new/@orpc/node@1965

@orpc/openapi

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

@orpc/opentelemetry

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

@orpc/pinia-colada

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

@orpc/pino

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

@orpc/publisher

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

@orpc/ratelimit

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

@orpc/server

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

@orpc/shared

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

@orpc/swr

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

@orpc/tanstack-query

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

@orpc/trpc

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

@orpc/valibot

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

@orpc/zod

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

commit: 6726802

@codecov

codecov Bot commented Aug 28, 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 Aug 28, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 30 untouched benchmarks


Comparing dinwwwh:claude/effect-safe-client-util-b4d912 (6726802) with main (6810a09)

Open in CodSpeed

@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 — Reviewed the full diff of commit fa1df6d1 (10 files).

  • createEffectClient (packages/effect/src/client.ts): a proxy that wraps every oRPC procedure as a lazy Effect.Effect<TOutput, TError> via Effect.tryPromise, merging the Effect interruption signal with the user's signal through anyAbortSignal, and caching recursively-proxied nested clients for referentially stable accessors.
  • catchORPCError* type soundness (packages/effect/src/error.ts): runtime bodies are unchanged; only the types now account for hidden ORPCErrors when a non-ORPCError failure type (Error, TypeError, string, unknown) is in the channel — handlers widened to ORPCError<...unknown>, codes/keys unrestricted, while pure ORPCError channels keep exact typed data and restricted codes.
  • New @orpc/client dependency + tsconfig reference and index.ts export.
  • Docs: "Client Calls" section and a hidden-error note in apps/content/docs/integrations/effect.mdx; both #client-calls and #catching-orpcerrors backlink anchors verified present.
  • Tests: new runtime + exact-type tests for the client, and extended error.test-d.ts hidden-error cases.

I verified pnpm --filter @orpc/experimental-effect type:check (covers all *.test-d.ts) and the effect runtime tests pass. I also traced the conditional types by hand — the E extends AnyORPCError ? ... and E extends ORPCError<infer TCode, any> ? ... conditionals distribute over the error-channel union, so pure ORPCError channels keep precise typed data and restricted code suggestions while mixed/Error/unknown channels widen honestly. The runtime matching (via instanceof ORPCError) already handled hidden errors, so this change makes the types consistent with actual behavior rather than altering runtime semantics — a genuine soundness improvement, and the isInferableError escape hatch keeps fully-typed recovery available where it's justified.

Pullfrog  | Fix it ➔View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@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 — Incremental review of the two commits since the prior pullfrog review (fa1df6d).

  • bc5945ba docs: apps/content/docs/integrations/effect.mdx now explicitly states createEffectClient wraps any oRPC client, server-side or client-side.
  • 02e9e6c2 test: packages/effect/src/client.test.ts adds a runtime test pinning RECURSIVE_CLIENT_UNWRAP_KEYS behavior — then/bind/valueOf/toString/toJSON resolve to the raw accessor (no recursive proxy), await effectClient is not thenable and resolves to itself, and accessors stay referentially stable.

I verified the new test (8 client runtime tests) and pnpm --filter @orpc/experimental-effect type:check both pass on the new head. The unwrap-key test is meaningful: its assertions mirror the actual key set in packages/client/src/consts.ts, so it would genuinely fail if the proxy over-wrapped then into a thenable or regressed stable-accessor caching. No new issues in this delta.

Pullfrog  | Fix it ➔View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…idden

Guards the hidden-ORPCError fallback with AnyORPCError extends E in
catchORPCErrorCode handlers, catchORPCErrorCodes handlers, and the
undeclared-key restriction, matching catchORPCError. Channels whose
non-ORPCError members cannot hold an ORPCError (e.g. string literals)
keep fully typed data and restricted codes/keys.

@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 — Incremental review of commit 2f62e689 (the delta since the prior pullfrog review at 02e9e6c), which refines the hidden-ORPCError widening.

  • Precision fix to the catch utilities (packages/effect/src/error.ts): the non-ORPCError branch of each handler type changed from an unconditional ORPCError<...unknown> to AnyORPCError extends E ? ORPCError<...unknown> : never. Since ORPCError extends Error, channels holding object supertypes (Error, TypeError, unknown) still widen, while primitive channels (string, 'boom') — which structurally cannot contain an ORPCError instance — now keep typed data and restricted codes/keys.
  • Exact-type tests (error.test-d.ts): cover both directions, including @ts-expect-error assertions that undeclared codes/keys stay rejected on channels that cannot hold hidden errors, unknown-channel widening, and updated result-channel Exclude types.

I traced each new conditional by hand over the distributed error-channel union (e.g. ORPCError<'NOT_FOUND', { id: string }> | 'boom' keeps the handler at exactly ORPCError<'NOT_FOUND', { id: string }>, while ... | Error widens to | ORPCError<ORPCErrorCode, unknown>), and pnpm --filter @orpc/experimental-effect type:check passes on the new head. This closes an over-widening gap from the initial revision — channels typed with string/number failures previously lost their typed data for no reason. No runtime behavior changed.

ℹ️ Stale PR description

The PR body still advertises the broader widening, including explicitly that string channels widen. Commit 2f62e689 deliberately removed that: only failure types that structurally hold an ORPCError (object supertypes, unknown) trigger the hidden-error branch. A one-line update to the PR description would keep it aligned with the shipped behavior.

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

@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 critical issues — one minor docs coverage note.

Reviewed changes — Incremental review of commit 2aa9d4a00 (the delta since the prior pullfrog review at 2f62e689).

  • Simplified callAsEffect (packages/effect/src/client.ts): dropped the now-unneeded ClientOptions/PromiseWithError imports and the call cast — Effect.tryPromise.try invokes client(input, ...) directly, resolveClientRest(rest) relies on inferred type args, and the proxy passes ...rest straight through (dropping as [unknown]). Verified pnpm --filter @orpc/experimental-effect type:check (tsc -b, covers all *.test-d.ts) and all 28 effect runtime tests pass on the new head.
  • Removed the hidden-ORPCError caveat paragraph from the "Catching ORPCErrors" section of apps/content/docs/integrations/effect.mdx — the only guide-level explanation that channels holding types like Error can carry hidden ORPCErrors and widen catch-handler data to unknown.

ℹ️ Docs coverage

That caveat paragraph was the only place in the guide explaining the hidden-ORPCError behavior this PR adds — that failure types like Error can hold ORPCError instances at runtime, so catch-handler data widens to unknown and undeclared codes/keys become catchable. The JSDoc in error.ts still documents it, but a reader following the guide's own catchORPCErrorCode('NOT_FOUND', error => Effect.succeed(error.data.id)) example against a channel that includes Error will hit unknown data with no guide-level explanation of why. Not blocking — the behavior is well-documented in the API reference and pinned by the type tests.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread apps/content/docs/integrations/effect.mdx

Copilot AI 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.

Pull request overview

Adds lazy Effect-based oRPC clients and improves ORPCError catch utility type soundness.

Changes:

  • Adds createEffectClient with cancellation, retries, and recursive proxying.
  • Widens catch utility types for hidden runtime ORPCErrors.
  • Adds tests, dependencies, and integration documentation.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/effect/src/client.ts Implements Effect client wrapper.
packages/effect/src/client.test.ts Tests runtime client behavior.
packages/effect/src/client.test-d.ts Tests client typings.
packages/effect/src/error.ts Updates catch utility types.
packages/effect/src/error.test-d.ts Tests hidden-error typing.
packages/effect/src/index.ts Exports client APIs.
packages/effect/package.json Adds client dependency.
packages/effect/tsconfig.json Adds client project reference.
apps/content/docs/integrations/effect.mdx Documents Effect client calls.
pnpm-lock.yaml Updates dependency resolution.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@@ -1,3 +1,4 @@
export * from './client'
Comment on lines +120 to +123
& { [K in E extends ORPCError<infer TCode, any> ? TCode : AnyORPCError extends E ? ORPCErrorCode : never]?: string extends K
? (error: never) => Effect.Effect<any, any, any>
: K extends infer C extends ORPCErrorCode
? (error: E extends AnyORPCError ? Extract<E, ORPCError<C, any>> : AnyORPCError extends E ? ORPCError<C, unknown> : never) => Effect.Effect<any, any, any>
@dinwwwh
dinwwwh merged commit dcc1484 into middleapi:main Aug 29, 2026
9 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.

2 participants