1import * as Effect from "effect/Effect";
2import type { Output } from "./Output.ts";
3import { type BaseRuntimeContext, RuntimeContext } from "./RuntimeContext.ts";
4
5/**
6 * Runtime context bridge that lets an {@link Action} read Resource Outputs.
7 *
8 * Actions are peculiar: their init Effect runs at plan/stack-eval time (before
9 * any resource exists) while their body runs at apply time (after upstreams are
10 * materialized). To make `yield* db.databaseId` work inside an Action we split
11 * the {@link RuntimeContext} into two cooperating halves that share nothing but
12 * a sanitized key:
13 *
14 * - {@link makeCaptureContext} is provided while the init Effect runs. Its
15 * `set(key, output)` records the referenced {@link Output} (so the engine can
16 * add a dependency edge and later resolve it) and its `get(key)` hands back a
17 * *deferred* accessor — an Effect that re-reads whatever `RuntimeContext` is
18 * ambient when it actually runs.
19 * - {@link makeResolveContext} is provided around the Action body at apply
20 * time. Its `get(key)` returns the already-resolved value for that key.
21 *
22 * Because the accessor produced at init re-reads `RuntimeContext`, yielding it
23 * inside the body resolves against the apply-time context — no shared mutable
24 * state, no infinite recursion (the resolve context's `get` is terminal).
25 */
26
27const base = (id: string): Omit<BaseRuntimeContext, "get" | "set"> => ({
28 Type: "Action",
29 id,
30 env: {},
31});
32
33/**
34 * Capture context — provided while an Action's init Effect runs. Records every
35 * Output referenced via `yield* output` into `captures`, keyed by the Output's
36 * sanitized key, and returns deferred accessors.
37 */
38export const makeCaptureContext = (
39 captures: Record<string, Output>,
40): BaseRuntimeContext => ({
41 ...base("capture"),
42 set: (key, output) =>
43 Effect.sync(() => {
44 captures[key] = output as unknown as Output;
45 return key;
46 }),
47 // Deferred: re-read the ambient RuntimeContext at run time. At apply this is
48 // the resolve context below; its `get` returns the materialized value. The
49 // `RuntimeContext` requirement is erased at the Action boundary (`Run` is
50 // typed `Effect<Out, any, any>`) and satisfied by the resolve context, so
51 // the accessor presents as `Effect<T | undefined>`.
52 get: (<T>(key: string) =>
53 Effect.flatMap(RuntimeContext, (ctx) =>
54 ctx.get<T>(key),
55 )) as BaseRuntimeContext["get"],
56});
57
58/**
59 * Resolve context — provided around an Action body at apply time. `resolved`
60 * maps each captured key to its value (already evaluated against the tracker).
61 */
62export const makeResolveContext = (
63 resolved: Record<string, unknown>,
64): BaseRuntimeContext => ({
65 ...base("resolve"),
66 set: (key) => Effect.succeed(key),
67 get: <T>(key: string) => Effect.succeed(resolved[key] as T | undefined),
68});