effect-orpc 0.4.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.
- package/README.md +81 -93
- package/dist/index.js +350 -80
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/effect-builder.ts +36 -22
- package/src/effect-procedure.ts +31 -7
- package/src/effect-runtime.ts +622 -80
- package/src/tests/effect-builder.proxy.test.ts +15 -17
- package/src/tests/effect-builder.test.ts +296 -139
- package/src/tests/effect-callback-shapes.test.ts +410 -0
- package/src/tests/effect-error-map.test.ts +4 -6
- package/src/tests/effect-procedure.test.ts +44 -5
- package/src/tests/parity-shared.ts +2 -2
- package/src/types/index.ts +70 -40
|
@@ -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,6 +8,7 @@ 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";
|
|
@@ -51,6 +52,56 @@ const def = {
|
|
|
51
52
|
|
|
52
53
|
const builder = new EffectBuilder(def);
|
|
53
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
|
+
|
|
54
105
|
beforeEach(() => vi.clearAllMocks());
|
|
55
106
|
|
|
56
107
|
describe("effectBuilder", () => {
|
|
@@ -77,15 +128,55 @@ describe("effectBuilder", () => {
|
|
|
77
128
|
});
|
|
78
129
|
|
|
79
130
|
describe(".use", () => {
|
|
80
|
-
it("without map input", () => {
|
|
81
|
-
const mid2 = vi.fn()
|
|
131
|
+
it("without map input", async () => {
|
|
132
|
+
const mid2 = vi.fn(({ next }) =>
|
|
133
|
+
next({ context: { fromMiddleware: true } }),
|
|
134
|
+
);
|
|
82
135
|
const applied = builder.use(mid2);
|
|
83
136
|
|
|
84
137
|
expect(applied).instanceOf(EffectBuilder);
|
|
85
138
|
expect(applied).not.toBe(builder);
|
|
86
|
-
expect(applied["~effect"]).
|
|
87
|
-
|
|
88
|
-
|
|
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 },
|
|
89
180
|
});
|
|
90
181
|
});
|
|
91
182
|
});
|
|
@@ -380,6 +471,17 @@ describe("makeEffectORPC factory", () => {
|
|
|
380
471
|
expect(result).toBe(3);
|
|
381
472
|
});
|
|
382
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
|
+
|
|
383
485
|
it("chains builder methods correctly", () => {
|
|
384
486
|
const effectBuilder = makeEffectORPC(runtime);
|
|
385
487
|
|
|
@@ -532,10 +634,8 @@ describe("effect with services", () => {
|
|
|
532
634
|
{ id: string }
|
|
533
635
|
>() {}
|
|
534
636
|
|
|
535
|
-
const
|
|
536
|
-
user: { id: string }
|
|
537
|
-
}>();
|
|
538
|
-
const procedure = effectBuilder
|
|
637
|
+
const procedure = eos
|
|
638
|
+
.$context<{ user: { id: string } }>()
|
|
539
639
|
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
540
640
|
.effect(function* () {
|
|
541
641
|
return yield* CurrentUser;
|
|
@@ -546,6 +646,33 @@ describe("effect with services", () => {
|
|
|
546
646
|
).resolves.toEqual({ id: "u-1" });
|
|
547
647
|
});
|
|
548
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
|
+
|
|
549
676
|
it(".provide service overrides the same service from the runtime", async () => {
|
|
550
677
|
class CurrentUser extends Context.Tag("CurrentUserOverride")<
|
|
551
678
|
CurrentUser,
|
|
@@ -575,7 +702,7 @@ describe("effect with services", () => {
|
|
|
575
702
|
|
|
576
703
|
it("Effect .use yield* next() without return runs handler once", async () => {
|
|
577
704
|
let runs = 0;
|
|
578
|
-
const procedure =
|
|
705
|
+
const procedure = eos
|
|
579
706
|
.use(function* ({ next }) {
|
|
580
707
|
yield* Effect.void;
|
|
581
708
|
yield* next();
|
|
@@ -591,7 +718,7 @@ describe("effect with services", () => {
|
|
|
591
718
|
|
|
592
719
|
it("Effect .use guard-only middleware without next runs handler once", async () => {
|
|
593
720
|
let runs = 0;
|
|
594
|
-
const procedure =
|
|
721
|
+
const procedure = eos
|
|
595
722
|
.use(function* () {
|
|
596
723
|
yield* Effect.void;
|
|
597
724
|
})
|
|
@@ -625,10 +752,8 @@ describe("effect with services", () => {
|
|
|
625
752
|
>() {}
|
|
626
753
|
|
|
627
754
|
let seenUser: { id: string } | undefined;
|
|
628
|
-
const
|
|
629
|
-
user: { id: string }
|
|
630
|
-
}>();
|
|
631
|
-
const procedure = effectBuilder
|
|
755
|
+
const procedure = eos
|
|
756
|
+
.$context<{ user: { id: string } }>()
|
|
632
757
|
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
633
758
|
.use(function* () {
|
|
634
759
|
seenUser = yield* CurrentUser;
|
|
@@ -644,8 +769,6 @@ describe("effect with services", () => {
|
|
|
644
769
|
});
|
|
645
770
|
|
|
646
771
|
it("Effect .middleware can create reusable generator middleware", async () => {
|
|
647
|
-
const eos = makeEffectORPC(runtime);
|
|
648
|
-
|
|
649
772
|
const reusable = eos.middleware(function* ({ next }, input: string) {
|
|
650
773
|
expectTypeOf(input).toEqualTypeOf<string>();
|
|
651
774
|
return yield* next({ context: { seenInput: input } });
|
|
@@ -668,32 +791,74 @@ describe("effect with services", () => {
|
|
|
668
791
|
{ value: string }
|
|
669
792
|
>() {}
|
|
670
793
|
|
|
671
|
-
const
|
|
794
|
+
const builder = eos.provide(MiddlewareService, () =>
|
|
672
795
|
Effect.succeed({ value: "provided" }),
|
|
673
796
|
);
|
|
674
797
|
|
|
675
|
-
const reusable =
|
|
798
|
+
const reusable = builder.middleware(function* ({ next }) {
|
|
676
799
|
const service = yield* MiddlewareService;
|
|
677
800
|
return yield* next({ context: { serviceValue: service.value } });
|
|
678
801
|
});
|
|
679
802
|
|
|
680
|
-
const procedure =
|
|
803
|
+
const procedure = builder.use(reusable).effect(function* ({ context }) {
|
|
681
804
|
return context.serviceValue;
|
|
682
805
|
});
|
|
683
806
|
|
|
684
807
|
await expect(call(procedure, undefined)).resolves.toBe("provided");
|
|
685
808
|
});
|
|
686
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
|
+
|
|
687
854
|
it("Effect .use can enrich context through next", async () => {
|
|
688
855
|
class CurrentUser extends Context.Tag("NextCurrentUser")<
|
|
689
856
|
CurrentUser,
|
|
690
857
|
{ id: string }
|
|
691
858
|
>() {}
|
|
692
859
|
|
|
693
|
-
const
|
|
694
|
-
user: { id: string }
|
|
695
|
-
}>();
|
|
696
|
-
const procedure = effectBuilder
|
|
860
|
+
const procedure = eos
|
|
861
|
+
.$context<{ user: { id: string } }>()
|
|
697
862
|
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
698
863
|
.use(function* ({ next }, _input) {
|
|
699
864
|
const user = yield* CurrentUser;
|
|
@@ -709,7 +874,7 @@ describe("effect with services", () => {
|
|
|
709
874
|
});
|
|
710
875
|
|
|
711
876
|
it("Effect .use can transform downstream output", async () => {
|
|
712
|
-
const procedure =
|
|
877
|
+
const procedure = eos
|
|
713
878
|
.use(function* ({ next }, _input, output) {
|
|
714
879
|
const result = yield* next();
|
|
715
880
|
return yield* output(`${result.output}-wrapped`);
|
|
@@ -722,7 +887,7 @@ describe("effect with services", () => {
|
|
|
722
887
|
});
|
|
723
888
|
|
|
724
889
|
it("Effect .use can transform typed downstream output after .output", async () => {
|
|
725
|
-
const procedure =
|
|
890
|
+
const procedure = eos
|
|
726
891
|
.output(z.string())
|
|
727
892
|
.use(function* ({ next }, _input, output) {
|
|
728
893
|
const result = yield* next();
|
|
@@ -737,7 +902,7 @@ describe("effect with services", () => {
|
|
|
737
902
|
});
|
|
738
903
|
|
|
739
904
|
it("Effect .use can read typed input after .input", async () => {
|
|
740
|
-
const procedure =
|
|
905
|
+
const procedure = eos
|
|
741
906
|
.input(z.object({ value: z.number() }))
|
|
742
907
|
.use(function* ({ next }, input) {
|
|
743
908
|
expectTypeOf(input).toMatchTypeOf<{ value: number }>();
|
|
@@ -751,7 +916,7 @@ describe("effect with services", () => {
|
|
|
751
916
|
});
|
|
752
917
|
|
|
753
918
|
it("Effect .use can read typed input and output after .input().output()", async () => {
|
|
754
|
-
const procedure =
|
|
919
|
+
const procedure = eos
|
|
755
920
|
.input(z.object({ value: z.number() }))
|
|
756
921
|
.output(z.string())
|
|
757
922
|
.use(function* ({ next }, input, output) {
|
|
@@ -768,7 +933,7 @@ describe("effect with services", () => {
|
|
|
768
933
|
});
|
|
769
934
|
|
|
770
935
|
it("Effect .use can transform typed downstream output after .effect", async () => {
|
|
771
|
-
const procedure =
|
|
936
|
+
const procedure = eos
|
|
772
937
|
.effect(function* () {
|
|
773
938
|
return "ok";
|
|
774
939
|
})
|
|
@@ -840,7 +1005,7 @@ describe("effect with services", () => {
|
|
|
840
1005
|
{ id: string }
|
|
841
1006
|
>() {}
|
|
842
1007
|
|
|
843
|
-
const procedure =
|
|
1008
|
+
const procedure = eos
|
|
844
1009
|
.$context<{ user?: { id: string } }>()
|
|
845
1010
|
.provideOptional(CurrentUser, ({ context }) =>
|
|
846
1011
|
Effect.succeed(Option.fromNullable(context.user)),
|
|
@@ -854,13 +1019,34 @@ describe("effect with services", () => {
|
|
|
854
1019
|
).resolves.toEqual(Option.some({ id: "u-6" }));
|
|
855
1020
|
});
|
|
856
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
|
+
|
|
857
1043
|
it(".provideOptional leaves absent request-scoped services unavailable", async () => {
|
|
858
1044
|
class CurrentUser extends Context.Tag("OptionalCurrentUserAbsent")<
|
|
859
1045
|
CurrentUser,
|
|
860
1046
|
{ id: string }
|
|
861
1047
|
>() {}
|
|
862
1048
|
|
|
863
|
-
const procedure =
|
|
1049
|
+
const procedure = eos
|
|
864
1050
|
.$context<{ user?: { id: string } }>()
|
|
865
1051
|
.provideOptional(CurrentUser, ({ context }) =>
|
|
866
1052
|
Effect.succeed(Option.fromNullable(context.user)),
|
|
@@ -880,7 +1066,7 @@ describe("effect with services", () => {
|
|
|
880
1066
|
{ readonly value: string }
|
|
881
1067
|
>() {}
|
|
882
1068
|
|
|
883
|
-
|
|
1069
|
+
eos
|
|
884
1070
|
.provideOptional(OptionalService, () =>
|
|
885
1071
|
Effect.succeed(Option.some({ value: "provided" })),
|
|
886
1072
|
)
|
|
@@ -895,12 +1081,10 @@ describe("effect with services", () => {
|
|
|
895
1081
|
|
|
896
1082
|
describe(".traced", () => {
|
|
897
1083
|
it("creates an EffectBuilder with span config", () => {
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
const traced = effectBuilder.traced("users.getUser");
|
|
1084
|
+
const traced = eos.traced("users.getUser");
|
|
901
1085
|
|
|
902
1086
|
expect(traced).instanceOf(EffectBuilder);
|
|
903
|
-
expect(traced).not.toBe(
|
|
1087
|
+
expect(traced).not.toBe(eos);
|
|
904
1088
|
expect(traced["~effect"].spanConfig).toBeDefined();
|
|
905
1089
|
expect(traced["~effect"].spanConfig?.name).toBe("users.getUser");
|
|
906
1090
|
expect(traced["~effect"].spanConfig?.captureStackTrace).toBeInstanceOf(
|
|
@@ -909,9 +1093,7 @@ describe(".traced", () => {
|
|
|
909
1093
|
});
|
|
910
1094
|
|
|
911
1095
|
it("preserves span config through chained methods", () => {
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
const procedure = effectBuilder
|
|
1096
|
+
const procedure = eos
|
|
915
1097
|
.input(z.object({ id: z.string() }))
|
|
916
1098
|
.traced("users.getUser")
|
|
917
1099
|
.effect(function* () {
|
|
@@ -922,56 +1104,60 @@ describe(".traced", () => {
|
|
|
922
1104
|
// The span wrapping happens in the handler, so we just verify the procedure was created
|
|
923
1105
|
});
|
|
924
1106
|
|
|
925
|
-
it("traced procedure
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
const procedure = effectBuilder
|
|
1107
|
+
it("traced procedure runs through a routed client", async () => {
|
|
1108
|
+
const procedure = eos
|
|
929
1109
|
.input(z.object({ id: z.string() }))
|
|
930
1110
|
.traced("users.getUser")
|
|
931
1111
|
.effect(function* ({ input }) {
|
|
932
1112
|
return { id: input.id, name: "Alice" };
|
|
933
1113
|
});
|
|
1114
|
+
const client = createRouterClient({ users: { getUser: procedure } });
|
|
934
1115
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
path: ["users", "getUser"],
|
|
939
|
-
procedure: procedure as any,
|
|
940
|
-
signal: undefined,
|
|
941
|
-
lastEventId: undefined,
|
|
942
|
-
errors: {},
|
|
1116
|
+
await expect(client.users.getUser({ id: "123" })).resolves.toEqual({
|
|
1117
|
+
id: "123",
|
|
1118
|
+
name: "Alice",
|
|
943
1119
|
});
|
|
944
|
-
|
|
945
|
-
expect(result).toEqual({ id: "123", name: "Alice" });
|
|
946
1120
|
});
|
|
947
1121
|
|
|
948
|
-
it("traced procedure
|
|
949
|
-
const
|
|
1122
|
+
it("traced procedure uses the explicit span name at runtime", async () => {
|
|
1123
|
+
const recorded = makeRecordedRuntime();
|
|
1124
|
+
const effectBuilder = makeEffectORPC(recorded.runtime);
|
|
1125
|
+
|
|
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
|
+
});
|
|
950
1146
|
|
|
951
|
-
|
|
1147
|
+
it("traced procedure with generator syntax runs through a routed client", async () => {
|
|
1148
|
+
const procedure = eos.traced("math.add").effect(function* () {
|
|
952
1149
|
const a = yield* Effect.succeed(10);
|
|
953
1150
|
const b = yield* Effect.succeed(20);
|
|
954
1151
|
return a + b;
|
|
955
1152
|
});
|
|
1153
|
+
const client = createRouterClient({ math: { add: procedure } });
|
|
956
1154
|
|
|
957
|
-
|
|
958
|
-
context: {},
|
|
959
|
-
input: undefined,
|
|
960
|
-
path: ["math", "add"],
|
|
961
|
-
procedure: procedure as any,
|
|
962
|
-
signal: undefined,
|
|
963
|
-
lastEventId: undefined,
|
|
964
|
-
errors: {},
|
|
965
|
-
});
|
|
966
|
-
|
|
967
|
-
expect(result).toBe(30);
|
|
1155
|
+
await expect(client.math.add(undefined)).resolves.toBe(30);
|
|
968
1156
|
});
|
|
969
1157
|
|
|
970
1158
|
it("captures stack trace at definition time", () => {
|
|
971
|
-
const effectBuilder = makeEffectORPC(runtime);
|
|
972
|
-
|
|
973
1159
|
// The stack trace is captured when .traced() is called
|
|
974
|
-
const traced =
|
|
1160
|
+
const traced = eos.traced("test.procedure");
|
|
975
1161
|
|
|
976
1162
|
const stackTrace = traced["~effect"].spanConfig?.captureStackTrace();
|
|
977
1163
|
// The stack trace should be a string containing the file location
|
|
@@ -983,71 +1169,49 @@ describe(".traced", () => {
|
|
|
983
1169
|
});
|
|
984
1170
|
|
|
985
1171
|
describe("default tracing (without .traced())", () => {
|
|
986
|
-
it("procedure without .traced()
|
|
987
|
-
const
|
|
988
|
-
|
|
989
|
-
// No .traced() call - should still work and use path as span name
|
|
990
|
-
const procedure = effectBuilder
|
|
1172
|
+
it("procedure without .traced() runs through a routed client", async () => {
|
|
1173
|
+
const procedure = eos
|
|
991
1174
|
.input(z.object({ id: z.string() }))
|
|
992
1175
|
.effect(function* ({ input }) {
|
|
993
1176
|
return { id: input.id, name: "Bob" };
|
|
994
1177
|
});
|
|
1178
|
+
const client = createRouterClient({ users: { findById: procedure } });
|
|
995
1179
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
path: ["users", "findById"],
|
|
1000
|
-
procedure: procedure as any,
|
|
1001
|
-
signal: undefined,
|
|
1002
|
-
lastEventId: undefined,
|
|
1003
|
-
errors: {},
|
|
1180
|
+
await expect(client.users.findById({ id: "456" })).resolves.toEqual({
|
|
1181
|
+
id: "456",
|
|
1182
|
+
name: "Bob",
|
|
1004
1183
|
});
|
|
1005
|
-
|
|
1006
|
-
expect(result).toEqual({ id: "456", name: "Bob" });
|
|
1007
1184
|
});
|
|
1008
1185
|
|
|
1009
|
-
it("uses procedure path as default span name", async () => {
|
|
1010
|
-
const
|
|
1186
|
+
it("uses procedure path as default span name at runtime", async () => {
|
|
1187
|
+
const recorded = makeRecordedRuntime();
|
|
1188
|
+
const effectBuilder = makeEffectORPC(recorded.runtime);
|
|
1011
1189
|
|
|
1012
|
-
// Without .traced(), the span name should be derived from path
|
|
1013
1190
|
const procedure = effectBuilder.effect(function* () {
|
|
1014
1191
|
return "hello";
|
|
1015
1192
|
});
|
|
1193
|
+
const client = createRouterClient({ api: { v1: { greet: procedure } } });
|
|
1016
1194
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
});
|
|
1027
|
-
|
|
1028
|
-
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
|
+
}
|
|
1029
1204
|
});
|
|
1030
1205
|
|
|
1031
|
-
it("default tracing works with
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
const procedure = effectBuilder.effect(function* () {
|
|
1206
|
+
it("default tracing works with generator handlers through a routed client", async () => {
|
|
1207
|
+
const procedure = eos.effect(function* () {
|
|
1035
1208
|
const x = 5;
|
|
1036
1209
|
const y = 10;
|
|
1037
1210
|
return x * y;
|
|
1038
1211
|
});
|
|
1212
|
+
const client = createRouterClient({ math: { multiply: procedure } });
|
|
1039
1213
|
|
|
1040
|
-
|
|
1041
|
-
context: {},
|
|
1042
|
-
input: undefined,
|
|
1043
|
-
path: ["math", "multiply"],
|
|
1044
|
-
procedure: procedure as any,
|
|
1045
|
-
signal: undefined,
|
|
1046
|
-
lastEventId: undefined,
|
|
1047
|
-
errors: {},
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
expect(result).toBe(50);
|
|
1214
|
+
await expect(client.math.multiply(undefined)).resolves.toBe(50);
|
|
1051
1215
|
});
|
|
1052
1216
|
|
|
1053
1217
|
it("default tracing works with services from runtime", async () => {
|
|
@@ -1068,36 +1232,29 @@ describe("default tracing (without .traced())", () => {
|
|
|
1068
1232
|
.effect(function* ({ input }) {
|
|
1069
1233
|
return yield* Greeter.greet(input.name);
|
|
1070
1234
|
});
|
|
1235
|
+
const client = createRouterClient({ greeting: { say: procedure } });
|
|
1071
1236
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
errors: {},
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
expect(result).toBe("Hello, World!");
|
|
1083
|
-
|
|
1084
|
-
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
|
+
}
|
|
1085
1244
|
});
|
|
1086
1245
|
|
|
1087
1246
|
it("no spanConfig is set when .traced() is not called", () => {
|
|
1088
|
-
const effectBuilder = makeEffectORPC(runtime);
|
|
1089
|
-
|
|
1090
1247
|
// Without .traced(), spanConfig should be undefined
|
|
1091
|
-
expect(
|
|
1248
|
+
expect(eos["~effect"].spanConfig).toBeUndefined();
|
|
1092
1249
|
|
|
1093
|
-
const withInput =
|
|
1250
|
+
const withInput = eos.input(z.string());
|
|
1094
1251
|
expect(withInput["~effect"].spanConfig).toBeUndefined();
|
|
1095
1252
|
});
|
|
1096
1253
|
|
|
1097
1254
|
it("enforces the declared output schema for effect handlers", () => {
|
|
1098
1255
|
const declaredOutputSchema = z.object({ name: z.string() });
|
|
1099
1256
|
|
|
1100
|
-
|
|
1257
|
+
eos
|
|
1101
1258
|
.input(z.string())
|
|
1102
1259
|
.output(declaredOutputSchema)
|
|
1103
1260
|
.effect(
|
|
@@ -1107,7 +1264,7 @@ describe("default tracing (without .traced())", () => {
|
|
|
1107
1264
|
},
|
|
1108
1265
|
);
|
|
1109
1266
|
|
|
1110
|
-
const procedure =
|
|
1267
|
+
const procedure = eos
|
|
1111
1268
|
.output(declaredOutputSchema)
|
|
1112
1269
|
// @ts-expect-error output() should constrain the effect return type
|
|
1113
1270
|
.effect(function* () {
|
|
@@ -1126,14 +1283,14 @@ describe("default tracing (without .traced())", () => {
|
|
|
1126
1283
|
{ readonly value: string }
|
|
1127
1284
|
>() {}
|
|
1128
1285
|
|
|
1129
|
-
|
|
1286
|
+
eos.effect(
|
|
1130
1287
|
// @ts-expect-error MissingService is not available from the runtime or .provide
|
|
1131
1288
|
function* () {
|
|
1132
1289
|
return yield* MissingService;
|
|
1133
1290
|
},
|
|
1134
1291
|
);
|
|
1135
1292
|
|
|
1136
|
-
|
|
1293
|
+
eos
|
|
1137
1294
|
.provide(MissingService, () => Effect.succeed({ value: "provided" }))
|
|
1138
1295
|
.effect(function* () {
|
|
1139
1296
|
return yield* MissingService;
|
|
@@ -1145,7 +1302,7 @@ describe("default tracing (without .traced())", () => {
|
|
|
1145
1302
|
"MissingMiddlewareService",
|
|
1146
1303
|
)<MissingMiddlewareService, { readonly value: string }>() {}
|
|
1147
1304
|
|
|
1148
|
-
|
|
1305
|
+
eos
|
|
1149
1306
|
.use(
|
|
1150
1307
|
// @ts-expect-error MissingMiddlewareService is not available from the runtime or .provide
|
|
1151
1308
|
function* () {
|
|
@@ -1156,7 +1313,7 @@ describe("default tracing (without .traced())", () => {
|
|
|
1156
1313
|
return "ok";
|
|
1157
1314
|
});
|
|
1158
1315
|
|
|
1159
|
-
|
|
1316
|
+
eos
|
|
1160
1317
|
.provide(MissingMiddlewareService, () =>
|
|
1161
1318
|
Effect.succeed({ value: "provided" }),
|
|
1162
1319
|
)
|