effect-orpc 0.2.2 → 0.4.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 +225 -105
- package/dist/{chunk-VOWRLWZZ.js → chunk-IJP6L2XR.js} +6 -2
- package/dist/chunk-IJP6L2XR.js.map +1 -0
- package/dist/index.js +770 -282
- package/dist/index.js.map +1 -1
- package/dist/node.js +4 -3
- package/dist/node.js.map +1 -1
- package/package.json +1 -1
- package/src/contract.ts +46 -11
- package/src/effect-builder.ts +295 -33
- package/src/effect-enhance-router.ts +3 -3
- package/src/effect-procedure.ts +205 -11
- package/src/effect-runtime.ts +445 -23
- package/src/extension/create-node-proxy.ts +17 -1
- package/src/extension/state.ts +16 -20
- package/src/fiber-context-bridge.ts +13 -0
- package/src/index.ts +1 -0
- package/src/node.ts +2 -1
- package/src/runtime-source.ts +76 -0
- package/src/tagged-error.ts +1 -10
- package/src/tests/contract.test.ts +21 -0
- package/src/tests/effect-builder.proxy.test.ts +4 -4
- package/src/tests/effect-builder.test.ts +544 -7
- package/src/tests/effect-error-map.test.ts +10 -10
- package/src/tests/effect-procedure.test.ts +9 -6
- package/src/tests/node-side-effect.test.ts +80 -0
- package/src/tests/parity.effect-builder.test.ts +10 -3
- package/src/tests/parity.effect-procedure.test.ts +24 -8
- package/src/tests/shared.ts +1 -25
- package/src/types/effect-builder-surface.ts +117 -1
- package/src/types/effect-procedure-surface.ts +98 -1
- package/src/types/index.ts +291 -5
- package/src/types/variants.ts +351 -18
- package/dist/chunk-VOWRLWZZ.js.map +0 -1
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import type { InferSchemaOutput } from "@orpc/contract";
|
|
2
2
|
import { isContractProcedure } from "@orpc/contract";
|
|
3
|
-
import { os } from "@orpc/server";
|
|
4
|
-
import {
|
|
3
|
+
import { call, os } from "@orpc/server";
|
|
4
|
+
import {
|
|
5
|
+
Context,
|
|
6
|
+
Effect,
|
|
7
|
+
FiberRef,
|
|
8
|
+
Layer,
|
|
9
|
+
ManagedRuntime,
|
|
10
|
+
Option,
|
|
11
|
+
} from "effect";
|
|
5
12
|
import { beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest";
|
|
6
13
|
import z from "zod";
|
|
7
14
|
|
|
8
|
-
import { EffectBuilder, makeEffectORPC } from "../effect-builder";
|
|
15
|
+
import { EffectBuilder, eos, makeEffectORPC } from "../effect-builder";
|
|
9
16
|
import { EffectDecoratedProcedure } from "../effect-procedure";
|
|
10
17
|
import { withFiberContext } from "../node";
|
|
11
|
-
import {
|
|
18
|
+
import { makeEffectRuntimeRunner } from "../runtime-source";
|
|
19
|
+
import { ORPCTaggedError, effectErrorMapToErrorMap } from "../tagged-error";
|
|
12
20
|
import {
|
|
13
21
|
baseErrorMap,
|
|
14
22
|
baseMeta,
|
|
@@ -20,6 +28,7 @@ import {
|
|
|
20
28
|
|
|
21
29
|
const mid = vi.fn();
|
|
22
30
|
const runtime = ManagedRuntime.make(Layer.empty);
|
|
31
|
+
const runner = makeEffectRuntimeRunner(runtime);
|
|
23
32
|
|
|
24
33
|
const def = {
|
|
25
34
|
config: {
|
|
@@ -36,6 +45,7 @@ const def = {
|
|
|
36
45
|
outputValidationIndex: 88,
|
|
37
46
|
route: baseRoute,
|
|
38
47
|
dedupeLeadingMiddlewares: true,
|
|
48
|
+
runner,
|
|
39
49
|
runtime,
|
|
40
50
|
};
|
|
41
51
|
|
|
@@ -135,7 +145,7 @@ describe("effectBuilder", () => {
|
|
|
135
145
|
const applied = builder.effect(effectFn);
|
|
136
146
|
|
|
137
147
|
expect(applied).instanceOf(EffectDecoratedProcedure);
|
|
138
|
-
expect(applied["~effect"].runtime).toBe(runtime);
|
|
148
|
+
expect(applied["~effect"].runner.runtime).toBe(runtime);
|
|
139
149
|
expect(applied["~effect"].handler).toBeInstanceOf(Function);
|
|
140
150
|
});
|
|
141
151
|
|
|
@@ -264,7 +274,7 @@ describe("makeEffectORPC factory", () => {
|
|
|
264
274
|
const effectBuilder = makeEffectORPC(runtime);
|
|
265
275
|
|
|
266
276
|
expect(effectBuilder).instanceOf(EffectBuilder);
|
|
267
|
-
expect(effectBuilder["~effect"].runtime).toBe(runtime);
|
|
277
|
+
expect(effectBuilder["~effect"].runner.runtime).toBe(runtime);
|
|
268
278
|
// Should inherit os's default definition
|
|
269
279
|
expect(effectBuilder["~effect"].middlewares).toEqual(
|
|
270
280
|
os["~orpc"].middlewares,
|
|
@@ -278,7 +288,7 @@ describe("makeEffectORPC factory", () => {
|
|
|
278
288
|
const effectBuilder = makeEffectORPC(runtime, os);
|
|
279
289
|
|
|
280
290
|
expect(effectBuilder).instanceOf(EffectBuilder);
|
|
281
|
-
expect(effectBuilder["~effect"].runtime).toBe(runtime);
|
|
291
|
+
expect(effectBuilder["~effect"].runner.runtime).toBe(runtime);
|
|
282
292
|
expect(effectBuilder["~effect"].middlewares).toEqual(
|
|
283
293
|
os["~orpc"].middlewares,
|
|
284
294
|
);
|
|
@@ -287,6 +297,46 @@ describe("makeEffectORPC factory", () => {
|
|
|
287
297
|
);
|
|
288
298
|
});
|
|
289
299
|
|
|
300
|
+
it("exports an eos builder that works with provide", async () => {
|
|
301
|
+
class Counter extends Effect.Tag("EosCounter")<
|
|
302
|
+
Counter,
|
|
303
|
+
{ increment: (n: number) => Effect.Effect<number> }
|
|
304
|
+
>() {}
|
|
305
|
+
|
|
306
|
+
const procedure = eos
|
|
307
|
+
.provide(
|
|
308
|
+
Layer.succeed(Counter, {
|
|
309
|
+
increment: (n: number) => Effect.succeed(n + 1),
|
|
310
|
+
}),
|
|
311
|
+
)
|
|
312
|
+
.input(z.number())
|
|
313
|
+
.effect(function* ({ input }) {
|
|
314
|
+
const counter = yield* Counter;
|
|
315
|
+
return yield* counter.increment(input as number);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expect(eos["~effect"].runner.runtime).toBeUndefined();
|
|
319
|
+
await expect(call(procedure, 5)).resolves.toBe(6);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("does not own a ManagedRuntime when no source is provided", () => {
|
|
323
|
+
const effectBuilder = makeEffectORPC();
|
|
324
|
+
|
|
325
|
+
expect(effectBuilder["~effect"].runner.runtime).toBeUndefined();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("does not own a ManagedRuntime when only a builder is provided", () => {
|
|
329
|
+
const effectBuilder = makeEffectORPC(os);
|
|
330
|
+
|
|
331
|
+
expect(effectBuilder["~effect"].runner.runtime).toBeUndefined();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("does not own a ManagedRuntime when a Layer is provided", () => {
|
|
335
|
+
const effectBuilder = makeEffectORPC(Layer.empty);
|
|
336
|
+
|
|
337
|
+
expect(effectBuilder["~effect"].runner.runtime).toBeUndefined();
|
|
338
|
+
});
|
|
339
|
+
|
|
290
340
|
it("creates working procedure with default os", async () => {
|
|
291
341
|
const effectBuilder = makeEffectORPC(runtime);
|
|
292
342
|
|
|
@@ -402,6 +452,445 @@ describe("effect with services", () => {
|
|
|
402
452
|
// Cleanup
|
|
403
453
|
await serviceRuntime.dispose();
|
|
404
454
|
});
|
|
455
|
+
|
|
456
|
+
it("can create a builder directly from a Layer", async () => {
|
|
457
|
+
class Counter extends Effect.Tag("LayerCounter")<
|
|
458
|
+
Counter,
|
|
459
|
+
{ increment: (n: number) => Effect.Effect<number> }
|
|
460
|
+
>() {}
|
|
461
|
+
|
|
462
|
+
const CounterLive = Layer.succeed(Counter, {
|
|
463
|
+
increment: (n: number) => Effect.succeed(n + 1),
|
|
464
|
+
});
|
|
465
|
+
const effectBuilder = makeEffectORPC(CounterLive);
|
|
466
|
+
const procedure = effectBuilder.input(z.number()).effect(function* ({
|
|
467
|
+
input,
|
|
468
|
+
}) {
|
|
469
|
+
const counter = yield* Counter;
|
|
470
|
+
return yield* counter.increment(input as number);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(effectBuilder["~effect"].runner.runtime).toBeUndefined();
|
|
474
|
+
await expect(call(procedure, 5)).resolves.toBe(6);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("can start without a runtime and provide a Layer", async () => {
|
|
478
|
+
class Counter extends Effect.Tag("ProvidedLayerCounter")<
|
|
479
|
+
Counter,
|
|
480
|
+
{ increment: (n: number) => Effect.Effect<number> }
|
|
481
|
+
>() {}
|
|
482
|
+
|
|
483
|
+
const CounterLive = Layer.succeed(Counter, {
|
|
484
|
+
increment: (n: number) => Effect.succeed(n + 1),
|
|
485
|
+
});
|
|
486
|
+
const effectBuilder = makeEffectORPC().provide(CounterLive);
|
|
487
|
+
const procedure = effectBuilder.input(z.number()).effect(function* ({
|
|
488
|
+
input,
|
|
489
|
+
}) {
|
|
490
|
+
const counter = yield* Counter;
|
|
491
|
+
return yield* counter.increment(input as number);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
expect(effectBuilder["~effect"].runner.runtime).toBeUndefined();
|
|
495
|
+
await expect(call(procedure, 5)).resolves.toBe(6);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("can wrap a custom builder without a runtime and provide a Layer", async () => {
|
|
499
|
+
class Counter extends Effect.Tag("ProvidedLayerCustomBuilderCounter")<
|
|
500
|
+
Counter,
|
|
501
|
+
{ increment: (n: number) => Effect.Effect<number> }
|
|
502
|
+
>() {}
|
|
503
|
+
|
|
504
|
+
const CounterLive = Layer.succeed(Counter, {
|
|
505
|
+
increment: (n: number) => Effect.succeed(n + 1),
|
|
506
|
+
});
|
|
507
|
+
const customBuilder = os.use(({ next }) =>
|
|
508
|
+
next({ context: { fromCustomBuilder: true } }),
|
|
509
|
+
);
|
|
510
|
+
const effectBuilder = makeEffectORPC(customBuilder).provide(CounterLive);
|
|
511
|
+
const procedure = effectBuilder.input(z.number()).effect(function* ({
|
|
512
|
+
context,
|
|
513
|
+
input,
|
|
514
|
+
}) {
|
|
515
|
+
const counter = yield* Counter;
|
|
516
|
+
return {
|
|
517
|
+
fromCustomBuilder: context.fromCustomBuilder,
|
|
518
|
+
value: yield* counter.increment(input as number),
|
|
519
|
+
};
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
expect(effectBuilder["~effect"].runner.runtime).toBeUndefined();
|
|
523
|
+
await expect(call(procedure, 5)).resolves.toEqual({
|
|
524
|
+
fromCustomBuilder: true,
|
|
525
|
+
value: 6,
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it(".provide makes a request-scoped service available to handlers", async () => {
|
|
530
|
+
class CurrentUser extends Context.Tag("CurrentUser")<
|
|
531
|
+
CurrentUser,
|
|
532
|
+
{ id: string }
|
|
533
|
+
>() {}
|
|
534
|
+
|
|
535
|
+
const effectBuilder = makeEffectORPC(runtime).$context<{
|
|
536
|
+
user: { id: string };
|
|
537
|
+
}>();
|
|
538
|
+
const procedure = effectBuilder
|
|
539
|
+
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
540
|
+
.effect(function* () {
|
|
541
|
+
return yield* CurrentUser;
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
await expect(
|
|
545
|
+
call(procedure, undefined, { context: { user: { id: "u-1" } } }),
|
|
546
|
+
).resolves.toEqual({ id: "u-1" });
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it(".provide service overrides the same service from the runtime", async () => {
|
|
550
|
+
class CurrentUser extends Context.Tag("CurrentUserOverride")<
|
|
551
|
+
CurrentUser,
|
|
552
|
+
{ id: string }
|
|
553
|
+
>() {}
|
|
554
|
+
|
|
555
|
+
const serviceRuntime = ManagedRuntime.make(
|
|
556
|
+
Layer.succeed(CurrentUser, { id: "runtime" }),
|
|
557
|
+
);
|
|
558
|
+
const effectBuilder = makeEffectORPC(serviceRuntime).$context<{
|
|
559
|
+
user: { id: string };
|
|
560
|
+
}>();
|
|
561
|
+
const procedure = effectBuilder
|
|
562
|
+
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
563
|
+
.effect(function* () {
|
|
564
|
+
return yield* CurrentUser;
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
await expect(
|
|
569
|
+
call(procedure, undefined, { context: { user: { id: "request" } } }),
|
|
570
|
+
).resolves.toEqual({ id: "request" });
|
|
571
|
+
} finally {
|
|
572
|
+
await serviceRuntime.dispose();
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("Effect .use yield* next() without return runs handler once", async () => {
|
|
577
|
+
let runs = 0;
|
|
578
|
+
const procedure = makeEffectORPC(runtime)
|
|
579
|
+
.use(function* ({ next }) {
|
|
580
|
+
yield* Effect.void;
|
|
581
|
+
yield* next();
|
|
582
|
+
})
|
|
583
|
+
.effect(function* () {
|
|
584
|
+
runs += 1;
|
|
585
|
+
return "ok";
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
await expect(call(procedure, undefined)).resolves.toBe("ok");
|
|
589
|
+
expect(runs).toBe(1);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("Effect .use guard-only middleware without next runs handler once", async () => {
|
|
593
|
+
let runs = 0;
|
|
594
|
+
const procedure = makeEffectORPC(runtime)
|
|
595
|
+
.use(function* () {
|
|
596
|
+
yield* Effect.void;
|
|
597
|
+
})
|
|
598
|
+
.effect(function* () {
|
|
599
|
+
runs += 1;
|
|
600
|
+
return "ok";
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
await expect(call(procedure, undefined)).resolves.toBe("ok");
|
|
604
|
+
expect(runs).toBe(1);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("Effect .use yield* next() without return stays in one runtime boundary", async () => {
|
|
608
|
+
const runPromiseExit = vi.spyOn(runtime, "runPromiseExit");
|
|
609
|
+
const procedure = makeEffectORPC(runtime)
|
|
610
|
+
.use(function* ({ next }) {
|
|
611
|
+
yield* next();
|
|
612
|
+
})
|
|
613
|
+
.effect(function* () {
|
|
614
|
+
return "ok";
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
await expect(call(procedure, undefined)).resolves.toBe("ok");
|
|
618
|
+
expect(runPromiseExit).toHaveBeenCalledTimes(1);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("Effect .use can read services from upstream .provide", async () => {
|
|
622
|
+
class CurrentUser extends Context.Tag("MiddlewareCurrentUser")<
|
|
623
|
+
CurrentUser,
|
|
624
|
+
{ id: string }
|
|
625
|
+
>() {}
|
|
626
|
+
|
|
627
|
+
let seenUser: { id: string } | undefined;
|
|
628
|
+
const effectBuilder = makeEffectORPC(runtime).$context<{
|
|
629
|
+
user: { id: string };
|
|
630
|
+
}>();
|
|
631
|
+
const procedure = effectBuilder
|
|
632
|
+
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
633
|
+
.use(function* () {
|
|
634
|
+
seenUser = yield* CurrentUser;
|
|
635
|
+
})
|
|
636
|
+
.effect(function* () {
|
|
637
|
+
return "ok";
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
await expect(
|
|
641
|
+
call(procedure, undefined, { context: { user: { id: "u-2" } } }),
|
|
642
|
+
).resolves.toBe("ok");
|
|
643
|
+
expect(seenUser).toEqual({ id: "u-2" });
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it("Effect .middleware can create reusable generator middleware", async () => {
|
|
647
|
+
const eos = makeEffectORPC(runtime);
|
|
648
|
+
|
|
649
|
+
const reusable = eos.middleware(function* ({ next }, input: string) {
|
|
650
|
+
expectTypeOf(input).toEqualTypeOf<string>();
|
|
651
|
+
return yield* next({ context: { seenInput: input } });
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const procedure = eos
|
|
655
|
+
.input(z.string())
|
|
656
|
+
.use(reusable)
|
|
657
|
+
.effect(function* ({ context }) {
|
|
658
|
+
expectTypeOf(context.seenInput).toEqualTypeOf<string>();
|
|
659
|
+
return context.seenInput;
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
await expect(call(procedure, "ok")).resolves.toBe("ok");
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it("Effect .middleware can use builder-provided services", async () => {
|
|
666
|
+
class MiddlewareService extends Context.Tag("MiddlewareService")<
|
|
667
|
+
MiddlewareService,
|
|
668
|
+
{ value: string }
|
|
669
|
+
>() {}
|
|
670
|
+
|
|
671
|
+
const eos = makeEffectORPC(runtime).provide(MiddlewareService, () =>
|
|
672
|
+
Effect.succeed({ value: "provided" }),
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
const reusable = eos.middleware(function* ({ next }) {
|
|
676
|
+
const service = yield* MiddlewareService;
|
|
677
|
+
return yield* next({ context: { serviceValue: service.value } });
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const procedure = eos.use(reusable).effect(function* ({ context }) {
|
|
681
|
+
return context.serviceValue;
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
await expect(call(procedure, undefined)).resolves.toBe("provided");
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it("Effect .use can enrich context through next", async () => {
|
|
688
|
+
class CurrentUser extends Context.Tag("NextCurrentUser")<
|
|
689
|
+
CurrentUser,
|
|
690
|
+
{ id: string }
|
|
691
|
+
>() {}
|
|
692
|
+
|
|
693
|
+
const effectBuilder = makeEffectORPC(runtime).$context<{
|
|
694
|
+
user: { id: string };
|
|
695
|
+
}>();
|
|
696
|
+
const procedure = effectBuilder
|
|
697
|
+
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
698
|
+
.use(function* ({ next }, _input) {
|
|
699
|
+
const user = yield* CurrentUser;
|
|
700
|
+
return yield* next({ context: { userId: user.id } });
|
|
701
|
+
})
|
|
702
|
+
.effect(function* ({ context }) {
|
|
703
|
+
return context.userId;
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
await expect(
|
|
707
|
+
call(procedure, undefined, { context: { user: { id: "u-3" } } }),
|
|
708
|
+
).resolves.toBe("u-3");
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it("Effect .use can transform downstream output", async () => {
|
|
712
|
+
const procedure = makeEffectORPC(runtime)
|
|
713
|
+
.use(function* ({ next }, _input, output) {
|
|
714
|
+
const result = yield* next();
|
|
715
|
+
return yield* output(`${result.output}-wrapped`);
|
|
716
|
+
})
|
|
717
|
+
.effect(function* () {
|
|
718
|
+
return "ok";
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
await expect(call(procedure, undefined)).resolves.toBe("ok-wrapped");
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it("Effect .use can transform typed downstream output after .output", async () => {
|
|
725
|
+
const procedure = makeEffectORPC(runtime)
|
|
726
|
+
.output(z.string())
|
|
727
|
+
.use(function* ({ next }, _input, output) {
|
|
728
|
+
const result = yield* next();
|
|
729
|
+
expectTypeOf(result.output).toEqualTypeOf<string>();
|
|
730
|
+
return yield* output(`${result.output}-wrapped`);
|
|
731
|
+
})
|
|
732
|
+
.effect(function* () {
|
|
733
|
+
return "ok";
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
await expect(call(procedure, undefined)).resolves.toBe("ok-wrapped");
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it("Effect .use can read typed input after .input", async () => {
|
|
740
|
+
const procedure = makeEffectORPC(runtime)
|
|
741
|
+
.input(z.object({ value: z.number() }))
|
|
742
|
+
.use(function* ({ next }, input) {
|
|
743
|
+
expectTypeOf(input).toMatchTypeOf<{ value: number }>();
|
|
744
|
+
return yield* next({ context: { doubled: input.value * 2 } });
|
|
745
|
+
})
|
|
746
|
+
.effect(function* ({ context }) {
|
|
747
|
+
return context.doubled;
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
await expect(call(procedure, { value: 21 })).resolves.toBe(42);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it("Effect .use can read typed input and output after .input().output()", async () => {
|
|
754
|
+
const procedure = makeEffectORPC(runtime)
|
|
755
|
+
.input(z.object({ value: z.number() }))
|
|
756
|
+
.output(z.string())
|
|
757
|
+
.use(function* ({ next }, input, output) {
|
|
758
|
+
expectTypeOf(input).toMatchTypeOf<{ value: number }>();
|
|
759
|
+
const result = yield* next();
|
|
760
|
+
expectTypeOf(result.output).toEqualTypeOf<string>();
|
|
761
|
+
return yield* output(`${input.value}:${result.output}`);
|
|
762
|
+
})
|
|
763
|
+
.effect(function* () {
|
|
764
|
+
return "ok";
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
await expect(call(procedure, { value: 21 })).resolves.toBe("21:ok");
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it("Effect .use can transform typed downstream output after .effect", async () => {
|
|
771
|
+
const procedure = makeEffectORPC(runtime)
|
|
772
|
+
.effect(function* () {
|
|
773
|
+
return "ok";
|
|
774
|
+
})
|
|
775
|
+
.use(function* ({ next }, _input, output) {
|
|
776
|
+
const result = yield* next();
|
|
777
|
+
expectTypeOf(result.output).toEqualTypeOf<string>();
|
|
778
|
+
return yield* output(`${result.output}-wrapped`);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
await expect(call(procedure, undefined)).resolves.toBe("ok-wrapped");
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it("runs contiguous Effect providers, middleware, and handler in one runtime boundary", async () => {
|
|
785
|
+
class CurrentUser extends Context.Tag("SingleBoundaryCurrentUser")<
|
|
786
|
+
CurrentUser,
|
|
787
|
+
{ id: string }
|
|
788
|
+
>() {}
|
|
789
|
+
|
|
790
|
+
const runPromiseExit = vi.spyOn(runtime, "runPromiseExit");
|
|
791
|
+
const procedure = makeEffectORPC(runtime)
|
|
792
|
+
.$context<{ user: { id: string } }>()
|
|
793
|
+
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
794
|
+
.use(function* ({ next }) {
|
|
795
|
+
const user = yield* CurrentUser;
|
|
796
|
+
return yield* next({ context: { userId: user.id } });
|
|
797
|
+
})
|
|
798
|
+
.effect(function* ({ context }) {
|
|
799
|
+
const user = yield* CurrentUser;
|
|
800
|
+
return `${context.userId}:${user.id}`;
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
await expect(
|
|
804
|
+
call(procedure, undefined, { context: { user: { id: "u-4" } } }),
|
|
805
|
+
).resolves.toBe("u-4:u-4");
|
|
806
|
+
expect(runPromiseExit).toHaveBeenCalledTimes(1);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it("runs procedure-level Effect providers and middleware with the handler in one runtime boundary", async () => {
|
|
810
|
+
class CurrentUser extends Context.Tag("ProcedureBoundaryCurrentUser")<
|
|
811
|
+
CurrentUser,
|
|
812
|
+
{ id: string }
|
|
813
|
+
>() {}
|
|
814
|
+
|
|
815
|
+
const runPromiseExit = vi.spyOn(runtime, "runPromiseExit");
|
|
816
|
+
const procedure = makeEffectORPC(runtime)
|
|
817
|
+
.$context<{ user: { id: string } }>()
|
|
818
|
+
.effect(function* () {
|
|
819
|
+
return "ok";
|
|
820
|
+
})
|
|
821
|
+
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
822
|
+
.use(function* ({ next }) {
|
|
823
|
+
const user = yield* CurrentUser;
|
|
824
|
+
const result = yield* next({ context: { userId: user.id } });
|
|
825
|
+
return {
|
|
826
|
+
...result,
|
|
827
|
+
output: `${result.output}:${user.id}`,
|
|
828
|
+
};
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
await expect(
|
|
832
|
+
call(procedure, undefined, { context: { user: { id: "u-5" } } }),
|
|
833
|
+
).resolves.toBe("ok:u-5");
|
|
834
|
+
expect(runPromiseExit).toHaveBeenCalledTimes(1);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it(".provideOptional makes present request-scoped services available", async () => {
|
|
838
|
+
class CurrentUser extends Context.Tag("OptionalCurrentUserPresent")<
|
|
839
|
+
CurrentUser,
|
|
840
|
+
{ id: string }
|
|
841
|
+
>() {}
|
|
842
|
+
|
|
843
|
+
const procedure = makeEffectORPC(runtime)
|
|
844
|
+
.$context<{ user?: { id: string } }>()
|
|
845
|
+
.provideOptional(CurrentUser, ({ context }) =>
|
|
846
|
+
Effect.succeed(Option.fromNullable(context.user)),
|
|
847
|
+
)
|
|
848
|
+
.effect(function* () {
|
|
849
|
+
return yield* Effect.serviceOption(CurrentUser);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
await expect(
|
|
853
|
+
call(procedure, undefined, { context: { user: { id: "u-6" } } }),
|
|
854
|
+
).resolves.toEqual(Option.some({ id: "u-6" }));
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it(".provideOptional leaves absent request-scoped services unavailable", async () => {
|
|
858
|
+
class CurrentUser extends Context.Tag("OptionalCurrentUserAbsent")<
|
|
859
|
+
CurrentUser,
|
|
860
|
+
{ id: string }
|
|
861
|
+
>() {}
|
|
862
|
+
|
|
863
|
+
const procedure = makeEffectORPC(runtime)
|
|
864
|
+
.$context<{ user?: { id: string } }>()
|
|
865
|
+
.provideOptional(CurrentUser, ({ context }) =>
|
|
866
|
+
Effect.succeed(Option.fromNullable(context.user)),
|
|
867
|
+
)
|
|
868
|
+
.effect(function* () {
|
|
869
|
+
return yield* Effect.serviceOption(CurrentUser);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
await expect(call(procedure, undefined, { context: {} })).resolves.toEqual(
|
|
873
|
+
Option.none(),
|
|
874
|
+
);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it(".provideOptional does not satisfy required service access", () => {
|
|
878
|
+
class OptionalService extends Context.Tag("OptionalServiceRequirement")<
|
|
879
|
+
OptionalService,
|
|
880
|
+
{ readonly value: string }
|
|
881
|
+
>() {}
|
|
882
|
+
|
|
883
|
+
makeEffectORPC(runtime)
|
|
884
|
+
.provideOptional(OptionalService, () =>
|
|
885
|
+
Effect.succeed(Option.some({ value: "provided" })),
|
|
886
|
+
)
|
|
887
|
+
.effect(
|
|
888
|
+
// @ts-expect-error provideOptional does not guarantee the service exists
|
|
889
|
+
function* () {
|
|
890
|
+
return yield* OptionalService;
|
|
891
|
+
},
|
|
892
|
+
);
|
|
893
|
+
});
|
|
405
894
|
});
|
|
406
895
|
|
|
407
896
|
describe(".traced", () => {
|
|
@@ -630,4 +1119,52 @@ describe("default tracing (without .traced())", () => {
|
|
|
630
1119
|
>;
|
|
631
1120
|
expectTypeOf<ProcedureOutput>().toEqualTypeOf<{ name: string }>();
|
|
632
1121
|
});
|
|
1122
|
+
|
|
1123
|
+
it("requires handler services to come from the runtime or .provide", () => {
|
|
1124
|
+
class MissingService extends Context.Tag("MissingService")<
|
|
1125
|
+
MissingService,
|
|
1126
|
+
{ readonly value: string }
|
|
1127
|
+
>() {}
|
|
1128
|
+
|
|
1129
|
+
makeEffectORPC(runtime).effect(
|
|
1130
|
+
// @ts-expect-error MissingService is not available from the runtime or .provide
|
|
1131
|
+
function* () {
|
|
1132
|
+
return yield* MissingService;
|
|
1133
|
+
},
|
|
1134
|
+
);
|
|
1135
|
+
|
|
1136
|
+
makeEffectORPC(runtime)
|
|
1137
|
+
.provide(MissingService, () => Effect.succeed({ value: "provided" }))
|
|
1138
|
+
.effect(function* () {
|
|
1139
|
+
return yield* MissingService;
|
|
1140
|
+
});
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
it("requires Effect middleware services to come from the runtime or .provide", () => {
|
|
1144
|
+
class MissingMiddlewareService extends Context.Tag(
|
|
1145
|
+
"MissingMiddlewareService",
|
|
1146
|
+
)<MissingMiddlewareService, { readonly value: string }>() {}
|
|
1147
|
+
|
|
1148
|
+
makeEffectORPC(runtime)
|
|
1149
|
+
.use(
|
|
1150
|
+
// @ts-expect-error MissingMiddlewareService is not available from the runtime or .provide
|
|
1151
|
+
function* () {
|
|
1152
|
+
yield* MissingMiddlewareService;
|
|
1153
|
+
},
|
|
1154
|
+
)
|
|
1155
|
+
.effect(function* () {
|
|
1156
|
+
return "ok";
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
makeEffectORPC(runtime)
|
|
1160
|
+
.provide(MissingMiddlewareService, () =>
|
|
1161
|
+
Effect.succeed({ value: "provided" }),
|
|
1162
|
+
)
|
|
1163
|
+
.use(function* () {
|
|
1164
|
+
yield* MissingMiddlewareService;
|
|
1165
|
+
})
|
|
1166
|
+
.effect(function* () {
|
|
1167
|
+
return "ok";
|
|
1168
|
+
});
|
|
1169
|
+
});
|
|
633
1170
|
});
|
|
@@ -193,10 +193,10 @@ describe("effectErrorMapToErrorMap", () => {
|
|
|
193
193
|
|
|
194
194
|
describe("effectBuilder with EffectErrorMap", () => {
|
|
195
195
|
const runtime = ManagedRuntime.make(Layer.empty);
|
|
196
|
-
const
|
|
196
|
+
const effectProcedure = makeEffectORPC(runtime);
|
|
197
197
|
|
|
198
198
|
it("should support errors() with traditional format", () => {
|
|
199
|
-
const builder =
|
|
199
|
+
const builder = effectProcedure.errors({
|
|
200
200
|
BAD_REQUEST: { status: 400, message: "Bad request" },
|
|
201
201
|
});
|
|
202
202
|
|
|
@@ -206,7 +206,7 @@ describe("effectBuilder with EffectErrorMap", () => {
|
|
|
206
206
|
});
|
|
207
207
|
|
|
208
208
|
it("should support errors() with tagged error classes", () => {
|
|
209
|
-
const builder =
|
|
209
|
+
const builder = effectProcedure.errors({
|
|
210
210
|
USER_NOT_FOUND_ERROR: UserNotFoundError,
|
|
211
211
|
FORBIDDEN: PermissionDenied,
|
|
212
212
|
});
|
|
@@ -218,7 +218,7 @@ describe("effectBuilder with EffectErrorMap", () => {
|
|
|
218
218
|
});
|
|
219
219
|
|
|
220
220
|
it("should support mixed error format", () => {
|
|
221
|
-
const builder =
|
|
221
|
+
const builder = effectProcedure.errors({
|
|
222
222
|
BAD_REQUEST: { status: 400 },
|
|
223
223
|
USER_NOT_FOUND_ERROR: UserNotFoundError,
|
|
224
224
|
});
|
|
@@ -232,7 +232,7 @@ describe("effectBuilder with EffectErrorMap", () => {
|
|
|
232
232
|
});
|
|
233
233
|
|
|
234
234
|
it("should merge errors correctly", () => {
|
|
235
|
-
const builder =
|
|
235
|
+
const builder = effectProcedure
|
|
236
236
|
.errors({ BAD_REQUEST: { status: 400 } })
|
|
237
237
|
.errors({ USER_NOT_FOUND_ERROR: UserNotFoundError })
|
|
238
238
|
.errors({ FORBIDDEN: PermissionDenied });
|
|
@@ -245,7 +245,7 @@ describe("effectBuilder with EffectErrorMap", () => {
|
|
|
245
245
|
});
|
|
246
246
|
|
|
247
247
|
it("should create procedure with effect handler", async () => {
|
|
248
|
-
const procedure =
|
|
248
|
+
const procedure = effectProcedure
|
|
249
249
|
.errors({
|
|
250
250
|
USER_NOT_FOUND_ERROR: UserNotFoundError,
|
|
251
251
|
BAD_REQUEST: { status: 400 },
|
|
@@ -267,7 +267,7 @@ describe("effectBuilder with EffectErrorMap", () => {
|
|
|
267
267
|
});
|
|
268
268
|
|
|
269
269
|
it("should allow throwing tagged errors in effect handler", async () => {
|
|
270
|
-
const procedure =
|
|
270
|
+
const procedure = effectProcedure
|
|
271
271
|
.errors({
|
|
272
272
|
USER_NOT_FOUND_ERROR: UserNotFoundError,
|
|
273
273
|
})
|
|
@@ -310,10 +310,10 @@ describe("effectBuilder with EffectErrorMap", () => {
|
|
|
310
310
|
|
|
311
311
|
describe("effectDecoratedProcedure.errors()", () => {
|
|
312
312
|
const runtime = ManagedRuntime.make(Layer.empty);
|
|
313
|
-
const
|
|
313
|
+
const effectProcedure = makeEffectORPC(runtime);
|
|
314
314
|
|
|
315
315
|
it("should support adding errors to procedure", () => {
|
|
316
|
-
const procedure =
|
|
316
|
+
const procedure = effectProcedure
|
|
317
317
|
.input(z.object({ id: z.string() }))
|
|
318
318
|
.effect(function* ({ input }) {
|
|
319
319
|
return { id: input.id };
|
|
@@ -326,7 +326,7 @@ describe("effectDecoratedProcedure.errors()", () => {
|
|
|
326
326
|
});
|
|
327
327
|
|
|
328
328
|
it("should merge errors on procedure", () => {
|
|
329
|
-
const procedure =
|
|
329
|
+
const procedure = effectProcedure
|
|
330
330
|
.errors({ BAD_REQUEST: { status: 400 } })
|
|
331
331
|
.input(z.object({ id: z.string() }))
|
|
332
332
|
.effect(function* ({ input }) {
|