effect-orpc 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -146
- package/dist/index.js +421 -133
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/contract.ts +15 -12
- package/src/effect-builder.ts +63 -40
- package/src/effect-enhance-router.ts +3 -3
- package/src/effect-procedure.ts +38 -14
- package/src/effect-runtime.ts +647 -115
- package/src/extension/state.ts +3 -5
- package/src/index.ts +1 -0
- package/src/runtime-source.ts +70 -12
- package/src/tagged-error.ts +1 -1
- package/src/tests/contract.test.ts +5 -8
- package/src/tests/effect-builder.proxy.test.ts +15 -17
- package/src/tests/effect-builder.test.ts +352 -161
- package/src/tests/effect-callback-shapes.test.ts +410 -0
- package/src/tests/effect-error-map.test.ts +12 -14
- package/src/tests/effect-procedure.test.ts +53 -11
- package/src/tests/parity-shared.ts +2 -2
- package/src/types/effect-builder-surface.ts +1 -1
- package/src/types/index.ts +76 -51
- package/src/types/variants.ts +5 -5
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import { call, createRouterClient } from "@orpc/server";
|
|
3
|
+
import type { Context as ORPCContext } from "@orpc/server";
|
|
4
|
+
import { Context, Effect, Layer, ManagedRuntime, Option, Tracer } from "effect";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import z from "zod";
|
|
7
|
+
|
|
8
|
+
import { implementEffect } from "../contract";
|
|
9
|
+
import { eos, makeEffectORPC } from "../effect-builder";
|
|
10
|
+
import type { EffectOrORPCMiddleware } from "../types";
|
|
11
|
+
|
|
12
|
+
type TestMiddleware<
|
|
13
|
+
TOutContext extends ORPCContext = { readonly value: string },
|
|
14
|
+
> = EffectOrORPCMiddleware<
|
|
15
|
+
ORPCContext | Record<never, never>,
|
|
16
|
+
TOutContext,
|
|
17
|
+
unknown,
|
|
18
|
+
unknown,
|
|
19
|
+
Record<never, never>,
|
|
20
|
+
never,
|
|
21
|
+
Record<never, never>
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
type TestMiddlewareOptions = Parameters<TestMiddleware>[0];
|
|
25
|
+
type SpanMiddlewareOptions = Parameters<
|
|
26
|
+
TestMiddleware<Record<never, never>>
|
|
27
|
+
>[0];
|
|
28
|
+
|
|
29
|
+
type MiddlewareShape = {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly middleware: TestMiddleware;
|
|
32
|
+
readonly expected: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function effectHandlerShapes() {
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
name: "function*",
|
|
39
|
+
handler: function* ({ input }: { input: number }) {
|
|
40
|
+
const increment = yield* Effect.succeed(1);
|
|
41
|
+
return input + increment;
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "named Effect.fn",
|
|
46
|
+
handler: Effect.fn("test.effect.named")(function* ({
|
|
47
|
+
input,
|
|
48
|
+
}: {
|
|
49
|
+
input: number;
|
|
50
|
+
}) {
|
|
51
|
+
const increment = yield* Effect.succeed(1);
|
|
52
|
+
return input + increment;
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "anonymous Effect.fn",
|
|
57
|
+
handler: Effect.fn(function* ({ input }: { input: number }) {
|
|
58
|
+
const increment = yield* Effect.succeed(1);
|
|
59
|
+
return input + increment;
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "Effect.gen-returning function",
|
|
64
|
+
handler: ({ input }: { input: number }) =>
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
const increment = yield* Effect.succeed(1);
|
|
67
|
+
return input + increment;
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
] as const;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function providerShapes(suffix: string) {
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
name: "function*",
|
|
77
|
+
provider: function* ({ context }: { context: { value: string } }) {
|
|
78
|
+
yield* Effect.void;
|
|
79
|
+
return { value: `${context.value}:${suffix}:generator` };
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "named Effect.fn",
|
|
84
|
+
provider: Effect.fn("test.provider.named")(function* ({
|
|
85
|
+
context,
|
|
86
|
+
}: {
|
|
87
|
+
context: { value: string };
|
|
88
|
+
}) {
|
|
89
|
+
yield* Effect.void;
|
|
90
|
+
return { value: `${context.value}:${suffix}:named` };
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "anonymous Effect.fn",
|
|
95
|
+
provider: Effect.fn(function* ({
|
|
96
|
+
context,
|
|
97
|
+
}: {
|
|
98
|
+
context: { value: string };
|
|
99
|
+
}) {
|
|
100
|
+
yield* Effect.void;
|
|
101
|
+
return { value: `${context.value}:${suffix}:anonymous` };
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "Effect.gen-returning function",
|
|
106
|
+
provider: ({ context }: { context: { value: string } }) =>
|
|
107
|
+
Effect.gen(function* () {
|
|
108
|
+
yield* Effect.void;
|
|
109
|
+
return { value: `${context.value}:${suffix}:effect-gen` };
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
] as const;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function optionalProviderShapes(suffix: string) {
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
name: "function*",
|
|
119
|
+
provider: function* ({ context }: { context: { value?: string } }) {
|
|
120
|
+
yield* Effect.void;
|
|
121
|
+
return Option.map(Option.fromNullable(context.value), (value) => ({
|
|
122
|
+
value: `${value}:${suffix}:generator`,
|
|
123
|
+
}));
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "named Effect.fn",
|
|
128
|
+
provider: Effect.fn("test.optional-provider.named")(function* ({
|
|
129
|
+
context,
|
|
130
|
+
}: {
|
|
131
|
+
context: { value?: string };
|
|
132
|
+
}) {
|
|
133
|
+
yield* Effect.void;
|
|
134
|
+
return Option.map(Option.fromNullable(context.value), (value) => ({
|
|
135
|
+
value: `${value}:${suffix}:named`,
|
|
136
|
+
}));
|
|
137
|
+
}),
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "anonymous Effect.fn",
|
|
141
|
+
provider: Effect.fn(function* ({
|
|
142
|
+
context,
|
|
143
|
+
}: {
|
|
144
|
+
context: { value?: string };
|
|
145
|
+
}) {
|
|
146
|
+
yield* Effect.void;
|
|
147
|
+
return Option.map(Option.fromNullable(context.value), (value) => ({
|
|
148
|
+
value: `${value}:${suffix}:anonymous`,
|
|
149
|
+
}));
|
|
150
|
+
}),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "Effect.gen-returning function",
|
|
154
|
+
provider: ({ context }: { context: { value?: string } }) =>
|
|
155
|
+
Effect.gen(function* () {
|
|
156
|
+
yield* Effect.void;
|
|
157
|
+
return Option.map(Option.fromNullable(context.value), (value) => ({
|
|
158
|
+
value: `${value}:${suffix}:effect-gen`,
|
|
159
|
+
}));
|
|
160
|
+
}),
|
|
161
|
+
},
|
|
162
|
+
] as const;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function middlewareShapes(): ReadonlyArray<MiddlewareShape> {
|
|
166
|
+
return [
|
|
167
|
+
{
|
|
168
|
+
name: "native guard-only function",
|
|
169
|
+
middleware: () => {},
|
|
170
|
+
expected: "handler",
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "native next-returning function",
|
|
174
|
+
middleware: ({ next }) => next({ context: { value: "native-next" } }),
|
|
175
|
+
expected: "native-next",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "function*",
|
|
179
|
+
middleware: function* ({ next }: TestMiddlewareOptions) {
|
|
180
|
+
yield* Effect.void;
|
|
181
|
+
return yield* next({ context: { value: "generator" } });
|
|
182
|
+
},
|
|
183
|
+
expected: "generator",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "named Effect.fn",
|
|
187
|
+
middleware: Effect.fn("test.middleware.named")(function* ({
|
|
188
|
+
next,
|
|
189
|
+
}: TestMiddlewareOptions) {
|
|
190
|
+
yield* Effect.void;
|
|
191
|
+
return yield* next({ context: { value: "named" } });
|
|
192
|
+
}),
|
|
193
|
+
expected: "named",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: "anonymous Effect.fn",
|
|
197
|
+
middleware: Effect.fn(function* ({ next }: TestMiddlewareOptions) {
|
|
198
|
+
yield* Effect.void;
|
|
199
|
+
return yield* next({ context: { value: "anonymous" } });
|
|
200
|
+
}),
|
|
201
|
+
expected: "anonymous",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "Effect.gen-returning function",
|
|
205
|
+
middleware: ({ next }) =>
|
|
206
|
+
Effect.gen(function* () {
|
|
207
|
+
yield* Effect.void;
|
|
208
|
+
return yield* next({ context: { value: "effect-gen" } });
|
|
209
|
+
}),
|
|
210
|
+
expected: "effect-gen",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "Effect.gen-returning guard-only function",
|
|
214
|
+
middleware: () =>
|
|
215
|
+
Effect.gen(function* () {
|
|
216
|
+
yield* Effect.void;
|
|
217
|
+
}),
|
|
218
|
+
expected: "handler",
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
describe("Effect callback shapes", () => {
|
|
224
|
+
for (const { name, handler } of effectHandlerShapes()) {
|
|
225
|
+
it(`.effect supports ${name}`, async () => {
|
|
226
|
+
const procedure = eos.input(z.number()).effect(handler);
|
|
227
|
+
|
|
228
|
+
await expect(call(procedure, 41)).resolves.toBe(42);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
it("contract implementer .effect supports Effect-returning handlers", async () => {
|
|
233
|
+
const contract = {
|
|
234
|
+
increment: oc.input(z.number()).output(z.number()),
|
|
235
|
+
};
|
|
236
|
+
const implementer = implementEffect(contract, Layer.empty);
|
|
237
|
+
|
|
238
|
+
const named = implementer.increment.effect(
|
|
239
|
+
Effect.fn("test.contract.effect")(function* ({ input }) {
|
|
240
|
+
const increment = yield* Effect.succeed(1);
|
|
241
|
+
return input + increment;
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
await expect(call(named, 41)).resolves.toBe(42);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
for (const { name, provider } of providerShapes("provide")) {
|
|
249
|
+
it(`.provide supports ${name}`, async () => {
|
|
250
|
+
class RequestValue extends Context.Tag(`RequestValue:${name}`)<
|
|
251
|
+
RequestValue,
|
|
252
|
+
{ readonly value: string }
|
|
253
|
+
>() {}
|
|
254
|
+
|
|
255
|
+
const procedure = eos
|
|
256
|
+
.$context<{ value: string }>()
|
|
257
|
+
.provide(RequestValue, provider)
|
|
258
|
+
.effect(function* () {
|
|
259
|
+
const service = yield* RequestValue;
|
|
260
|
+
return service.value;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await expect(
|
|
264
|
+
call(procedure, undefined, { context: { value: "request" } }),
|
|
265
|
+
).resolves.toContain("request:provide");
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const { name, provider } of optionalProviderShapes("optional")) {
|
|
270
|
+
it(`.provideOptional supports ${name}`, async () => {
|
|
271
|
+
class RequestValue extends Context.Tag(`OptionalRequestValue:${name}`)<
|
|
272
|
+
RequestValue,
|
|
273
|
+
{ readonly value: string }
|
|
274
|
+
>() {}
|
|
275
|
+
|
|
276
|
+
const procedure = eos
|
|
277
|
+
.$context<{ value?: string }>()
|
|
278
|
+
.provideOptional(RequestValue, provider)
|
|
279
|
+
.effect(function* () {
|
|
280
|
+
return yield* Effect.serviceOption(RequestValue);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await expect(
|
|
284
|
+
call(procedure, undefined, { context: { value: "request" } }),
|
|
285
|
+
).resolves.toSatisfy(
|
|
286
|
+
(option: Option.Option<{ readonly value: string }>) =>
|
|
287
|
+
Option.isSome(option) &&
|
|
288
|
+
option.value.value.includes("request:optional"),
|
|
289
|
+
);
|
|
290
|
+
await expect(
|
|
291
|
+
call(procedure, undefined, { context: {} }),
|
|
292
|
+
).resolves.toEqual(Option.none());
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for (const { name, middleware, expected } of middlewareShapes()) {
|
|
297
|
+
it(`.use supports ${name}`, async () => {
|
|
298
|
+
const procedure = eos.use(middleware).effect(function* ({ context }) {
|
|
299
|
+
return "value" in context ? context.value : "handler";
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await expect(call(procedure, undefined)).resolves.toBe(expected);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (const { name, middleware, expected } of middlewareShapes().filter(
|
|
307
|
+
({ name }) => !name.startsWith("native"),
|
|
308
|
+
)) {
|
|
309
|
+
it(`.middleware supports ${name}`, async () => {
|
|
310
|
+
const reusable = eos.middleware(middleware);
|
|
311
|
+
const procedure = eos.use(reusable).effect(function* ({ context }) {
|
|
312
|
+
return "value" in context ? context.value : "handler";
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await expect(call(procedure, undefined)).resolves.toBe(expected);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
it("Effect handlers keep their own spans inside routed procedure spans", async () => {
|
|
320
|
+
const spans: Array<{
|
|
321
|
+
readonly name: string;
|
|
322
|
+
readonly parentName: string | undefined;
|
|
323
|
+
}> = [];
|
|
324
|
+
const spanNamesById = new Map<string, string>();
|
|
325
|
+
const tracer = Tracer.make({
|
|
326
|
+
context: (f) => f(),
|
|
327
|
+
span(name, parent, context, links, startTime, kind) {
|
|
328
|
+
const spanId = `span-${spans.length + 1}`;
|
|
329
|
+
spans.push({
|
|
330
|
+
name,
|
|
331
|
+
parentName: Option.match(parent, {
|
|
332
|
+
onNone: () => undefined,
|
|
333
|
+
onSome: (span) => spanNamesById.get(span.spanId),
|
|
334
|
+
}),
|
|
335
|
+
});
|
|
336
|
+
spanNamesById.set(spanId, name);
|
|
337
|
+
const attributes = new Map<string, unknown>();
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
_tag: "Span" as const,
|
|
341
|
+
name,
|
|
342
|
+
spanId,
|
|
343
|
+
traceId: "trace",
|
|
344
|
+
parent,
|
|
345
|
+
context,
|
|
346
|
+
status: { _tag: "Started" as const, startTime },
|
|
347
|
+
attributes,
|
|
348
|
+
links,
|
|
349
|
+
sampled: true,
|
|
350
|
+
kind,
|
|
351
|
+
end() {},
|
|
352
|
+
attribute(key: string, value: unknown) {
|
|
353
|
+
attributes.set(key, value);
|
|
354
|
+
},
|
|
355
|
+
event() {},
|
|
356
|
+
addLinks() {},
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
const tracedRuntime = ManagedRuntime.make(Layer.setTracer(tracer));
|
|
361
|
+
const effectFnProcedure = makeEffectORPC(tracedRuntime)
|
|
362
|
+
.use(
|
|
363
|
+
Effect.fn("custom.middleware.span")(function* ({
|
|
364
|
+
next,
|
|
365
|
+
}: SpanMiddlewareOptions) {
|
|
366
|
+
return yield* next();
|
|
367
|
+
}),
|
|
368
|
+
)
|
|
369
|
+
.effect(
|
|
370
|
+
Effect.fn("custom.handler.span")(function* () {
|
|
371
|
+
return "ok";
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
const withSpanProcedure = makeEffectORPC(tracedRuntime).effect(() =>
|
|
375
|
+
Effect.succeed("ok").pipe(Effect.withSpan("custom.with-span.handler")),
|
|
376
|
+
);
|
|
377
|
+
const client = createRouterClient({
|
|
378
|
+
users: {
|
|
379
|
+
effectFn: effectFnProcedure,
|
|
380
|
+
withSpan: withSpanProcedure,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
await expect(client.users.effectFn(undefined)).resolves.toBe("ok");
|
|
386
|
+
await expect(client.users.withSpan(undefined)).resolves.toBe("ok");
|
|
387
|
+
expect(spans).toContainEqual({
|
|
388
|
+
name: "users.effectFn",
|
|
389
|
+
parentName: undefined,
|
|
390
|
+
});
|
|
391
|
+
expect(spans).toContainEqual({
|
|
392
|
+
name: "custom.handler.span",
|
|
393
|
+
parentName: "users.effectFn",
|
|
394
|
+
});
|
|
395
|
+
expect(spans).toContainEqual({
|
|
396
|
+
name: "users.withSpan",
|
|
397
|
+
parentName: undefined,
|
|
398
|
+
});
|
|
399
|
+
expect(spans).toContainEqual({
|
|
400
|
+
name: "custom.with-span.handler",
|
|
401
|
+
parentName: "users.withSpan",
|
|
402
|
+
});
|
|
403
|
+
expect(
|
|
404
|
+
spans.filter(({ name }) => name === "custom.middleware.span"),
|
|
405
|
+
).toHaveLength(1);
|
|
406
|
+
} finally {
|
|
407
|
+
await tracedRuntime.dispose();
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { fallbackORPCErrorMessage, ORPCError } from "@orpc/client";
|
|
2
|
-
import { Effect
|
|
2
|
+
import { Effect } from "effect";
|
|
3
3
|
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { eos } from "../effect-builder";
|
|
7
7
|
import type {
|
|
8
8
|
EffectErrorMap,
|
|
9
9
|
EffectErrorMapToUnion,
|
|
@@ -192,11 +192,10 @@ describe("effectErrorMapToErrorMap", () => {
|
|
|
192
192
|
});
|
|
193
193
|
|
|
194
194
|
describe("effectBuilder with EffectErrorMap", () => {
|
|
195
|
-
const
|
|
196
|
-
const effectOs = makeEffectORPC(runtime);
|
|
195
|
+
const effectProcedure = eos;
|
|
197
196
|
|
|
198
197
|
it("should support errors() with traditional format", () => {
|
|
199
|
-
const builder =
|
|
198
|
+
const builder = effectProcedure.errors({
|
|
200
199
|
BAD_REQUEST: { status: 400, message: "Bad request" },
|
|
201
200
|
});
|
|
202
201
|
|
|
@@ -206,7 +205,7 @@ describe("effectBuilder with EffectErrorMap", () => {
|
|
|
206
205
|
});
|
|
207
206
|
|
|
208
207
|
it("should support errors() with tagged error classes", () => {
|
|
209
|
-
const builder =
|
|
208
|
+
const builder = effectProcedure.errors({
|
|
210
209
|
USER_NOT_FOUND_ERROR: UserNotFoundError,
|
|
211
210
|
FORBIDDEN: PermissionDenied,
|
|
212
211
|
});
|
|
@@ -218,7 +217,7 @@ describe("effectBuilder with EffectErrorMap", () => {
|
|
|
218
217
|
});
|
|
219
218
|
|
|
220
219
|
it("should support mixed error format", () => {
|
|
221
|
-
const builder =
|
|
220
|
+
const builder = effectProcedure.errors({
|
|
222
221
|
BAD_REQUEST: { status: 400 },
|
|
223
222
|
USER_NOT_FOUND_ERROR: UserNotFoundError,
|
|
224
223
|
});
|
|
@@ -232,7 +231,7 @@ describe("effectBuilder with EffectErrorMap", () => {
|
|
|
232
231
|
});
|
|
233
232
|
|
|
234
233
|
it("should merge errors correctly", () => {
|
|
235
|
-
const builder =
|
|
234
|
+
const builder = effectProcedure
|
|
236
235
|
.errors({ BAD_REQUEST: { status: 400 } })
|
|
237
236
|
.errors({ USER_NOT_FOUND_ERROR: UserNotFoundError })
|
|
238
237
|
.errors({ FORBIDDEN: PermissionDenied });
|
|
@@ -245,7 +244,7 @@ describe("effectBuilder with EffectErrorMap", () => {
|
|
|
245
244
|
});
|
|
246
245
|
|
|
247
246
|
it("should create procedure with effect handler", async () => {
|
|
248
|
-
const procedure =
|
|
247
|
+
const procedure = effectProcedure
|
|
249
248
|
.errors({
|
|
250
249
|
USER_NOT_FOUND_ERROR: UserNotFoundError,
|
|
251
250
|
BAD_REQUEST: { status: 400 },
|
|
@@ -267,7 +266,7 @@ describe("effectBuilder with EffectErrorMap", () => {
|
|
|
267
266
|
});
|
|
268
267
|
|
|
269
268
|
it("should allow throwing tagged errors in effect handler", async () => {
|
|
270
|
-
const procedure =
|
|
269
|
+
const procedure = effectProcedure
|
|
271
270
|
.errors({
|
|
272
271
|
USER_NOT_FOUND_ERROR: UserNotFoundError,
|
|
273
272
|
})
|
|
@@ -309,11 +308,10 @@ describe("effectBuilder with EffectErrorMap", () => {
|
|
|
309
308
|
});
|
|
310
309
|
|
|
311
310
|
describe("effectDecoratedProcedure.errors()", () => {
|
|
312
|
-
const
|
|
313
|
-
const effectOs = makeEffectORPC(runtime);
|
|
311
|
+
const effectProcedure = eos;
|
|
314
312
|
|
|
315
313
|
it("should support adding errors to procedure", () => {
|
|
316
|
-
const procedure =
|
|
314
|
+
const procedure = effectProcedure
|
|
317
315
|
.input(z.object({ id: z.string() }))
|
|
318
316
|
.effect(function* ({ input }) {
|
|
319
317
|
return { id: input.id };
|
|
@@ -326,7 +324,7 @@ describe("effectDecoratedProcedure.errors()", () => {
|
|
|
326
324
|
});
|
|
327
325
|
|
|
328
326
|
it("should merge errors on procedure", () => {
|
|
329
|
-
const procedure =
|
|
327
|
+
const procedure = effectProcedure
|
|
330
328
|
.errors({ BAD_REQUEST: { status: 400 } })
|
|
331
329
|
.input(z.object({ id: z.string() }))
|
|
332
330
|
.effect(function* ({ input }) {
|
|
@@ -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,24 +115,63 @@ 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", () => {
|
|
119
|
-
it("without map input", () => {
|
|
120
|
-
const mid = vi.fn()
|
|
122
|
+
it("without map input", async () => {
|
|
123
|
+
const mid = vi.fn(({ next }) =>
|
|
124
|
+
next({ context: { fromMiddleware: true } }),
|
|
125
|
+
);
|
|
121
126
|
|
|
122
127
|
const applied = decorated.use(mid);
|
|
123
128
|
expect(applied).not.toBe(decorated);
|
|
124
129
|
expect(applied).toBeInstanceOf(EffectDecoratedProcedure);
|
|
125
130
|
|
|
126
|
-
expect(applied["~effect"]).
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
expect(applied["~effect"].middlewares).toHaveLength(
|
|
132
|
+
def.middlewares.length + 1,
|
|
133
|
+
);
|
|
134
|
+
expect(applied["~effect"].middlewares[0]).toBe(def.middlewares[0]);
|
|
135
|
+
|
|
136
|
+
const wrapped = applied["~effect"].middlewares[1]!;
|
|
137
|
+
let nextCalls = 0;
|
|
138
|
+
let nextOptions: unknown;
|
|
139
|
+
const next = <TContext extends Record<PropertyKey, unknown>>(options?: {
|
|
140
|
+
context?: TContext;
|
|
141
|
+
}) => {
|
|
142
|
+
nextCalls++;
|
|
143
|
+
nextOptions = options;
|
|
144
|
+
return Promise.resolve({
|
|
145
|
+
output: "ok",
|
|
146
|
+
context: options?.context ?? ({} as TContext),
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
await expect(
|
|
150
|
+
wrapped(
|
|
151
|
+
{
|
|
152
|
+
context: {},
|
|
153
|
+
errors: {},
|
|
154
|
+
path: [],
|
|
155
|
+
procedure: applied,
|
|
156
|
+
signal: undefined,
|
|
157
|
+
lastEventId: undefined,
|
|
158
|
+
next,
|
|
159
|
+
},
|
|
160
|
+
"input",
|
|
161
|
+
vi.fn(),
|
|
162
|
+
),
|
|
163
|
+
).resolves.toEqual({
|
|
164
|
+
output: "ok",
|
|
165
|
+
context: { fromMiddleware: true },
|
|
166
|
+
});
|
|
167
|
+
expect(mid).toHaveBeenCalledOnce();
|
|
168
|
+
expect(nextCalls).toBe(1);
|
|
169
|
+
expect(nextOptions).toEqual({
|
|
170
|
+
context: { fromMiddleware: true },
|
|
129
171
|
});
|
|
130
172
|
|
|
131
173
|
// Preserves runtime
|
|
132
|
-
expect(applied["~effect"].runtime).toBe(runtime);
|
|
174
|
+
expect(applied["~effect"].runner.runtime).toBe(runtime);
|
|
133
175
|
});
|
|
134
176
|
|
|
135
177
|
it("with map input", () => {
|
|
@@ -146,7 +188,7 @@ describe("effectDecoratedProcedure", () => {
|
|
|
146
188
|
});
|
|
147
189
|
|
|
148
190
|
// Preserves runtime
|
|
149
|
-
expect(applied["~effect"].runtime).toBe(runtime);
|
|
191
|
+
expect(applied["~effect"].runner.runtime).toBe(runtime);
|
|
150
192
|
});
|
|
151
193
|
});
|
|
152
194
|
|
|
@@ -207,7 +249,7 @@ describe("effectDecoratedProcedure chaining", () => {
|
|
|
207
249
|
.route({ path: "/custom" });
|
|
208
250
|
|
|
209
251
|
expect(applied).toBeInstanceOf(EffectDecoratedProcedure);
|
|
210
|
-
expect(applied["~effect"].runtime).toBe(runtime);
|
|
252
|
+
expect(applied["~effect"].runner.runtime).toBe(runtime);
|
|
211
253
|
expect(applied["~effect"].errorMap).toHaveProperty("CUSTOM");
|
|
212
254
|
});
|
|
213
255
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Layer
|
|
1
|
+
import { Layer } from "effect";
|
|
2
2
|
|
|
3
3
|
import { eoc } from "../index";
|
|
4
4
|
import {
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
export type InitialContext = { db: string };
|
|
13
13
|
export type CurrentContext = InitialContext & { auth: boolean };
|
|
14
14
|
|
|
15
|
-
export const runtime =
|
|
15
|
+
export const runtime = Layer.empty;
|
|
16
16
|
|
|
17
17
|
export const typedContract = {
|
|
18
18
|
ping: eoc
|
|
@@ -474,7 +474,7 @@ export interface EffectBuilderSurface<
|
|
|
474
474
|
>;
|
|
475
475
|
/**
|
|
476
476
|
* Defines the handler of the procedure using an Effect.
|
|
477
|
-
* The Effect is executed using the
|
|
477
|
+
* The Effect is executed using the configured Effect runtime source.
|
|
478
478
|
* The effect is automatically wrapped with `Effect.withSpan`.
|
|
479
479
|
*
|
|
480
480
|
* @see {@link https://orpc.dev/docs/procedure Procedure Docs}
|