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.
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import z from "zod";
5
5
 
6
6
  import { EffectDecoratedProcedure } from "../effect-procedure";
7
+ import { makeEffectRuntimeRunner } from "../runtime-source";
7
8
  import {
8
9
  baseErrorMap,
9
10
  baseMeta,
@@ -25,6 +26,7 @@ vi.mock("@orpc/server", async (importOriginal) => {
25
26
  });
26
27
 
27
28
  const runtime = ManagedRuntime.make(Layer.empty);
29
+ const runner = makeEffectRuntimeRunner(runtime);
28
30
 
29
31
  const handler = vi.fn();
30
32
  const middleware = vi.fn();
@@ -40,6 +42,7 @@ const def = {
40
42
  meta: baseMeta,
41
43
  route: baseRoute,
42
44
  handler,
45
+ runner,
43
46
  runtime,
44
47
  };
45
48
 
@@ -80,7 +83,7 @@ describe("effectDecoratedProcedure", () => {
80
83
  });
81
84
 
82
85
  // Preserves runtime
83
- expect(applied["~effect"].runtime).toBe(runtime);
86
+ expect(applied["~effect"].runner.runtime).toBe(runtime);
84
87
  });
85
88
 
86
89
  it(".meta", () => {
@@ -96,7 +99,7 @@ describe("effectDecoratedProcedure", () => {
96
99
  });
97
100
 
98
101
  // Preserves runtime
99
- expect(applied["~effect"].runtime).toBe(runtime);
102
+ expect(applied["~effect"].runner.runtime).toBe(runtime);
100
103
  });
101
104
 
102
105
  it(".route", () => {
@@ -112,7 +115,7 @@ describe("effectDecoratedProcedure", () => {
112
115
  });
113
116
 
114
117
  // Preserves runtime
115
- expect(applied["~effect"].runtime).toBe(runtime);
118
+ expect(applied["~effect"].runner.runtime).toBe(runtime);
116
119
  });
117
120
 
118
121
  describe(".use", () => {
@@ -129,7 +132,7 @@ describe("effectDecoratedProcedure", () => {
129
132
  });
130
133
 
131
134
  // Preserves runtime
132
- expect(applied["~effect"].runtime).toBe(runtime);
135
+ expect(applied["~effect"].runner.runtime).toBe(runtime);
133
136
  });
134
137
 
135
138
  it("with map input", () => {
@@ -146,7 +149,7 @@ describe("effectDecoratedProcedure", () => {
146
149
  });
147
150
 
148
151
  // Preserves runtime
149
- expect(applied["~effect"].runtime).toBe(runtime);
152
+ expect(applied["~effect"].runner.runtime).toBe(runtime);
150
153
  });
151
154
  });
152
155
 
@@ -207,7 +210,7 @@ describe("effectDecoratedProcedure chaining", () => {
207
210
  .route({ path: "/custom" });
208
211
 
209
212
  expect(applied).toBeInstanceOf(EffectDecoratedProcedure);
210
- expect(applied["~effect"].runtime).toBe(runtime);
213
+ expect(applied["~effect"].runner.runtime).toBe(runtime);
211
214
  expect(applied["~effect"].errorMap).toHaveProperty("CUSTOM");
212
215
  });
213
216
  });
@@ -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 withMiddlewares = rootBuilder.use(({ next }) =>
46
- next({ context: { auth: true as boolean } }),
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);
@@ -12,12 +12,21 @@ import {
12
12
  outputSchema,
13
13
  } from "./shared";
14
14
 
15
+ const authMiddleware: Middleware<
16
+ InitialContext,
17
+ { auth: boolean },
18
+ { input: string },
19
+ unknown,
20
+ any,
21
+ BaseMeta
22
+ > = ({ next }) => next({ context: { auth: true as boolean } });
23
+
15
24
  const procedure = makeEffectORPC(runtime)
16
25
  .$context<InitialContext>()
17
26
  .$meta({ mode: "dev" } as BaseMeta)
18
27
  .$input(inputSchema)
19
28
  .errors(baseErrorMap)
20
- .use(({ next }) => next({ context: { auth: true as boolean } }))
29
+ .use(authMiddleware)
21
30
  .output(outputSchema)
22
31
  .effect(function* () {
23
32
  return { output: 456 };
@@ -61,13 +70,20 @@ describe("parity: @orpc/server procedure-decorated.test-d.ts", () => {
61
70
 
62
71
  describe(".use", () => {
63
72
  it("without map input", () => {
64
- expectTypeOf(
65
- procedure.use(({ next }, input, output) => {
66
- expectTypeOf(input).toEqualTypeOf<{ input: string }>();
67
- expectTypeOf(output).toBeFunction();
68
- return next({ context: { extra: true } });
69
- }),
70
- ).toBeObject();
73
+ const middleware: Middleware<
74
+ CurrentContext,
75
+ { extra: boolean },
76
+ { input: string },
77
+ { output: number },
78
+ any,
79
+ BaseMeta
80
+ > = ({ next }, input, output) => {
81
+ expectTypeOf(input).toEqualTypeOf<{ input: string }>();
82
+ expectTypeOf(output).toBeFunction();
83
+ return next({ context: { extra: true } });
84
+ };
85
+
86
+ expectTypeOf(procedure.use(middleware)).toBeObject();
71
87
 
72
88
  procedure.use(
73
89
  // @ts-expect-error - invalid TInContext
@@ -1,5 +1,5 @@
1
1
  import type { Meta, Schema } from "@orpc/contract";
2
- import { ContractProcedure, eventIterator } from "@orpc/contract";
2
+ import { ContractProcedure } from "@orpc/contract";
3
3
  import * as z from "zod";
4
4
 
5
5
  export const inputSchema = z.object({
@@ -53,28 +53,4 @@ export const pong = new ContractProcedure<
53
53
  route: {},
54
54
  });
55
55
 
56
- export const router = {
57
- ping,
58
- pong,
59
- nested: {
60
- ping,
61
- pong,
62
- },
63
- };
64
-
65
- export const streamedOutputSchema = eventIterator(outputSchema);
66
-
67
- export const streamed = new ContractProcedure<
68
- typeof inputSchema,
69
- typeof streamedOutputSchema,
70
- typeof baseErrorMap,
71
- Meta
72
- >({
73
- errorMap: baseErrorMap,
74
- meta: {},
75
- route: {},
76
- inputSchema,
77
- outputSchema: streamedOutputSchema,
78
- });
79
-
80
56
  export type AssertExtends<_TActual extends TExpected, TExpected> = true;
@@ -20,13 +20,17 @@ import type {
20
20
  Router,
21
21
  } from "@orpc/server";
22
22
  import type { IntersectPick } from "@orpc/shared";
23
+ import type { Context as EffectContext, Layer } from "effect";
23
24
 
24
25
  import type {
25
26
  EffectBuilderDef,
26
27
  EffectErrorMapToErrorMap,
28
+ EffectOrORPCMiddleware,
29
+ EffectOptionalProvider,
27
30
  EffectProcedureBuilderWithInput,
28
31
  EffectProcedureBuilderWithOutput,
29
32
  EffectProcedureHandler,
33
+ EffectProvider,
30
34
  EffectRouterBuilder,
31
35
  EnhancedEffectRouter,
32
36
  } from ".";
@@ -37,6 +41,9 @@ import type {
37
41
  MergedEffectErrorMap,
38
42
  } from "../tagged-error";
39
43
 
44
+ type EffectTagIdentifier<T extends EffectContext.Tag<any, any>> =
45
+ T extends EffectContext.Tag<infer I, any> ? I : never;
46
+
40
47
  export interface EffectBuilderSurface<
41
48
  TInitialContext extends Context,
42
49
  TCurrentContext extends Context,
@@ -158,6 +165,28 @@ export interface EffectBuilderSurface<
158
165
  *
159
166
  * @see {@link https://orpc.dev/docs/middleware Middleware Docs}
160
167
  */
168
+ middleware<
169
+ UOutContext extends IntersectPick<TCurrentContext, UOutContext>,
170
+ TInput,
171
+ TOutput = any,
172
+ >(
173
+ middleware: EffectOrORPCMiddleware<
174
+ TInitialContext,
175
+ UOutContext,
176
+ TInput,
177
+ TOutput,
178
+ TEffectErrorMap,
179
+ TRequirementsProvided,
180
+ TMeta
181
+ >,
182
+ ): DecoratedMiddleware<
183
+ TInitialContext,
184
+ UOutContext,
185
+ TInput,
186
+ TOutput,
187
+ any,
188
+ TMeta
189
+ >;
161
190
  middleware<
162
191
  UOutContext extends IntersectPick<TCurrentContext, UOutContext>,
163
192
  TInput,
@@ -212,6 +241,32 @@ export interface EffectBuilderSurface<
212
241
  TRequirementsProvided,
213
242
  TRuntimeError
214
243
  >;
244
+ /**
245
+ * Uses an Effect middleware or native oRPC middleware to modify the context, throw early, or modify the response.
246
+ */
247
+ use<
248
+ UOutContext extends IntersectPick<TCurrentContext, UOutContext>,
249
+ UInContext extends Context = TCurrentContext,
250
+ >(
251
+ middleware: EffectOrORPCMiddleware<
252
+ UInContext | TCurrentContext,
253
+ UOutContext,
254
+ InferSchemaOutput<TInputSchema>,
255
+ unknown,
256
+ TEffectErrorMap,
257
+ TRequirementsProvided,
258
+ TMeta
259
+ >,
260
+ ): EffectBuilderSurface<
261
+ MergedInitialContext<TInitialContext, UInContext, TCurrentContext>,
262
+ MergedCurrentContext<TCurrentContext, UOutContext>,
263
+ TInputSchema,
264
+ TOutputSchema,
265
+ TEffectErrorMap,
266
+ TMeta,
267
+ TRequirementsProvided,
268
+ TRuntimeError
269
+ >;
215
270
  /**
216
271
  * Uses a middleware to modify the context or improve the pipeline.
217
272
  *
@@ -241,6 +296,67 @@ export interface EffectBuilderSurface<
241
296
  TRequirementsProvided,
242
297
  TRuntimeError
243
298
  >;
299
+ /**
300
+ * Provides an Effect layer to downstream procedures.
301
+ */
302
+ provide<TLayerOut, TLayerError, TLayerIn extends TRequirementsProvided>(
303
+ layer: Layer.Layer<TLayerOut, TLayerError, TLayerIn>,
304
+ ): EffectBuilderSurface<
305
+ TInitialContext,
306
+ TCurrentContext,
307
+ TInputSchema,
308
+ TOutputSchema,
309
+ TEffectErrorMap,
310
+ TMeta,
311
+ TRequirementsProvided | TLayerOut,
312
+ TRuntimeError | TLayerError
313
+ >;
314
+ /**
315
+ * Provides a request-scoped Effect service to downstream procedures.
316
+ */
317
+ provide<TTag extends EffectContext.Tag<any, any>>(
318
+ tag: TTag,
319
+ provider: EffectProvider<
320
+ TCurrentContext,
321
+ InferSchemaOutput<TInputSchema>,
322
+ TEffectErrorMap,
323
+ TRequirementsProvided,
324
+ TMeta,
325
+ TTag
326
+ >,
327
+ ): EffectBuilderSurface<
328
+ TInitialContext,
329
+ TCurrentContext,
330
+ TInputSchema,
331
+ TOutputSchema,
332
+ TEffectErrorMap,
333
+ TMeta,
334
+ TRequirementsProvided | EffectTagIdentifier<TTag>,
335
+ TRuntimeError
336
+ >;
337
+ /**
338
+ * Optionally provides a request-scoped Effect service to downstream procedures.
339
+ */
340
+ provideOptional<TTag extends EffectContext.Tag<any, any>>(
341
+ tag: TTag,
342
+ provider: EffectOptionalProvider<
343
+ TCurrentContext,
344
+ InferSchemaOutput<TInputSchema>,
345
+ TEffectErrorMap,
346
+ TRequirementsProvided,
347
+ TMeta,
348
+ TTag
349
+ >,
350
+ ): EffectBuilderSurface<
351
+ TInitialContext,
352
+ TCurrentContext,
353
+ TInputSchema,
354
+ TOutputSchema,
355
+ TEffectErrorMap,
356
+ TMeta,
357
+ TRequirementsProvided,
358
+ TRuntimeError
359
+ >;
244
360
  /**
245
361
  * Sets or updates the metadata.
246
362
  * The provided metadata is spared-merged with any existing metadata.
@@ -358,7 +474,7 @@ export interface EffectBuilderSurface<
358
474
  >;
359
475
  /**
360
476
  * Defines the handler of the procedure using an Effect.
361
- * The Effect is executed using the ManagedRuntime provided during builder creation.
477
+ * The Effect is executed using the configured Effect runtime source.
362
478
  * The effect is automatically wrapped with `Effect.withSpan`.
363
479
  *
364
480
  * @see {@link https://orpc.dev/docs/procedure Procedure Docs}
@@ -18,8 +18,15 @@ import type {
18
18
  ProcedureDef,
19
19
  } from "@orpc/server";
20
20
  import type { IntersectPick, MaybeOptionalOptions } from "@orpc/shared";
21
+ import type { Context as EffectContext, Layer } from "effect";
21
22
 
22
- import type { EffectProcedureDef, EffectErrorMapToErrorMap } from ".";
23
+ import type {
24
+ EffectErrorMapToErrorMap,
25
+ EffectOrORPCMiddleware,
26
+ EffectOptionalProvider,
27
+ EffectProcedureDef,
28
+ EffectProvider,
29
+ } from ".";
23
30
  import type { EffectDecoratedProcedure } from "../effect-procedure";
24
31
  import type {
25
32
  EffectErrorConstructorMap,
@@ -27,6 +34,9 @@ import type {
27
34
  MergedEffectErrorMap,
28
35
  } from "../tagged-error";
29
36
 
37
+ type EffectTagIdentifier<T extends EffectContext.Tag<any, any>> =
38
+ T extends EffectContext.Tag<infer I, any> ? I : never;
39
+
30
40
  export interface EffectDecoratedProcedureSurface<
31
41
  TInitialContext extends Context,
32
42
  TCurrentContext extends Context,
@@ -117,6 +127,93 @@ export interface EffectDecoratedProcedureSurface<
117
127
  TRequirementsProvided,
118
128
  TRuntimeError
119
129
  >;
130
+ /**
131
+ * Provides an Effect layer to downstream procedures.
132
+ */
133
+ provide<TLayerOut, TLayerError, TLayerIn extends TRequirementsProvided>(
134
+ layer: Layer.Layer<TLayerOut, TLayerError, TLayerIn>,
135
+ ): EffectDecoratedProcedure<
136
+ TInitialContext,
137
+ TCurrentContext,
138
+ TInputSchema,
139
+ TOutputSchema,
140
+ TEffectErrorMap,
141
+ TMeta,
142
+ TRequirementsProvided | TLayerOut,
143
+ TRuntimeError | TLayerError
144
+ >;
145
+ /**
146
+ * Provides a request-scoped Effect service to downstream procedures.
147
+ */
148
+ provide<TTag extends EffectContext.Tag<any, any>>(
149
+ tag: TTag,
150
+ provider: EffectProvider<
151
+ TCurrentContext,
152
+ InferSchemaOutput<TInputSchema>,
153
+ TEffectErrorMap,
154
+ TRequirementsProvided,
155
+ TMeta,
156
+ TTag
157
+ >,
158
+ ): EffectDecoratedProcedure<
159
+ TInitialContext,
160
+ TCurrentContext,
161
+ TInputSchema,
162
+ TOutputSchema,
163
+ TEffectErrorMap,
164
+ TMeta,
165
+ TRequirementsProvided | EffectTagIdentifier<TTag>,
166
+ TRuntimeError
167
+ >;
168
+ /**
169
+ * Optionally provides a request-scoped Effect service to downstream procedures.
170
+ */
171
+ provideOptional<TTag extends EffectContext.Tag<any, any>>(
172
+ tag: TTag,
173
+ provider: EffectOptionalProvider<
174
+ TCurrentContext,
175
+ InferSchemaOutput<TInputSchema>,
176
+ TEffectErrorMap,
177
+ TRequirementsProvided,
178
+ TMeta,
179
+ TTag
180
+ >,
181
+ ): EffectDecoratedProcedure<
182
+ TInitialContext,
183
+ TCurrentContext,
184
+ TInputSchema,
185
+ TOutputSchema,
186
+ TEffectErrorMap,
187
+ TMeta,
188
+ TRequirementsProvided,
189
+ TRuntimeError
190
+ >;
191
+ /**
192
+ * Uses an Effect middleware or native oRPC middleware to modify the context, throw early, or modify the response.
193
+ */
194
+ use<
195
+ UOutContext extends IntersectPick<TCurrentContext, UOutContext>,
196
+ UInContext extends Context = TCurrentContext,
197
+ >(
198
+ middleware: EffectOrORPCMiddleware<
199
+ UInContext | TCurrentContext,
200
+ UOutContext,
201
+ InferSchemaOutput<TInputSchema>,
202
+ InferSchemaInput<TOutputSchema>,
203
+ TEffectErrorMap,
204
+ TRequirementsProvided,
205
+ TMeta
206
+ >,
207
+ ): EffectDecoratedProcedure<
208
+ MergedInitialContext<TInitialContext, UInContext, TCurrentContext>,
209
+ MergedCurrentContext<TCurrentContext, UOutContext>,
210
+ TInputSchema,
211
+ TOutputSchema,
212
+ TEffectErrorMap,
213
+ TMeta,
214
+ TRequirementsProvided,
215
+ TRuntimeError
216
+ >;
120
217
  /**
121
218
  * Uses a middleware to modify the context or improve the pipeline.
122
219
  *