/** * @since 2.0.0 */ import type { NonEmptyArray } from "./Array.ts" import * as Arr from "./Array.ts" import * as Cache from "./Cache.ts" import * as Context from "./Context.ts" import type * as Duration from "./Duration.ts" import * as Effect from "./Effect.ts" import * as Exit from "./Exit.ts" import { constTrue, dual, identity } from "./Function.ts" import { exitFail, exitSucceed } from "./internal/core.ts" import * as effect from "./internal/effect.ts" import * as internal from "./internal/request.ts" import * as Iterable from "./Iterable.ts" import * as MutableHashMap from "./MutableHashMap.ts" import { type Pipeable, pipeArguments } from "./Pipeable.ts" import { hasProperty } from "./Predicate.ts" import type * as Request from "./Request.ts" import type * as Schema from "./Schema.ts" import type { Scope } from "./Scope.ts" import * as Tracer from "./Tracer.ts" import type * as Types from "./Types.ts" import type * as Persistable from "./unstable/persistence/Persistable.ts" import * as Persistence from "./unstable/persistence/Persistence.ts" const TypeId = "~effect/RequestResolver" /** * The `RequestResolver` interface requires an environment `R` and handles * the execution of requests of type `A`. * * Implementations must provide a `runAll` method, which processes a collection * of requests and produces an effect that fulfills these requests. Requests are * organized into a `Array>`, where the outer `Array` groups requests * into batches that are executed sequentially, and each inner `Array` contains * requests that can be executed in parallel. This structure allows * implementations to analyze all incoming requests collectively and optimize * query execution accordingly. * * Implementations are typically specialized for a subtype of `Request`. * However, they are not strictly limited to these subtypes as long as they can * map any given request type to `Request`. Implementations should inspect * the collection of requests to identify the needed information and execute the * corresponding queries. It is imperative that implementations resolve all the * requests they receive. Failing to do so will lead to a `QueryFailure` error * during query execution. * * @example * ```ts * import type { Request } from "effect" * import { Effect, Exit, RequestResolver } from "effect" * * interface GetUserRequest extends Request.Request { * readonly _tag: "GetUserRequest" * readonly id: number * } * * // In practice, you would typically use RequestResolver.make() instead * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`User ${entry.request.id}`)) * } * }) * ) * ``` * * @since 2.0.0 * @category models */ export interface RequestResolver extends RequestResolver.Variance, Pipeable { readonly delay: Effect.Effect /** * Get a batch key for the given request. */ batchKey(entry: Request.Entry): unknown /** * An optional pre-check function that can be used to filter requests before * they are added to a batch. If the function returns `false`, the request * will not be processed. */ readonly preCheck: ((entry: Request.Entry) => boolean) | undefined /** * Should the resolver continue collecting requests? Otherwise, it will * immediately execute the collected requests cutting the delay short. */ collectWhile(entries: ReadonlySet>): boolean /** * Execute a collection of requests. */ runAll(entries: NonEmptyArray>, key: unknown): Effect.Effect> } /** * @since 2.0.0 * @category models */ export declare namespace RequestResolver { /** * @since 2.0.0 * @category models */ export interface Variance { readonly [TypeId]: { readonly _A: Types.Contravariant } } } const RequestResolverProto = { [TypeId]: { _A: identity, _R: identity }, pipe() { return pipeArguments(this, arguments) } } /** * Returns `true` if the specified value is a `RequestResolver`, `false` otherwise. * * @since 2.0.0 * @category guards */ export const isRequestResolver = (u: unknown): u is RequestResolver => hasProperty(u, TypeId) /** * Low-level constructor for creating a request resolver with fine-grained * control over its behavior. * * @since 4.0.0 * @category constructors */ export const makeWith = (options: { readonly batchKey: (request: Request.Entry) => unknown readonly preCheck?: ((entry: Request.Entry) => boolean) | undefined readonly delay: Effect.Effect readonly collectWhile: (requests: ReadonlySet>) => boolean readonly runAll: (entries: NonEmptyArray>, key: unknown) => Effect.Effect> }): RequestResolver => { const self = Object.create(RequestResolverProto) self.batchKey = options.batchKey self.preCheck = options.preCheck self.delay = options.delay self.collectWhile = options.collectWhile self.runAll = options.runAll return self } const defaultKeyObject = {} const defaultKey = (_request: unknown): unknown => defaultKeyObject /** * Constructs a request resolver with the specified method to run requests. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * // Define a request type * interface GetUserRequest extends Request.Request { * readonly _tag: "GetUserRequest" * readonly id: number * } * const GetUserRequest = Request.tagged("GetUserRequest") * * // Create a resolver that handles the requests * const UserResolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { * // Complete each request with a result * entry.completeUnsafe(Exit.succeed(`User ${entry.request.id}`)) * } * }) * ) * * // Use the resolver to handle requests * const getUserEffect = Effect.request(GetUserRequest({ id: 123 }), UserResolver) * ``` * * @since 2.0.0 * @category constructors */ export const make = ( runAll: (entries: NonEmptyArray>, key: unknown) => Effect.Effect> ): RequestResolver => makeWith({ batchKey: defaultKey, delay: Effect.yieldNow, collectWhile: constTrue, runAll }) /** * Constructs a request resolver with the requests grouped by a calculated key. * * The key can use the Equal trait to determine if two keys are equal. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetUserByRole extends Request.Request { * readonly _tag: "GetUserByRole" * readonly role: string * readonly id: number * } * const GetUserByRole = Request.tagged("GetUserByRole") * * // Group requests by role for efficient batch processing * const UserByRoleResolver = RequestResolver.makeGrouped({ * key: ({ request }) => request.role, * resolver: (entries, role) => * Effect.sync(() => { * console.log(`Processing ${entries.length} requests for role: ${role}`) * for (const entry of entries) { * entry.completeUnsafe( * Exit.succeed(`User ${entry.request.id} with role ${role}`) * ) * } * }) * }) * ``` * * @since 4.0.0 * @category constructors */ export const makeGrouped = (options: { readonly key: (entry: Request.Entry) => K readonly resolver: (entries: NonEmptyArray>, key: K) => Effect.Effect> }): RequestResolver => makeWith({ batchKey: hashGroupKey(options.key), delay: Effect.yieldNow, collectWhile: constTrue, runAll: options.resolver as any }) const hashGroupKey = (get: (entry: Request.Entry) => K) => { const groupKeys = MutableHashMap.empty() return (entry: Request.Entry): unknown => { const key = get(entry) const okey = MutableHashMap.get(groupKeys, key) if (okey._tag === "Some") { return okey.value } MutableHashMap.set(groupKeys, key, key) return key } } /** * Constructs a request resolver from a pure function. * * @example * ```ts * import { Effect, Request, RequestResolver } from "effect" * * interface GetSquareRequest extends Request.Request { * readonly _tag: "GetSquareRequest" * readonly value: number * } * const GetSquareRequest = Request.tagged("GetSquareRequest") * * // Create a resolver from a pure function * const SquareResolver = RequestResolver.fromFunction( * (entry) => entry.request.value * entry.request.value * ) * * // Usage * const getSquareEffect = Effect.request( * GetSquareRequest({ value: 5 }), * SquareResolver * ) * // Will resolve to 25 * ``` * * @since 2.0.0 * @category constructors */ export const fromFunction = ( f: (entry: Request.Entry) => Request.Success ): RequestResolver => make( (entries) => Effect.sync(() => { for (let i = 0; i < entries.length; i++) { const entry = entries[i] entry.completeUnsafe(exitSucceed(f(entry))) } }) ) /** * Constructs a request resolver from a pure function that takes a list of requests * and returns a list of results of the same size. Each item in the result * list must correspond to the item at the same index in the request list. * * @example * ```ts * import { Effect, Request, RequestResolver } from "effect" * * interface GetDoubleRequest extends Request.Request { * readonly _tag: "GetDoubleRequest" * readonly value: number * } * const GetDoubleRequest = Request.tagged("GetDoubleRequest") * * // Create a resolver that processes multiple requests in a batch * const DoubleResolver = RequestResolver.fromFunctionBatched( * (entries) => entries.map((entry) => entry.request.value * 2) * ) * * // Usage with multiple requests * const effects = [1, 2, 3].map((value) => * Effect.request(GetDoubleRequest({ value }), DoubleResolver) * ) * const batchedEffect = Effect.all(effects) // [2, 4, 6] * ``` * * @since 2.0.0 * @category constructors */ export const fromFunctionBatched = ( f: (entries: NonEmptyArray>) => Iterable> ): RequestResolver => make( (entries) => Effect.sync(() => { let i = 0 for (const result of f(entries)) { const entry = entries[i++] entry.completeUnsafe(exitSucceed(result)) } }) ) /** * Constructs a request resolver from an effectual function. * * @example * ```ts * import { Effect, Request, RequestResolver } from "effect" * * interface GetUserFromAPIRequest extends Request.Request { * readonly _tag: "GetUserFromAPIRequest" * readonly id: number * } * const GetUserFromAPIRequest = Request.tagged( * "GetUserFromAPIRequest" * ) * * // Create a resolver that uses effects (like HTTP calls) * const UserAPIResolver = RequestResolver.fromEffect( * (entry) => * Effect.gen(function*() { * // Simulate an API call * yield* Effect.sleep("100 millis") * // Just return the result without error handling for simplicity * return `User ${entry.request.id} from API` * }) * ) * * // Usage * const getUserEffect = Effect.request( * GetUserFromAPIRequest({ id: 123 }), * UserAPIResolver * ) * ``` * * @since 2.0.0 * @category constructors */ export const fromEffect = ( f: (entry: Request.Entry) => Effect.Effect, Request.Error> ): RequestResolver => { effect.interruptChildrenPatch() // ensure middleware is registered return make((entries) => Effect.callback((resume) => { const parent = effect.getCurrentFiber()! let done = 0 for (let i = 0; i < entries.length; i++) { const entry = entries[i] const fiber = effect.forkUnsafe(parent as any, f(entry), true) fiber.addObserver((exit) => { entry.completeUnsafe(exit) done++ if (done === entries.length) { resume(effect.void) } }) } }) ) } /** * Constructs a request resolver from a list of tags paired to functions, that takes * a list of requests and returns a list of results of the same size. Each item * in the result list must correspond to the item at the same index in the * request list. * * @example * ```ts * import type { Request } from "effect" * import { Effect, RequestResolver } from "effect" * * interface GetUser extends Request.Request { * readonly _tag: "GetUser" * readonly id: number * } * * interface GetPost extends Request.Request { * readonly _tag: "GetPost" * readonly id: number * } * * type MyRequest = GetUser | GetPost * * // Create a resolver that handles different request types * const MyResolver = RequestResolver.fromEffectTagged()({ * GetUser: (requests) => * Effect.succeed(requests.map((req) => `User ${req.request.id}`)), * GetPost: (requests) => * Effect.succeed(requests.map((req) => `Post ${req.request.id}`)) * }) * ``` * * @since 2.0.0 * @category constructors */ export const fromEffectTagged = () => < Fns extends { readonly [Tag in A["_tag"]]: [Extract] extends [infer Req] ? Req extends Request.Request ? (requests: Array>) => Effect.Effect, ReqE> : never : never } >( fns: Fns ): RequestResolver => make( (entries): Effect.Effect => { const grouped = new Map>>() for (let i = 0, len = entries.length; i < len; i++) { const group = grouped.get(entries[i].request._tag) if (group) { group.push(entries[i]) } else { grouped.set(entries[i].request._tag, [entries[i]]) } } return Effect.forEach( grouped, ([tag, requests]) => Effect.matchCause((fns[tag] as any)(requests) as Effect.Effect, unknown, unknown>, { onFailure: (cause) => { for (let i = 0; i < requests.length; i++) { const entry = requests[i] entry.completeUnsafe(exitFail(cause) as any) } }, onSuccess: (res) => { for (let i = 0; i < res.length; i++) { const entry = requests[i] entry.completeUnsafe(exitSucceed(res[i]) as any) } } }), { concurrency: "unbounded", discard: true } ) as Effect.Effect } ) as any /** * Sets the batch delay effect for this request resolver. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed("data")) * } * }) * ) * * // Set a custom delay effect (e.g., with logging) * const resolverWithCustomDelay = RequestResolver.setDelayEffect( * resolver, * Effect.gen(function*() { * yield* Effect.log("Waiting before processing batch...") * yield* Effect.sleep("50 millis") * }) * ) * ``` * * @since 4.0.0 * @category delay */ export const setDelayEffect: { /** * Sets the batch delay effect for this request resolver. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed("data")) * } * }) * ) * * // Set a custom delay effect (e.g., with logging) * const resolverWithCustomDelay = RequestResolver.setDelayEffect( * resolver, * Effect.gen(function*() { * yield* Effect.log("Waiting before processing batch...") * yield* Effect.sleep("50 millis") * }) * ) * ``` * * @since 4.0.0 * @category delay */ (delay: Effect.Effect): (self: RequestResolver) => RequestResolver /** * Sets the batch delay effect for this request resolver. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed("data")) * } * }) * ) * * // Set a custom delay effect (e.g., with logging) * const resolverWithCustomDelay = RequestResolver.setDelayEffect( * resolver, * Effect.gen(function*() { * yield* Effect.log("Waiting before processing batch...") * yield* Effect.sleep("50 millis") * }) * ) * ``` * * @since 4.0.0 * @category delay */ (self: RequestResolver, delay: Effect.Effect): RequestResolver } = dual( 2, (self: RequestResolver, delay: Effect.Effect): RequestResolver => makeWith({ ...self, delay }) ) /** * Sets the batch delay window for this request resolver to the specified duration. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed("data")) * } * }) * ) * * // Add a 100ms delay to batch requests together * const delayedResolver = RequestResolver.setDelay(resolver, "100 millis") * * // Can also use number for milliseconds * const delayedResolver2 = RequestResolver.setDelay(resolver, 100) * ``` * * @since 4.0.0 * @category delay */ export const setDelay: { /** * Sets the batch delay window for this request resolver to the specified duration. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed("data")) * } * }) * ) * * // Add a 100ms delay to batch requests together * const delayedResolver = RequestResolver.setDelay(resolver, "100 millis") * * // Can also use number for milliseconds * const delayedResolver2 = RequestResolver.setDelay(resolver, 100) * ``` * * @since 4.0.0 * @category delay */ (duration: Duration.Input): (self: RequestResolver) => RequestResolver /** * Sets the batch delay window for this request resolver to the specified duration. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed("data")) * } * }) * ) * * // Add a 100ms delay to batch requests together * const delayedResolver = RequestResolver.setDelay(resolver, "100 millis") * * // Can also use number for milliseconds * const delayedResolver2 = RequestResolver.setDelay(resolver, 100) * ``` * * @since 4.0.0 * @category delay */ (self: RequestResolver, duration: Duration.Input): RequestResolver } = dual( 2, (self: RequestResolver, duration: Duration.Input): RequestResolver => makeWith({ ...self, delay: Effect.sleep(duration) }) ) /** * A request resolver aspect that executes requests between two effects, `before` * and `after`, where the result of `before` can be used by `after`. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed("data")) * } * }) * ) * * // Add setup and cleanup around request execution * const resolverWithAround = RequestResolver.around( * resolver, * (entries) => * Effect.gen(function*() { * yield* Effect.log(`Starting batch of ${entries.length} requests`) * return Date.now() * }), * (entries, startTime) => * Effect.gen(function*() { * const duration = Date.now() - startTime * yield* Effect.log(`Batch completed in ${duration}ms`) * }) * ) * ``` * * @since 2.0.0 * @category combinators */ export const around: { /** * A request resolver aspect that executes requests between two effects, `before` * and `after`, where the result of `before` can be used by `after`. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed("data")) * } * }) * ) * * // Add setup and cleanup around request execution * const resolverWithAround = RequestResolver.around( * resolver, * (entries) => * Effect.gen(function*() { * yield* Effect.log(`Starting batch of ${entries.length} requests`) * return Date.now() * }), * (entries, startTime) => * Effect.gen(function*() { * const duration = Date.now() - startTime * yield* Effect.log(`Batch completed in ${duration}ms`) * }) * ) * ``` * * @since 2.0.0 * @category combinators */ ( before: (entries: NonEmptyArray>>) => Effect.Effect>, after: (entries: NonEmptyArray>>, a: A2) => Effect.Effect> ): (self: RequestResolver) => RequestResolver /** * A request resolver aspect that executes requests between two effects, `before` * and `after`, where the result of `before` can be used by `after`. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed("data")) * } * }) * ) * * // Add setup and cleanup around request execution * const resolverWithAround = RequestResolver.around( * resolver, * (entries) => * Effect.gen(function*() { * yield* Effect.log(`Starting batch of ${entries.length} requests`) * return Date.now() * }), * (entries, startTime) => * Effect.gen(function*() { * const duration = Date.now() - startTime * yield* Effect.log(`Batch completed in ${duration}ms`) * }) * ) * ``` * * @since 2.0.0 * @category combinators */ ( self: RequestResolver, before: (entries: NonEmptyArray>>) => Effect.Effect>, after: (entries: NonEmptyArray>>, a: A2) => Effect.Effect> ): RequestResolver } = dual(3, ( self: RequestResolver, before: (entries: NonEmptyArray>>) => Effect.Effect>, after: (entries: NonEmptyArray>>, a: A2) => Effect.Effect> ): RequestResolver => makeWith({ ...self, runAll: (entries, key) => Effect.acquireUseRelease( before(entries), () => self.runAll(entries, key), (a) => after(entries, a) ) })) /** * A request resolver that never executes requests. * * @since 2.0.0 * @category constructors */ export const never: RequestResolver = make(() => Effect.never) /** * Returns a request resolver that executes at most `n` requests in parallel. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * readonly id: number * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * console.log(`Processing batch of ${entries.length} requests`) * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`data-${entry.request.id}`)) * } * }) * ) * * // Limit batches to maximum 5 requests * const limitedResolver = RequestResolver.batchN(resolver, 5) * * // When more than 5 requests are made, they'll be split into multiple batches * const requests = Array.from( * { length: 12 }, * (_, i) => Effect.request(GetDataRequest({ id: i }), limitedResolver) * ) * ``` * * @since 2.0.0 * @category combinators */ export const batchN: { /** * Returns a request resolver that executes at most `n` requests in parallel. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * readonly id: number * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * console.log(`Processing batch of ${entries.length} requests`) * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`data-${entry.request.id}`)) * } * }) * ) * * // Limit batches to maximum 5 requests * const limitedResolver = RequestResolver.batchN(resolver, 5) * * // When more than 5 requests are made, they'll be split into multiple batches * const requests = Array.from( * { length: 12 }, * (_, i) => Effect.request(GetDataRequest({ id: i }), limitedResolver) * ) * ``` * * @since 2.0.0 * @category combinators */ (n: number): (self: RequestResolver) => RequestResolver /** * Returns a request resolver that executes at most `n` requests in parallel. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * readonly id: number * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * console.log(`Processing batch of ${entries.length} requests`) * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`data-${entry.request.id}`)) * } * }) * ) * * // Limit batches to maximum 5 requests * const limitedResolver = RequestResolver.batchN(resolver, 5) * * // When more than 5 requests are made, they'll be split into multiple batches * const requests = Array.from( * { length: 12 }, * (_, i) => Effect.request(GetDataRequest({ id: i }), limitedResolver) * ) * ``` * * @since 2.0.0 * @category combinators */ (self: RequestResolver, n: number): RequestResolver } = dual(2, (self: RequestResolver, n: number): RequestResolver => makeWith({ ...self, collectWhile: (requests) => requests.size < n })) /** * Transform a request resolver by grouping requests using the specified key * function. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetUserRequest extends Request.Request { * readonly _tag: "GetUserRequest" * readonly userId: number * readonly department: string * } * const GetUserRequest = Request.tagged("GetUserRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * console.log(`Processing ${entries.length} users`) * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`User ${entry.request.userId}`)) * } * }) * ) * * // Group requests by department for more efficient processing * const groupedResolver = RequestResolver.grouped( * resolver, * ({ request }) => request.department * ) * * // Requests for the same department will be batched together * const requests = [ * Effect.request( * GetUserRequest({ userId: 1, department: "Engineering" }), * groupedResolver * ), * Effect.request( * GetUserRequest({ userId: 2, department: "Engineering" }), * groupedResolver * ), * Effect.request( * GetUserRequest({ userId: 3, department: "Marketing" }), * groupedResolver * ) * ] * ``` * * @since 4.0.0 * @category combinators */ export const grouped: { /** * Transform a request resolver by grouping requests using the specified key * function. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetUserRequest extends Request.Request { * readonly _tag: "GetUserRequest" * readonly userId: number * readonly department: string * } * const GetUserRequest = Request.tagged("GetUserRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * console.log(`Processing ${entries.length} users`) * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`User ${entry.request.userId}`)) * } * }) * ) * * // Group requests by department for more efficient processing * const groupedResolver = RequestResolver.grouped( * resolver, * ({ request }) => request.department * ) * * // Requests for the same department will be batched together * const requests = [ * Effect.request( * GetUserRequest({ userId: 1, department: "Engineering" }), * groupedResolver * ), * Effect.request( * GetUserRequest({ userId: 2, department: "Engineering" }), * groupedResolver * ), * Effect.request( * GetUserRequest({ userId: 3, department: "Marketing" }), * groupedResolver * ) * ] * ``` * * @since 4.0.0 * @category combinators */ (f: (entry: Request.Entry) => K): (self: RequestResolver) => RequestResolver /** * Transform a request resolver by grouping requests using the specified key * function. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetUserRequest extends Request.Request { * readonly _tag: "GetUserRequest" * readonly userId: number * readonly department: string * } * const GetUserRequest = Request.tagged("GetUserRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * console.log(`Processing ${entries.length} users`) * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`User ${entry.request.userId}`)) * } * }) * ) * * // Group requests by department for more efficient processing * const groupedResolver = RequestResolver.grouped( * resolver, * ({ request }) => request.department * ) * * // Requests for the same department will be batched together * const requests = [ * Effect.request( * GetUserRequest({ userId: 1, department: "Engineering" }), * groupedResolver * ), * Effect.request( * GetUserRequest({ userId: 2, department: "Engineering" }), * groupedResolver * ), * Effect.request( * GetUserRequest({ userId: 3, department: "Marketing" }), * groupedResolver * ) * ] * ``` * * @since 4.0.0 * @category combinators */ (self: RequestResolver, f: (entry: Request.Entry) => K): RequestResolver } = dual( 2, (self: RequestResolver, f: (entry: Request.Entry) => K): RequestResolver => makeWith({ ...self, batchKey: hashGroupKey(f) }) ) /** * Returns a new request resolver that executes requests by sending them to this * request resolver and that request resolver, returning the results from the first data * source to complete and safely interrupting the loser. * * The batch delay is determined by the first request resolver. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * readonly id: number * } * const GetDataRequest = Request.tagged("GetDataRequest") * * // Fast resolver (simulating cache) * const fastResolver = RequestResolver.make((entries) => * Effect.gen(function*() { * yield* Effect.sleep("10 millis") * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`fast-${entry.request.id}`)) * } * }) * ) * * // Slow resolver (simulating database) * const slowResolver = RequestResolver.make((entries) => * Effect.gen(function*() { * yield* Effect.sleep("100 millis") * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`slow-${entry.request.id}`)) * } * }) * ) * * // Race resolvers - will use whichever completes first * const racingResolver = RequestResolver.race(fastResolver, slowResolver) * ``` * * @since 2.0.0 * @category combinators */ export const race: { /** * Returns a new request resolver that executes requests by sending them to this * request resolver and that request resolver, returning the results from the first data * source to complete and safely interrupting the loser. * * The batch delay is determined by the first request resolver. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * readonly id: number * } * const GetDataRequest = Request.tagged("GetDataRequest") * * // Fast resolver (simulating cache) * const fastResolver = RequestResolver.make((entries) => * Effect.gen(function*() { * yield* Effect.sleep("10 millis") * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`fast-${entry.request.id}`)) * } * }) * ) * * // Slow resolver (simulating database) * const slowResolver = RequestResolver.make((entries) => * Effect.gen(function*() { * yield* Effect.sleep("100 millis") * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`slow-${entry.request.id}`)) * } * }) * ) * * // Race resolvers - will use whichever completes first * const racingResolver = RequestResolver.race(fastResolver, slowResolver) * ``` * * @since 2.0.0 * @category combinators */ (that: RequestResolver): (self: RequestResolver) => RequestResolver /** * Returns a new request resolver that executes requests by sending them to this * request resolver and that request resolver, returning the results from the first data * source to complete and safely interrupting the loser. * * The batch delay is determined by the first request resolver. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * readonly id: number * } * const GetDataRequest = Request.tagged("GetDataRequest") * * // Fast resolver (simulating cache) * const fastResolver = RequestResolver.make((entries) => * Effect.gen(function*() { * yield* Effect.sleep("10 millis") * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`fast-${entry.request.id}`)) * } * }) * ) * * // Slow resolver (simulating database) * const slowResolver = RequestResolver.make((entries) => * Effect.gen(function*() { * yield* Effect.sleep("100 millis") * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`slow-${entry.request.id}`)) * } * }) * ) * * // Race resolvers - will use whichever completes first * const racingResolver = RequestResolver.race(fastResolver, slowResolver) * ``` * * @since 2.0.0 * @category combinators */ (self: RequestResolver, that: RequestResolver): RequestResolver } = dual(2, ( self: RequestResolver, that: RequestResolver ): RequestResolver => make( (requests, key) => effect.race(self.runAll(requests, key), that.runAll(requests, key)) )) /** * Add a tracing span to the request resolver, which will also add any span * links from the request's. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * readonly id: number * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`data-${entry.request.id}`)) * } * }) * ) * * // Add tracing span with custom name and attributes * const tracedResolver = RequestResolver.withSpan( * resolver, * "user-data-resolver", * { * attributes: { * "resolver.type": "user-data", * "resolver.version": "1.0" * } * } * ) * * // Spans will automatically include batch size and request links * const effect = Effect.request(GetDataRequest({ id: 123 }), tracedResolver) * ``` * * @since 4.0.0 * @category combinators */ export const withSpan: { /** * Add a tracing span to the request resolver, which will also add any span * links from the request's. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * readonly id: number * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`data-${entry.request.id}`)) * } * }) * ) * * // Add tracing span with custom name and attributes * const tracedResolver = RequestResolver.withSpan( * resolver, * "user-data-resolver", * { * attributes: { * "resolver.type": "user-data", * "resolver.version": "1.0" * } * } * ) * * // Spans will automatically include batch size and request links * const effect = Effect.request(GetDataRequest({ id: 123 }), tracedResolver) * ``` * * @since 4.0.0 * @category combinators */ ( name: string, options?: Tracer.SpanOptions | ((entries: NonEmptyArray>) => Tracer.SpanOptions) | undefined ): (self: RequestResolver) => RequestResolver /** * Add a tracing span to the request resolver, which will also add any span * links from the request's. * * @example * ```ts * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { * readonly _tag: "GetDataRequest" * readonly id: number * } * const GetDataRequest = Request.tagged("GetDataRequest") * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`data-${entry.request.id}`)) * } * }) * ) * * // Add tracing span with custom name and attributes * const tracedResolver = RequestResolver.withSpan( * resolver, * "user-data-resolver", * { * attributes: { * "resolver.type": "user-data", * "resolver.version": "1.0" * } * } * ) * * // Spans will automatically include batch size and request links * const effect = Effect.request(GetDataRequest({ id: 123 }), tracedResolver) * ``` * * @since 4.0.0 * @category combinators */ ( self: RequestResolver, name: string, options?: Tracer.SpanOptions | ((entries: NonEmptyArray>) => Tracer.SpanOptions) | undefined ): RequestResolver } = dual((args) => isRequestResolver(args[0]), ( self: RequestResolver, name: string, options?: Tracer.SpanOptions | ((entries: NonEmptyArray>) => Tracer.SpanOptions) | undefined ): RequestResolver => makeWith({ ...self, runAll: (entries, key) => Effect.suspend(() => { const opts = typeof options === "function" ? options(entries) : options const links = opts?.links ? opts.links.slice() : [] const seen = new Set() for (const entry of entries) { const span = Context.getOption(entry.context, Tracer.ParentSpan) if (span._tag === "None" || seen.has(span.value)) continue seen.add(span.value) links.push({ span: span.value, attributes: {} }) } return Effect.withSpan(self.runAll(entries, key), name, { ...options, links, attributes: { batchSize: entries.length, ...opts?.attributes } }) }) })) /** * Wraps a request resolver in a cache, allowing it to cache results up to a * specified capacity and optional time-to-live. * * @since 4.0.0 * @category Caching */ export const asCache: { /** * Wraps a request resolver in a cache, allowing it to cache results up to a * specified capacity and optional time-to-live. * * @since 4.0.0 * @category Caching */ < A extends Request.Any, ServiceMode extends "lookup" | "construction" = never >( options: { readonly capacity: number readonly timeToLive?: ((exit: Request.Result, request: A) => Duration.Input) | undefined readonly requireServicesAt?: ServiceMode | undefined } ): (self: RequestResolver) => Effect.Effect< Cache.Cache< A, Request.Success, Request.Error, "construction" extends ServiceMode ? never : Request.Services >, never, "construction" extends ServiceMode ? Request.Services : never > /** * Wraps a request resolver in a cache, allowing it to cache results up to a * specified capacity and optional time-to-live. * * @since 4.0.0 * @category Caching */ < A extends Request.Any, ServiceMode extends "lookup" | "construction" = never >( self: RequestResolver, options: { readonly capacity: number readonly timeToLive?: ((exit: Request.Result, request: A) => Duration.Input) | undefined readonly requireServicesAt?: ServiceMode | undefined } ): Effect.Effect< Cache.Cache< A, Request.Success, Request.Error, "construction" extends ServiceMode ? never : Request.Services >, never, "construction" extends ServiceMode ? Request.Services : never > } = dual(2, < A extends Request.Any, ServiceMode extends "lookup" | "construction" = never >(self: RequestResolver, options: { readonly capacity: number readonly timeToLive?: ((exit: Request.Result, request: A) => Duration.Input) | undefined readonly requireServicesAt?: ServiceMode | undefined }): Effect.Effect< Cache.Cache< A, Request.Success, Request.Error, "construction" extends ServiceMode ? never : Request.Services >, never, "construction" extends ServiceMode ? Request.Services : never > => Cache.makeWith((req: A) => internal.request(req, self), { capacity: options.capacity, timeToLive: options.timeToLive as any, requireServicesAt: options.requireServicesAt ?? "lookup" as ServiceMode }) as any) /** * Adds caching capabilities to a request resolver, allowing it to cache * results up to a specified capacity. * * @since 4.0.0 * @category Caching */ export const withCache: { /** * Adds caching capabilities to a request resolver, allowing it to cache * results up to a specified capacity. * * @since 4.0.0 * @category Caching */ ( options: { readonly capacity: number readonly strategy?: "lru" | "fifo" | undefined } ): (self: RequestResolver) => Effect.Effect> /** * Adds caching capabilities to a request resolver, allowing it to cache * results up to a specified capacity. * * @since 4.0.0 * @category Caching */ ( self: RequestResolver, options: { readonly capacity: number readonly strategy?: "lru" | "fifo" | undefined } ): Effect.Effect> } = dual(2, (self: RequestResolver, options: { readonly capacity: number readonly strategy?: "lru" | "fifo" | undefined }): Effect.Effect> => Effect.sync(() => { const strategy = options.strategy ?? "lru" const cache = MutableHashMap.empty; exit: Request.Result | undefined; }>() return makeWith({ ...self, runAll(entries, key) { return Effect.onExit(self.runAll(entries, key), () => { let toRemove = MutableHashMap.size(cache) - options.capacity if (toRemove <= 0) return Effect.void for (const k of MutableHashMap.keys(cache)) { MutableHashMap.remove(cache, k) toRemove-- if (toRemove <= 0) break } return Effect.void }) }, preCheck(entry) { const ocached = MutableHashMap.get(cache, entry.request) if (ocached._tag === "None") { const cached = { entry, exit: undefined as Request.Result | undefined } MutableHashMap.set(cache, entry.request, cached) const prevComplete = entry.completeUnsafe entry.completeUnsafe = function(exit) { cached.exit = exit as any prevComplete(exit) } return true } const cached = ocached.value if (cached.exit) { if (strategy === "lru") { MutableHashMap.remove(cache, cached.entry.request) MutableHashMap.set(cache, cached.entry.request, cached) } entry.completeUnsafe(cached.exit as any) } else { cached.entry.uninterruptible = true const prevComplete = cached.entry.completeUnsafe cached.entry.completeUnsafe = function(exit) { prevComplete(exit) entry.completeUnsafe(exit) } } return false } }) })) /** * @since 4.0.0 * @category Persistence */ export const persisted: { /** * @since 4.0.0 * @category Persistence */ & Persistable.Any>( options: { readonly storeId: string readonly timeToLive?: ((exit: Request.Result, request: A) => Duration.Input) | undefined readonly staleWhileRevalidate?: ((exit: Request.Result, request: A) => boolean) | undefined } ): (self: RequestResolver) => Effect.Effect< RequestResolver, never, Persistence.Persistence | Scope > /** * @since 4.0.0 * @category Persistence */ < A extends Request.Request & Persistable.Any >( self: RequestResolver, options: { readonly storeId: string readonly timeToLive?: ((exit: Request.Result, request: A) => Duration.Input) | undefined readonly staleWhileRevalidate?: ((exit: Request.Result, request: A) => boolean) | undefined } ): Effect.Effect< RequestResolver, never, Persistence.Persistence | Scope > } = dual( 2, Effect.fnUntraced(function*< A extends Request.Request & Persistable.Any >( self: RequestResolver, options: { readonly storeId: string readonly timeToLive?: ((exit: Request.Result, request: A) => Duration.Input) | undefined readonly staleWhileRevalidate?: ((exit: Request.Result, request: A) => boolean) | undefined } ) { const store = yield* (yield* Persistence.Persistence).make(options as any) return makeWith({ ...self, runAll: Effect.fnUntraced(function*(entries, key) { const results = yield* (store.getMany(Iterable.map(entries, (_) => _.request)).pipe( Effect.provideContext(entries[0].context) ) as Effect.Effect< Array | undefined>, Request.Error >) const leftover: Array> = [] const toPersist = new Map>() for (let i = 0; i < results.length; i++) { const entry = entries[i] const exit = results[i] if ( exit === undefined || (options.staleWhileRevalidate && options.staleWhileRevalidate(exit as any, entry.request)) ) { const prevComplete = entry.completeUnsafe entry.completeUnsafe = function(exit) { toPersist.set(entry.request, exit as any) prevComplete(exit) } leftover.push(entry) if (exit === undefined) continue } entry.completeUnsafe(exit as any) } if (!Arr.isArrayNonEmpty(leftover)) { return } yield* Effect.catchCause(self.runAll(leftover, key), (cause) => { for (let i = 0; i < leftover.length; i++) { const entry = leftover[i] if (!toPersist.has(entry.request)) continue entry.completeUnsafe(Exit.failCause(cause) as any) } return Effect.void }) yield* (store.setMany(toPersist).pipe( Effect.provideContext(entries[0].context) ) as Effect.Effect>) }) }); }) )