effect-orpc 0.3.0 → 0.5.0

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.
@@ -1,6 +1,6 @@
1
1
  import type { InferSchemaOutput } from "@orpc/contract";
2
2
  import { isContractProcedure } from "@orpc/contract";
3
- import { call, os } from "@orpc/server";
3
+ import { call, createRouterClient, os } from "@orpc/server";
4
4
  import {
5
5
  Context,
6
6
  Effect,
@@ -8,13 +8,15 @@ import {
8
8
  Layer,
9
9
  ManagedRuntime,
10
10
  Option,
11
+ Tracer,
11
12
  } from "effect";
12
13
  import { beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest";
13
14
  import z from "zod";
14
15
 
15
- import { EffectBuilder, makeEffectORPC } from "../effect-builder";
16
+ import { EffectBuilder, eos, makeEffectORPC } from "../effect-builder";
16
17
  import { EffectDecoratedProcedure } from "../effect-procedure";
17
18
  import { withFiberContext } from "../node";
19
+ import { makeEffectRuntimeRunner } from "../runtime-source";
18
20
  import { ORPCTaggedError, effectErrorMapToErrorMap } from "../tagged-error";
19
21
  import {
20
22
  baseErrorMap,
@@ -27,6 +29,7 @@ import {
27
29
 
28
30
  const mid = vi.fn();
29
31
  const runtime = ManagedRuntime.make(Layer.empty);
32
+ const runner = makeEffectRuntimeRunner(runtime);
30
33
 
31
34
  const def = {
32
35
  config: {
@@ -43,11 +46,62 @@ const def = {
43
46
  outputValidationIndex: 88,
44
47
  route: baseRoute,
45
48
  dedupeLeadingMiddlewares: true,
49
+ runner,
46
50
  runtime,
47
51
  };
48
52
 
49
53
  const builder = new EffectBuilder(def);
50
54
 
55
+ type RecordedSpan = {
56
+ readonly name: string;
57
+ readonly parentName: string | undefined;
58
+ };
59
+
60
+ function makeRecordedRuntime() {
61
+ const spans: Array<RecordedSpan> = [];
62
+ const spanNamesById = new Map<string, string>();
63
+ const tracer = Tracer.make({
64
+ context: (f) => f(),
65
+ span(name, parent, context, links, startTime, kind) {
66
+ const spanId = `span-${spans.length + 1}`;
67
+ spans.push({
68
+ name,
69
+ parentName: Option.match(parent, {
70
+ onNone: () => undefined,
71
+ onSome: (span) => spanNamesById.get(span.spanId),
72
+ }),
73
+ });
74
+ spanNamesById.set(spanId, name);
75
+ const attributes = new Map<string, unknown>();
76
+
77
+ return {
78
+ _tag: "Span" as const,
79
+ name,
80
+ spanId,
81
+ traceId: "trace",
82
+ parent,
83
+ context,
84
+ status: { _tag: "Started" as const, startTime },
85
+ attributes,
86
+ links,
87
+ sampled: true,
88
+ kind,
89
+ end() {},
90
+ attribute(key: string, value: unknown) {
91
+ attributes.set(key, value);
92
+ },
93
+ event() {},
94
+ addLinks() {},
95
+ };
96
+ },
97
+ });
98
+
99
+ return {
100
+ runtime: ManagedRuntime.make(Layer.setTracer(tracer)),
101
+ spans,
102
+ };
103
+ }
104
+
51
105
  beforeEach(() => vi.clearAllMocks());
52
106
 
53
107
  describe("effectBuilder", () => {
@@ -74,15 +128,55 @@ describe("effectBuilder", () => {
74
128
  });
75
129
 
76
130
  describe(".use", () => {
77
- it("without map input", () => {
78
- const mid2 = vi.fn();
131
+ it("without map input", async () => {
132
+ const mid2 = vi.fn(({ next }) =>
133
+ next({ context: { fromMiddleware: true } }),
134
+ );
79
135
  const applied = builder.use(mid2);
80
136
 
81
137
  expect(applied).instanceOf(EffectBuilder);
82
138
  expect(applied).not.toBe(builder);
83
- expect(applied["~effect"]).toEqual({
84
- ...def,
85
- middlewares: [mid, mid2],
139
+ expect(applied["~effect"].middlewares).toHaveLength(2);
140
+ expect(applied["~effect"].middlewares[0]).toBe(mid);
141
+
142
+ const wrapped = applied["~effect"].middlewares[1]!;
143
+ const procedure = eos.effect(function* () {
144
+ return "ok";
145
+ });
146
+ let nextCalls = 0;
147
+ let nextOptions: unknown;
148
+ const next = <TContext extends Record<PropertyKey, unknown>>(options?: {
149
+ context?: TContext;
150
+ }) => {
151
+ nextCalls++;
152
+ nextOptions = options;
153
+ return Promise.resolve({
154
+ output: "ok",
155
+ context: options?.context ?? ({} as TContext),
156
+ });
157
+ };
158
+ await expect(
159
+ wrapped(
160
+ {
161
+ context: {},
162
+ errors: {},
163
+ path: [],
164
+ procedure,
165
+ signal: undefined,
166
+ lastEventId: undefined,
167
+ next,
168
+ },
169
+ "input",
170
+ vi.fn(),
171
+ ),
172
+ ).resolves.toEqual({
173
+ output: "ok",
174
+ context: { fromMiddleware: true },
175
+ });
176
+ expect(mid2).toHaveBeenCalledOnce();
177
+ expect(nextCalls).toBe(1);
178
+ expect(nextOptions).toEqual({
179
+ context: { fromMiddleware: true },
86
180
  });
87
181
  });
88
182
  });
@@ -142,7 +236,7 @@ describe("effectBuilder", () => {
142
236
  const applied = builder.effect(effectFn);
143
237
 
144
238
  expect(applied).instanceOf(EffectDecoratedProcedure);
145
- expect(applied["~effect"].runtime).toBe(runtime);
239
+ expect(applied["~effect"].runner.runtime).toBe(runtime);
146
240
  expect(applied["~effect"].handler).toBeInstanceOf(Function);
147
241
  });
148
242
 
@@ -271,7 +365,7 @@ describe("makeEffectORPC factory", () => {
271
365
  const effectBuilder = makeEffectORPC(runtime);
272
366
 
273
367
  expect(effectBuilder).instanceOf(EffectBuilder);
274
- expect(effectBuilder["~effect"].runtime).toBe(runtime);
368
+ expect(effectBuilder["~effect"].runner.runtime).toBe(runtime);
275
369
  // Should inherit os's default definition
276
370
  expect(effectBuilder["~effect"].middlewares).toEqual(
277
371
  os["~orpc"].middlewares,
@@ -285,7 +379,7 @@ describe("makeEffectORPC factory", () => {
285
379
  const effectBuilder = makeEffectORPC(runtime, os);
286
380
 
287
381
  expect(effectBuilder).instanceOf(EffectBuilder);
288
- expect(effectBuilder["~effect"].runtime).toBe(runtime);
382
+ expect(effectBuilder["~effect"].runner.runtime).toBe(runtime);
289
383
  expect(effectBuilder["~effect"].middlewares).toEqual(
290
384
  os["~orpc"].middlewares,
291
385
  );
@@ -294,6 +388,46 @@ describe("makeEffectORPC factory", () => {
294
388
  );
295
389
  });
296
390
 
391
+ it("exports an eos builder that works with provide", async () => {
392
+ class Counter extends Effect.Tag("EosCounter")<
393
+ Counter,
394
+ { increment: (n: number) => Effect.Effect<number> }
395
+ >() {}
396
+
397
+ const procedure = eos
398
+ .provide(
399
+ Layer.succeed(Counter, {
400
+ increment: (n: number) => Effect.succeed(n + 1),
401
+ }),
402
+ )
403
+ .input(z.number())
404
+ .effect(function* ({ input }) {
405
+ const counter = yield* Counter;
406
+ return yield* counter.increment(input as number);
407
+ });
408
+
409
+ expect(eos["~effect"].runner.runtime).toBeUndefined();
410
+ await expect(call(procedure, 5)).resolves.toBe(6);
411
+ });
412
+
413
+ it("does not own a ManagedRuntime when no source is provided", () => {
414
+ const effectBuilder = makeEffectORPC();
415
+
416
+ expect(effectBuilder["~effect"].runner.runtime).toBeUndefined();
417
+ });
418
+
419
+ it("does not own a ManagedRuntime when only a builder is provided", () => {
420
+ const effectBuilder = makeEffectORPC(os);
421
+
422
+ expect(effectBuilder["~effect"].runner.runtime).toBeUndefined();
423
+ });
424
+
425
+ it("does not own a ManagedRuntime when a Layer is provided", () => {
426
+ const effectBuilder = makeEffectORPC(Layer.empty);
427
+
428
+ expect(effectBuilder["~effect"].runner.runtime).toBeUndefined();
429
+ });
430
+
297
431
  it("creates working procedure with default os", async () => {
298
432
  const effectBuilder = makeEffectORPC(runtime);
299
433
 
@@ -337,6 +471,17 @@ describe("makeEffectORPC factory", () => {
337
471
  expect(result).toBe(3);
338
472
  });
339
473
 
474
+ it("supports Effect.fn handlers", async () => {
475
+ const procedure = eos.input(z.number()).effect(
476
+ Effect.fn("test.effect-handler")(function* ({ input }) {
477
+ const increment = yield* Effect.succeed(1);
478
+ return input + increment;
479
+ }),
480
+ );
481
+
482
+ await expect(call(procedure, 41)).resolves.toBe(42);
483
+ });
484
+
340
485
  it("chains builder methods correctly", () => {
341
486
  const effectBuilder = makeEffectORPC(runtime);
342
487
 
@@ -427,11 +572,8 @@ describe("effect with services", () => {
427
572
  return yield* counter.increment(input as number);
428
573
  });
429
574
 
430
- try {
431
- await expect(call(procedure, 5)).resolves.toBe(6);
432
- } finally {
433
- await effectBuilder["~effect"].runtime.dispose();
434
- }
575
+ expect(effectBuilder["~effect"].runner.runtime).toBeUndefined();
576
+ await expect(call(procedure, 5)).resolves.toBe(6);
435
577
  });
436
578
 
437
579
  it("can start without a runtime and provide a Layer", async () => {
@@ -451,11 +593,8 @@ describe("effect with services", () => {
451
593
  return yield* counter.increment(input as number);
452
594
  });
453
595
 
454
- try {
455
- await expect(call(procedure, 5)).resolves.toBe(6);
456
- } finally {
457
- await effectBuilder["~effect"].runtime.dispose();
458
- }
596
+ expect(effectBuilder["~effect"].runner.runtime).toBeUndefined();
597
+ await expect(call(procedure, 5)).resolves.toBe(6);
459
598
  });
460
599
 
461
600
  it("can wrap a custom builder without a runtime and provide a Layer", async () => {
@@ -482,14 +621,11 @@ describe("effect with services", () => {
482
621
  };
483
622
  });
484
623
 
485
- try {
486
- await expect(call(procedure, 5)).resolves.toEqual({
487
- fromCustomBuilder: true,
488
- value: 6,
489
- });
490
- } finally {
491
- await effectBuilder["~effect"].runtime.dispose();
492
- }
624
+ expect(effectBuilder["~effect"].runner.runtime).toBeUndefined();
625
+ await expect(call(procedure, 5)).resolves.toEqual({
626
+ fromCustomBuilder: true,
627
+ value: 6,
628
+ });
493
629
  });
494
630
 
495
631
  it(".provide makes a request-scoped service available to handlers", async () => {
@@ -498,10 +634,8 @@ describe("effect with services", () => {
498
634
  { id: string }
499
635
  >() {}
500
636
 
501
- const effectBuilder = makeEffectORPC(runtime).$context<{
502
- user: { id: string };
503
- }>();
504
- const procedure = effectBuilder
637
+ const procedure = eos
638
+ .$context<{ user: { id: string } }>()
505
639
  .provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
506
640
  .effect(function* () {
507
641
  return yield* CurrentUser;
@@ -512,6 +646,33 @@ describe("effect with services", () => {
512
646
  ).resolves.toEqual({ id: "u-1" });
513
647
  });
514
648
 
649
+ it(".provide supports generator request-scoped providers", async () => {
650
+ class UserPrefix extends Context.Tag("ProviderUserPrefix")<
651
+ UserPrefix,
652
+ { prefix: string }
653
+ >() {}
654
+ class CurrentUser extends Context.Tag("GeneratorProviderCurrentUser")<
655
+ CurrentUser,
656
+ { id: string }
657
+ >() {}
658
+
659
+ const procedure = eos
660
+ .$context<{ user: { id: string } }>()
661
+ .provide(UserPrefix, () => Effect.succeed({ prefix: "user:" }))
662
+ .provide(CurrentUser, function* ({ context }) {
663
+ expectTypeOf(context.user).toEqualTypeOf<{ id: string }>();
664
+ const prefix = yield* UserPrefix;
665
+ return { id: `${prefix.prefix}${context.user.id}` };
666
+ })
667
+ .effect(function* () {
668
+ return yield* CurrentUser;
669
+ });
670
+
671
+ await expect(
672
+ call(procedure, undefined, { context: { user: { id: "u-1" } } }),
673
+ ).resolves.toEqual({ id: "user:u-1" });
674
+ });
675
+
515
676
  it(".provide service overrides the same service from the runtime", async () => {
516
677
  class CurrentUser extends Context.Tag("CurrentUserOverride")<
517
678
  CurrentUser,
@@ -541,7 +702,7 @@ describe("effect with services", () => {
541
702
 
542
703
  it("Effect .use yield* next() without return runs handler once", async () => {
543
704
  let runs = 0;
544
- const procedure = makeEffectORPC(runtime)
705
+ const procedure = eos
545
706
  .use(function* ({ next }) {
546
707
  yield* Effect.void;
547
708
  yield* next();
@@ -557,7 +718,7 @@ describe("effect with services", () => {
557
718
 
558
719
  it("Effect .use guard-only middleware without next runs handler once", async () => {
559
720
  let runs = 0;
560
- const procedure = makeEffectORPC(runtime)
721
+ const procedure = eos
561
722
  .use(function* () {
562
723
  yield* Effect.void;
563
724
  })
@@ -591,10 +752,8 @@ describe("effect with services", () => {
591
752
  >() {}
592
753
 
593
754
  let seenUser: { id: string } | undefined;
594
- const effectBuilder = makeEffectORPC(runtime).$context<{
595
- user: { id: string };
596
- }>();
597
- const procedure = effectBuilder
755
+ const procedure = eos
756
+ .$context<{ user: { id: string } }>()
598
757
  .provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
599
758
  .use(function* () {
600
759
  seenUser = yield* CurrentUser;
@@ -610,8 +769,6 @@ describe("effect with services", () => {
610
769
  });
611
770
 
612
771
  it("Effect .middleware can create reusable generator middleware", async () => {
613
- const eos = makeEffectORPC(runtime);
614
-
615
772
  const reusable = eos.middleware(function* ({ next }, input: string) {
616
773
  expectTypeOf(input).toEqualTypeOf<string>();
617
774
  return yield* next({ context: { seenInput: input } });
@@ -634,32 +791,74 @@ describe("effect with services", () => {
634
791
  { value: string }
635
792
  >() {}
636
793
 
637
- const eos = makeEffectORPC(runtime).provide(MiddlewareService, () =>
794
+ const builder = eos.provide(MiddlewareService, () =>
638
795
  Effect.succeed({ value: "provided" }),
639
796
  );
640
797
 
641
- const reusable = eos.middleware(function* ({ next }) {
798
+ const reusable = builder.middleware(function* ({ next }) {
642
799
  const service = yield* MiddlewareService;
643
800
  return yield* next({ context: { serviceValue: service.value } });
644
801
  });
645
802
 
646
- const procedure = eos.use(reusable).effect(function* ({ context }) {
803
+ const procedure = builder.use(reusable).effect(function* ({ context }) {
647
804
  return context.serviceValue;
648
805
  });
649
806
 
650
807
  await expect(call(procedure, undefined)).resolves.toBe("provided");
651
808
  });
652
809
 
810
+ it("Effect .use supports Effect.fn middleware", async () => {
811
+ const procedure = eos
812
+ .use(
813
+ Effect.fn("test.middleware")(function* ({ next }) {
814
+ yield* Effect.void;
815
+ return yield* next({ context: { fromEffectFn: true } });
816
+ }),
817
+ )
818
+ .effect(function* ({ context }) {
819
+ return context.fromEffectFn;
820
+ });
821
+
822
+ await expect(call(procedure, undefined)).resolves.toBe(true);
823
+ });
824
+
825
+ it("Effect .use supports Effect.gen-returning middleware", async () => {
826
+ const procedure = eos
827
+ .use(({ next }) =>
828
+ Effect.gen(function* () {
829
+ yield* Effect.void;
830
+ return yield* next({ context: { fromEffectGen: true } });
831
+ }),
832
+ )
833
+ .effect(function* ({ context }) {
834
+ return context.fromEffectGen;
835
+ });
836
+
837
+ await expect(call(procedure, undefined)).resolves.toBe(true);
838
+ });
839
+
840
+ it("Effect .use supports normal guard-only middleware", async () => {
841
+ let guarded = false;
842
+ const procedure = eos
843
+ .use(() => {
844
+ guarded = true;
845
+ })
846
+ .effect(function* () {
847
+ return "ok";
848
+ });
849
+
850
+ await expect(call(procedure, undefined)).resolves.toBe("ok");
851
+ expect(guarded).toBe(true);
852
+ });
853
+
653
854
  it("Effect .use can enrich context through next", async () => {
654
855
  class CurrentUser extends Context.Tag("NextCurrentUser")<
655
856
  CurrentUser,
656
857
  { id: string }
657
858
  >() {}
658
859
 
659
- const effectBuilder = makeEffectORPC(runtime).$context<{
660
- user: { id: string };
661
- }>();
662
- const procedure = effectBuilder
860
+ const procedure = eos
861
+ .$context<{ user: { id: string } }>()
663
862
  .provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
664
863
  .use(function* ({ next }, _input) {
665
864
  const user = yield* CurrentUser;
@@ -675,7 +874,7 @@ describe("effect with services", () => {
675
874
  });
676
875
 
677
876
  it("Effect .use can transform downstream output", async () => {
678
- const procedure = makeEffectORPC(runtime)
877
+ const procedure = eos
679
878
  .use(function* ({ next }, _input, output) {
680
879
  const result = yield* next();
681
880
  return yield* output(`${result.output}-wrapped`);
@@ -688,7 +887,7 @@ describe("effect with services", () => {
688
887
  });
689
888
 
690
889
  it("Effect .use can transform typed downstream output after .output", async () => {
691
- const procedure = makeEffectORPC(runtime)
890
+ const procedure = eos
692
891
  .output(z.string())
693
892
  .use(function* ({ next }, _input, output) {
694
893
  const result = yield* next();
@@ -703,7 +902,7 @@ describe("effect with services", () => {
703
902
  });
704
903
 
705
904
  it("Effect .use can read typed input after .input", async () => {
706
- const procedure = makeEffectORPC(runtime)
905
+ const procedure = eos
707
906
  .input(z.object({ value: z.number() }))
708
907
  .use(function* ({ next }, input) {
709
908
  expectTypeOf(input).toMatchTypeOf<{ value: number }>();
@@ -717,7 +916,7 @@ describe("effect with services", () => {
717
916
  });
718
917
 
719
918
  it("Effect .use can read typed input and output after .input().output()", async () => {
720
- const procedure = makeEffectORPC(runtime)
919
+ const procedure = eos
721
920
  .input(z.object({ value: z.number() }))
722
921
  .output(z.string())
723
922
  .use(function* ({ next }, input, output) {
@@ -734,7 +933,7 @@ describe("effect with services", () => {
734
933
  });
735
934
 
736
935
  it("Effect .use can transform typed downstream output after .effect", async () => {
737
- const procedure = makeEffectORPC(runtime)
936
+ const procedure = eos
738
937
  .effect(function* () {
739
938
  return "ok";
740
939
  })
@@ -806,7 +1005,7 @@ describe("effect with services", () => {
806
1005
  { id: string }
807
1006
  >() {}
808
1007
 
809
- const procedure = makeEffectORPC(runtime)
1008
+ const procedure = eos
810
1009
  .$context<{ user?: { id: string } }>()
811
1010
  .provideOptional(CurrentUser, ({ context }) =>
812
1011
  Effect.succeed(Option.fromNullable(context.user)),
@@ -820,13 +1019,34 @@ describe("effect with services", () => {
820
1019
  ).resolves.toEqual(Option.some({ id: "u-6" }));
821
1020
  });
822
1021
 
1022
+ it(".provideOptional supports generator request-scoped providers", async () => {
1023
+ class CurrentUser extends Context.Tag("GeneratorOptionalCurrentUser")<
1024
+ CurrentUser,
1025
+ { id: string }
1026
+ >() {}
1027
+
1028
+ const procedure = eos
1029
+ .$context<{ user?: { id: string } }>()
1030
+ .provideOptional(CurrentUser, function* ({ context }) {
1031
+ yield* Effect.void;
1032
+ return Option.fromNullable(context.user);
1033
+ })
1034
+ .effect(function* () {
1035
+ return yield* Effect.serviceOption(CurrentUser);
1036
+ });
1037
+
1038
+ await expect(
1039
+ call(procedure, undefined, { context: { user: { id: "u-7" } } }),
1040
+ ).resolves.toEqual(Option.some({ id: "u-7" }));
1041
+ });
1042
+
823
1043
  it(".provideOptional leaves absent request-scoped services unavailable", async () => {
824
1044
  class CurrentUser extends Context.Tag("OptionalCurrentUserAbsent")<
825
1045
  CurrentUser,
826
1046
  { id: string }
827
1047
  >() {}
828
1048
 
829
- const procedure = makeEffectORPC(runtime)
1049
+ const procedure = eos
830
1050
  .$context<{ user?: { id: string } }>()
831
1051
  .provideOptional(CurrentUser, ({ context }) =>
832
1052
  Effect.succeed(Option.fromNullable(context.user)),
@@ -846,7 +1066,7 @@ describe("effect with services", () => {
846
1066
  { readonly value: string }
847
1067
  >() {}
848
1068
 
849
- makeEffectORPC(runtime)
1069
+ eos
850
1070
  .provideOptional(OptionalService, () =>
851
1071
  Effect.succeed(Option.some({ value: "provided" })),
852
1072
  )
@@ -861,12 +1081,10 @@ describe("effect with services", () => {
861
1081
 
862
1082
  describe(".traced", () => {
863
1083
  it("creates an EffectBuilder with span config", () => {
864
- const effectBuilder = makeEffectORPC(runtime);
865
-
866
- const traced = effectBuilder.traced("users.getUser");
1084
+ const traced = eos.traced("users.getUser");
867
1085
 
868
1086
  expect(traced).instanceOf(EffectBuilder);
869
- expect(traced).not.toBe(effectBuilder);
1087
+ expect(traced).not.toBe(eos);
870
1088
  expect(traced["~effect"].spanConfig).toBeDefined();
871
1089
  expect(traced["~effect"].spanConfig?.name).toBe("users.getUser");
872
1090
  expect(traced["~effect"].spanConfig?.captureStackTrace).toBeInstanceOf(
@@ -875,9 +1093,7 @@ describe(".traced", () => {
875
1093
  });
876
1094
 
877
1095
  it("preserves span config through chained methods", () => {
878
- const effectBuilder = makeEffectORPC(runtime);
879
-
880
- const procedure = effectBuilder
1096
+ const procedure = eos
881
1097
  .input(z.object({ id: z.string() }))
882
1098
  .traced("users.getUser")
883
1099
  .effect(function* () {
@@ -888,56 +1104,60 @@ describe(".traced", () => {
888
1104
  // The span wrapping happens in the handler, so we just verify the procedure was created
889
1105
  });
890
1106
 
891
- it("traced procedure handler runs successfully", async () => {
892
- const effectBuilder = makeEffectORPC(runtime);
893
-
894
- const procedure = effectBuilder
1107
+ it("traced procedure runs through a routed client", async () => {
1108
+ const procedure = eos
895
1109
  .input(z.object({ id: z.string() }))
896
1110
  .traced("users.getUser")
897
1111
  .effect(function* ({ input }) {
898
1112
  return { id: input.id, name: "Alice" };
899
1113
  });
1114
+ const client = createRouterClient({ users: { getUser: procedure } });
900
1115
 
901
- const result = await procedure["~effect"].handler({
902
- context: {},
903
- input: { id: "123" },
904
- path: ["users", "getUser"],
905
- procedure: procedure as any,
906
- signal: undefined,
907
- lastEventId: undefined,
908
- errors: {},
1116
+ await expect(client.users.getUser({ id: "123" })).resolves.toEqual({
1117
+ id: "123",
1118
+ name: "Alice",
909
1119
  });
910
-
911
- expect(result).toEqual({ id: "123", name: "Alice" });
912
1120
  });
913
1121
 
914
- it("traced procedure with Effect.fn generator syntax", async () => {
915
- const effectBuilder = makeEffectORPC(runtime);
1122
+ it("traced procedure uses the explicit span name at runtime", async () => {
1123
+ const recorded = makeRecordedRuntime();
1124
+ const effectBuilder = makeEffectORPC(recorded.runtime);
916
1125
 
917
- const procedure = effectBuilder.traced("math.add").effect(function* () {
1126
+ const procedure = effectBuilder
1127
+ .traced("custom.users.getUser")
1128
+ .effect(function* () {
1129
+ return "ok";
1130
+ });
1131
+ const client = createRouterClient({ users: { getUser: procedure } });
1132
+
1133
+ try {
1134
+ await expect(client.users.getUser(undefined)).resolves.toBe("ok");
1135
+ expect(recorded.spans).toContainEqual({
1136
+ name: "custom.users.getUser",
1137
+ parentName: undefined,
1138
+ });
1139
+ expect(recorded.spans.map(({ name }) => name)).not.toContain(
1140
+ "users.getUser",
1141
+ );
1142
+ } finally {
1143
+ await recorded.runtime.dispose();
1144
+ }
1145
+ });
1146
+
1147
+ it("traced procedure with generator syntax runs through a routed client", async () => {
1148
+ const procedure = eos.traced("math.add").effect(function* () {
918
1149
  const a = yield* Effect.succeed(10);
919
1150
  const b = yield* Effect.succeed(20);
920
1151
  return a + b;
921
1152
  });
1153
+ const client = createRouterClient({ math: { add: procedure } });
922
1154
 
923
- const result = await procedure["~effect"].handler({
924
- context: {},
925
- input: undefined,
926
- path: ["math", "add"],
927
- procedure: procedure as any,
928
- signal: undefined,
929
- lastEventId: undefined,
930
- errors: {},
931
- });
932
-
933
- expect(result).toBe(30);
1155
+ await expect(client.math.add(undefined)).resolves.toBe(30);
934
1156
  });
935
1157
 
936
1158
  it("captures stack trace at definition time", () => {
937
- const effectBuilder = makeEffectORPC(runtime);
938
-
939
1159
  // The stack trace is captured when .traced() is called
940
- const traced = effectBuilder.traced("test.procedure");
1160
+ const traced = eos.traced("test.procedure");
941
1161
 
942
1162
  const stackTrace = traced["~effect"].spanConfig?.captureStackTrace();
943
1163
  // The stack trace should be a string containing the file location
@@ -949,71 +1169,49 @@ describe(".traced", () => {
949
1169
  });
950
1170
 
951
1171
  describe("default tracing (without .traced())", () => {
952
- it("procedure without .traced() still runs successfully", async () => {
953
- const effectBuilder = makeEffectORPC(runtime);
954
-
955
- // No .traced() call - should still work and use path as span name
956
- const procedure = effectBuilder
1172
+ it("procedure without .traced() runs through a routed client", async () => {
1173
+ const procedure = eos
957
1174
  .input(z.object({ id: z.string() }))
958
1175
  .effect(function* ({ input }) {
959
1176
  return { id: input.id, name: "Bob" };
960
1177
  });
1178
+ const client = createRouterClient({ users: { findById: procedure } });
961
1179
 
962
- const result = await procedure["~effect"].handler({
963
- context: {},
964
- input: { id: "456" },
965
- path: ["users", "findById"],
966
- procedure: procedure as any,
967
- signal: undefined,
968
- lastEventId: undefined,
969
- errors: {},
1180
+ await expect(client.users.findById({ id: "456" })).resolves.toEqual({
1181
+ id: "456",
1182
+ name: "Bob",
970
1183
  });
971
-
972
- expect(result).toEqual({ id: "456", name: "Bob" });
973
1184
  });
974
1185
 
975
- it("uses procedure path as default span name", async () => {
976
- const effectBuilder = makeEffectORPC(runtime);
1186
+ it("uses procedure path as default span name at runtime", async () => {
1187
+ const recorded = makeRecordedRuntime();
1188
+ const effectBuilder = makeEffectORPC(recorded.runtime);
977
1189
 
978
- // Without .traced(), the span name should be derived from path
979
1190
  const procedure = effectBuilder.effect(function* () {
980
1191
  return "hello";
981
1192
  });
1193
+ const client = createRouterClient({ api: { v1: { greet: procedure } } });
982
1194
 
983
- // The procedure should work with any path
984
- const result = await procedure["~effect"].handler({
985
- context: {},
986
- input: undefined,
987
- path: ["api", "v1", "greet"],
988
- procedure: procedure as any,
989
- signal: undefined,
990
- lastEventId: undefined,
991
- errors: {},
992
- });
993
-
994
- expect(result).toBe("hello");
1195
+ try {
1196
+ await expect(client.api.v1.greet(undefined)).resolves.toBe("hello");
1197
+ expect(recorded.spans).toContainEqual({
1198
+ name: "api.v1.greet",
1199
+ parentName: undefined,
1200
+ });
1201
+ } finally {
1202
+ await recorded.runtime.dispose();
1203
+ }
995
1204
  });
996
1205
 
997
- it("default tracing works with Effect.fn generator", async () => {
998
- const effectBuilder = makeEffectORPC(runtime);
999
-
1000
- const procedure = effectBuilder.effect(function* () {
1206
+ it("default tracing works with generator handlers through a routed client", async () => {
1207
+ const procedure = eos.effect(function* () {
1001
1208
  const x = 5;
1002
1209
  const y = 10;
1003
1210
  return x * y;
1004
1211
  });
1212
+ const client = createRouterClient({ math: { multiply: procedure } });
1005
1213
 
1006
- const result = await procedure["~effect"].handler({
1007
- context: {},
1008
- input: undefined,
1009
- path: ["math", "multiply"],
1010
- procedure: procedure as any,
1011
- signal: undefined,
1012
- lastEventId: undefined,
1013
- errors: {},
1014
- });
1015
-
1016
- expect(result).toBe(50);
1214
+ await expect(client.math.multiply(undefined)).resolves.toBe(50);
1017
1215
  });
1018
1216
 
1019
1217
  it("default tracing works with services from runtime", async () => {
@@ -1034,36 +1232,29 @@ describe("default tracing (without .traced())", () => {
1034
1232
  .effect(function* ({ input }) {
1035
1233
  return yield* Greeter.greet(input.name);
1036
1234
  });
1235
+ const client = createRouterClient({ greeting: { say: procedure } });
1037
1236
 
1038
- const result = await procedure["~effect"].handler({
1039
- context: {},
1040
- input: { name: "World" },
1041
- path: ["greeting", "say"],
1042
- procedure: procedure as any,
1043
- signal: undefined,
1044
- lastEventId: undefined,
1045
- errors: {},
1046
- });
1047
-
1048
- expect(result).toBe("Hello, World!");
1049
-
1050
- await serviceRuntime.dispose();
1237
+ try {
1238
+ await expect(client.greeting.say({ name: "World" })).resolves.toBe(
1239
+ "Hello, World!",
1240
+ );
1241
+ } finally {
1242
+ await serviceRuntime.dispose();
1243
+ }
1051
1244
  });
1052
1245
 
1053
1246
  it("no spanConfig is set when .traced() is not called", () => {
1054
- const effectBuilder = makeEffectORPC(runtime);
1055
-
1056
1247
  // Without .traced(), spanConfig should be undefined
1057
- expect(effectBuilder["~effect"].spanConfig).toBeUndefined();
1248
+ expect(eos["~effect"].spanConfig).toBeUndefined();
1058
1249
 
1059
- const withInput = effectBuilder.input(z.string());
1250
+ const withInput = eos.input(z.string());
1060
1251
  expect(withInput["~effect"].spanConfig).toBeUndefined();
1061
1252
  });
1062
1253
 
1063
1254
  it("enforces the declared output schema for effect handlers", () => {
1064
1255
  const declaredOutputSchema = z.object({ name: z.string() });
1065
1256
 
1066
- makeEffectORPC(runtime)
1257
+ eos
1067
1258
  .input(z.string())
1068
1259
  .output(declaredOutputSchema)
1069
1260
  .effect(
@@ -1073,7 +1264,7 @@ describe("default tracing (without .traced())", () => {
1073
1264
  },
1074
1265
  );
1075
1266
 
1076
- const procedure = makeEffectORPC(runtime)
1267
+ const procedure = eos
1077
1268
  .output(declaredOutputSchema)
1078
1269
  // @ts-expect-error output() should constrain the effect return type
1079
1270
  .effect(function* () {
@@ -1092,14 +1283,14 @@ describe("default tracing (without .traced())", () => {
1092
1283
  { readonly value: string }
1093
1284
  >() {}
1094
1285
 
1095
- makeEffectORPC(runtime).effect(
1286
+ eos.effect(
1096
1287
  // @ts-expect-error MissingService is not available from the runtime or .provide
1097
1288
  function* () {
1098
1289
  return yield* MissingService;
1099
1290
  },
1100
1291
  );
1101
1292
 
1102
- makeEffectORPC(runtime)
1293
+ eos
1103
1294
  .provide(MissingService, () => Effect.succeed({ value: "provided" }))
1104
1295
  .effect(function* () {
1105
1296
  return yield* MissingService;
@@ -1111,7 +1302,7 @@ describe("default tracing (without .traced())", () => {
1111
1302
  "MissingMiddlewareService",
1112
1303
  )<MissingMiddlewareService, { readonly value: string }>() {}
1113
1304
 
1114
- makeEffectORPC(runtime)
1305
+ eos
1115
1306
  .use(
1116
1307
  // @ts-expect-error MissingMiddlewareService is not available from the runtime or .provide
1117
1308
  function* () {
@@ -1122,7 +1313,7 @@ describe("default tracing (without .traced())", () => {
1122
1313
  return "ok";
1123
1314
  });
1124
1315
 
1125
- makeEffectORPC(runtime)
1316
+ eos
1126
1317
  .provide(MissingMiddlewareService, () =>
1127
1318
  Effect.succeed({ value: "provided" }),
1128
1319
  )