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.
- package/README.md +133 -146
- package/dist/index.js +421 -133
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/contract.ts +15 -12
- package/src/effect-builder.ts +63 -40
- package/src/effect-enhance-router.ts +3 -3
- package/src/effect-procedure.ts +38 -14
- package/src/effect-runtime.ts +647 -115
- package/src/extension/state.ts +3 -5
- package/src/index.ts +1 -0
- package/src/runtime-source.ts +70 -12
- package/src/tagged-error.ts +1 -1
- package/src/tests/contract.test.ts +5 -8
- package/src/tests/effect-builder.proxy.test.ts +15 -17
- package/src/tests/effect-builder.test.ts +352 -161
- package/src/tests/effect-callback-shapes.test.ts +410 -0
- package/src/tests/effect-error-map.test.ts +12 -14
- package/src/tests/effect-procedure.test.ts +53 -11
- package/src/tests/parity-shared.ts +2 -2
- package/src/types/effect-builder-surface.ts +1 -1
- package/src/types/index.ts +76 -51
- package/src/types/variants.ts +5 -5
|
@@ -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"]).
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
431
|
-
|
|
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
|
-
|
|
455
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
794
|
+
const builder = eos.provide(MiddlewareService, () =>
|
|
638
795
|
Effect.succeed({ value: "provided" }),
|
|
639
796
|
);
|
|
640
797
|
|
|
641
|
-
const reusable =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
892
|
-
const
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
|
915
|
-
const
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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()
|
|
953
|
-
const
|
|
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
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
|
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
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
|
998
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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(
|
|
1248
|
+
expect(eos["~effect"].spanConfig).toBeUndefined();
|
|
1058
1249
|
|
|
1059
|
-
const withInput =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1316
|
+
eos
|
|
1126
1317
|
.provide(MissingMiddlewareService, () =>
|
|
1127
1318
|
Effect.succeed({ value: "provided" }),
|
|
1128
1319
|
)
|