effect-orpc 0.1.4 → 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,346 @@
1
+ import { oc, type InferSchemaOutput } from "@orpc/contract";
2
+ import { call, ORPCError, type Router } from "@orpc/server";
3
+ import { Effect, FiberRef, Layer, ManagedRuntime } from "effect";
4
+ import { describe, expect, expectTypeOf, it } from "vitest";
5
+ import z from "zod";
6
+
7
+ import { eoc, implementEffect, ORPCTaggedError } from "../index";
8
+ import { withFiberContext } from "../node";
9
+
10
+ class Counter extends Effect.Tag("Counter")<
11
+ Counter,
12
+ {
13
+ readonly increment: (n: number) => Effect.Effect<number>;
14
+ }
15
+ >() {}
16
+
17
+ const requestIdRef = FiberRef.unsafeMake("missing");
18
+
19
+ const runtime = ManagedRuntime.make(
20
+ Layer.succeed(Counter, {
21
+ increment: (n: number) => Effect.succeed(n + 1),
22
+ }),
23
+ );
24
+
25
+ const contract = {
26
+ users: {
27
+ list: oc
28
+ .input(z.object({ amount: z.number() }))
29
+ .output(z.object({ next: z.number(), requestId: z.string() })),
30
+ },
31
+ };
32
+
33
+ describe("implementEffect", () => {
34
+ it("mirrors the contract tree and adds effect support on leaves", async () => {
35
+ const oe = implementEffect(contract, runtime);
36
+
37
+ expect(oe.users).toBeDefined();
38
+ expect(oe.users.list).toBeDefined();
39
+ expect(oe.users.list.handler).toBeTypeOf("function");
40
+ expect(oe.users.list.effect).toBeTypeOf("function");
41
+
42
+ const procedure = oe.users.list.effect(function* ({ input }) {
43
+ const counter = yield* Counter;
44
+ const requestId = yield* FiberRef.get(requestIdRef);
45
+
46
+ return {
47
+ next: yield* counter.increment(input.amount),
48
+ requestId,
49
+ };
50
+ });
51
+
52
+ const result = await Effect.runPromise(
53
+ Effect.gen(function* () {
54
+ yield* FiberRef.set(requestIdRef, "req-123");
55
+ return yield* withFiberContext(() => call(procedure, { amount: 2 }));
56
+ }),
57
+ );
58
+
59
+ expect(result).toEqual({
60
+ next: 3,
61
+ requestId: "req-123",
62
+ });
63
+ });
64
+
65
+ it("preserves contract enforcement at the root router", async () => {
66
+ const oe = implementEffect(contract, runtime);
67
+
68
+ const router = oe.router({
69
+ users: {
70
+ list: oe.users.list.effect(function* ({ input }) {
71
+ const counter = yield* Counter;
72
+
73
+ return {
74
+ next: yield* counter.increment(input.amount),
75
+ requestId: yield* FiberRef.get(requestIdRef),
76
+ };
77
+ }),
78
+ },
79
+ });
80
+
81
+ const result = await Effect.runPromise(
82
+ Effect.gen(function* () {
83
+ yield* FiberRef.set(requestIdRef, "req-456");
84
+ return yield* withFiberContext(() =>
85
+ call(router.users.list, { amount: 4 }),
86
+ );
87
+ }),
88
+ );
89
+
90
+ expect(result).toEqual({
91
+ next: 5,
92
+ requestId: "req-456",
93
+ });
94
+
95
+ expectTypeOf(router.users.list["~effect"]).toBeObject();
96
+ expectTypeOf(router).toExtend<
97
+ Router<typeof contract, Record<string, never>>
98
+ >();
99
+ });
100
+
101
+ it("maps tagged errors and raw ORPCError values the same way as EffectBuilder", async () => {
102
+ class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", {
103
+ code: "NOT_FOUND",
104
+ schema: z.object({ userId: z.string() }),
105
+ }) {}
106
+
107
+ const taggedContract = {
108
+ users: {
109
+ find: eoc
110
+ .errors({
111
+ NOT_FOUND: UserNotFoundError,
112
+ })
113
+ .input(z.object({ userId: z.string() }))
114
+ .output(z.object({ userId: z.string() })),
115
+ },
116
+ };
117
+
118
+ const oe = implementEffect(taggedContract, runtime);
119
+
120
+ const taggedProcedure = oe.users.find.effect(function* ({ input }) {
121
+ return yield* Effect.fail(
122
+ new UserNotFoundError({ data: { userId: input.userId } }),
123
+ );
124
+ });
125
+
126
+ await expect(
127
+ call(taggedProcedure, { userId: "u-1" }),
128
+ ).rejects.toMatchObject({
129
+ code: "NOT_FOUND",
130
+ data: { userId: "u-1" },
131
+ });
132
+
133
+ const rawProcedure = oe.users.find.effect(function* () {
134
+ return yield* Effect.fail(
135
+ new ORPCError("FORBIDDEN", {
136
+ data: { userId: "nope" },
137
+ }),
138
+ );
139
+ });
140
+
141
+ await expect(call(rawProcedure, { userId: "u-2" })).rejects.toMatchObject({
142
+ code: "FORBIDDEN",
143
+ data: { userId: "nope" },
144
+ });
145
+ });
146
+
147
+ it("preserves tagged error constructors from eoc inside effect handlers", async () => {
148
+ class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", {
149
+ schema: z.object({ userId: z.string() }),
150
+ }) {}
151
+
152
+ const taggedContract = {
153
+ users: {
154
+ find: eoc
155
+ .errors({
156
+ NOT_FOUND: UserNotFoundError,
157
+ FORBIDDEN: {},
158
+ })
159
+ .input(z.object({ userId: z.string() }))
160
+ .output(z.object({ userId: z.string() })),
161
+ },
162
+ };
163
+
164
+ const oe = implementEffect(taggedContract, runtime);
165
+ let taggedError:
166
+ | ORPCError<"USER_NOT_FOUND_ERROR", { userId: string }>
167
+ | undefined;
168
+ let forbiddenError: ORPCError<"FORBIDDEN", unknown> | undefined;
169
+
170
+ const procedure = oe.users.find.effect(function* ({ input, errors }) {
171
+ taggedError = errors.NOT_FOUND({
172
+ data: { userId: input.userId },
173
+ });
174
+ forbiddenError = errors.FORBIDDEN();
175
+
176
+ return yield* Effect.fail(taggedError);
177
+ });
178
+
179
+ const result = call(procedure, { userId: "u-3" });
180
+ await expect(result).rejects.toMatchObject({
181
+ code: "USER_NOT_FOUND_ERROR",
182
+ data: { userId: "u-3" },
183
+ });
184
+ await expect(result).rejects.toBeInstanceOf(ORPCError);
185
+
186
+ expect(taggedError).toBeInstanceOf(UserNotFoundError);
187
+ expect(forbiddenError).toBeInstanceOf(ORPCError);
188
+ });
189
+
190
+ it("preserves tagged error constructors with custom code from eoc inside effect handlers", async () => {
191
+ class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", {
192
+ schema: z.object({ userId: z.string() }),
193
+ code: "NOT_FOUND",
194
+ }) {}
195
+
196
+ const taggedContract = {
197
+ users: {
198
+ find: eoc
199
+ .errors({
200
+ UserNotFoundError,
201
+ FORBIDDEN: {},
202
+ })
203
+ .input(z.object({ userId: z.string() }))
204
+ .output(z.object({ userId: z.string() })),
205
+ },
206
+ };
207
+
208
+ const oe = implementEffect(taggedContract, runtime);
209
+ let taggedError: ORPCError<"NOT_FOUND", { userId: string }> | undefined;
210
+ let forbiddenError: ORPCError<"FORBIDDEN", unknown> | undefined;
211
+
212
+ const procedure = oe.users.find.effect(function* ({ input, errors }) {
213
+ taggedError = errors.UserNotFoundError({
214
+ data: { userId: input.userId },
215
+ });
216
+ forbiddenError = errors.FORBIDDEN();
217
+
218
+ return yield* Effect.fail(taggedError);
219
+ });
220
+
221
+ const result = call(procedure, { userId: "u-3" });
222
+ await expect(result).rejects.toMatchObject({
223
+ code: "NOT_FOUND",
224
+ data: { userId: "u-3" },
225
+ });
226
+ await expect(result).rejects.toBeInstanceOf(ORPCError);
227
+
228
+ expect(taggedError).toBeInstanceOf(UserNotFoundError);
229
+ expect(forbiddenError).toBeInstanceOf(ORPCError);
230
+ });
231
+
232
+ it("preserves tagged error constructors through eoc.router contracts", async () => {
233
+ class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", {
234
+ schema: z.object({ userId: z.string() }),
235
+ }) {}
236
+
237
+ const routedContract = eoc
238
+ .errors({
239
+ NOT_FOUND: UserNotFoundError,
240
+ })
241
+ .router({
242
+ users: {
243
+ find: oc
244
+ .input(z.object({ userId: z.string() }))
245
+ .output(z.object({ userId: z.string() })),
246
+ },
247
+ });
248
+
249
+ const oe = implementEffect(routedContract, runtime);
250
+ let taggedError:
251
+ | ORPCError<"USER_NOT_FOUND_ERROR", { userId: string }>
252
+ | undefined;
253
+
254
+ const procedure = oe.users.find.effect(function* ({ input, errors }) {
255
+ taggedError = errors.NOT_FOUND({ data: { userId: input.userId } });
256
+ return yield* Effect.fail(taggedError);
257
+ });
258
+
259
+ const result = call(procedure, { userId: "u-4" });
260
+ await expect(result).rejects.toMatchObject({
261
+ code: "USER_NOT_FOUND_ERROR",
262
+ data: { userId: "u-4" },
263
+ });
264
+ await expect(result).rejects.toBeInstanceOf(ORPCError);
265
+
266
+ expect(taggedError).toBeInstanceOf(UserNotFoundError);
267
+ });
268
+
269
+ it("preserves tagged error constructors with custom code through eoc.router contracts", async () => {
270
+ class UserNotFoundError extends ORPCTaggedError("UserNotFoundError", {
271
+ schema: z.object({ userId: z.string() }),
272
+ code: "NOT_FOUND",
273
+ }) {}
274
+
275
+ const routedContract = eoc
276
+ .errors({
277
+ UserNotFoundError,
278
+ })
279
+ .router({
280
+ users: {
281
+ find: oc
282
+ .input(z.object({ userId: z.string() }))
283
+ .output(z.object({ userId: z.string() })),
284
+ },
285
+ });
286
+
287
+ const oe = implementEffect(routedContract, runtime);
288
+ let taggedError: ORPCError<"NOT_FOUND", { userId: string }> | undefined;
289
+
290
+ const procedure = oe.users.find.effect(function* ({ input, errors }) {
291
+ taggedError = errors.UserNotFoundError({
292
+ data: { userId: input.userId },
293
+ });
294
+ return yield* Effect.fail(taggedError);
295
+ });
296
+
297
+ const result = call(procedure, { userId: "u-4" });
298
+ await expect(result).rejects.toMatchObject({
299
+ code: "NOT_FOUND",
300
+ data: { userId: "u-4" },
301
+ });
302
+ await expect(result).rejects.toBeInstanceOf(ORPCError);
303
+
304
+ expect(taggedError).toBeInstanceOf(UserNotFoundError);
305
+ });
306
+
307
+ it("retains contract-first restrictions at the type level", () => {
308
+ const oe = implementEffect(contract, runtime);
309
+
310
+ expectTypeOf(oe.users.list.handler).toBeFunction();
311
+ expectTypeOf(oe.users.list.effect).toBeFunction();
312
+ // @ts-expect-error input is not a property of the implementer
313
+ expect(oe.users.list.input).toBeUndefined();
314
+ });
315
+
316
+ it("enforces contract output typing for handlers", () => {
317
+ const oe = implementEffect(contract, runtime);
318
+
319
+ oe.users.list.effect(
320
+ // @ts-expect-error effect() must return the contract output shape
321
+ function* () {
322
+ return { next: "wrong", requestId: 123 };
323
+ },
324
+ );
325
+
326
+ oe.users.list.handler(
327
+ // @ts-expect-error handler() must return the contract output shape
328
+ () => ({ next: "wrong", requestId: 123 }),
329
+ );
330
+
331
+ const procedure = oe.users.list.effect(function* ({ input }) {
332
+ return {
333
+ next: input.amount + 1,
334
+ requestId: "req-ok",
335
+ };
336
+ });
337
+
338
+ type ProcedureOutput = InferSchemaOutput<
339
+ NonNullable<(typeof procedure)["~orpc"]["outputSchema"]>
340
+ >;
341
+ expectTypeOf<ProcedureOutput>().toEqualTypeOf<{
342
+ next: number;
343
+ requestId: string;
344
+ }>();
345
+ });
346
+ });
@@ -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
+ });
@@ -50,8 +50,12 @@ describe("effectErrorMap types", () => {
50
50
  });
51
51
 
52
52
  it("should infer correct union type from EffectErrorMap", () => {
53
+ const zBadRequestData = z.object({
54
+ status: z.number(),
55
+ message: z.string(),
56
+ });
53
57
  type TestErrorMap = {
54
- BAD_REQUEST: { status?: number; message?: string };
58
+ BAD_REQUEST: { data: typeof zBadRequestData };
55
59
  USER_NOT_FOUND_ERROR: typeof UserNotFoundError;
56
60
  FORBIDDEN: typeof PermissionDenied;
57
61
  };
@@ -59,8 +63,8 @@ describe("effectErrorMap types", () => {
59
63
  type ErrorUnion = EffectErrorMapToUnion<TestErrorMap>;
60
64
 
61
65
  // The union should include ORPCError for traditional and tagged error instances for classes
62
- expectTypeOf<ErrorUnion>().toMatchTypeOf<
63
- | ORPCError<"BAD_REQUEST", unknown>
66
+ expectTypeOf<ErrorUnion>().toExtend<
67
+ | ORPCError<"BAD_REQUEST", typeof zBadRequestData>
64
68
  | ORPCTaggedErrorInstance<"UserNotFoundError", "USER_NOT_FOUND_ERROR">
65
69
  | ORPCTaggedErrorInstance<"PermissionDenied", "FORBIDDEN">
66
70
  >();
@@ -84,6 +88,21 @@ describe("isORPCTaggedErrorClass", () => {
84
88
  });
85
89
 
86
90
  describe("createEffectErrorConstructorMap", () => {
91
+ it("preserves declaration keys even when a tagged error class uses a different code", () => {
92
+ const errorMap = {
93
+ NOT_FOUND: UserNotFoundError,
94
+ } satisfies EffectErrorMap;
95
+
96
+ const constructorMap = createEffectErrorConstructorMap(errorMap);
97
+ const userNotFoundError = constructorMap.NOT_FOUND({
98
+ data: { userId: "123" },
99
+ });
100
+
101
+ expect(constructorMap).not.toHaveProperty("USER_NOT_FOUND_ERROR");
102
+ expect(userNotFoundError).toBeInstanceOf(UserNotFoundError);
103
+ expect(userNotFoundError.code).toBe("USER_NOT_FOUND_ERROR");
104
+ });
105
+
87
106
  it("should pass through tagged error classes", () => {
88
107
  const errorMap = {
89
108
  USER_NOT_FOUND_ERROR: UserNotFoundError,
@@ -0,0 +1,32 @@
1
+ import { Layer, ManagedRuntime } from "effect";
2
+
3
+ import { eoc } from "../index";
4
+ import {
5
+ baseErrorMap,
6
+ baseMeta,
7
+ inputSchema,
8
+ outputSchema,
9
+ pong,
10
+ } from "./shared";
11
+
12
+ export type InitialContext = { db: string };
13
+ export type CurrentContext = InitialContext & { auth: boolean };
14
+
15
+ export const runtime = ManagedRuntime.make(Layer.empty);
16
+
17
+ export const typedContract = {
18
+ ping: eoc
19
+ .errors(baseErrorMap)
20
+ .meta(baseMeta)
21
+ .input(inputSchema)
22
+ .output(outputSchema),
23
+ pong,
24
+ nested: {
25
+ ping: eoc
26
+ .errors(baseErrorMap)
27
+ .meta(baseMeta)
28
+ .input(inputSchema)
29
+ .output(outputSchema),
30
+ pong,
31
+ },
32
+ };