1import { cachedInScope } from "@/Util/Memoize.ts";
2import { describe, expect, it } from "alchemy-test";
3import * as Cause from "effect/Cause";
4import * as Effect from "effect/Effect";
5import * as Deferred from "effect/Deferred";
6import * as Exit from "effect/Exit";
7import * as Fiber from "effect/Fiber";
8import * as Scope from "effect/Scope";
9
10// `Effect.cached` runs the computation on the first caller's fiber, so
11// interrupting that caller while others wait fails every waiter with an
12// interrupt-only cause and leaves the cache poisoned. These pin the
13// behaviour `cachedInScope` exists for.
14describe("cachedInScope", { tags: ["unit", "local"] }, () => {
15 it.live("survives the first caller being interrupted", () =>
16 Effect.gen(function* () {
17 const scope = yield* Scope.make();
18 let runs = 0;
19 const memo = yield* cachedInScope(scope)(
20 Effect.sleep("100 millis").pipe(
21 Effect.map(() => {
22 runs++;
23 return 42;
24 }),
25 ),
26 );
27 const owner = yield* Effect.forkChild(memo);
28 yield* Effect.sleep("10 millis");
29 const waiter = yield* Effect.forkChild(memo);
30 yield* Effect.sleep("10 millis");
31 yield* Fiber.interrupt(owner);
32
33 expect(yield* Fiber.join(waiter)).toBe(42);
34 expect(yield* memo).toBe(42);
35 expect(runs).toBe(1);
36 yield* Scope.close(scope, Exit.void);
37 }),
38 );
39
40 it.live("runs once and shares failures with every waiter", () =>
41 Effect.gen(function* () {
42 const scope = yield* Scope.make();
43 let runs = 0;
44 const memo = yield* cachedInScope(scope)(
45 Effect.suspend(() => {
46 runs++;
47 return Effect.fail("boom");
48 }),
49 );
50 const [a, b] = yield* Effect.all(
51 [Effect.result(memo), Effect.result(memo)],
52 { concurrency: "unbounded" },
53 );
54 expect(a._tag).toBe("Failure");
55 expect(b._tag).toBe("Failure");
56 expect(runs).toBe(1);
57 yield* Scope.close(scope, Exit.void);
58 }),
59 );
60
61 it.live(
62 "closing the scope cancels the computation for its waiters",
63 () =>
64 Effect.gen(function* () {
65 const scope = yield* Scope.make();
66 const started = yield* Deferred.make<void>();
67 const memo = yield* cachedInScope(scope)(
68 Deferred.succeed(started, undefined).pipe(
69 Effect.andThen(Effect.never),
70 ),
71 );
72 const waiter = yield* Effect.forkChild(memo);
73 yield* Deferred.await(started);
74 yield* Scope.close(scope, Exit.void);
75 const exit = yield* Fiber.await(waiter);
76 expect(
77 Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause),
78 ).toBe(true);
79 }),
80 { timeout: 5_000, retry: 0 },
81 );
82});