@shivaedev/effect-trpc 0.0.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE +21 -0
  3. package/README.md +142 -0
  4. package/dist/adapter.d.ts +16 -0
  5. package/dist/adapter.d.ts.map +1 -0
  6. package/dist/adapter.js +12 -0
  7. package/dist/adapter.js.map +1 -0
  8. package/dist/errors.d.ts +11 -0
  9. package/dist/errors.d.ts.map +1 -0
  10. package/dist/errors.js +11 -0
  11. package/dist/errors.js.map +1 -0
  12. package/dist/index.d.ts +6 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +4 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/internal/context-bridge.d.ts +7 -0
  17. package/dist/internal/context-bridge.d.ts.map +1 -0
  18. package/dist/internal/context-bridge.js +9 -0
  19. package/dist/internal/context-bridge.js.map +1 -0
  20. package/dist/internal/procedure-handler.d.ts +13 -0
  21. package/dist/internal/procedure-handler.d.ts.map +1 -0
  22. package/dist/internal/procedure-handler.js +12 -0
  23. package/dist/internal/procedure-handler.js.map +1 -0
  24. package/dist/internal/procedure-types.d.ts +9 -0
  25. package/dist/internal/procedure-types.d.ts.map +1 -0
  26. package/dist/internal/procedure-types.js +2 -0
  27. package/dist/internal/procedure-types.js.map +1 -0
  28. package/dist/internal/runtime.d.ts +16 -0
  29. package/dist/internal/runtime.d.ts.map +1 -0
  30. package/dist/internal/runtime.js +60 -0
  31. package/dist/internal/runtime.js.map +1 -0
  32. package/dist/internal/stack-trace.d.ts +2 -0
  33. package/dist/internal/stack-trace.d.ts.map +1 -0
  34. package/dist/internal/stack-trace.js +19 -0
  35. package/dist/internal/stack-trace.js.map +1 -0
  36. package/dist/internal/vitest-trpc.d.ts +18 -0
  37. package/dist/internal/vitest-trpc.d.ts.map +1 -0
  38. package/dist/internal/vitest-trpc.js +70 -0
  39. package/dist/internal/vitest-trpc.js.map +1 -0
  40. package/dist/procedure.d.ts +27 -0
  41. package/dist/procedure.d.ts.map +1 -0
  42. package/dist/procedure.js +34 -0
  43. package/dist/procedure.js.map +1 -0
  44. package/dist/request-services.d.ts +7 -0
  45. package/dist/request-services.d.ts.map +1 -0
  46. package/dist/request-services.js +4 -0
  47. package/dist/request-services.js.map +1 -0
  48. package/dist/testing/caller.d.ts +10 -0
  49. package/dist/testing/caller.d.ts.map +1 -0
  50. package/dist/testing/caller.js +41 -0
  51. package/dist/testing/caller.js.map +1 -0
  52. package/dist/testing/types.d.ts +31 -0
  53. package/dist/testing/types.d.ts.map +1 -0
  54. package/dist/testing/types.js +2 -0
  55. package/dist/testing/types.js.map +1 -0
  56. package/dist/testing/vitest.d.ts +4 -0
  57. package/dist/testing/vitest.d.ts.map +1 -0
  58. package/dist/testing/vitest.js +28 -0
  59. package/dist/testing/vitest.js.map +1 -0
  60. package/dist/testing.d.ts +4 -0
  61. package/dist/testing.d.ts.map +1 -0
  62. package/dist/testing.js +3 -0
  63. package/dist/testing.js.map +1 -0
  64. package/dist/types.d.ts +14 -0
  65. package/dist/types.d.ts.map +1 -0
  66. package/dist/types.js +2 -0
  67. package/dist/types.js.map +1 -0
  68. package/package.json +73 -0
  69. package/src/adapter.ts +85 -0
  70. package/src/errors.ts +30 -0
  71. package/src/index.ts +28 -0
  72. package/src/internal/context-bridge.ts +20 -0
  73. package/src/internal/procedure-handler.ts +48 -0
  74. package/src/internal/procedure-types.ts +49 -0
  75. package/src/internal/runtime.ts +131 -0
  76. package/src/internal/stack-trace.ts +19 -0
  77. package/src/internal/vitest-trpc.ts +202 -0
  78. package/src/procedure.ts +191 -0
  79. package/src/request-services.ts +43 -0
  80. package/src/testing/caller.ts +80 -0
  81. package/src/testing/types.ts +81 -0
  82. package/src/testing/vitest.ts +64 -0
  83. package/src/testing.ts +13 -0
  84. package/src/types.ts +24 -0
@@ -0,0 +1,131 @@
1
+ import { TRPCError } from "@trpc/server";
2
+ import {
3
+ Cause,
4
+ Context,
5
+ Effect,
6
+ Exit,
7
+ type ManagedRuntime,
8
+ Option,
9
+ Result,
10
+ } from "effect";
11
+ import type {
12
+ EffectTRPCErrorMapper,
13
+ EffectTRPCInstrument,
14
+ ProcedureInfo,
15
+ } from "../types.js";
16
+ import type { ContextBridge } from "./context-bridge.js";
17
+
18
+ interface RunEffectOptions {
19
+ readonly procedure: ProcedureInfo;
20
+ readonly signal?: AbortSignal;
21
+ }
22
+
23
+ export interface RuntimeBridge<Requirements> {
24
+ readonly runEffect: <Value>(
25
+ effect: Effect.Effect<Value, unknown, Requirements>,
26
+ options: RunEffectOptions,
27
+ ) => Promise<Value>;
28
+ }
29
+
30
+ const internalError = (cause?: unknown): TRPCError =>
31
+ new TRPCError({
32
+ cause,
33
+ code: "INTERNAL_SERVER_ERROR",
34
+ message: "Internal server error",
35
+ });
36
+
37
+ const mapError = (
38
+ error: unknown,
39
+ origin: "defect" | "failure",
40
+ procedure: ProcedureInfo,
41
+ consumerMapper: EffectTRPCErrorMapper | undefined,
42
+ ): TRPCError => {
43
+ if (error instanceof TRPCError) {
44
+ return error;
45
+ }
46
+
47
+ try {
48
+ return (
49
+ consumerMapper?.(error, { ...procedure, origin }) ?? internalError(error)
50
+ );
51
+ } catch (mapperDefect) {
52
+ return internalError(mapperDefect);
53
+ }
54
+ };
55
+
56
+ export const makeRuntimeBridge = <Requirements, RuntimeError>(
57
+ runtime: ManagedRuntime.ManagedRuntime<Requirements, RuntimeError>,
58
+ contextBridge: ContextBridge,
59
+ options: {
60
+ readonly instrument?: EffectTRPCInstrument;
61
+ readonly mapError?: EffectTRPCErrorMapper;
62
+ },
63
+ ): RuntimeBridge<Requirements> => ({
64
+ runEffect: async (effect, runOptions) => {
65
+ const traced = effect.pipe(
66
+ Effect.withSpan(
67
+ runOptions.procedure.path,
68
+ {
69
+ attributes: {
70
+ "rpc.method": runOptions.procedure.path,
71
+ "rpc.system": "trpc",
72
+ "trpc.type": runOptions.procedure.type,
73
+ },
74
+ },
75
+ {
76
+ captureStackTrace: runOptions.procedure.captureStackTrace,
77
+ },
78
+ ),
79
+ );
80
+ const instrumented = Effect.suspend(() =>
81
+ options.instrument === undefined
82
+ ? traced
83
+ : options.instrument(traced, runOptions.procedure),
84
+ );
85
+ const ambient = contextBridge.current();
86
+ const runnable =
87
+ ambient === undefined
88
+ ? instrumented
89
+ : Effect.flatMap(runtime.contextEffect, (base) =>
90
+ Effect.provideContext(instrumented, Context.merge(base, ambient)),
91
+ );
92
+ const exit = await runtime.runPromiseExit(
93
+ runnable,
94
+ runOptions.signal === undefined
95
+ ? undefined
96
+ : { signal: runOptions.signal },
97
+ );
98
+
99
+ if (Exit.isSuccess(exit)) {
100
+ return exit.value;
101
+ }
102
+ if (Cause.hasInterruptsOnly(exit.cause)) {
103
+ throw new TRPCError({
104
+ code: "CLIENT_CLOSED_REQUEST",
105
+ message: "Request cancelled",
106
+ });
107
+ }
108
+
109
+ const failure = Cause.findErrorOption(exit.cause);
110
+ if (Option.isSome(failure)) {
111
+ throw mapError(
112
+ failure.value,
113
+ "failure",
114
+ runOptions.procedure,
115
+ options.mapError,
116
+ );
117
+ }
118
+
119
+ const defect = Cause.findDefect(exit.cause);
120
+ if (Result.isSuccess(defect)) {
121
+ throw mapError(
122
+ defect.success,
123
+ "defect",
124
+ runOptions.procedure,
125
+ options.mapError,
126
+ );
127
+ }
128
+
129
+ throw internalError();
130
+ },
131
+ });
@@ -0,0 +1,19 @@
1
+ export const captureStackTrace = (): (() => string | undefined) => {
2
+ const limit = Error.stackTraceLimit;
3
+ Error.stackTraceLimit = 3;
4
+ const traceError = new Error();
5
+ Error.stackTraceLimit = limit;
6
+
7
+ let cache: false | string = false;
8
+ return () => {
9
+ if (cache !== false) {
10
+ return cache;
11
+ }
12
+ const frame = traceError.stack?.split("\n")[3];
13
+ if (frame !== undefined) {
14
+ cache = frame.trim();
15
+ return cache;
16
+ }
17
+ return undefined;
18
+ };
19
+ };
@@ -0,0 +1,202 @@
1
+ import type { TestContext, TestOptions } from "@effect/vitest";
2
+ import { Cause, type Context, Effect, Exit, Layer, type Scope } from "effect";
3
+ import * as TestClock from "effect/testing/TestClock";
4
+ import * as TestConsole from "effect/testing/TestConsole";
5
+ import { makeEffectCallerFactory } from "../testing/caller.js";
6
+ import type {
7
+ TrpcTest,
8
+ TrpcTester,
9
+ TrpcTestRuntimeOptions,
10
+ } from "../testing/types.js";
11
+
12
+ export const fixtureName = "__effectTrpcContext";
13
+
14
+ export type TrpcFixture<Provided> = TestContext & {
15
+ readonly [fixtureName]: Context.Context<Provided>;
16
+ };
17
+
18
+ export interface FixtureTestApi<Provided> {
19
+ (
20
+ name: string,
21
+ options: TestOptions,
22
+ body: (context: TrpcFixture<Provided>) => Promise<unknown>,
23
+ ): void;
24
+ readonly skip: FixtureTestApi<Provided>;
25
+ readonly skipIf: (condition: unknown) => FixtureTestApi<Provided>;
26
+ readonly runIf: (condition: unknown) => FixtureTestApi<Provided>;
27
+ readonly only: FixtureTestApi<Provided>;
28
+ readonly fails: FixtureTestApi<Provided>;
29
+ readonly for: <Item>(
30
+ cases: ReadonlyArray<Item>,
31
+ ) => (
32
+ name: string,
33
+ options: TestOptions,
34
+ body: (item: Item, context: TrpcFixture<Provided>) => Promise<unknown>,
35
+ ) => void;
36
+ }
37
+
38
+ const TestEnvironment = Layer.mergeAll(TestConsole.layer, TestClock.layer());
39
+
40
+ const normalizeTestOptions = (
41
+ options: number | TestOptions | undefined,
42
+ ): TestOptions =>
43
+ typeof options === "number" ? { timeout: options } : (options ?? {});
44
+
45
+ const restoreContext = <Provided>(
46
+ fixture: Context.Context<Provided>,
47
+ context: Omit<TrpcFixture<Provided>, typeof fixtureName>,
48
+ ): TrpcFixture<Provided> =>
49
+ ({ ...context, [fixtureName]: fixture }) as TrpcFixture<Provided>;
50
+
51
+ const contextFrom = <Provided>(
52
+ context: TestContext,
53
+ ): Context.Context<Provided> => (context as TrpcFixture<Provided>)[fixtureName];
54
+
55
+ const runEffectTest = <A, E>(
56
+ effect: Effect.Effect<A, E, Scope.Scope>,
57
+ context: TestContext,
58
+ ): Promise<A> =>
59
+ Effect.runPromise(
60
+ Effect.gen(function* () {
61
+ const exit = yield* Effect.exit(effect);
62
+ if (Exit.isFailure(exit)) {
63
+ for (const error of Cause.prettyErrors(exit.cause)) {
64
+ yield* Effect.logError(error);
65
+ }
66
+ }
67
+ return yield* exit;
68
+ }).pipe(Effect.scoped, Effect.provide(TestEnvironment)),
69
+ { signal: context.signal },
70
+ );
71
+
72
+ export const makeTrpcTester = <
73
+ Options,
74
+ Caller extends object,
75
+ Provided,
76
+ LayerError,
77
+ >(
78
+ fixtureIt: FixtureTestApi<Provided>,
79
+ options: TrpcTestRuntimeOptions<Options, Caller, Provided, LayerError>,
80
+ ): TrpcTester<Options, Caller, Provided> => {
81
+ const run = <A, Eff extends Effect.Effect<unknown, unknown, Provided>>(
82
+ body: (
83
+ trpc: ReturnType<
84
+ typeof makeEffectCallerFactory<Options, Caller, Provided>
85
+ >,
86
+ context: TestContext,
87
+ ) => Generator<Eff, A, never>,
88
+ context: TestContext,
89
+ ): Effect.Effect<A, unknown, Scope.Scope> => {
90
+ const program = Effect.gen(function* () {
91
+ const services = yield* Effect.context<Provided>();
92
+ const trpc = makeEffectCallerFactory(
93
+ options.adapter,
94
+ options.createCaller,
95
+ services,
96
+ );
97
+ return yield* Effect.gen(() => body(trpc, context));
98
+ });
99
+ // The body constraint proves every yielded service is part of Provided.
100
+ // TypeScript cannot reduce that generic generator requirement here.
101
+ const ready = program as Effect.Effect<A, unknown, Provided>;
102
+ const wrapped = options.around?.(ready) ?? ready;
103
+ return wrapped.pipe(
104
+ Effect.provide(contextFrom<Provided>(context)),
105
+ ) as Effect.Effect<A, unknown, Scope.Scope>;
106
+ };
107
+
108
+ const register =
109
+ (current: FixtureTestApi<Provided>): TrpcTest<Options, Caller, Provided> =>
110
+ (name, body, testOptions) =>
111
+ current(
112
+ name,
113
+ normalizeTestOptions(testOptions),
114
+ ({
115
+ __effectTrpcContext,
116
+ task,
117
+ signal,
118
+ onTestFailed,
119
+ onTestFinished,
120
+ skip,
121
+ annotate,
122
+ expect,
123
+ _local,
124
+ }: TrpcFixture<Provided>) => {
125
+ const context = restoreContext(__effectTrpcContext, {
126
+ task,
127
+ signal,
128
+ onTestFailed,
129
+ onTestFinished,
130
+ skip,
131
+ annotate,
132
+ expect,
133
+ _local,
134
+ });
135
+ return runEffectTest(run(body, context), context);
136
+ },
137
+ );
138
+
139
+ const make = (
140
+ current: FixtureTestApi<Provided>,
141
+ ): TrpcTester<Options, Caller, Provided> => {
142
+ const test = register(current);
143
+ return Object.assign(test, {
144
+ skip: register(current.skip),
145
+ skipIf: (condition: unknown) => register(current.skipIf(condition)),
146
+ runIf: (condition: unknown) => register(current.runIf(condition)),
147
+ only: register(current.only),
148
+ each:
149
+ <Item>(cases: ReadonlyArray<Item>) =>
150
+ <A, Eff extends Effect.Effect<unknown, unknown, Provided>>(
151
+ name: string,
152
+ body: (
153
+ item: Item,
154
+ trpc: ReturnType<
155
+ typeof makeEffectCallerFactory<Options, Caller, Provided>
156
+ >,
157
+ context: TestContext,
158
+ ) => Generator<Eff, A, never>,
159
+ testOptions?: number | TestOptions,
160
+ ) =>
161
+ fixtureIt.for(cases)(
162
+ name,
163
+ normalizeTestOptions(testOptions),
164
+ (
165
+ item,
166
+ {
167
+ __effectTrpcContext,
168
+ task,
169
+ signal,
170
+ onTestFailed,
171
+ onTestFinished,
172
+ skip,
173
+ annotate,
174
+ expect,
175
+ _local,
176
+ }: TrpcFixture<Provided>,
177
+ ) => {
178
+ const context = restoreContext(__effectTrpcContext, {
179
+ task,
180
+ signal,
181
+ onTestFailed,
182
+ onTestFinished,
183
+ skip,
184
+ annotate,
185
+ expect,
186
+ _local,
187
+ });
188
+ return runEffectTest(
189
+ run(
190
+ (trpc, testContext) => body(item, trpc, testContext),
191
+ context,
192
+ ),
193
+ context,
194
+ );
195
+ },
196
+ ),
197
+ fails: register(current.fails),
198
+ });
199
+ };
200
+
201
+ return make(fixtureIt);
202
+ };
@@ -0,0 +1,191 @@
1
+ import type {
2
+ TRPCMutationProcedure,
3
+ TRPCProcedureBuilder,
4
+ TRPCQueryProcedure,
5
+ } from "@trpc/server";
6
+ import { Schema } from "effect";
7
+ import { makeProcedureHandler } from "./internal/procedure-handler.js";
8
+ import type {
9
+ DefaultValue,
10
+ EffectProcedureResolver,
11
+ IntersectIfDefined,
12
+ ResolverContext,
13
+ } from "./internal/procedure-types.js";
14
+ import type { RuntimeBridge } from "./internal/runtime.js";
15
+ import { captureStackTrace } from "./internal/stack-trace.js";
16
+ import type { EffectProcedureRequestServices } from "./request-services.js";
17
+
18
+ type Builder<
19
+ Context,
20
+ Meta,
21
+ ContextOverrides,
22
+ InputIn,
23
+ InputOut,
24
+ OutputIn,
25
+ OutputOut,
26
+ > = TRPCProcedureBuilder<
27
+ Context,
28
+ Meta,
29
+ ContextOverrides,
30
+ InputIn,
31
+ InputOut,
32
+ OutputIn,
33
+ OutputOut,
34
+ false
35
+ >;
36
+
37
+ export class EffectProcedureBuilder<
38
+ Context,
39
+ Meta,
40
+ ContextOverrides,
41
+ InputIn,
42
+ InputOut,
43
+ OutputIn,
44
+ OutputOut,
45
+ ProvidedServices,
46
+ LayerError,
47
+ RuntimeRequirements,
48
+ > {
49
+ constructor(
50
+ private readonly builder: Builder<
51
+ Context,
52
+ Meta,
53
+ ContextOverrides,
54
+ InputIn,
55
+ InputOut,
56
+ OutputIn,
57
+ OutputOut
58
+ >,
59
+ private readonly requestServices: EffectProcedureRequestServices<
60
+ ResolverContext<
61
+ Builder<
62
+ Context,
63
+ Meta,
64
+ ContextOverrides,
65
+ InputIn,
66
+ InputOut,
67
+ OutputIn,
68
+ OutputOut
69
+ >
70
+ >,
71
+ ProvidedServices,
72
+ LayerError
73
+ >,
74
+ private readonly runtime: RuntimeBridge<RuntimeRequirements>,
75
+ ) {}
76
+
77
+ input<SchemaValue extends Schema.ConstraintDecoder<unknown>>(
78
+ schema: SchemaValue,
79
+ ): EffectProcedureBuilder<
80
+ Context,
81
+ Meta,
82
+ ContextOverrides,
83
+ IntersectIfDefined<InputIn, SchemaValue["Encoded"]>,
84
+ IntersectIfDefined<InputOut, SchemaValue["Type"]>,
85
+ OutputIn,
86
+ OutputOut,
87
+ ProvidedServices,
88
+ LayerError,
89
+ RuntimeRequirements
90
+ > {
91
+ const next = this.builder.input(Schema.toStandardSchemaV1(schema) as never);
92
+ return new EffectProcedureBuilder(
93
+ next as unknown as Builder<
94
+ Context,
95
+ Meta,
96
+ ContextOverrides,
97
+ IntersectIfDefined<InputIn, SchemaValue["Encoded"]>,
98
+ IntersectIfDefined<InputOut, SchemaValue["Type"]>,
99
+ OutputIn,
100
+ OutputOut
101
+ >,
102
+ this.requestServices,
103
+ this.runtime,
104
+ );
105
+ }
106
+
107
+ output<SchemaValue extends Schema.ConstraintDecoder<unknown>>(
108
+ schema: SchemaValue,
109
+ ): EffectProcedureBuilder<
110
+ Context,
111
+ Meta,
112
+ ContextOverrides,
113
+ InputIn,
114
+ InputOut,
115
+ IntersectIfDefined<OutputIn, SchemaValue["Encoded"]>,
116
+ IntersectIfDefined<OutputOut, SchemaValue["Type"]>,
117
+ ProvidedServices,
118
+ LayerError,
119
+ RuntimeRequirements
120
+ > {
121
+ const next = this.builder.output(Schema.toStandardSchemaV1(schema));
122
+ return new EffectProcedureBuilder(
123
+ next as unknown as Builder<
124
+ Context,
125
+ Meta,
126
+ ContextOverrides,
127
+ InputIn,
128
+ InputOut,
129
+ IntersectIfDefined<OutputIn, SchemaValue["Encoded"]>,
130
+ IntersectIfDefined<OutputOut, SchemaValue["Type"]>
131
+ >,
132
+ this.requestServices,
133
+ this.runtime,
134
+ );
135
+ }
136
+
137
+ query<Output>(
138
+ resolver: EffectProcedureResolver<
139
+ DefaultValue<InputOut, undefined>,
140
+ ProvidedServices | NoInfer<RuntimeRequirements>,
141
+ DefaultValue<OutputIn, Output>
142
+ >,
143
+ ): TRPCQueryProcedure<{
144
+ input: DefaultValue<InputIn, void>;
145
+ output: DefaultValue<OutputOut, Output>;
146
+ meta: Meta;
147
+ }> {
148
+ return this.dispatch("query", resolver) as TRPCQueryProcedure<{
149
+ input: DefaultValue<InputIn, void>;
150
+ output: DefaultValue<OutputOut, Output>;
151
+ meta: Meta;
152
+ }>;
153
+ }
154
+
155
+ mutation<Output>(
156
+ resolver: EffectProcedureResolver<
157
+ DefaultValue<InputOut, undefined>,
158
+ ProvidedServices | NoInfer<RuntimeRequirements>,
159
+ DefaultValue<OutputIn, Output>
160
+ >,
161
+ ): TRPCMutationProcedure<{
162
+ input: DefaultValue<InputIn, void>;
163
+ output: DefaultValue<OutputOut, Output>;
164
+ meta: Meta;
165
+ }> {
166
+ return this.dispatch("mutation", resolver) as TRPCMutationProcedure<{
167
+ input: DefaultValue<InputIn, void>;
168
+ output: DefaultValue<OutputOut, Output>;
169
+ meta: Meta;
170
+ }>;
171
+ }
172
+
173
+ private dispatch<Output>(
174
+ type: "mutation" | "query",
175
+ resolver: EffectProcedureResolver<
176
+ DefaultValue<InputOut, undefined>,
177
+ ProvidedServices | NoInfer<RuntimeRequirements>,
178
+ Output
179
+ >,
180
+ ): unknown {
181
+ const handler = makeProcedureHandler(
182
+ this.runtime,
183
+ resolver,
184
+ this.requestServices,
185
+ { captureStackTrace: captureStackTrace(), type },
186
+ ) as never;
187
+ return type === "query"
188
+ ? this.builder.query(handler)
189
+ : this.builder.mutation(handler);
190
+ }
191
+ }
@@ -0,0 +1,43 @@
1
+ import { type Layer as EffectLayer, Layer } from "effect";
2
+
3
+ export interface EffectProcedureRequestServices<
4
+ Context,
5
+ Requirements,
6
+ Error = never,
7
+ > {
8
+ readonly layer: (context: Context) => EffectLayer.Layer<Requirements, Error>;
9
+ }
10
+
11
+ export const makeRequestServices = <Context, Requirements, Error = never>(
12
+ layer: (context: Context) => EffectLayer.Layer<Requirements, Error>,
13
+ ): EffectProcedureRequestServices<Context, Requirements, Error> => ({ layer });
14
+
15
+ export const extendRequestServices = <
16
+ BaseContext,
17
+ Context extends BaseContext,
18
+ BaseRequirements,
19
+ BaseError,
20
+ AdditionalRequirements,
21
+ AdditionalError,
22
+ AdditionalDependencies extends BaseRequirements = BaseRequirements,
23
+ >(
24
+ base: EffectProcedureRequestServices<
25
+ BaseContext,
26
+ BaseRequirements,
27
+ BaseError
28
+ >,
29
+ additional: (
30
+ context: Context,
31
+ ) => EffectLayer.Layer<
32
+ AdditionalRequirements,
33
+ AdditionalError,
34
+ AdditionalDependencies
35
+ >,
36
+ ): EffectProcedureRequestServices<
37
+ Context,
38
+ BaseRequirements | AdditionalRequirements,
39
+ BaseError | AdditionalError
40
+ > =>
41
+ makeRequestServices((context: Context) =>
42
+ Layer.provideMerge(additional(context), base.layer(context)),
43
+ );
@@ -0,0 +1,80 @@
1
+ import { getTRPCErrorFromUnknown, type TRPCError } from "@trpc/server";
2
+ import { type Context, Effect } from "effect";
3
+ import type { EffectTRPCAdapter } from "../adapter.js";
4
+
5
+ export type EffectCaller<Caller> = {
6
+ readonly [Key in keyof Caller]: Caller[Key] extends (
7
+ ...arguments_: infer Arguments
8
+ ) => Promise<infer Result>
9
+ ? (...arguments_: Arguments) => Effect.Effect<Result, TRPCError>
10
+ : Caller[Key] extends (...arguments_: infer Arguments) => infer Result
11
+ ? (...arguments_: Arguments) => Result
12
+ : Caller[Key] extends Record<string, unknown>
13
+ ? EffectCaller<Caller[Key]>
14
+ : Caller[Key];
15
+ };
16
+
17
+ export type EffectCallerFactory<Options, Caller> = EffectCaller<Caller> &
18
+ ((options?: Options) => EffectCaller<Caller>);
19
+
20
+ export const makeEffectCaller = <Caller extends object, Services>(
21
+ adapter: Pick<EffectTRPCAdapter<never>, "runWithServices">,
22
+ promiseCaller: Caller,
23
+ services: Context.Context<Services>,
24
+ ): EffectCaller<Caller> => {
25
+ const build = (path: ReadonlyArray<string>): unknown =>
26
+ new Proxy(
27
+ Object.assign(() => undefined, { path }),
28
+ {
29
+ apply(_target, _this, argumentsList) {
30
+ return Effect.tryPromise({
31
+ catch: getTRPCErrorFromUnknown,
32
+ try: () =>
33
+ adapter.runWithServices(services, () => {
34
+ let leaf: unknown = promiseCaller;
35
+ for (const segment of path) {
36
+ leaf = (leaf as Record<string, unknown>)[segment];
37
+ }
38
+ return (leaf as (...args: unknown[]) => Promise<unknown>)(
39
+ ...argumentsList,
40
+ );
41
+ }),
42
+ });
43
+ },
44
+ get(_target, property) {
45
+ if (typeof property !== "string" || property === "then") {
46
+ return undefined;
47
+ }
48
+ return build([...path, property]);
49
+ },
50
+ },
51
+ );
52
+
53
+ return build([]) as EffectCaller<Caller>;
54
+ };
55
+
56
+ export const makeEffectCallerFactory = <
57
+ Options,
58
+ Caller extends object,
59
+ Services,
60
+ >(
61
+ adapter: Pick<EffectTRPCAdapter<never>, "runWithServices">,
62
+ createCaller: (options?: Options) => Caller,
63
+ services: Context.Context<Services>,
64
+ ): EffectCallerFactory<Options, Caller> => {
65
+ const defaultCaller = makeEffectCaller(adapter, createCaller(), services);
66
+ const target = (options?: Options) =>
67
+ makeEffectCaller(adapter, createCaller(options), services);
68
+
69
+ return new Proxy(target, {
70
+ get(_target, property, receiver) {
71
+ if (property === "then") {
72
+ return undefined;
73
+ }
74
+ if (typeof property !== "string") {
75
+ return Reflect.get(_target, property, receiver);
76
+ }
77
+ return Reflect.get(defaultCaller, property);
78
+ },
79
+ }) as EffectCallerFactory<Options, Caller>;
80
+ };