1import * as Deferred from "effect/Deferred";
2import * as Effect from "effect/Effect";
3
4/**
5 * Options for creating a cached function.
6 */
7export 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 */
40export 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 });