1import * as Deferred from "effect/Deferred";
2import * as Effect from "effect/Effect";
3import * as Fiber from "effect/Fiber";
4import * as Scope from "effect/Scope";
5
6/**
7 * A lazily evaluated, memoized effect whose computation runs in a fiber of
8 * its own, owned by `scope`.
9 *
10 * `Effect.cached` evaluates on the fiber of whichever caller gets there
11 * first. When that caller is interrupted while others are waiting — a
12 * concurrent plan cancelling sibling diffs — every waiter fails
13 * interrupt-only and the cache stays poisoned for the rest of the process.
14 * A computation that many fibers share must not have that failure mode: here
15 * it runs detached from every caller, so interrupting a waiter never touches
16 * it, and only closing `scope` cancels it (its waiters then see the
17 * interruption, as they should).
18 *
19 * Requirements are captured from the first caller, exactly as
20 * `Effect.cached` does.
21 */
22export const cachedInScope =
23 (scope: Scope.Scope) =>
24 <A, E, R>(
25 effect: Effect.Effect<A, E, R>,
26 ): Effect.Effect<Effect.Effect<A, E>, never, R> =>
27 Effect.gen(function* () {
28 const context = yield* Effect.context<R>();
29 const deferred = yield* Deferred.make<A, E>();
30 let started = false;
31 return Effect.suspend(() => {
32 if (started) return Deferred.await(deferred);
33 started = true;
34 return effect.pipe(
35 Effect.provideContext(context),
36 // `onExit` also runs on interruption, so the waiters always learn
37 // how the computation ended.
38 Effect.onExit((exit) => Deferred.done(deferred, exit)),
39 // Not a child of the caller: the caller's interruption must not
40 // reach it. The scope owns its lifetime instead.
41 Effect.forkDetach,
42 Effect.flatMap((fiber) =>
43 Scope.addFinalizer(scope, Fiber.interrupt(fiber)),
44 ),
45 Effect.andThen(Deferred.await(deferred)),
46 );
47 });
48 });