Skip to content
Source
Sign inSign up
Open History to see the last commit

ActionRuntimeContext.ts

62 lines2.8 KB

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 = (captures: Record<string, Output>): BaseRuntimeContext => ({
39 ...base("capture"),
40 set: (key, output) =>
41 Effect.sync(() => {
42 captures[key] = output as unknown as Output;
43 return key;
44 }),
45 // Deferred: re-read the ambient RuntimeContext at run time. At apply this is
46 // the resolve context below; its `get` returns the materialized value. The
47 // `RuntimeContext` requirement is erased at the Action boundary (`Run` is
48 // typed `Effect<Out, any, any>`) and satisfied by the resolve context, so
49 // the accessor presents as `Effect<T | undefined>`.
50 get: (<T>(key: string) =>
51 Effect.flatMap(RuntimeContext, (ctx) => ctx.get<T>(key))) as BaseRuntimeContext["get"],
52});
53​
54/**
55 * Resolve context — provided around an Action body at apply time. `resolved`
56 * maps each captured key to its value (already evaluated against the tracker).
57 */
58export const makeResolveContext = (resolved: Record<string, unknown>): BaseRuntimeContext => ({
59 ...base("resolve"),
60 set: (key) => Effect.succeed(key),
61 get: <T>(key: string) => Effect.succeed(resolved[key] as T | undefined),
62});