1/**
2 * Resolves data requests made with `Effect.request`.
3 *
4 * A `Request` describes what a fiber needs, while a `RequestResolver` describes
5 * how to collect request entries, group them into batches, run backend work,
6 * and complete each waiting entry. This module includes constructors for common
7 * resolver shapes and tools for controlling batching, grouping, delays,
8 * tracing, caching, racing, hooks around resolver execution, and persistence.
9 *
10 * @since 2.0.0
11 */
12import type { NonEmptyArray } from "./Array.ts"
13import * as Arr from "./Array.ts"
14import * as Cache from "./Cache.ts"
15import * as Context from "./Context.ts"
16import type * as Duration from "./Duration.ts"
17import * as Effect from "./Effect.ts"
18import * as Exit from "./Exit.ts"
19import { constTrue, dual, identity } from "./Function.ts"
20import { exitSucceed } from "./internal/core.ts"
21import * as Count from "./internal/count.ts"
22import * as effect from "./internal/effect.ts"
23import * as internal from "./internal/request.ts"
24import * as Iterable from "./Iterable.ts"
25import * as MutableHashMap from "./MutableHashMap.ts"
26import type * as Persistable from "./persistence/Persistable.ts"
27import * as Persistence from "./persistence/Persistence.ts"
28import { type Pipeable, pipeArguments } from "./Pipeable.ts"
29import { hasProperty } from "./Predicate.ts"
30import type * as Request from "./Request.ts"
31import type * as Schema from "./Schema.ts"
32import type { Scope } from "./Scope.ts"
33import * as Tracer from "./Tracer.ts"
34import type * as Types from "./Types.ts"
35
36const TypeId = "~effect/RequestResolver"
37
38/**
39 * A resolver that executes and completes batched `Request` entries.
40 *
41 * **Details**
42 *
43 * A resolver controls how requests are grouped, delayed, optionally
44 * pre-checked, and finally run. Its `runAll` method receives a non-empty batch
45 * of `Request.Entry` values for a single batch key and must complete every
46 * received entry, usually by calling `completeUnsafe` or one of the `Request`
47 * completion helpers.
48 *
49 * **Gotchas**
50 *
51 * If a resolver finishes without completing an entry, the waiting request fails
52 * because the resolver did not supply a result.
53 *
54 * **Example** (Defining a request resolver)
55 *
56 * ```ts import.meta.vitest
57 * import { Effect, Exit, Request, RequestResolver } from "effect"
58 *
59 * interface GetUserRequest extends Request.Request<string, Error> {
60 * readonly _tag: "GetUserRequest"
61 * readonly id: number
62 * }
63 * const GetUserRequest = Request.tagged<GetUserRequest>("GetUserRequest")
64 *
65 * // In practice, you would typically use RequestResolver.make() instead
66 * const resolver = RequestResolver.make<GetUserRequest>((entries) =>
67 * Effect.sync(() => {
68 * for (const entry of entries) {
69 * entry.completeUnsafe(Exit.succeed(`User ${entry.request.id}`))
70 * }
71 * })
72 * )
73 *
74 * const program = Effect.request(GetUserRequest({ id: 1 }), resolver)
75 * await Effect.runPromise(program) // => "User 1"
76 * ```
77 *
78 * @category models
79 * @since 2.0.0
80 */
81export interface RequestResolver<in A extends Request.Any> extends RequestResolver.Variance<A>, Pipeable {
82 readonly delay: Effect.Effect<void>
83
84 /**
85 * Get a batch key for the given request.
86 */
87 batchKey(entry: Request.Entry<A>): unknown
88
89 /**
90 * An optional pre-check function that can be used to filter requests before
91 * they are added to a batch. If the function returns `false`, the request
92 * will not be processed.
93 */
94 readonly preCheck: ((entry: Request.Entry<A>) => boolean) | undefined
95
96 /**
97 * Should the resolver continue collecting requests? Otherwise, it will
98 * immediately execute the collected requests cutting the delay short.
99 */
100 collectWhile(entries: ReadonlySet<Request.Entry<A>>): boolean
101
102 /**
103 * Execute a collection of requests.
104 */
105 runAll(entries: NonEmptyArray<Request.Entry<A>>, key: unknown): Effect.Effect<void, Request.Error<A>>
106}
107
108/**
109 * Namespace containing type-level helpers associated with `RequestResolver`.
110 *
111 * @since 2.0.0
112 */
113export declare namespace RequestResolver {
114 /**
115 * Variance marker carried by every `RequestResolver`.
116 *
117 * **Details**
118 *
119 * This marker preserves the request type accepted by the resolver for
120 * Effect's type-level machinery. Users normally do not implement it directly.
121 *
122 * @category models
123 * @since 2.0.0
124 */
125 export interface Variance<in A> {
126 readonly [TypeId]: {
127 readonly _A: Types.Contravariant<A>
128 }
129 }
130}
131
132const RequestResolverProto = {
133 [TypeId]: {
134 _A: identity,
135 _R: identity
136 },
137 pipe() {
138 return pipeArguments(this, arguments)
139 }
140}
141
142/**
143 * Returns `true` if the specified value is a `RequestResolver`, `false` otherwise.
144 *
145 * **When to use**
146 *
147 * Use to narrow unknown values before passing them to APIs that require a
148 * `RequestResolver`.
149 *
150 * @see {@link RequestResolver} for the type narrowed by this guard
151 *
152 * @category guards
153 * @since 2.0.0
154 */
155export const isRequestResolver = (u: unknown): u is RequestResolver<any> => hasProperty(u, TypeId)
156
157/**
158 * Creates a request resolver with fine-grained
159 * control over its behavior.
160 *
161 * **When to use**
162 *
163 * Use when you need to supply the resolver batching primitives directly,
164 * including the batch key, optional pre-check, delay effect, collection cutoff,
165 * and batch runner.
166 *
167 * **Details**
168 *
169 * `batchKey` groups request entries, `delay` schedules batch execution,
170 * `collectWhile` can end collection early, and `runAll` receives a non-empty
171 * batch for one key.
172 *
173 * **Gotchas**
174 *
175 * Accepted entries must be completed. If `runAll` succeeds with incomplete
176 * entries, waiting requests fail. If `preCheck` returns `false`, the entry is
177 * not batched, so it must be completed or linked to another completion path.
178 *
179 * @see {@link make} for constructing a resolver from a batch runner
180 * @see {@link makeGrouped} for constructing a resolver that groups requests by key
181 *
182 * @category constructors
183 * @since 4.0.0
184 */
185export const makeWith = <A extends Request.Any>(options: {
186 readonly batchKey: (request: Request.Entry<A>) => unknown
187 readonly preCheck?: ((entry: Request.Entry<A>) => boolean) | undefined
188 readonly delay: Effect.Effect<void>
189 readonly collectWhile: (requests: ReadonlySet<Request.Entry<A>>) => boolean
190 readonly runAll: (entries: NonEmptyArray<Request.Entry<A>>, key: unknown) => Effect.Effect<void, Request.Error<A>>
191}): RequestResolver<A> => {
192 const self = Object.create(RequestResolverProto)
193 self.batchKey = options.batchKey
194 self.preCheck = options.preCheck
195 self.delay = options.delay
196 self.collectWhile = options.collectWhile
197 self.runAll = options.runAll
198 return self
199}
200
201const defaultKeyObject = {}
202const defaultKey = (_request: unknown): unknown => defaultKeyObject
203
204/**
205 * Constructs a request resolver with the specified method to run requests.
206 *
207 * **Example** (Creating a request resolver)
208 *
209 * ```ts import.meta.vitest
210 * import { Effect, Exit, Request, RequestResolver } from "effect"
211 *
212 * // Define a request type
213 * interface GetUserRequest extends Request.Request<string, Error> {
214 * readonly _tag: "GetUserRequest"
215 * readonly id: number
216 * }
217 * const GetUserRequest = Request.tagged<GetUserRequest>("GetUserRequest")
218 *
219 * // Create a resolver that handles the requests
220 * const UserResolver = RequestResolver.make<GetUserRequest>((entries) =>
221 * Effect.sync(() => {
222 * for (const entry of entries) {
223 * // Complete each request with a result
224 * entry.completeUnsafe(Exit.succeed(`User ${entry.request.id}`))
225 * }
226 * })
227 * )
228 *
229 * // Use the resolver to handle requests
230 * const getUserEffect = Effect.request(GetUserRequest({ id: 123 }), UserResolver)
231 * await Effect.runPromise(getUserEffect) // => "User 123"
232 * ```
233 *
234 * @category constructors
235 * @since 2.0.0
236 */
237export const make = <A extends Request.Any>(
238 runAll: (entries: NonEmptyArray<Request.Entry<A>>, key: unknown) => Effect.Effect<void, Request.Error<A>>
239): RequestResolver<A> =>
240 makeWith({
241 batchKey: defaultKey,
242 delay: Effect.yieldNow,
243 collectWhile: constTrue,
244 runAll
245 })
246
247/**
248 * Constructs a request resolver with the requests grouped by a calculated key.
249 *
250 * **Details**
251 *
252 * The key can use the Equal trait to determine if two keys are equal.
253 *
254 * **Example** (Grouping requests by key)
255 *
256 * ```ts import.meta.vitest
257 * import { Effect, Exit, Request, RequestResolver } from "effect"
258 *
259 * interface GetUserByRole extends Request.Request<string, Error> {
260 * readonly _tag: "GetUserByRole"
261 * readonly role: string
262 * readonly id: number
263 * }
264 * const GetUserByRole = Request.tagged<GetUserByRole>("GetUserByRole")
265 *
266 * const batches: Array<[role: string, size: number]> = []
267 *
268 * // Group requests by role for efficient batch processing
269 * const UserByRoleResolver = RequestResolver.makeGrouped<GetUserByRole, string>({
270 * key: ({ request }) => request.role,
271 * resolver: (entries, role) =>
272 * Effect.sync(() => {
273 * batches.push([role, entries.length])
274 * for (const entry of entries) {
275 * entry.completeUnsafe(
276 * Exit.succeed(`User ${entry.request.id} with role ${role}`)
277 * )
278 * }
279 * })
280 * })
281 *
282 * const program = Effect.all([
283 * Effect.request<GetUserByRole>(GetUserByRole({ role: "admin", id: 1 }), UserByRoleResolver),
284 * Effect.request<GetUserByRole>(GetUserByRole({ role: "admin", id: 2 }), UserByRoleResolver)
285 * ] as const, { concurrency: "unbounded" })
286 * const result = await Effect.runPromise(program)
287 *
288 * batches // => [["admin", 2]]
289 * result // => ["User 1 with role admin", "User 2 with role admin"]
290 * ```
291 *
292 * @category constructors
293 * @since 4.0.0
294 */
295export const makeGrouped = <A extends Request.Any, K>(options: {
296 readonly key: (entry: Request.Entry<A>) => K
297 readonly resolver: (entries: NonEmptyArray<Request.Entry<A>>, key: K) => Effect.Effect<void, Request.Error<A>>
298}): RequestResolver<A> =>
299 makeWith({
300 batchKey: hashGroupKey(options.key),
301 delay: Effect.yieldNow,
302 collectWhile: constTrue,
303 runAll: options.resolver as any
304 })
305
306const hashGroupKey = <A, K>(get: (entry: Request.Entry<A>) => K) => {
307 const groupKeys = MutableHashMap.empty<K, K>()
308 return (entry: Request.Entry<A>): unknown => {
309 const key = get(entry)
310 const okey = MutableHashMap.get(groupKeys, key)
311 if (okey._tag === "Some") {
312 return okey.value
313 }
314 MutableHashMap.set(groupKeys, key, key)
315 return key
316 }
317}
318
319/**
320 * Constructs a request resolver from a pure function.
321 *
322 * **Example** (Creating a resolver from a pure function)
323 *
324 * ```ts import.meta.vitest
325 * import { Effect, Request, RequestResolver } from "effect"
326 *
327 * interface GetSquareRequest extends Request.Request<number> {
328 * readonly _tag: "GetSquareRequest"
329 * readonly value: number
330 * }
331 * const GetSquareRequest = Request.tagged<GetSquareRequest>("GetSquareRequest")
332 *
333 * // Create a resolver from a pure function
334 * const SquareResolver = RequestResolver.fromFunction<GetSquareRequest>(
335 * (entry) => entry.request.value * entry.request.value
336 * )
337 *
338 * // Usage
339 * const getSquareEffect = Effect.request(
340 * GetSquareRequest({ value: 5 }),
341 * SquareResolver
342 * )
343 * await Effect.runPromise(getSquareEffect) // => 25
344 * ```
345 *
346 * @category constructors
347 * @since 2.0.0
348 */
349export const fromFunction = <A extends Request.Any>(
350 f: (entry: Request.Entry<A>) => Request.Success<A>
351): RequestResolver<A> =>
352 make(
353 (entries) =>
354 Effect.sync(() => {
355 for (let i = 0; i < entries.length; i++) {
356 const entry = entries[i]
357 entry.completeUnsafe(exitSucceed(f(entry)))
358 }
359 })
360 )
361
362/**
363 * Constructs a request resolver from a pure function that takes a list of requests
364 * and returns a list of results of the same size. Each item in the result
365 * list must correspond to the item at the same index in the request list.
366 *
367 * **Example** (Batching pure request handling)
368 *
369 * ```ts import.meta.vitest
370 * import { Effect, Request, RequestResolver } from "effect"
371 *
372 * interface GetDoubleRequest extends Request.Request<number> {
373 * readonly _tag: "GetDoubleRequest"
374 * readonly value: number
375 * }
376 * const GetDoubleRequest = Request.tagged<GetDoubleRequest>("GetDoubleRequest")
377 *
378 * // Create a resolver that processes multiple requests in a batch
379 * const DoubleResolver = RequestResolver.fromFunctionBatched<GetDoubleRequest>(
380 * (entries) => entries.map((entry) => entry.request.value * 2)
381 * )
382 *
383 * // Usage with multiple requests
384 * const effects = [1, 2, 3].map((value) =>
385 * Effect.request(GetDoubleRequest({ value }), DoubleResolver)
386 * )
387 * const batchedEffect = Effect.all(effects)
388 * await Effect.runPromise(batchedEffect) // => [2, 4, 6]
389 * ```
390 *
391 * @category constructors
392 * @since 2.0.0
393 */
394export const fromFunctionBatched = <A extends Request.Any>(
395 f: (entries: NonEmptyArray<Request.Entry<A>>) => Iterable<Request.Success<A>>
396): RequestResolver<A> =>
397 make(
398 (entries) =>
399 Effect.sync(() => {
400 let i = 0
401 for (const result of f(entries)) {
402 const entry = entries[i++]
403 entry.completeUnsafe(exitSucceed(result))
404 }
405 })
406 )
407
408/**
409 * Constructs a request resolver from an effectual function.
410 *
411 * **Example** (Creating a resolver from an effectful function)
412 *
413 * ```ts import.meta.vitest
414 * import { Effect, Request, RequestResolver } from "effect"
415 *
416 * interface GetUserFromAPIRequest extends Request.Request<string> {
417 * readonly _tag: "GetUserFromAPIRequest"
418 * readonly id: number
419 * }
420 * const GetUserFromAPIRequest = Request.tagged<GetUserFromAPIRequest>(
421 * "GetUserFromAPIRequest"
422 * )
423 *
424 * // Create a resolver that uses effects (like HTTP calls)
425 * const UserAPIResolver = RequestResolver.fromEffect<GetUserFromAPIRequest>(
426 * (entry) => Effect.succeed(`User ${entry.request.id} from API`)
427 * )
428 *
429 * // Usage
430 * const getUserEffect = Effect.request(
431 * GetUserFromAPIRequest({ id: 123 }),
432 * UserAPIResolver
433 * )
434 * await Effect.runPromise(getUserEffect) // => "User 123 from API"
435 * ```
436 *
437 * @category constructors
438 * @since 2.0.0
439 */
440export const fromEffect = <A extends Request.Any>(
441 f: (entry: Request.Entry<A>) => Effect.Effect<Request.Success<A>, Request.Error<A>>
442): RequestResolver<A> => {
443 effect.interruptChildrenPatch() // ensure middleware is registered
444 return make((entries) =>
445 Effect.callback<void>((resume) => {
446 const parent = effect.getCurrentFiber()!
447 let done = 0
448 for (let i = 0; i < entries.length; i++) {
449 const entry = entries[i]
450 const fiber = effect.forkUnsafe(parent as any, f(entry), true)
451 fiber.addObserver((exit) => {
452 entry.completeUnsafe(exit)
453 done++
454 if (done === entries.length) {
455 resume(effect.void)
456 }
457 })
458 }
459 })
460 )
461}
462
463/**
464 * Constructs a request resolver from a list of tags paired to functions, that takes
465 * a list of requests and returns a list of results of the same size. Each item
466 * in the result list must correspond to the item at the same index in the
467 * request list.
468 *
469 * **Example** (Handling tagged request batches)
470 *
471 * ```ts import.meta.vitest
472 * import { Effect, Request, RequestResolver } from "effect"
473 *
474 * interface GetUser extends Request.Request<string, Error> {
475 * readonly _tag: "GetUser"
476 * readonly id: number
477 * }
478 *
479 * interface GetPost extends Request.Request<string, Error> {
480 * readonly _tag: "GetPost"
481 * readonly id: number
482 * }
483 *
484 * type MyRequest = GetUser | GetPost
485 * const GetUser = Request.tagged<GetUser>("GetUser")
486 * const GetPost = Request.tagged<GetPost>("GetPost")
487 *
488 * // Create a resolver that handles different request types
489 * const MyResolver = RequestResolver.fromEffectTagged<MyRequest>()({
490 * GetUser: (requests) =>
491 * Effect.succeed(requests.map((req) => `User ${req.request.id}`)),
492 * GetPost: (requests) =>
493 * Effect.succeed(requests.map((req) => `Post ${req.request.id}`))
494 * })
495 *
496 * const program = Effect.all([
497 * Effect.request<GetUser>(GetUser({ id: 1 }), MyResolver),
498 * Effect.request<GetPost>(GetPost({ id: 2 }), MyResolver)
499 * ] as const)
500 * await Effect.runPromise(program) // => ["User 1", "Post 2"]
501 * ```
502 *
503 * @category constructors
504 * @since 2.0.0
505 */
506export const fromEffectTagged = <A extends Request.Any & { readonly _tag: string }>() =>
507<
508 Fns extends {
509 readonly [Tag in A["_tag"]]: [Extract<A, { readonly _tag: Tag }>] extends [infer Req]
510 ? Req extends Request.Request<infer ReqA, infer ReqE, infer _ReqR> ?
511 (requests: Array<Request.Entry<Req>>) => Effect.Effect<Iterable<ReqA>, ReqE>
512 : never
513 : never
514 }
515>(
516 fns: Fns
517): RequestResolver<A> =>
518 make<A>(
519 (entries): Effect.Effect<void> => {
520 const grouped = new Map<A["_tag"], Array<Request.Entry<A>>>()
521 for (let i = 0, len = entries.length; i < len; i++) {
522 const group = grouped.get(entries[i].request._tag)
523 if (group) {
524 group.push(entries[i])
525 } else {
526 grouped.set(entries[i].request._tag, [entries[i]])
527 }
528 }
529 return Effect.forEach(
530 grouped,
531 ([tag, requests]) =>
532 Effect.matchCause((fns[tag] as any)(requests) as Effect.Effect<Iterable<any>, unknown, unknown>, {
533 onFailure: (cause) => {
534 for (let i = 0; i < requests.length; i++) {
535 const entry = requests[i]
536 entry.completeUnsafe(Exit.failCause(cause) as any)
537 }
538 },
539 onSuccess: (res) => {
540 let i = 0
541 for (const result of res) {
542 const entry = requests[i++]
543 entry.completeUnsafe(exitSucceed(result) as any)
544 }
545 }
546 }),
547 { concurrency: "unbounded", discard: true }
548 ) as Effect.Effect<void>
549 }
550 ) as any
551
552/**
553 * Sets the batch delay effect for this request resolver.
554 *
555 * **Example** (Setting an effectful batch delay)
556 *
557 * ```ts import.meta.vitest
558 * import { Effect, Exit, Request, RequestResolver } from "effect"
559 *
560 * interface GetDataRequest extends Request.Request<string> {
561 * readonly _tag: "GetDataRequest"
562 * }
563 * const GetDataRequest = Request.tagged<GetDataRequest>("GetDataRequest")
564 *
565 * const resolver = RequestResolver.make<GetDataRequest>((entries) =>
566 * Effect.sync(() => {
567 * for (const entry of entries) {
568 * entry.completeUnsafe(Exit.succeed("data"))
569 * }
570 * })
571 * )
572 *
573 * let delayRan = false
574 *
575 * // Set a custom delay effect
576 * const resolverWithCustomDelay = RequestResolver.setDelayEffect(
577 * resolver,
578 * Effect.sync(() => {
579 * delayRan = true
580 * })
581 * )
582 *
583 * await Effect.runPromise(resolverWithCustomDelay.delay)
584 * Array.of(delayRan, RequestResolver.isRequestResolver(resolverWithCustomDelay)) // => [true, true]
585 * ```
586 *
587 * @category delays & timeouts
588 * @since 4.0.0
589 */
590export const setDelayEffect: {
591 (delay: Effect.Effect<void>): <A extends Request.Any>(self: RequestResolver<A>) => RequestResolver<A>
592 <A extends Request.Any>(self: RequestResolver<A>, delay: Effect.Effect<void>): RequestResolver<A>
593} = dual(
594 2,
595 <A extends Request.Any>(self: RequestResolver<A>, delay: Effect.Effect<void>): RequestResolver<A> =>
596 makeWith({
597 ...self,
598 delay
599 })
600)
601
602/**
603 * Sets the batch delay window for this request resolver to the specified duration.
604 *
605 * **Example** (Setting a batch delay)
606 *
607 * ```ts import.meta.vitest
608 * import { Effect, Exit, Request, RequestResolver } from "effect"
609 *
610 * interface GetDataRequest extends Request.Request<string> {
611 * readonly _tag: "GetDataRequest"
612 * }
613 * const GetDataRequest = Request.tagged<GetDataRequest>("GetDataRequest")
614 *
615 * const resolver = RequestResolver.make<GetDataRequest>((entries) =>
616 * Effect.sync(() => {
617 * for (const entry of entries) {
618 * entry.completeUnsafe(Exit.succeed("data"))
619 * }
620 * })
621 * )
622 *
623 * // Add a 100ms delay to batch requests together
624 * const delayedResolver = RequestResolver.setDelay(resolver, "100 millis")
625 *
626 * const program = Effect.request(GetDataRequest(), delayedResolver)
627 * await Effect.runPromise(program) // => "data"
628 * ```
629 *
630 * @category delays & timeouts
631 * @since 4.0.0
632 */
633export const setDelay: {
634 (duration: Duration.Input): <A extends Request.Any>(self: RequestResolver<A>) => RequestResolver<A>
635 <A extends Request.Any>(self: RequestResolver<A>, duration: Duration.Input): RequestResolver<A>
636} = dual(
637 2,
638 <A extends Request.Any>(self: RequestResolver<A>, duration: Duration.Input): RequestResolver<A> =>
639 makeWith({
640 ...self,
641 delay: Effect.sleep(duration)
642 })
643)
644
645/**
646 * Wraps request resolver execution between `before` and `after` effects.
647 *
648 * **Example** (Running effects around request resolution)
649 *
650 * ```ts import.meta.vitest
651 * import { Effect, Exit, Request, RequestResolver } from "effect"
652 *
653 * interface GetDataRequest extends Request.Request<string> {
654 * readonly _tag: "GetDataRequest"
655 * }
656 * const GetDataRequest = Request.tagged<GetDataRequest>("GetDataRequest")
657 *
658 * const events: Array<string> = []
659 *
660 * const resolver = RequestResolver.make<GetDataRequest>((entries) =>
661 * Effect.sync(() => {
662 * for (const entry of entries) {
663 * entry.completeUnsafe(Exit.succeed("data"))
664 * }
665 * })
666 * )
667 *
668 * // Add setup and cleanup around request execution
669 * const resolverWithAround = RequestResolver.around(
670 * resolver,
671 * (entries) =>
672 * Effect.gen(function*() {
673 * events.push(`Starting batch of ${entries.length} requests`)
674 * return entries.length
675 * }),
676 * (entries, initialSize) =>
677 * Effect.sync(() => {
678 * events.push(`Batch completed with ${entries.length} requests (started with ${initialSize})`)
679 * })
680 * )
681 *
682 * const program = Effect.request(GetDataRequest(), resolverWithAround)
683 * const result = await Effect.runPromise(program)
684 *
685 * events // => ["Starting batch of 1 requests", "Batch completed with 1 requests (started with 1)"]
686 * result // => "data"
687 * ```
688 *
689 * @category combinators
690 * @since 2.0.0
691 */
692export const around: {
693 <A extends Request.Any, A2, X>(
694 before: (entries: NonEmptyArray<Request.Entry<NoInfer<A>>>) => Effect.Effect<A2, Request.Error<A>>,
695 after: (entries: NonEmptyArray<Request.Entry<NoInfer<A>>>, a: A2) => Effect.Effect<X, Request.Error<A>>
696 ): (self: RequestResolver<A>) => RequestResolver<A>
697 <A extends Request.Any, A2, X>(
698 self: RequestResolver<A>,
699 before: (entries: NonEmptyArray<Request.Entry<NoInfer<A>>>) => Effect.Effect<A2, Request.Error<A>>,
700 after: (entries: NonEmptyArray<Request.Entry<NoInfer<A>>>, a: A2) => Effect.Effect<X, Request.Error<A>>
701 ): RequestResolver<A>
702} = dual(3, <A extends Request.Any, A2, X>(
703 self: RequestResolver<A>,
704 before: (entries: NonEmptyArray<Request.Entry<NoInfer<A>>>) => Effect.Effect<A2, Request.Error<A>>,
705 after: (entries: NonEmptyArray<Request.Entry<NoInfer<A>>>, a: A2) => Effect.Effect<X, Request.Error<A>>
706): RequestResolver<A> =>
707 makeWith({
708 ...self,
709 runAll: (entries, key) =>
710 Effect.acquireUseRelease(
711 before(entries),
712 () => self.runAll(entries, key),
713 (a) => after(entries, a)
714 )
715 }))
716
717/**
718 * Creates a request resolver that never executes requests.
719 *
720 * **When to use**
721 *
722 * Use as a resolver value for request types that are statically impossible and
723 * should never be issued.
724 *
725 * **Gotchas**
726 *
727 * If this resolver is used for an actual request, the request waits forever
728 * unless the fiber is interrupted.
729 *
730 * @see {@link make} for constructing a resolver that executes batches and completes request entries
731 *
732 * @category constructors
733 * @since 2.0.0
734 */
735export const never: RequestResolver<never> = make(() => Effect.never)
736
737/**
738 * Returns a request resolver that collects at most `n` requests into each
739 * batch.
740 *
741 * **Details**
742 *
743 * When more than `n` requests are waiting for the same resolver and batch key,
744 * the current batch is run and additional requests are collected into later
745 * batches. Finite fractional values of `n` are rounded down. `NaN` and
746 * non-positive values are treated as `1` so that every batch remains non-empty.
747 *
748 * **Example** (Limiting parallel request batches)
749 *
750 * ```ts import.meta.vitest
751 * import { Effect, Exit, Request, RequestResolver } from "effect"
752 *
753 * interface GetDataRequest extends Request.Request<string> {
754 * readonly _tag: "GetDataRequest"
755 * readonly id: number
756 * }
757 * const GetDataRequest = Request.tagged<GetDataRequest>("GetDataRequest")
758 *
759 * const batchSizes: Array<number> = []
760 *
761 * const resolver = RequestResolver.make<GetDataRequest>((entries) =>
762 * Effect.sync(() => {
763 * batchSizes.push(entries.length)
764 * for (const entry of entries) {
765 * entry.completeUnsafe(Exit.succeed(`data-${entry.request.id}`))
766 * }
767 * })
768 * )
769 *
770 * // Limit batches to maximum 5 requests
771 * const limitedResolver = RequestResolver.batchN(resolver, 5)
772 *
773 * // When more than 5 requests are made, they'll be split into multiple batches
774 * const requests = Array.from(
775 * { length: 12 },
776 * (_, i) => Effect.request(GetDataRequest({ id: i }), limitedResolver)
777 * )
778 *
779 * const result = await Effect.runPromise(Effect.all(requests, { concurrency: "unbounded" }))
780 * batchSizes // => [5, 5, 2]
781 *
782 * result.length // => 12
783 *
784 * Array.of(result[0], result[11]) // => ["data-0", "data-11"]
785 * ```
786 *
787 * @category combinators
788 * @since 2.0.0
789 */
790export const batchN: {
791 (n: number): <A extends Request.Any>(self: RequestResolver<A>) => RequestResolver<A>
792 <A extends Request.Any>(self: RequestResolver<A>, n: number): RequestResolver<A>
793} = dual(2, <A extends Request.Any>(self: RequestResolver<A>, n: number): RequestResolver<A> => {
794 const size = Count.normalizeNonEmpty(n)
795 return makeWith({
796 ...self,
797 collectWhile: (requests) => requests.size < size
798 })
799})
800
801/**
802 * Transforms a request resolver by grouping requests using the specified key
803 * function.
804 *
805 * **Example** (Grouping resolver requests)
806 *
807 * ```ts import.meta.vitest
808 * import { Effect, Exit, Request, RequestResolver } from "effect"
809 *
810 * interface GetUserRequest extends Request.Request<string> {
811 * readonly _tag: "GetUserRequest"
812 * readonly userId: number
813 * readonly department: string
814 * }
815 * const GetUserRequest = Request.tagged<GetUserRequest>("GetUserRequest")
816 *
817 * const batchSizes: Array<number> = []
818 *
819 * const resolver = RequestResolver.make<GetUserRequest>((entries) =>
820 * Effect.sync(() => {
821 * batchSizes.push(entries.length)
822 * for (const entry of entries) {
823 * entry.completeUnsafe(Exit.succeed(`User ${entry.request.userId}`))
824 * }
825 * })
826 * )
827 *
828 * // Group requests by department for more efficient processing
829 * const groupedResolver = RequestResolver.grouped(
830 * resolver,
831 * ({ request }) => request.department
832 * )
833 *
834 * // Requests for the same department will be batched together
835 * const requests = [
836 * Effect.request(
837 * GetUserRequest({ userId: 1, department: "Engineering" }),
838 * groupedResolver
839 * ),
840 * Effect.request(
841 * GetUserRequest({ userId: 2, department: "Engineering" }),
842 * groupedResolver
843 * ),
844 * Effect.request(
845 * GetUserRequest({ userId: 3, department: "Marketing" }),
846 * groupedResolver
847 * )
848 * ]
849 *
850 * const result = await Effect.runPromise(Effect.all(requests, { concurrency: "unbounded" }))
851 * batchSizes.sort()
852 *
853 * batchSizes // => [1, 2]
854 *
855 * result // => ["User 1", "User 2", "User 3"]
856 * ```
857 *
858 * @category combinators
859 * @since 4.0.0
860 */
861export const grouped: {
862 <A extends Request.Any, K>(f: (entry: Request.Entry<A>) => K): (self: RequestResolver<A>) => RequestResolver<A>
863 <A extends Request.Any, K>(self: RequestResolver<A>, f: (entry: Request.Entry<A>) => K): RequestResolver<A>
864} = dual(
865 2,
866 <A extends Request.Any, K>(self: RequestResolver<A>, f: (entry: Request.Entry<A>) => K): RequestResolver<A> =>
867 makeWith({
868 ...self,
869 batchKey: hashGroupKey(f)
870 })
871)
872
873/**
874 * Returns a request resolver that sends each batch to both resolvers and
875 * completes with the first resolver to finish.
876 *
877 * **Details**
878 *
879 * The losing resolver run is interrupted after the winning resolver completes
880 * the batch.
881 *
882 * **Example** (Racing request resolvers)
883 *
884 * ```ts import.meta.vitest
885 * import { Effect, Exit, Request, RequestResolver } from "effect"
886 *
887 * interface GetDataRequest extends Request.Request<string> {
888 * readonly _tag: "GetDataRequest"
889 * readonly id: number
890 * }
891 * const GetDataRequest = Request.tagged<GetDataRequest>("GetDataRequest")
892 *
893 * // Fast resolver (simulating cache)
894 * const fastResolver = RequestResolver.make<GetDataRequest>((entries) =>
895 * Effect.gen(function*() {
896 * yield* Effect.sleep("10 millis")
897 * for (const entry of entries) {
898 * entry.completeUnsafe(Exit.succeed(`fast-${entry.request.id}`))
899 * }
900 * })
901 * )
902 *
903 * // Slow resolver (simulating database)
904 * const slowResolver = RequestResolver.make<GetDataRequest>((entries) =>
905 * Effect.gen(function*() {
906 * yield* Effect.sleep("100 millis")
907 * for (const entry of entries) {
908 * entry.completeUnsafe(Exit.succeed(`slow-${entry.request.id}`))
909 * }
910 * })
911 * )
912 *
913 * // Race resolvers - will use whichever completes first
914 * const racingResolver = RequestResolver.race(fastResolver, slowResolver)
915 * const program = Effect.request(GetDataRequest({ id: 1 }), racingResolver)
916 * await Effect.runPromise(program) // => "fast-1"
917 * ```
918 *
919 * @category combinators
920 * @since 2.0.0
921 */
922export const race: {
923 <A2 extends Request.Any>(
924 that: RequestResolver<A2>
925 ): <A extends Request.Any>(self: RequestResolver<A>) => RequestResolver<A2 & A>
926 <A extends Request.Any, A2 extends Request.Any>(
927 self: RequestResolver<A>,
928 that: RequestResolver<A2>
929 ): RequestResolver<A & A2>
930} = dual(2, <A extends Request.Any, A2 extends Request.Any>(
931 self: RequestResolver<A>,
932 that: RequestResolver<A2>
933): RequestResolver<A & A2> =>
934 make(
935 (requests, key) => effect.race(self.runAll(requests, key), that.runAll(requests, key))
936 ))
937
938/**
939 * Adds a tracing span to the request resolver, which will also add any span
940 * links from the request's.
941 *
942 * **Example** (Adding a tracing span)
943 *
944 * ```ts import.meta.vitest
945 * import { Effect, Exit, Request, RequestResolver } from "effect"
946 *
947 * interface GetDataRequest extends Request.Request<string> {
948 * readonly _tag: "GetDataRequest"
949 * readonly id: number
950 * }
951 * const GetDataRequest = Request.tagged<GetDataRequest>("GetDataRequest")
952 *
953 * const resolver = RequestResolver.make<GetDataRequest>((entries) =>
954 * Effect.sync(() => {
955 * for (const entry of entries) {
956 * entry.completeUnsafe(Exit.succeed(`data-${entry.request.id}`))
957 * }
958 * })
959 * )
960 *
961 * // Add tracing span with custom name and attributes
962 * const tracedResolver = RequestResolver.withSpan(
963 * resolver,
964 * "user-data-resolver",
965 * {
966 * attributes: {
967 * "resolver.type": "user-data",
968 * "resolver.version": "1.0"
969 * }
970 * }
971 * )
972 *
973 * // Spans will automatically include batch size and request links
974 * const effect = Effect.request(GetDataRequest({ id: 123 }), tracedResolver)
975 * await Effect.runPromise(effect) // => "data-123"
976 * ```
977 *
978 * @category combinators
979 * @since 4.0.0
980 */
981export const withSpan: {
982 <A extends Request.Any>(
983 name: string,
984 options?: Tracer.SpanOptions | ((entries: NonEmptyArray<Request.Entry<A>>) => Tracer.SpanOptions) | undefined
985 ): (self: RequestResolver<A>) => RequestResolver<A>
986 <A extends Request.Any>(
987 self: RequestResolver<A>,
988 name: string,
989 options?: Tracer.SpanOptions | ((entries: NonEmptyArray<Request.Entry<A>>) => Tracer.SpanOptions) | undefined
990 ): RequestResolver<A>
991} = dual((args) => isRequestResolver(args[0]), <A extends Request.Any>(
992 self: RequestResolver<A>,
993 name: string,
994 options?: Tracer.SpanOptions | ((entries: NonEmptyArray<Request.Entry<A>>) => Tracer.SpanOptions) | undefined
995): RequestResolver<A> =>
996 makeWith({
997 ...self,
998 runAll: (entries, key) =>
999 Effect.suspend(() => {
1000 const opts = typeof options === "function" ? options(entries) : options
1001 const links = opts?.links ? opts.links.slice() : []
1002 const seen = new Set<Tracer.AnySpan>()
1003 for (const entry of entries) {
1004 const span = Context.getOption(entry.context, Tracer.ParentSpan)
1005 if (span._tag === "None" || seen.has(span.value)) continue
1006 seen.add(span.value)
1007 links.push({ span: span.value, attributes: {} })
1008 }
1009 return Effect.withSpan(self.runAll(entries, key), name, {
1010 ...options,
1011 links,
1012 attributes: {
1013 batchSize: entries.length,
1014 ...opts?.attributes
1015 }
1016 })
1017 })
1018 }))
1019
1020/**
1021 * Wraps a request resolver in a cache, allowing it to cache results up to a
1022 * specified capacity and optional time-to-live.
1023 *
1024 * **When to use**
1025 *
1026 * Use to turn a request resolver into a first-class `Cache` when callers need
1027 * cache lookup, refresh, invalidation, or inspection around request results.
1028 *
1029 * **Details**
1030 *
1031 * The request value is the cache key. Cache misses run the resolver via
1032 * `Effect.request`, `timeToLive` receives the request `Exit` and the request,
1033 * and `requireServicesAt` controls whether services are required at lookup time
1034 * or construction time.
1035 *
1036 * **Gotchas**
1037 *
1038 * Cache hits depend on the request value's equality semantics.
1039 *
1040 * @see {@link withCache} for keeping caching behind a resolver used with `Effect.request`
1041 * @see {@link persisted} for storing persistable request results outside process memory
1042 * @see {@link Cache.Cache} for operations available on the returned cache
1043 *
1044 * @category caching
1045 * @since 4.0.0
1046 */
1047export const asCache: {
1048 <
1049 A extends Request.Any,
1050 ServiceMode extends "lookup" | "construction" = never
1051 >(options: {
1052 readonly capacity: number
1053 readonly timeToLive?: ((exit: Request.Result<A>, request: A) => Duration.Input) | undefined
1054 readonly requireServicesAt?: ServiceMode | undefined
1055 }): (self: RequestResolver<A>) => Effect.Effect<
1056 Cache.Cache<
1057 A,
1058 Request.Success<A>,
1059 Request.Error<A>,
1060 "construction" extends ServiceMode ? never : Request.Services<A>
1061 >,
1062 never,
1063 "construction" extends ServiceMode ? Request.Services<A> : never
1064 >
1065 <
1066 A extends Request.Any,
1067 ServiceMode extends "lookup" | "construction" = never
1068 >(self: RequestResolver<A>, options: {
1069 readonly capacity: number
1070 readonly timeToLive?: ((exit: Request.Result<A>, request: A) => Duration.Input) | undefined
1071 readonly requireServicesAt?: ServiceMode | undefined
1072 }): Effect.Effect<
1073 Cache.Cache<
1074 A,
1075 Request.Success<A>,
1076 Request.Error<A>,
1077 "construction" extends ServiceMode ? never : Request.Services<A>
1078 >,
1079 never,
1080 "construction" extends ServiceMode ? Request.Services<A> : never
1081 >
1082} = dual(2, <
1083 A extends Request.Any,
1084 ServiceMode extends "lookup" | "construction" = never
1085>(self: RequestResolver<A>, options: {
1086 readonly capacity: number
1087 readonly timeToLive?: ((exit: Request.Result<A>, request: A) => Duration.Input) | undefined
1088 readonly requireServicesAt?: ServiceMode | undefined
1089}): Effect.Effect<
1090 Cache.Cache<
1091 A,
1092 Request.Success<A>,
1093 Request.Error<A>,
1094 "construction" extends ServiceMode ? never : Request.Services<A>
1095 >,
1096 never,
1097 "construction" extends ServiceMode ? Request.Services<A> : never
1098> =>
1099 Cache.makeWith((req: A) => internal.request(req, self), {
1100 capacity: options.capacity,
1101 timeToLive: options.timeToLive as any,
1102 requireServicesAt: options.requireServicesAt ?? "lookup" as ServiceMode
1103 }) as any)
1104
1105/**
1106 * Adds a bounded in-memory cache to a request resolver.
1107 *
1108 * **When to use**
1109 *
1110 * Use to reuse completed results for repeated equal request values while still
1111 * passing a `RequestResolver` to `Effect.request`.
1112 *
1113 * **Details**
1114 *
1115 * Running the returned effect creates the cache and returns a wrapped resolver.
1116 * The cache stores completed success or failure results by request equality up
1117 * to `capacity`. The `strategy` option controls eviction order and defaults to
1118 * `"lru"`; `"fifo"` keeps insertion order.
1119 *
1120 * **Gotchas**
1121 *
1122 * Entries do not expire by time, and completed failures without interruptions
1123 * are cached the same as successes. Results containing interruptions are not
1124 * cached. Request equality controls cache hits.
1125 *
1126 * @see {@link asCache} for exposing the resolver as a `Cache` with time-to-live and service lookup controls
1127 * @see {@link persisted} for backing persistable requests with the configured persistence store
1128 *
1129 * @category caching
1130 * @since 4.0.0
1131 */
1132export const withCache: {
1133 <A extends Request.Any>(options: {
1134 readonly capacity: number
1135 readonly strategy?: "lru" | "fifo" | undefined
1136 }): (self: RequestResolver<A>) => Effect.Effect<RequestResolver<A>>
1137 <A extends Request.Any>(self: RequestResolver<A>, options: {
1138 readonly capacity: number
1139 readonly strategy?: "lru" | "fifo" | undefined
1140 }): Effect.Effect<RequestResolver<A>>
1141} = dual(2, <A extends Request.Any>(self: RequestResolver<A>, options: {
1142 readonly capacity: number
1143 readonly strategy?: "lru" | "fifo" | undefined
1144}): Effect.Effect<RequestResolver<A>> =>
1145 Effect.sync(() => {
1146 const strategy = options.strategy ?? "lru"
1147 const cache = MutableHashMap.empty<A, {
1148 readonly entry: Request.Entry<A>
1149 exit: Request.Result<A> | undefined
1150 }>()
1151 return makeWith({
1152 ...self,
1153 runAll(entries, key) {
1154 return Effect.onExit(self.runAll(entries, key), () => {
1155 let toRemove = MutableHashMap.size(cache) - options.capacity
1156 if (toRemove <= 0) return Effect.void
1157 for (const k of MutableHashMap.keys(cache)) {
1158 MutableHashMap.remove(cache, k)
1159 toRemove--
1160 if (toRemove <= 0) break
1161 }
1162 return Effect.void
1163 })
1164 },
1165 preCheck(entry) {
1166 const ocached = MutableHashMap.get(cache, entry.request)
1167 if (ocached._tag === "None") {
1168 const cached = { entry, exit: undefined as Request.Result<A> | undefined }
1169 MutableHashMap.set(cache, entry.request, cached)
1170 const prevComplete = entry.completeUnsafe
1171 entry.completeUnsafe = function(exit) {
1172 if (Exit.hasInterrupts(exit)) {
1173 if (cached.exit === undefined) {
1174 const current = MutableHashMap.get(cache, entry.request)
1175 if (current._tag === "Some" && current.value === cached) {
1176 MutableHashMap.remove(cache, entry.request)
1177 }
1178 }
1179 } else {
1180 cached.exit = exit as any
1181 }
1182 prevComplete(exit)
1183 }
1184 return true
1185 }
1186 const cached = ocached.value
1187 if (cached.exit) {
1188 if (strategy === "lru") {
1189 MutableHashMap.remove(cache, cached.entry.request)
1190 MutableHashMap.set(cache, cached.entry.request, cached)
1191 }
1192 entry.completeUnsafe(cached.exit as any)
1193 } else {
1194 cached.entry.uninterruptible = true
1195 const prevComplete = cached.entry.completeUnsafe
1196 cached.entry.completeUnsafe = function(exit) {
1197 prevComplete(exit)
1198 entry.completeUnsafe(exit)
1199 }
1200 }
1201 return false
1202 }
1203 })
1204 }))
1205
1206/**
1207 * Wraps a request resolver with persistent storage for persistable requests.
1208 *
1209 * **When to use**
1210 *
1211 * Use to keep a `RequestResolver` interface while reusing completed
1212 * `Persistable` request results through a `Persistence` store.
1213 *
1214 * **Details**
1215 *
1216 * Cached results are loaded from the configured persistence store before
1217 * running the underlying resolver. Missing entries are resolved normally and
1218 * written back to the store. Entries marked stale by `staleWhileRevalidate`
1219 * receive the stored result and are also resolved again so the refreshed result
1220 * can be written back to the store. Creating the persisted resolver requires
1221 * `Persistence.Persistence` and `Scope`.
1222 *
1223 * @see {@link withCache} for in-memory resolver caching that does not require persistable request values or a persistence store
1224 * @see {@link asCache} for exposing resolver results through a `Cache` instead of returning another resolver
1225 *
1226 * @category caching
1227 * @since 4.0.0
1228 */
1229export const persisted: {
1230 <A extends Request.Request<any, Persistence.PersistenceError | Schema.SchemaError, any> & Persistable.Any>(
1231 options: {
1232 readonly storeId: string
1233 readonly timeToLive?: ((exit: Request.Result<A>, request: A) => Duration.Input) | undefined
1234 readonly staleWhileRevalidate?: ((exit: Request.Result<A>, request: A) => boolean) | undefined
1235 }
1236 ): (self: RequestResolver<A>) => Effect.Effect<
1237 RequestResolver<A>,
1238 never,
1239 Persistence.Persistence | Scope
1240 >
1241 <
1242 A extends Request.Request<any, Persistence.PersistenceError | Schema.SchemaError, any> & Persistable.Any
1243 >(
1244 self: RequestResolver<A>,
1245 options: {
1246 readonly storeId: string
1247 readonly timeToLive?: ((exit: Request.Result<A>, request: A) => Duration.Input) | undefined
1248 readonly staleWhileRevalidate?: ((exit: Request.Result<A>, request: A) => boolean) | undefined
1249 }
1250 ): Effect.Effect<
1251 RequestResolver<A>,
1252 never,
1253 Persistence.Persistence | Scope
1254 >
1255} = dual(
1256 2,
1257 Effect.fnUntraced(function*<
1258 A extends Request.Request<any, Persistence.PersistenceError | Schema.SchemaError, any> & Persistable.Any
1259 >(
1260 self: RequestResolver<A>,
1261 options: {
1262 readonly storeId: string
1263 readonly timeToLive?: ((exit: Request.Result<A>, request: A) => Duration.Input) | undefined
1264 readonly staleWhileRevalidate?: ((exit: Request.Result<A>, request: A) => boolean) | undefined
1265 }
1266 ) {
1267 const store = yield* (yield* Persistence.Persistence).make(options as any)
1268 return makeWith<A>({
1269 ...self,
1270 runAll: Effect.fnUntraced(function*(entries, key) {
1271 const results = yield* (store.getMany(Iterable.map(entries, (_) => _.request)).pipe(
1272 Effect.provideContext(entries[0].context)
1273 ) as Effect.Effect<
1274 Array<Exit.Exit<unknown, unknown> | undefined>,
1275 Request.Error<A>
1276 >)
1277 const leftover: Array<Request.Entry<A>> = []
1278 const toPersist = new Map<A, Request.Result<A>>()
1279 const completed = new Set<Request.Entry<A>>()
1280 for (let i = 0; i < results.length; i++) {
1281 const entry = entries[i]
1282 const exit = results[i]
1283 if (
1284 exit === undefined ||
1285 (options.staleWhileRevalidate && options.staleWhileRevalidate(exit as any, entry.request))
1286 ) {
1287 const prevComplete = entry.completeUnsafe
1288 entry.completeUnsafe = function(exit) {
1289 completed.add(entry)
1290 toPersist.set(entry.request, exit as any)
1291 prevComplete(exit)
1292 }
1293 leftover.push(entry)
1294 if (exit === undefined) continue
1295 }
1296 entry.completeUnsafe(exit as any)
1297 }
1298 if (!Arr.isArrayNonEmpty(leftover)) {
1299 return
1300 }
1301 yield* Effect.catchCause(Effect.suspend(() => self.runAll(leftover, key)), (cause) => {
1302 for (let i = 0; i < leftover.length; i++) {
1303 const entry = leftover[i]
1304 if (completed.has(entry)) continue
1305 entry.completeUnsafe(Exit.failCause(cause) as any)
1306 }
1307 return Effect.void
1308 })
1309 yield* (store.setMany(toPersist).pipe(
1310 Effect.provideContext(entries[0].context)
1311 ) as Effect.Effect<void, Request.Error<A>>)
1312 })
1313 })
1314 })
1315)