| 1 | import * as Deferred from "effect/Deferred"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | |
| 4 | /** |
| 5 | * Options for creating a cached function. |
| 6 | */ |
| 7 | export interface CachedFunctionOptions<A> { |
| 8 | /** |
| 9 | * Function to convert arguments to a cache key string. |
| 10 | * Defaults to `JSON.stringify`. |
| 11 | */ |
| 12 | readonly key?: (args: A) => string; |
| 13 | } |
| 14 | |
| 15 | /** |
| 16 | * Creates a memoized version of a function that returns an Effect. |
| 17 | * |
| 18 | * The key feature is deduplication of concurrent calls with the same inputs - |
| 19 | * only one execution happens while other callers wait for and receive the same result. |
| 20 | * |
| 21 | * @example |
| 22 | * ```ts |
| 23 | * import * as Effect from "effect/Effect"; |
| 24 | * import { cachedFunction } from "~/lib/cached-function"; |
| 25 | * |
| 26 | * const fetchUser = (id: string) => |
| 27 | * Effect.promise(() => fetch(`/users/${id}`).then(r => r.json())); |
| 28 | * |
| 29 | * const program = Effect.gen(function* () { |
| 30 | * const cachedFetchUser = yield* cachedFunction(fetchUser); |
| 31 | * |
| 32 | * // These concurrent calls will only trigger one fetch |
| 33 | * const [user1, user2] = yield* Effect.all([ |
| 34 | * cachedFetchUser("123"), |
| 35 | * cachedFetchUser("123"), |
| 36 | * ]); |
| 37 | * }); |
| 38 | * ``` |
| 39 | */ |
| 40 | export const cachedFunction = <A extends Array<any>, B, E, R>( |
| 41 | fn: (...args: A) => Effect.Effect<B, E, R>, |
| 42 | options?: CachedFunctionOptions<A>, |
| 43 | ): Effect.Effect<(...args: A) => Effect.Effect<B, E, R>> => |
| 44 | Effect.sync(() => { |
| 45 | const keyFn = options?.key ?? JSON.stringify; |
| 46 | const cache = new Map<string, Deferred.Deferred<B, E>>(); |
| 47 | |
| 48 | return (...args: A): Effect.Effect<B, E, R> => |
| 49 | Effect.suspend(() => { |
| 50 | const cacheKey = keyFn(args); |
| 51 | const existing = cache.get(cacheKey); |
| 52 | |
| 53 | // If there's already a deferred for this key, wait on it |
| 54 | if (existing) { |
| 55 | return Deferred.await(existing); |
| 56 | } |
| 57 | |
| 58 | // Create a new deferred and store it |
| 59 | return Effect.gen(function* () { |
| 60 | const deferred = yield* Deferred.make<B, E>(); |
| 61 | cache.set(cacheKey, deferred); |
| 62 | |
| 63 | // Execute the effect and complete the deferred |
| 64 | const exit = yield* Effect.exit(fn(...args)); |
| 65 | yield* Deferred.done(deferred, exit); |
| 66 | |
| 67 | if (exit._tag === "Failure") { |
| 68 | cache.delete(cacheKey); |
| 69 | } |
| 70 | |
| 71 | // Return the result |
| 72 | return yield* exit; |
| 73 | }); |
| 74 | }); |
| 75 | }); |