effect-orpc 0.2.1 → 0.3.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 +198 -77
- package/dist/{chunk-VOWRLWZZ.js → chunk-IJP6L2XR.js} +6 -2
- package/dist/chunk-IJP6L2XR.js.map +1 -0
- package/dist/index.js +736 -266
- 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 +34 -2
- package/src/effect-builder.ts +452 -18
- package/src/effect-procedure.ts +247 -9
- package/src/effect-runtime.ts +453 -21
- package/src/extension/create-node-proxy.ts +17 -1
- package/src/extension/state.ts +13 -15
- package/src/fiber-context-bridge.ts +13 -0
- package/src/node.ts +2 -1
- package/src/runtime-source.ts +18 -0
- package/src/tagged-error.ts +0 -9
- package/src/tests/contract.test.ts +24 -0
- package/src/tests/effect-builder.test.ts +506 -3
- 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 +116 -0
- package/src/types/effect-procedure-surface.ts +98 -1
- package/src/types/index.ts +292 -1
- package/src/types/variants.ts +346 -13
- package/dist/chunk-VOWRLWZZ.js.map +0 -1
|
@@ -62,6 +62,30 @@ describe("implementEffect", () => {
|
|
|
62
62
|
});
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
it("can implement a contract directly from a Layer", async () => {
|
|
66
|
+
const CounterLive = Layer.succeed(Counter, {
|
|
67
|
+
increment: (n: number) => Effect.succeed(n + 1),
|
|
68
|
+
});
|
|
69
|
+
const oe = implementEffect(contract, CounterLive);
|
|
70
|
+
const procedure = oe.users.list.effect(function* ({ input }) {
|
|
71
|
+
const counter = yield* Counter;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
next: yield* counter.increment(input.amount),
|
|
75
|
+
requestId: "layer",
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await expect(call(procedure, { amount: 2 })).resolves.toEqual({
|
|
81
|
+
next: 3,
|
|
82
|
+
requestId: "layer",
|
|
83
|
+
});
|
|
84
|
+
} finally {
|
|
85
|
+
await procedure["~effect"].runtime.dispose();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
65
89
|
it("preserves contract enforcement at the root router", async () => {
|
|
66
90
|
const oe = implementEffect(contract, runtime);
|
|
67
91
|
|
|
@@ -1,14 +1,21 @@
|
|
|
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
15
|
import { EffectBuilder, makeEffectORPC } from "../effect-builder";
|
|
9
16
|
import { EffectDecoratedProcedure } from "../effect-procedure";
|
|
10
17
|
import { withFiberContext } from "../node";
|
|
11
|
-
import {
|
|
18
|
+
import { ORPCTaggedError, effectErrorMapToErrorMap } from "../tagged-error";
|
|
12
19
|
import {
|
|
13
20
|
baseErrorMap,
|
|
14
21
|
baseMeta,
|
|
@@ -402,6 +409,454 @@ describe("effect with services", () => {
|
|
|
402
409
|
// Cleanup
|
|
403
410
|
await serviceRuntime.dispose();
|
|
404
411
|
});
|
|
412
|
+
|
|
413
|
+
it("can create a builder directly from a Layer", async () => {
|
|
414
|
+
class Counter extends Effect.Tag("LayerCounter")<
|
|
415
|
+
Counter,
|
|
416
|
+
{ increment: (n: number) => Effect.Effect<number> }
|
|
417
|
+
>() {}
|
|
418
|
+
|
|
419
|
+
const CounterLive = Layer.succeed(Counter, {
|
|
420
|
+
increment: (n: number) => Effect.succeed(n + 1),
|
|
421
|
+
});
|
|
422
|
+
const effectBuilder = makeEffectORPC(CounterLive);
|
|
423
|
+
const procedure = effectBuilder.input(z.number()).effect(function* ({
|
|
424
|
+
input,
|
|
425
|
+
}) {
|
|
426
|
+
const counter = yield* Counter;
|
|
427
|
+
return yield* counter.increment(input as number);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
await expect(call(procedure, 5)).resolves.toBe(6);
|
|
432
|
+
} finally {
|
|
433
|
+
await effectBuilder["~effect"].runtime.dispose();
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("can start without a runtime and provide a Layer", async () => {
|
|
438
|
+
class Counter extends Effect.Tag("ProvidedLayerCounter")<
|
|
439
|
+
Counter,
|
|
440
|
+
{ increment: (n: number) => Effect.Effect<number> }
|
|
441
|
+
>() {}
|
|
442
|
+
|
|
443
|
+
const CounterLive = Layer.succeed(Counter, {
|
|
444
|
+
increment: (n: number) => Effect.succeed(n + 1),
|
|
445
|
+
});
|
|
446
|
+
const effectBuilder = makeEffectORPC().provide(CounterLive);
|
|
447
|
+
const procedure = effectBuilder.input(z.number()).effect(function* ({
|
|
448
|
+
input,
|
|
449
|
+
}) {
|
|
450
|
+
const counter = yield* Counter;
|
|
451
|
+
return yield* counter.increment(input as number);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
await expect(call(procedure, 5)).resolves.toBe(6);
|
|
456
|
+
} finally {
|
|
457
|
+
await effectBuilder["~effect"].runtime.dispose();
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("can wrap a custom builder without a runtime and provide a Layer", async () => {
|
|
462
|
+
class Counter extends Effect.Tag("ProvidedLayerCustomBuilderCounter")<
|
|
463
|
+
Counter,
|
|
464
|
+
{ increment: (n: number) => Effect.Effect<number> }
|
|
465
|
+
>() {}
|
|
466
|
+
|
|
467
|
+
const CounterLive = Layer.succeed(Counter, {
|
|
468
|
+
increment: (n: number) => Effect.succeed(n + 1),
|
|
469
|
+
});
|
|
470
|
+
const customBuilder = os.use(({ next }) =>
|
|
471
|
+
next({ context: { fromCustomBuilder: true } }),
|
|
472
|
+
);
|
|
473
|
+
const effectBuilder = makeEffectORPC(customBuilder).provide(CounterLive);
|
|
474
|
+
const procedure = effectBuilder.input(z.number()).effect(function* ({
|
|
475
|
+
context,
|
|
476
|
+
input,
|
|
477
|
+
}) {
|
|
478
|
+
const counter = yield* Counter;
|
|
479
|
+
return {
|
|
480
|
+
fromCustomBuilder: context.fromCustomBuilder,
|
|
481
|
+
value: yield* counter.increment(input as number),
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
await expect(call(procedure, 5)).resolves.toEqual({
|
|
487
|
+
fromCustomBuilder: true,
|
|
488
|
+
value: 6,
|
|
489
|
+
});
|
|
490
|
+
} finally {
|
|
491
|
+
await effectBuilder["~effect"].runtime.dispose();
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it(".provide makes a request-scoped service available to handlers", async () => {
|
|
496
|
+
class CurrentUser extends Context.Tag("CurrentUser")<
|
|
497
|
+
CurrentUser,
|
|
498
|
+
{ id: string }
|
|
499
|
+
>() {}
|
|
500
|
+
|
|
501
|
+
const effectBuilder = makeEffectORPC(runtime).$context<{
|
|
502
|
+
user: { id: string };
|
|
503
|
+
}>();
|
|
504
|
+
const procedure = effectBuilder
|
|
505
|
+
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
506
|
+
.effect(function* () {
|
|
507
|
+
return yield* CurrentUser;
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
await expect(
|
|
511
|
+
call(procedure, undefined, { context: { user: { id: "u-1" } } }),
|
|
512
|
+
).resolves.toEqual({ id: "u-1" });
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it(".provide service overrides the same service from the runtime", async () => {
|
|
516
|
+
class CurrentUser extends Context.Tag("CurrentUserOverride")<
|
|
517
|
+
CurrentUser,
|
|
518
|
+
{ id: string }
|
|
519
|
+
>() {}
|
|
520
|
+
|
|
521
|
+
const serviceRuntime = ManagedRuntime.make(
|
|
522
|
+
Layer.succeed(CurrentUser, { id: "runtime" }),
|
|
523
|
+
);
|
|
524
|
+
const effectBuilder = makeEffectORPC(serviceRuntime).$context<{
|
|
525
|
+
user: { id: string };
|
|
526
|
+
}>();
|
|
527
|
+
const procedure = effectBuilder
|
|
528
|
+
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
529
|
+
.effect(function* () {
|
|
530
|
+
return yield* CurrentUser;
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
await expect(
|
|
535
|
+
call(procedure, undefined, { context: { user: { id: "request" } } }),
|
|
536
|
+
).resolves.toEqual({ id: "request" });
|
|
537
|
+
} finally {
|
|
538
|
+
await serviceRuntime.dispose();
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("Effect .use yield* next() without return runs handler once", async () => {
|
|
543
|
+
let runs = 0;
|
|
544
|
+
const procedure = makeEffectORPC(runtime)
|
|
545
|
+
.use(function* ({ next }) {
|
|
546
|
+
yield* Effect.void;
|
|
547
|
+
yield* next();
|
|
548
|
+
})
|
|
549
|
+
.effect(function* () {
|
|
550
|
+
runs += 1;
|
|
551
|
+
return "ok";
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
await expect(call(procedure, undefined)).resolves.toBe("ok");
|
|
555
|
+
expect(runs).toBe(1);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("Effect .use guard-only middleware without next runs handler once", async () => {
|
|
559
|
+
let runs = 0;
|
|
560
|
+
const procedure = makeEffectORPC(runtime)
|
|
561
|
+
.use(function* () {
|
|
562
|
+
yield* Effect.void;
|
|
563
|
+
})
|
|
564
|
+
.effect(function* () {
|
|
565
|
+
runs += 1;
|
|
566
|
+
return "ok";
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
await expect(call(procedure, undefined)).resolves.toBe("ok");
|
|
570
|
+
expect(runs).toBe(1);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("Effect .use yield* next() without return stays in one runtime boundary", async () => {
|
|
574
|
+
const runPromiseExit = vi.spyOn(runtime, "runPromiseExit");
|
|
575
|
+
const procedure = makeEffectORPC(runtime)
|
|
576
|
+
.use(function* ({ next }) {
|
|
577
|
+
yield* next();
|
|
578
|
+
})
|
|
579
|
+
.effect(function* () {
|
|
580
|
+
return "ok";
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
await expect(call(procedure, undefined)).resolves.toBe("ok");
|
|
584
|
+
expect(runPromiseExit).toHaveBeenCalledTimes(1);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it("Effect .use can read services from upstream .provide", async () => {
|
|
588
|
+
class CurrentUser extends Context.Tag("MiddlewareCurrentUser")<
|
|
589
|
+
CurrentUser,
|
|
590
|
+
{ id: string }
|
|
591
|
+
>() {}
|
|
592
|
+
|
|
593
|
+
let seenUser: { id: string } | undefined;
|
|
594
|
+
const effectBuilder = makeEffectORPC(runtime).$context<{
|
|
595
|
+
user: { id: string };
|
|
596
|
+
}>();
|
|
597
|
+
const procedure = effectBuilder
|
|
598
|
+
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
599
|
+
.use(function* () {
|
|
600
|
+
seenUser = yield* CurrentUser;
|
|
601
|
+
})
|
|
602
|
+
.effect(function* () {
|
|
603
|
+
return "ok";
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
await expect(
|
|
607
|
+
call(procedure, undefined, { context: { user: { id: "u-2" } } }),
|
|
608
|
+
).resolves.toBe("ok");
|
|
609
|
+
expect(seenUser).toEqual({ id: "u-2" });
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it("Effect .middleware can create reusable generator middleware", async () => {
|
|
613
|
+
const eos = makeEffectORPC(runtime);
|
|
614
|
+
|
|
615
|
+
const reusable = eos.middleware(function* ({ next }, input: string) {
|
|
616
|
+
expectTypeOf(input).toEqualTypeOf<string>();
|
|
617
|
+
return yield* next({ context: { seenInput: input } });
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const procedure = eos
|
|
621
|
+
.input(z.string())
|
|
622
|
+
.use(reusable)
|
|
623
|
+
.effect(function* ({ context }) {
|
|
624
|
+
expectTypeOf(context.seenInput).toEqualTypeOf<string>();
|
|
625
|
+
return context.seenInput;
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
await expect(call(procedure, "ok")).resolves.toBe("ok");
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("Effect .middleware can use builder-provided services", async () => {
|
|
632
|
+
class MiddlewareService extends Context.Tag("MiddlewareService")<
|
|
633
|
+
MiddlewareService,
|
|
634
|
+
{ value: string }
|
|
635
|
+
>() {}
|
|
636
|
+
|
|
637
|
+
const eos = makeEffectORPC(runtime).provide(MiddlewareService, () =>
|
|
638
|
+
Effect.succeed({ value: "provided" }),
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
const reusable = eos.middleware(function* ({ next }) {
|
|
642
|
+
const service = yield* MiddlewareService;
|
|
643
|
+
return yield* next({ context: { serviceValue: service.value } });
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const procedure = eos.use(reusable).effect(function* ({ context }) {
|
|
647
|
+
return context.serviceValue;
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
await expect(call(procedure, undefined)).resolves.toBe("provided");
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("Effect .use can enrich context through next", async () => {
|
|
654
|
+
class CurrentUser extends Context.Tag("NextCurrentUser")<
|
|
655
|
+
CurrentUser,
|
|
656
|
+
{ id: string }
|
|
657
|
+
>() {}
|
|
658
|
+
|
|
659
|
+
const effectBuilder = makeEffectORPC(runtime).$context<{
|
|
660
|
+
user: { id: string };
|
|
661
|
+
}>();
|
|
662
|
+
const procedure = effectBuilder
|
|
663
|
+
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
664
|
+
.use(function* ({ next }, _input) {
|
|
665
|
+
const user = yield* CurrentUser;
|
|
666
|
+
return yield* next({ context: { userId: user.id } });
|
|
667
|
+
})
|
|
668
|
+
.effect(function* ({ context }) {
|
|
669
|
+
return context.userId;
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
await expect(
|
|
673
|
+
call(procedure, undefined, { context: { user: { id: "u-3" } } }),
|
|
674
|
+
).resolves.toBe("u-3");
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it("Effect .use can transform downstream output", async () => {
|
|
678
|
+
const procedure = makeEffectORPC(runtime)
|
|
679
|
+
.use(function* ({ next }, _input, output) {
|
|
680
|
+
const result = yield* next();
|
|
681
|
+
return yield* output(`${result.output}-wrapped`);
|
|
682
|
+
})
|
|
683
|
+
.effect(function* () {
|
|
684
|
+
return "ok";
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
await expect(call(procedure, undefined)).resolves.toBe("ok-wrapped");
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it("Effect .use can transform typed downstream output after .output", async () => {
|
|
691
|
+
const procedure = makeEffectORPC(runtime)
|
|
692
|
+
.output(z.string())
|
|
693
|
+
.use(function* ({ next }, _input, output) {
|
|
694
|
+
const result = yield* next();
|
|
695
|
+
expectTypeOf(result.output).toEqualTypeOf<string>();
|
|
696
|
+
return yield* output(`${result.output}-wrapped`);
|
|
697
|
+
})
|
|
698
|
+
.effect(function* () {
|
|
699
|
+
return "ok";
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
await expect(call(procedure, undefined)).resolves.toBe("ok-wrapped");
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it("Effect .use can read typed input after .input", async () => {
|
|
706
|
+
const procedure = makeEffectORPC(runtime)
|
|
707
|
+
.input(z.object({ value: z.number() }))
|
|
708
|
+
.use(function* ({ next }, input) {
|
|
709
|
+
expectTypeOf(input).toMatchTypeOf<{ value: number }>();
|
|
710
|
+
return yield* next({ context: { doubled: input.value * 2 } });
|
|
711
|
+
})
|
|
712
|
+
.effect(function* ({ context }) {
|
|
713
|
+
return context.doubled;
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
await expect(call(procedure, { value: 21 })).resolves.toBe(42);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("Effect .use can read typed input and output after .input().output()", async () => {
|
|
720
|
+
const procedure = makeEffectORPC(runtime)
|
|
721
|
+
.input(z.object({ value: z.number() }))
|
|
722
|
+
.output(z.string())
|
|
723
|
+
.use(function* ({ next }, input, output) {
|
|
724
|
+
expectTypeOf(input).toMatchTypeOf<{ value: number }>();
|
|
725
|
+
const result = yield* next();
|
|
726
|
+
expectTypeOf(result.output).toEqualTypeOf<string>();
|
|
727
|
+
return yield* output(`${input.value}:${result.output}`);
|
|
728
|
+
})
|
|
729
|
+
.effect(function* () {
|
|
730
|
+
return "ok";
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
await expect(call(procedure, { value: 21 })).resolves.toBe("21:ok");
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it("Effect .use can transform typed downstream output after .effect", async () => {
|
|
737
|
+
const procedure = makeEffectORPC(runtime)
|
|
738
|
+
.effect(function* () {
|
|
739
|
+
return "ok";
|
|
740
|
+
})
|
|
741
|
+
.use(function* ({ next }, _input, output) {
|
|
742
|
+
const result = yield* next();
|
|
743
|
+
expectTypeOf(result.output).toEqualTypeOf<string>();
|
|
744
|
+
return yield* output(`${result.output}-wrapped`);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
await expect(call(procedure, undefined)).resolves.toBe("ok-wrapped");
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it("runs contiguous Effect providers, middleware, and handler in one runtime boundary", async () => {
|
|
751
|
+
class CurrentUser extends Context.Tag("SingleBoundaryCurrentUser")<
|
|
752
|
+
CurrentUser,
|
|
753
|
+
{ id: string }
|
|
754
|
+
>() {}
|
|
755
|
+
|
|
756
|
+
const runPromiseExit = vi.spyOn(runtime, "runPromiseExit");
|
|
757
|
+
const procedure = makeEffectORPC(runtime)
|
|
758
|
+
.$context<{ user: { id: string } }>()
|
|
759
|
+
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
760
|
+
.use(function* ({ next }) {
|
|
761
|
+
const user = yield* CurrentUser;
|
|
762
|
+
return yield* next({ context: { userId: user.id } });
|
|
763
|
+
})
|
|
764
|
+
.effect(function* ({ context }) {
|
|
765
|
+
const user = yield* CurrentUser;
|
|
766
|
+
return `${context.userId}:${user.id}`;
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
await expect(
|
|
770
|
+
call(procedure, undefined, { context: { user: { id: "u-4" } } }),
|
|
771
|
+
).resolves.toBe("u-4:u-4");
|
|
772
|
+
expect(runPromiseExit).toHaveBeenCalledTimes(1);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it("runs procedure-level Effect providers and middleware with the handler in one runtime boundary", async () => {
|
|
776
|
+
class CurrentUser extends Context.Tag("ProcedureBoundaryCurrentUser")<
|
|
777
|
+
CurrentUser,
|
|
778
|
+
{ id: string }
|
|
779
|
+
>() {}
|
|
780
|
+
|
|
781
|
+
const runPromiseExit = vi.spyOn(runtime, "runPromiseExit");
|
|
782
|
+
const procedure = makeEffectORPC(runtime)
|
|
783
|
+
.$context<{ user: { id: string } }>()
|
|
784
|
+
.effect(function* () {
|
|
785
|
+
return "ok";
|
|
786
|
+
})
|
|
787
|
+
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
788
|
+
.use(function* ({ next }) {
|
|
789
|
+
const user = yield* CurrentUser;
|
|
790
|
+
const result = yield* next({ context: { userId: user.id } });
|
|
791
|
+
return {
|
|
792
|
+
...result,
|
|
793
|
+
output: `${result.output}:${user.id}`,
|
|
794
|
+
};
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
await expect(
|
|
798
|
+
call(procedure, undefined, { context: { user: { id: "u-5" } } }),
|
|
799
|
+
).resolves.toBe("ok:u-5");
|
|
800
|
+
expect(runPromiseExit).toHaveBeenCalledTimes(1);
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it(".provideOptional makes present request-scoped services available", async () => {
|
|
804
|
+
class CurrentUser extends Context.Tag("OptionalCurrentUserPresent")<
|
|
805
|
+
CurrentUser,
|
|
806
|
+
{ id: string }
|
|
807
|
+
>() {}
|
|
808
|
+
|
|
809
|
+
const procedure = makeEffectORPC(runtime)
|
|
810
|
+
.$context<{ user?: { id: string } }>()
|
|
811
|
+
.provideOptional(CurrentUser, ({ context }) =>
|
|
812
|
+
Effect.succeed(Option.fromNullable(context.user)),
|
|
813
|
+
)
|
|
814
|
+
.effect(function* () {
|
|
815
|
+
return yield* Effect.serviceOption(CurrentUser);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
await expect(
|
|
819
|
+
call(procedure, undefined, { context: { user: { id: "u-6" } } }),
|
|
820
|
+
).resolves.toEqual(Option.some({ id: "u-6" }));
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it(".provideOptional leaves absent request-scoped services unavailable", async () => {
|
|
824
|
+
class CurrentUser extends Context.Tag("OptionalCurrentUserAbsent")<
|
|
825
|
+
CurrentUser,
|
|
826
|
+
{ id: string }
|
|
827
|
+
>() {}
|
|
828
|
+
|
|
829
|
+
const procedure = makeEffectORPC(runtime)
|
|
830
|
+
.$context<{ user?: { id: string } }>()
|
|
831
|
+
.provideOptional(CurrentUser, ({ context }) =>
|
|
832
|
+
Effect.succeed(Option.fromNullable(context.user)),
|
|
833
|
+
)
|
|
834
|
+
.effect(function* () {
|
|
835
|
+
return yield* Effect.serviceOption(CurrentUser);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
await expect(call(procedure, undefined, { context: {} })).resolves.toEqual(
|
|
839
|
+
Option.none(),
|
|
840
|
+
);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it(".provideOptional does not satisfy required service access", () => {
|
|
844
|
+
class OptionalService extends Context.Tag("OptionalServiceRequirement")<
|
|
845
|
+
OptionalService,
|
|
846
|
+
{ readonly value: string }
|
|
847
|
+
>() {}
|
|
848
|
+
|
|
849
|
+
makeEffectORPC(runtime)
|
|
850
|
+
.provideOptional(OptionalService, () =>
|
|
851
|
+
Effect.succeed(Option.some({ value: "provided" })),
|
|
852
|
+
)
|
|
853
|
+
.effect(
|
|
854
|
+
// @ts-expect-error provideOptional does not guarantee the service exists
|
|
855
|
+
function* () {
|
|
856
|
+
return yield* OptionalService;
|
|
857
|
+
},
|
|
858
|
+
);
|
|
859
|
+
});
|
|
405
860
|
});
|
|
406
861
|
|
|
407
862
|
describe(".traced", () => {
|
|
@@ -630,4 +1085,52 @@ describe("default tracing (without .traced())", () => {
|
|
|
630
1085
|
>;
|
|
631
1086
|
expectTypeOf<ProcedureOutput>().toEqualTypeOf<{ name: string }>();
|
|
632
1087
|
});
|
|
1088
|
+
|
|
1089
|
+
it("requires handler services to come from the runtime or .provide", () => {
|
|
1090
|
+
class MissingService extends Context.Tag("MissingService")<
|
|
1091
|
+
MissingService,
|
|
1092
|
+
{ readonly value: string }
|
|
1093
|
+
>() {}
|
|
1094
|
+
|
|
1095
|
+
makeEffectORPC(runtime).effect(
|
|
1096
|
+
// @ts-expect-error MissingService is not available from the runtime or .provide
|
|
1097
|
+
function* () {
|
|
1098
|
+
return yield* MissingService;
|
|
1099
|
+
},
|
|
1100
|
+
);
|
|
1101
|
+
|
|
1102
|
+
makeEffectORPC(runtime)
|
|
1103
|
+
.provide(MissingService, () => Effect.succeed({ value: "provided" }))
|
|
1104
|
+
.effect(function* () {
|
|
1105
|
+
return yield* MissingService;
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
it("requires Effect middleware services to come from the runtime or .provide", () => {
|
|
1110
|
+
class MissingMiddlewareService extends Context.Tag(
|
|
1111
|
+
"MissingMiddlewareService",
|
|
1112
|
+
)<MissingMiddlewareService, { readonly value: string }>() {}
|
|
1113
|
+
|
|
1114
|
+
makeEffectORPC(runtime)
|
|
1115
|
+
.use(
|
|
1116
|
+
// @ts-expect-error MissingMiddlewareService is not available from the runtime or .provide
|
|
1117
|
+
function* () {
|
|
1118
|
+
yield* MissingMiddlewareService;
|
|
1119
|
+
},
|
|
1120
|
+
)
|
|
1121
|
+
.effect(function* () {
|
|
1122
|
+
return "ok";
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
makeEffectORPC(runtime)
|
|
1126
|
+
.provide(MissingMiddlewareService, () =>
|
|
1127
|
+
Effect.succeed({ value: "provided" }),
|
|
1128
|
+
)
|
|
1129
|
+
.use(function* () {
|
|
1130
|
+
yield* MissingMiddlewareService;
|
|
1131
|
+
})
|
|
1132
|
+
.effect(function* () {
|
|
1133
|
+
return "ok";
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
633
1136
|
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
async function makeSplitProcedure(options: {
|
|
4
|
+
readonly installNodeBridge: boolean;
|
|
5
|
+
}) {
|
|
6
|
+
vi.resetModules();
|
|
7
|
+
|
|
8
|
+
if (options.installNodeBridge) {
|
|
9
|
+
await import("../node");
|
|
10
|
+
} else {
|
|
11
|
+
const { installFiberContextBridge } =
|
|
12
|
+
await import("../fiber-context-bridge");
|
|
13
|
+
installFiberContextBridge(undefined);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const [
|
|
17
|
+
{ call },
|
|
18
|
+
{ Context, Effect, Layer, ManagedRuntime },
|
|
19
|
+
{ makeEffectORPC },
|
|
20
|
+
] = await Promise.all([
|
|
21
|
+
import("@orpc/server"),
|
|
22
|
+
import("effect"),
|
|
23
|
+
import("../effect-builder"),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
class CurrentUser extends Context.Tag("SideEffectImportCurrentUser")<
|
|
27
|
+
CurrentUser,
|
|
28
|
+
{ readonly id: string }
|
|
29
|
+
>() {}
|
|
30
|
+
|
|
31
|
+
const runtime = ManagedRuntime.make(Layer.empty);
|
|
32
|
+
const runPromiseExit = vi.spyOn(runtime, "runPromiseExit");
|
|
33
|
+
const procedure = makeEffectORPC(runtime)
|
|
34
|
+
.$context<{ readonly user: { readonly id: string } }>()
|
|
35
|
+
.provide(CurrentUser, ({ context }) => Effect.succeed(context.user))
|
|
36
|
+
.use(function* ({ next }) {
|
|
37
|
+
return yield* next();
|
|
38
|
+
})
|
|
39
|
+
.use(({ next }) => next())
|
|
40
|
+
.use(function* ({ next }) {
|
|
41
|
+
const user = yield* CurrentUser;
|
|
42
|
+
return yield* next({ context: { userId: user.id } });
|
|
43
|
+
})
|
|
44
|
+
.effect(function* ({ context }) {
|
|
45
|
+
const user = yield* CurrentUser;
|
|
46
|
+
return `${context.userId}:${user.id}`;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return { call, procedure, runPromiseExit, runtime };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("node side-effect bridge", () => {
|
|
53
|
+
it("propagates FiberRefs across split Effect groups with only the side-effect import", async () => {
|
|
54
|
+
const { call, procedure, runPromiseExit, runtime } =
|
|
55
|
+
await makeSplitProcedure({ installNodeBridge: true });
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await expect(
|
|
59
|
+
call(procedure, undefined, { context: { user: { id: "u-side" } } }),
|
|
60
|
+
).resolves.toBe("u-side:u-side");
|
|
61
|
+
expect(runPromiseExit).toHaveBeenCalledTimes(2);
|
|
62
|
+
} finally {
|
|
63
|
+
await runtime.dispose();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("does not propagate FiberRefs across split Effect groups without the bridge", async () => {
|
|
68
|
+
const { call, procedure, runPromiseExit, runtime } =
|
|
69
|
+
await makeSplitProcedure({ installNodeBridge: false });
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await expect(
|
|
73
|
+
call(procedure, undefined, { context: { user: { id: "u-side" } } }),
|
|
74
|
+
).rejects.toThrow();
|
|
75
|
+
expect(runPromiseExit).toHaveBeenCalledTimes(2);
|
|
76
|
+
} finally {
|
|
77
|
+
await runtime.dispose();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -42,9 +42,16 @@ const rootBuilder = makeEffectORPC(runtime)
|
|
|
42
42
|
.$input(inputSchema)
|
|
43
43
|
.errors(baseErrorMap);
|
|
44
44
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const authMiddleware: Middleware<
|
|
46
|
+
InitialContext,
|
|
47
|
+
{ auth: boolean },
|
|
48
|
+
{ input: string },
|
|
49
|
+
unknown,
|
|
50
|
+
any,
|
|
51
|
+
BaseMeta
|
|
52
|
+
> = ({ next }) => next({ context: { auth: true as boolean } });
|
|
53
|
+
|
|
54
|
+
const withMiddlewares = rootBuilder.use(authMiddleware);
|
|
48
55
|
|
|
49
56
|
const procedureBuilder = withMiddlewares.meta(baseMeta);
|
|
50
57
|
const withInput = procedureBuilder.input(inputSchema);
|