effect-orpc 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,270 @@
1
+ import {
2
+ effectInternalsSymbol,
3
+ getEffectInternals,
4
+ type EffectExtensionState,
5
+ type EffectProxyTarget,
6
+ } from "./state";
7
+
8
+ const unhandledProperty = Symbol("effect-orpc/unhandledProperty");
9
+
10
+ export type UnhandledProperty = typeof unhandledProperty;
11
+
12
+ export interface NodeProxyContext<
13
+ TTarget extends EffectProxyTarget<TSource>,
14
+ TSource extends object,
15
+ > {
16
+ methodCache: Map<PropertyKey, unknown>;
17
+ state: EffectExtensionState;
18
+ target: TTarget;
19
+ upstream: TSource;
20
+ }
21
+
22
+ interface NodeProxyInternalConfig<
23
+ TTarget extends EffectProxyTarget<TSource>,
24
+ TSource extends object,
25
+ > {
26
+ getProperty?: (
27
+ context: NodeProxyContext<TTarget, TSource>,
28
+ prop: PropertyKey,
29
+ receiver: unknown,
30
+ ) => unknown | UnhandledProperty;
31
+ getVirtual?: (
32
+ context: NodeProxyContext<TTarget, TSource>,
33
+ prop: PropertyKey,
34
+ receiver: unknown,
35
+ ) => unknown | UnhandledProperty;
36
+ virtualDescriptors?: Partial<
37
+ Record<string | symbol, Pick<PropertyDescriptor, "enumerable">>
38
+ >;
39
+ virtualKeys?: readonly (string | symbol)[];
40
+ wrapResult?: (
41
+ context: NodeProxyContext<TTarget, TSource>,
42
+ prop: PropertyKey,
43
+ result: unknown,
44
+ receiver: unknown,
45
+ ) => unknown;
46
+ }
47
+
48
+ /**
49
+ * Configures how an Effect-aware node proxy exposes virtual properties and
50
+ * rewrites returned values while the upstream builder/procedure remains the
51
+ * source of truth for passthrough behavior.
52
+ */
53
+ export interface NodeProxyConfig<
54
+ TTarget extends EffectProxyTarget<TSource>,
55
+ TSource extends object,
56
+ > extends NodeProxyInternalConfig<TTarget, TSource> {
57
+ /**
58
+ * Returns a value for virtual properties such as `~effect` or custom
59
+ * proxy-backed methods. Return `unhandled()` to fall back to the next step.
60
+ */
61
+ getVirtual?: (
62
+ context: NodeProxyContext<TTarget, TSource>,
63
+ prop: PropertyKey,
64
+ receiver: unknown,
65
+ ) => unknown | UnhandledProperty;
66
+ /**
67
+ * Intercepts property access before upstream passthrough. Return
68
+ * `unhandled()` to delegate to the wrapped upstream node.
69
+ */
70
+ getProperty?: (
71
+ context: NodeProxyContext<TTarget, TSource>,
72
+ prop: PropertyKey,
73
+ receiver: unknown,
74
+ ) => unknown | UnhandledProperty;
75
+ /**
76
+ * Declares which virtual keys should appear in reflection APIs like `in`,
77
+ * `Object.keys`, and descriptor lookup.
78
+ */
79
+ virtualKeys?: readonly (string | symbol)[];
80
+ /**
81
+ * Controls enumerability for virtual keys exposed through the proxy.
82
+ */
83
+ virtualDescriptors?: Partial<
84
+ Record<string | symbol, Pick<PropertyDescriptor, "enumerable">>
85
+ >;
86
+ /**
87
+ * Rewraps upstream method results when they should stay inside the Effect
88
+ * extension model.
89
+ */
90
+ wrapResult?: (
91
+ context: NodeProxyContext<TTarget, TSource>,
92
+ prop: PropertyKey,
93
+ result: unknown,
94
+ receiver: unknown,
95
+ ) => unknown;
96
+ }
97
+
98
+ function createNodeProxyContext<
99
+ TTarget extends EffectProxyTarget<TSource>,
100
+ TSource extends object,
101
+ >(target: TTarget): NodeProxyContext<TTarget, TSource> {
102
+ const internals = getEffectInternals(target);
103
+ return {
104
+ methodCache: internals.methodCache,
105
+ state: internals.state,
106
+ target,
107
+ upstream: internals.upstream as TSource,
108
+ };
109
+ }
110
+
111
+ function createBoundMethod<
112
+ TTarget extends EffectProxyTarget<TSource>,
113
+ TSource extends object,
114
+ >(
115
+ context: NodeProxyContext<TTarget, TSource>,
116
+ prop: PropertyKey,
117
+ value: (...args: unknown[]) => unknown,
118
+ config: NodeProxyInternalConfig<TTarget, TSource>,
119
+ receiver: unknown,
120
+ ): (...args: unknown[]) => unknown {
121
+ const cache = context.methodCache;
122
+ if (cache.has(prop)) {
123
+ return cache.get(prop) as (...args: unknown[]) => unknown;
124
+ }
125
+
126
+ const wrapped = (...args: unknown[]) => {
127
+ const result = Reflect.apply(value, context.upstream, args);
128
+ return config.wrapResult?.(context, prop, result, receiver) ?? result;
129
+ };
130
+
131
+ cache.set(prop, wrapped);
132
+ return wrapped;
133
+ }
134
+
135
+ /**
136
+ * Creates an Effect-aware proxy around a local shell object.
137
+ *
138
+ * @param target The local Effect wrapper instance that already has upstream and
139
+ * state symbols attached via `attachEffectState`.
140
+ * @param config The extension hooks that define virtual properties,
141
+ * interception points, and result rewrapping behavior for the proxy.
142
+ */
143
+ export function createNodeProxy<
144
+ TTarget extends EffectProxyTarget<TSource>,
145
+ TSource extends object,
146
+ >(target: TTarget, config: NodeProxyConfig<TTarget, TSource>): TTarget {
147
+ const privateKeys = new Set<PropertyKey>([effectInternalsSymbol]);
148
+ const virtualKeys = new Set(config.virtualKeys ?? []);
149
+
150
+ return new Proxy(target, {
151
+ get(currentTarget, prop, receiver) {
152
+ if (privateKeys.has(prop)) {
153
+ return Reflect.get(currentTarget, prop, receiver);
154
+ }
155
+
156
+ const context = createNodeProxyContext<TTarget, TSource>(
157
+ currentTarget as TTarget,
158
+ );
159
+
160
+ const virtualValue = config.getVirtual?.(context, prop, receiver);
161
+ if (virtualValue !== undefined && virtualValue !== unhandledProperty) {
162
+ return virtualValue;
163
+ }
164
+
165
+ const propertyValue = config.getProperty?.(context, prop, receiver);
166
+ if (propertyValue !== undefined && propertyValue !== unhandledProperty) {
167
+ return propertyValue;
168
+ }
169
+
170
+ const sourceValue = Reflect.get(context.upstream, prop, context.upstream);
171
+
172
+ if (Reflect.has(context.upstream, prop)) {
173
+ if (typeof sourceValue === "function") {
174
+ return createBoundMethod(
175
+ context,
176
+ prop,
177
+ sourceValue as (...args: unknown[]) => unknown,
178
+ config,
179
+ receiver,
180
+ );
181
+ }
182
+
183
+ return sourceValue;
184
+ }
185
+
186
+ return Reflect.get(currentTarget, prop, receiver);
187
+ },
188
+
189
+ has(currentTarget, prop) {
190
+ if (virtualKeys.has(prop)) {
191
+ return true;
192
+ }
193
+
194
+ const context = createNodeProxyContext<TTarget, TSource>(currentTarget);
195
+ return (
196
+ Reflect.has(context.upstream, prop) || Reflect.has(currentTarget, prop)
197
+ );
198
+ },
199
+
200
+ ownKeys(currentTarget) {
201
+ const keys = new Set<string | symbol>();
202
+
203
+ for (const key of Reflect.ownKeys(currentTarget)) {
204
+ if (!privateKeys.has(key)) {
205
+ keys.add(key);
206
+ }
207
+ }
208
+
209
+ const context = createNodeProxyContext<TTarget, TSource>(
210
+ currentTarget as TTarget,
211
+ );
212
+
213
+ for (const key of Reflect.ownKeys(context.upstream)) {
214
+ keys.add(key);
215
+ }
216
+
217
+ for (const key of virtualKeys) {
218
+ keys.add(key);
219
+ }
220
+
221
+ return [...keys];
222
+ },
223
+
224
+ getOwnPropertyDescriptor(currentTarget, prop) {
225
+ const context = createNodeProxyContext<TTarget, TSource>(
226
+ currentTarget as TTarget,
227
+ );
228
+
229
+ if (virtualKeys.has(prop)) {
230
+ const value = config.getVirtual?.(context, prop, currentTarget);
231
+ if (value !== undefined && value !== unhandledProperty) {
232
+ return {
233
+ configurable: true,
234
+ enumerable: config.virtualDescriptors?.[prop]?.enumerable ?? false,
235
+ value,
236
+ writable: false,
237
+ };
238
+ }
239
+ }
240
+
241
+ const descriptor = Reflect.getOwnPropertyDescriptor(
242
+ context.upstream,
243
+ prop,
244
+ );
245
+
246
+ if (descriptor === undefined) {
247
+ return Reflect.getOwnPropertyDescriptor(currentTarget, prop);
248
+ }
249
+
250
+ if ("value" in descriptor && typeof descriptor.value === "function") {
251
+ return {
252
+ ...descriptor,
253
+ value: createBoundMethod(
254
+ context,
255
+ prop,
256
+ descriptor.value as (...args: unknown[]) => unknown,
257
+ config,
258
+ currentTarget,
259
+ ),
260
+ };
261
+ }
262
+
263
+ return descriptor;
264
+ },
265
+ });
266
+ }
267
+
268
+ export function unhandled(): UnhandledProperty {
269
+ return unhandledProperty;
270
+ }
@@ -0,0 +1,108 @@
1
+ import type { ManagedRuntime } from "effect";
2
+
3
+ import type { EffectErrorMap } from "../tagged-error";
4
+ import type { EffectSpanConfig } from "../types";
5
+
6
+ export interface EffectExtensionState<
7
+ TRequirementsProvided = any,
8
+ TRuntimeError = any,
9
+ > {
10
+ /**
11
+ * Extended error map that supports both traditional oRPC errors and ORPCTaggedError classes.
12
+ * @see {@link EffectErrorMap}
13
+ */
14
+ effectErrorMap: EffectErrorMap;
15
+ /**
16
+ * The Effect ManagedRuntime that provides services for Effect procedures.
17
+ * @see {@link ManagedRuntime.ManagedRuntime}
18
+ */
19
+ runtime: ManagedRuntime.ManagedRuntime<TRequirementsProvided, TRuntimeError>;
20
+ /**
21
+ * Configuration for Effect span tracing.
22
+ * @see {@link EffectSpanConfig}
23
+ */
24
+ spanConfig?: EffectSpanConfig;
25
+ }
26
+
27
+ export interface EffectInternals<TUpstream extends object = object> {
28
+ upstream: TUpstream;
29
+ state: EffectExtensionState;
30
+ methodCache: Map<PropertyKey, unknown>;
31
+ }
32
+
33
+ export const effectInternalsSymbol = Symbol("effect-orpc/internals");
34
+
35
+ export interface EffectProxyTarget<TUpstream extends object = object> {
36
+ [effectInternalsSymbol]: EffectInternals<TUpstream>;
37
+ }
38
+
39
+ export function attachEffectState<
40
+ TTarget extends object,
41
+ TUpstream extends object,
42
+ >(
43
+ target: TTarget,
44
+ upstream: TUpstream,
45
+ state: EffectExtensionState,
46
+ ): asserts target is TTarget & EffectProxyTarget<TUpstream> {
47
+ Object.defineProperties(target, {
48
+ [effectInternalsSymbol]: {
49
+ configurable: true,
50
+ value: {
51
+ methodCache: new Map<PropertyKey, unknown>(),
52
+ state,
53
+ upstream,
54
+ } satisfies EffectInternals<TUpstream>,
55
+ },
56
+ });
57
+ }
58
+
59
+ export function getEffectInternals<TUpstream extends object>(
60
+ target: EffectProxyTarget<TUpstream>,
61
+ ): EffectInternals<TUpstream> {
62
+ return target[effectInternalsSymbol];
63
+ }
64
+
65
+ export function getEffectUpstream<TUpstream extends object>(
66
+ target: EffectProxyTarget<TUpstream>,
67
+ ): TUpstream {
68
+ return getEffectInternals(target).upstream;
69
+ }
70
+
71
+ export function getEffectState(
72
+ target: EffectProxyTarget,
73
+ ): EffectExtensionState {
74
+ return getEffectInternals(target).state;
75
+ }
76
+
77
+ export function getEffectMethodCache(
78
+ target: EffectProxyTarget,
79
+ ): Map<PropertyKey, unknown> {
80
+ return getEffectInternals(target).methodCache;
81
+ }
82
+
83
+ export function hasEffectState(value: unknown): value is EffectProxyTarget {
84
+ return (
85
+ typeof value === "object" &&
86
+ value !== null &&
87
+ effectInternalsSymbol in (value as object)
88
+ );
89
+ }
90
+
91
+ export function assertEffectState<TUpstream extends object>(
92
+ value: object,
93
+ ): asserts value is EffectProxyTarget<TUpstream> {
94
+ if (!hasEffectState(value)) {
95
+ throw new Error("Expected effect state to be attached");
96
+ }
97
+ }
98
+
99
+ export function getEffectErrorMap(value: {
100
+ "~effect"?: { effectErrorMap: EffectErrorMap };
101
+ "~orpc": { errorMap: EffectErrorMap };
102
+ }): EffectErrorMap {
103
+ return value["~effect"]?.effectErrorMap ?? value["~orpc"].errorMap;
104
+ }
105
+
106
+ export function unwrapEffectUpstream<T extends object>(value: T): T {
107
+ return hasEffectState(value) ? (getEffectUpstream(value) as T) : value;
108
+ }
package/src/index.ts CHANGED
@@ -45,6 +45,8 @@ export type {
45
45
  export type {
46
46
  AnyBuilderLike,
47
47
  EffectBuilderDef,
48
+ EffectBuilderSurface,
49
+ EffectDecoratedProcedureSurface,
48
50
  EffectBuilderWithMiddlewares,
49
51
  EffectErrorMapToErrorMap,
50
52
  EffectProcedureBuilder,
@@ -0,0 +1,253 @@
1
+ import { isLazy, isProcedure, os, unlazy } from "@orpc/server";
2
+ import { Layer, ManagedRuntime } from "effect";
3
+ import { describe, expect, it } from "vitest";
4
+ import z from "zod";
5
+
6
+ import { EffectBuilder, makeEffectORPC } from "../effect-builder";
7
+ import { EffectDecoratedProcedure } from "../effect-procedure";
8
+ import { ORPCTaggedError } from "../tagged-error";
9
+ const runtime = ManagedRuntime.make(Layer.empty);
10
+
11
+ function makeCustomBuilder(meta: Record<string, unknown> = {}): {
12
+ "~orpc": (typeof os)["~orpc"];
13
+ customBuilderLike(label: string): any;
14
+ customValue(): unknown;
15
+ } {
16
+ return {
17
+ "~orpc": {
18
+ ...os["~orpc"],
19
+ meta,
20
+ },
21
+ customBuilderLike(this: any, label: string) {
22
+ return makeCustomBuilder({
23
+ ...(this["~orpc"].meta as Record<string, unknown>),
24
+ label,
25
+ });
26
+ },
27
+ customValue(this: any) {
28
+ return this["~orpc"].meta;
29
+ },
30
+ };
31
+ }
32
+
33
+ describe("effectBuilder proxy compatibility", () => {
34
+ it("preserves instanceof and virtual reflection surface", () => {
35
+ const builder = makeEffectORPC(runtime);
36
+
37
+ expect(builder).toBeInstanceOf(EffectBuilder);
38
+ expect("~orpc" in builder).toBe(true);
39
+ expect("~effect" in builder).toBe(true);
40
+ expect("errors" in builder).toBe(true);
41
+ expect("effect" in builder).toBe(true);
42
+ expect("traced" in builder).toBe(true);
43
+ expect("handler" in builder).toBe(true);
44
+ expect("router" in builder).toBe(true);
45
+ expect("lazy" in builder).toBe(true);
46
+ expect("middleware" in builder).toBe(true);
47
+
48
+ expect(Object.keys(builder)).toEqual(
49
+ expect.arrayContaining(["~effect", "~orpc"]),
50
+ );
51
+ expect(Reflect.ownKeys(builder)).toEqual(
52
+ expect.arrayContaining([
53
+ "~effect",
54
+ "~orpc",
55
+ "errors",
56
+ "effect",
57
+ "traced",
58
+ "handler",
59
+ "router",
60
+ "lazy",
61
+ ]),
62
+ );
63
+
64
+ expect(Object.prototype.hasOwnProperty.call(builder, "~orpc")).toBe(true);
65
+ expect(Object.prototype.hasOwnProperty.call(builder, "~effect")).toBe(true);
66
+ expect(Object.prototype.hasOwnProperty.call(builder, "effect")).toBe(true);
67
+
68
+ const orpcDescriptor = Object.getOwnPropertyDescriptor(builder, "~orpc");
69
+ expect(orpcDescriptor?.enumerable).toBe(true);
70
+ expect(orpcDescriptor?.value).toBe(builder["~orpc"]);
71
+
72
+ const effectDescriptor = Object.getOwnPropertyDescriptor(
73
+ builder,
74
+ "~effect",
75
+ );
76
+ expect(effectDescriptor?.enumerable).toBe(true);
77
+ expect(effectDescriptor?.value).toStrictEqual(builder["~effect"]);
78
+
79
+ const methodDescriptor = Object.getOwnPropertyDescriptor(builder, "effect");
80
+ expect(methodDescriptor?.enumerable).toBe(false);
81
+ expect(methodDescriptor?.value).toBeTypeOf("function");
82
+ });
83
+
84
+ it("keeps extracted forwarded and intercepted methods callable", () => {
85
+ const builder = makeEffectORPC(runtime).$meta({ mode: "dev" });
86
+
87
+ const meta = builder.meta;
88
+ const prefixed = builder.prefix;
89
+ const effect = builder.effect;
90
+
91
+ const withMeta = meta({ log: true } as any);
92
+ const withPrefix = prefixed("/api");
93
+ const procedure = effect(function* () {
94
+ return "ok";
95
+ });
96
+
97
+ expect(withMeta).toBeInstanceOf(EffectBuilder);
98
+ expect(withMeta["~effect"].meta).toEqual({ mode: "dev", log: true });
99
+ expect(withPrefix).toBeInstanceOf(EffectBuilder);
100
+ expect((withPrefix as any)["~effect"].prefix).toBe("/api");
101
+ expect(procedure).toBeInstanceOf(EffectDecoratedProcedure);
102
+ });
103
+
104
+ it("preserves wrapped chaining across forwarded and intercepted methods", () => {
105
+ const routedBuilder = makeEffectORPC(runtime).prefix("/api").tag("users");
106
+ const builder = makeEffectORPC(runtime)
107
+ .$meta({ scope: "users" })
108
+ .errors({ BAD_REQUEST: { message: "bad request" } })
109
+ .traced("users.list")
110
+ .input(z.object({ id: z.string() }))
111
+ .output(z.object({ id: z.string() }));
112
+
113
+ expect(routedBuilder).toBeInstanceOf(EffectBuilder);
114
+ expect((routedBuilder as any)["~effect"].prefix).toBe("/api");
115
+ expect((routedBuilder as any)["~effect"].tags).toEqual(["users"]);
116
+ expect(builder).toBeInstanceOf(EffectBuilder);
117
+ expect(builder["~effect"].meta).toEqual({ scope: "users" });
118
+ expect(builder["~effect"].spanConfig?.name).toBe("users.list");
119
+
120
+ const procedure = builder.handler(
121
+ ({ input }: { input: { id: string } }) => input,
122
+ );
123
+ expect(procedure).toBeInstanceOf(EffectDecoratedProcedure);
124
+ expect(procedure["~effect"].runtime).toBe(runtime);
125
+ });
126
+
127
+ it("preserves tagged class support in errors()", () => {
128
+ class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", {
129
+ code: "NOT_FOUND",
130
+ schema: z.object({ userId: z.string() }),
131
+ }) {}
132
+
133
+ const builder = makeEffectORPC(runtime).errors({
134
+ UserNotFoundError,
135
+ });
136
+
137
+ expect(builder["~effect"].effectErrorMap.UserNotFoundError).toBe(
138
+ UserNotFoundError,
139
+ );
140
+ expect(builder["~orpc"].errorMap).toHaveProperty("NOT_FOUND");
141
+ });
142
+
143
+ it("keeps handler, effect, router, and lazy return behavior unchanged", async () => {
144
+ const builder = makeEffectORPC(runtime);
145
+ const procedure = builder.effect(function* () {
146
+ return { output: "pong" };
147
+ });
148
+
149
+ const handled = builder.handler(() => "handled");
150
+ const effected = builder.effect(function* () {
151
+ return "effected";
152
+ });
153
+ const routed = builder.prefix("/v1").router({ ping: procedure });
154
+ const lazied = builder.lazy(async () => ({ default: { ping: procedure } }));
155
+
156
+ expect(handled).toBeInstanceOf(EffectDecoratedProcedure);
157
+ expect(effected).toBeInstanceOf(EffectDecoratedProcedure);
158
+ expect(routed.ping["~effect"].runtime).toBe(runtime);
159
+ expect(isLazy(lazied)).toBe(true);
160
+
161
+ const { default: resolved } = await unlazy(lazied as any);
162
+ expect(resolved.ping["~effect"].runtime).toBe(runtime);
163
+ });
164
+
165
+ it("preserves decorated procedure proxy reflection and extracted methods", () => {
166
+ const procedure = makeEffectORPC(runtime)
167
+ .route({ path: "/users" })
168
+ .handler(({ input }) => input);
169
+
170
+ expect(procedure).toBeInstanceOf(EffectDecoratedProcedure);
171
+ expect("~orpc" in procedure).toBe(true);
172
+ expect("~effect" in procedure).toBe(true);
173
+ expect("errors" in procedure).toBe(true);
174
+ expect("meta" in procedure).toBe(true);
175
+ expect("route" in procedure).toBe(true);
176
+ expect("use" in procedure).toBe(true);
177
+ expect("callable" in procedure).toBe(true);
178
+ expect("actionable" in procedure).toBe(true);
179
+
180
+ expect(Reflect.ownKeys(procedure)).toEqual(
181
+ expect.arrayContaining([
182
+ "~orpc",
183
+ "~effect",
184
+ "errors",
185
+ "meta",
186
+ "route",
187
+ "use",
188
+ "callable",
189
+ "actionable",
190
+ ]),
191
+ );
192
+
193
+ const meta = procedure.meta;
194
+ const route = procedure.route;
195
+ const errors = procedure.errors;
196
+
197
+ expect(meta({ scope: "users" } as any)["~effect"].meta).toEqual({
198
+ scope: "users",
199
+ });
200
+ expect(route({ method: "GET" })["~effect"].route).toEqual({
201
+ method: "GET",
202
+ path: "/users",
203
+ });
204
+ const withError = errors({ BAD_REQUEST: { message: "bad request" } });
205
+ expect(withError["~orpc"].errorMap.BAD_REQUEST.message).toBe("bad request");
206
+ });
207
+
208
+ it("keeps callable procedure clients executable while exposing the procedure surface", async () => {
209
+ const procedure = makeEffectORPC(runtime).handler(({ input }) => ({
210
+ echoed: input,
211
+ }));
212
+
213
+ const callable = procedure.callable();
214
+
215
+ await expect(callable("hello")).resolves.toEqual({ echoed: "hello" });
216
+ expect(callable).toSatisfy(isProcedure);
217
+ expect(callable["~effect"].runtime).toBe(runtime);
218
+ expect(callable.route({ path: "/echo" })).toBeInstanceOf(
219
+ EffectDecoratedProcedure,
220
+ );
221
+ });
222
+
223
+ it("applies builder route and middleware enhancements to routed procedures", () => {
224
+ const builder = makeEffectORPC(runtime);
225
+ const middleware = builder.middleware(({ next }) => next({}));
226
+ const procedure = builder.route({ path: "/ping" }).handler(() => "pong");
227
+
228
+ const routed = builder.use(middleware).prefix("/api").router({ procedure });
229
+
230
+ expect(routed.procedure["~orpc"].route.path).toBe("/api/ping");
231
+ expect(routed.procedure["~effect"].route.path).toBe("/api/ping");
232
+ expect(routed.procedure["~orpc"].middlewares).toHaveLength(
233
+ procedure["~orpc"].middlewares.length + 1,
234
+ );
235
+ expect(routed.procedure["~effect"].middlewares).toHaveLength(
236
+ procedure["~effect"].middlewares.length + 1,
237
+ );
238
+ });
239
+
240
+ it("rewraps unknown builder-like methods and passes through non-builder results", () => {
241
+ const builder = makeEffectORPC(runtime, makeCustomBuilder() as any) as any;
242
+
243
+ const customBuilderLike = builder.customBuilderLike;
244
+ const customValue = builder.customValue;
245
+
246
+ const next = customBuilderLike("proxy");
247
+
248
+ expect(next).toBeInstanceOf(EffectBuilder);
249
+ expect(next["~effect"].meta).toEqual({ label: "proxy" });
250
+ expect(customValue()).toEqual({});
251
+ expect(next.customValue()).toEqual({ label: "proxy" });
252
+ });
253
+ });
@@ -1,5 +1,6 @@
1
1
  import type { ContractProcedure, Schema } from "@orpc/contract";
2
2
  import type {
3
+ Builder,
3
4
  DecoratedMiddleware,
4
5
  Middleware,
5
6
  MiddlewareOutputFn,
@@ -51,6 +52,22 @@ const withOutput = procedureBuilder.output(outputSchema);
51
52
  const withInputOutput = withInput.output(outputSchema);
52
53
 
53
54
  describe("parity: @orpc/server builder.test-d.ts", () => {
55
+ it("exposes every upstream builder key", () => {
56
+ type MissingEffectBuilderKeys = Exclude<
57
+ keyof Builder<
58
+ InitialContext,
59
+ CurrentContext,
60
+ typeof inputSchema,
61
+ typeof outputSchema,
62
+ typeof baseErrorMap,
63
+ BaseMeta
64
+ >,
65
+ keyof typeof typedBuilder
66
+ >;
67
+
68
+ expectTypeOf<MissingEffectBuilderKeys>().toEqualTypeOf<never>();
69
+ });
70
+
54
71
  it("is a contract procedure", () => {
55
72
  expectTypeOf<
56
73
  AssertExtends<