| 1 | /** |
| 2 | * @title Batching requests with RequestResolver |
| 3 | * |
| 4 | * Define request types with `Request.Class`, resolve them in batches with `RequestResolver`. |
| 5 | */ |
| 6 | import { Context, Effect, Exit, Layer, Request, RequestResolver, Schema, Tracer } from "effect" |
| 7 | |
| 8 | export class User extends Schema.Class<User>("User")({ |
| 9 | id: Schema.Int, |
| 10 | name: Schema.String, |
| 11 | email: Schema.String |
| 12 | }) {} |
| 13 | |
| 14 | export class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", { |
| 15 | id: Schema.Int |
| 16 | }) {} |
| 17 | |
| 18 | export 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. |
| 82 | export 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 | }) |