1import * as Effect from "effect/Effect";
2import * as Scope from "effect/Scope";
3
4/**
5 * Per-execution memos, keyed on the execution's `Scope` object. Every bridge
6 * (Worker event, Durable Object call, Workflow run, Lambda invoke) provides a
7 * fresh `Scope` per execution, so an entry lives exactly as long as its
8 * execution and is garbage-collected with the scope object.
9 */
10const caches = new WeakMap<
11 Scope.Scope,
12 Map<symbol, Effect.Effect<any, any, any>>
13>();
14
15/**
16 * Allocate a per-execution memo cell for `build`. The returned accessor runs
17 * `build` at most once per execution scope (Worker fetch/queue/scheduled
18 * event, DO call, Workflow run, Lambda invoke) and builds it WITH that scope
19 * (`Layer.build` / `Scope`-requiring acquires see the execution scope), so
20 * release finalizers fire when the event settles — never held across events.
21 * This is the only legal pooling shape on workerd (sockets are
22 * IoContext-pinned) and the correct one on Lambda (the instance scope must
23 * not own disposables).
24 *
25 * The `Effect.sync` wrapper allocates the memo's symbol key once per *call
26 * site* (i.e. once per layer construction), so distinct clients built from
27 * distinct `makeExecutionMemo` calls never share a memo cell even inside the
28 * same execution.
29 *
30 * The memo key and the finalizer target are always the same scope object
31 * (`yield* Effect.scope`), so they cannot disagree: wrapping queries in a
32 * nested `Effect.scoped` narrows both the memo and the resource's lifetime
33 * to that block.
34 *
35 * The accessor's `Scope` requirement is erased from its type — every runtime
36 * bridge provides a fresh execution `Scope`, exactly like
37 * `Drizzle.Postgres` before this primitive was extracted. Evaluating the
38 * accessor outside an execution scope is a defect.
39 */
40export const makeExecutionMemo = <A, E, R>(
41 build: Effect.Effect<A, E, R | Scope.Scope>,
42): Effect.Effect<Effect.Effect<A, E, Exclude<R, Scope.Scope>>> =>
43 Effect.sync(function () {
44 const symbol = Symbol();
45
46 return Effect.gen(function* () {
47 const scope = yield* Effect.scope;
48 let cache = caches.get(scope);
49 if (cache === undefined) {
50 caches.set(scope, (cache = new Map()));
51 }
52 let memo = cache.get(symbol);
53 if (memo === undefined) {
54 // `Effect.cached` only allocates the memo cell — the build runs
55 // on first evaluation — so this yield is synchronous and the
56 // fiber cannot be interleaved between the miss check and the
57 // set: a concurrent first access joins this memo (evaluating the
58 // same cached effect) instead of building a second resource.
59 memo = yield* Effect.cached(build.pipe(Scope.provide(scope)));
60 cache.set(symbol, memo);
61 }
62 return yield* memo;
63 }) as Effect.Effect<A, E, Exclude<R, Scope.Scope>>;
64 });