1/**
2 * @title Batching requests with RequestResolver
3 *
4 * Define request types with `Request.Class`, resolve them in batches with `RequestResolver`.
5 */
6import { Context, Effect, Exit, Layer, Request, RequestResolver, Schema, Tracer } from "effect"
7
8export class User extends Schema.Class<User>("User")({
9 id: Schema.Int,
10 name: Schema.String,
11 email: Schema.String
12}) {}
13
14export class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", {
15 id: Schema.Int
16}) {}
17
18export class Users extends Context.Service<Users, {
19 getUserById(id: number): Effect.Effect<User, UserNotFound>
20}>()("app/Users") {
21 static readonly layer = Layer.effect(
22 Users,
23 Effect.gen(function*() {
24 // Request classes model a single external lookup.
25 class GetUserById extends Request.Class<
26 { readonly id: number },
27 User, // The success type of the request
28 UserNotFound, // The error type of the request
29 never // The requirements type of the request, if any
30 > {}
31
32 // Simulate an external data source that supports batched lookup.
33 const usersTable = new Map<number, User>([
34 [1, new User({ id: 1, name: "Ada Lovelace", email: "ada@acme.dev" })],
35 [2, new User({ id: 2, name: "Alan Turing", email: "alan@acme.dev" })],
36 [3, new User({ id: 3, name: "Grace Hopper", email: "grace@acme.dev" })]
37 ])
38
39 const resolver = yield* RequestResolver.make<GetUserById>(Effect.fn(function*(entries) {
40 for (const entry of entries) {
41 const user = usersTable.get(entry.request.id)
42
43 // If the request had requirements, you can access them with
44 // `entry.context`
45 const requestSpan = Context.getOption(entry.context, Tracer.ParentSpan)
46 console.log("Request span", requestSpan)
47
48 if (user) {
49 // Complete requests with .completeUnsafe and pass in an Exit value
50 entry.completeUnsafe(Exit.succeed(user))
51 } else {
52 entry.completeUnsafe(Exit.fail(new UserNotFound({ id: entry.request.id })))
53 }
54 }
55 })).pipe(
56 // Control the delay before the resolver is executed. This allows more
57 // requests to be batched together, but also adds latency to the first
58 // request.
59 RequestResolver.setDelay("10 millis"),
60 // RequestResolver.withSpan adds a span around the resolver execution,
61 // and also sets up span links for each request
62 RequestResolver.withSpan("Users.getUserById.resolver"),
63 // RequestResolver.withCache adds a simple LRU cache to avoid repeated
64 // lookups for the same ID.
65 RequestResolver.withCache({ capacity: 1024 })
66 )
67
68 // Wrap the resolver in a service method. The resolver batches calls to
69 // `getUserById` that occur within the delay window.
70 const getUserById = (id: number) =>
71 Effect.request(new GetUserById({ id }), resolver).pipe(
72 Effect.withSpan("Users.getUserById", { attributes: { userId: id } })
73 )
74
75 return { getUserById } as const
76 })
77 )
78}
79
80// Run multiple lookups concurrently. The resolver receives one batch and
81// internally deduplicates repeated IDs for the external call.
82export const batchedLookupExample = Effect.gen(function*() {
83 const { getUserById } = yield* Users
84
85 // This will only trigger a single call to the resolver with the unique IDs [1, 2, 3].
86 yield* Effect.forEach([1, 2, 1, 3, 2], getUserById, {
87 concurrency: "unbounded"
88 })
89})