import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; /** * Options for creating a cached function. */ export interface CachedFunctionOptions { /** * Function to convert arguments to a cache key string. * Defaults to `JSON.stringify`. */ readonly key?: (args: A) => string; } /** * Creates a memoized version of a function that returns an Effect. * * The key feature is deduplication of concurrent calls with the same inputs - * only one execution happens while other callers wait for and receive the same result. * * @example * ```ts * import * as Effect from "effect/Effect"; * import { cachedFunction } from "~/lib/cached-function"; * * const fetchUser = (id: string) => * Effect.promise(() => fetch(`/users/${id}`).then(r => r.json())); * * const program = Effect.gen(function* () { * const cachedFetchUser = yield* cachedFunction(fetchUser); * * // These concurrent calls will only trigger one fetch * const [user1, user2] = yield* Effect.all([ * cachedFetchUser("123"), * cachedFetchUser("123"), * ]); * }); * ``` */ export const cachedFunction = , B, E, R>( fn: (...args: A) => Effect.Effect, options?: CachedFunctionOptions, ): Effect.Effect<(...args: A) => Effect.Effect> => Effect.sync(() => { const keyFn = options?.key ?? JSON.stringify; const cache = new Map>(); return (...args: A): Effect.Effect => Effect.suspend(() => { const cacheKey = keyFn(args); const existing = cache.get(cacheKey); // If there's already a deferred for this key, wait on it if (existing) { return Deferred.await(existing); } // Create a new deferred and store it return Effect.gen(function* () { const deferred = yield* Deferred.make(); cache.set(cacheKey, deferred); // Execute the effect and complete the deferred const exit = yield* Effect.exit(fn(...args)); yield* Deferred.done(deferred, exit); if (exit._tag === "Failure") { cache.delete(cacheKey); } // Return the result return yield* exit; }); }); });