1/**
2 * `SfnEffect<A, E>` — the yieldable, pipeable Step Functions program type.
3 *
4 * It mirrors Effect's `<A, E>` channels (there is deliberately no `R`
5 * channel: an ASL program cannot require services — its "requirements" are
6 * the resources its Task states reference, collected structurally at
7 * compile). It is **not** an `Effect`: it is a shallow-embedded AST that
8 * *feels* like Effect (generator syntax, `catchTag` narrowing `E`) but
9 * constructs a data structure that `compile.ts` turns into ASL JSON.
10 *
11 * Yielding a real `Effect` inside `Sfn.gen` is a **type error** — only
12 * `SfnEffect`s (branded with {@link SfnTypeId}) are yieldable.
13 */
14import { pipeArguments } from "effect/Pipeable";
15import type { Pipeable } from "effect/Pipeable";
16import type { Expr, UnwrapExpr } from "./Jsonata.ts";
17import type { AslNode } from "./Node.ts";
18
19/** Brand distinguishing `SfnEffect` from `Effect` (and everything else). */
20export const SfnTypeId = Symbol.for("alchemy/AWS/StepFunctions/SfnEffect");
21export type SfnTypeId = typeof SfnTypeId;
22
23/**
24 * A typed Step Functions program producing `A` or failing with a tagged
25 * error `E`. Build with `Sfn.gen` and the `Sfn.*` combinators; compile with
26 * `StateMachine.fromProgram` (or `compileProgram`); run locally with
27 * `simulate`.
28 */
29export interface SfnEffect<out A, out E = never> extends Pipeable {
30 readonly [SfnTypeId]: {
31 readonly _A: (_: never) => A;
32 readonly _E: (_: never) => E;
33 };
34 /** The program AST node this effect wraps. */
35 readonly node: AslNode;
36 /**
37 * `yield*` protocol for `Sfn.gen`: yields this program once and resumes
38 * with a typed {@link Expr} reference to its result.
39 */
40 [Symbol.iterator](): Generator<SfnEffect<A, E>, Expr<A>, any>;
41}
42
43/** The success type of an {@link SfnEffect}. */
44export type Success<T> = T extends SfnEffect<infer A, any> ? A : never;
45/** The error type of an {@link SfnEffect}. */
46export type Error<T> = T extends SfnEffect<any, infer E> ? E : never;
47
48const variance = {
49 _A: undefined as never,
50 _E: undefined as never,
51};
52
53class SfnEffectImpl implements SfnEffect<any, any> {
54 readonly [SfnTypeId] = variance;
55 constructor(readonly node: AslNode) {}
56 pipe() {
57 // eslint-disable-next-line prefer-rest-params
58 return pipeArguments(this, arguments);
59 }
60 *[Symbol.iterator](): Generator<SfnEffect<any, any>, Expr<any>, any> {
61 // yield this program once; the tracer (compile/simulate) resumes the
62 // generator with a typed variable reference to its result
63 return yield this;
64 }
65}
66
67/** Construct an {@link SfnEffect} from an AST node. */
68export const make = <A, E = never>(node: AslNode): SfnEffect<A, E> =>
69 new SfnEffectImpl(node) as SfnEffect<A, E>;
70
71/** Runtime guard for {@link SfnEffect} values. */
72export const isSfnEffect = (value: unknown): value is SfnEffect<any, any> =>
73 (typeof value === "object" || typeof value === "function") &&
74 value !== null &&
75 SfnTypeId in value;
76
77/**
78 * Write a Step Functions program with generator syntax — the `Sfn`
79 * counterpart of `Effect.gen`. Each `yield*` of an `SfnEffect` resumes with
80 * a **typed reference** ({@link Expr}) to that step's result, usable in
81 * later payloads and conditions; the compiler turns the sequence into
82 * `Next`-chained ASL states.
83 *
84 * The body receives a typed reference to the execution input. Annotate the
85 * parameter to type it:
86 *
87 * ```typescript
88 * const program = Sfn.gen(function* (input: Sfn.Expr<{ orderId: string }>) {
89 * const order = yield* Sfn.invoke<Order>(getOrder, { id: input.orderId });
90 * return { total: order.total };
91 * });
92 * ```
93 *
94 * The generator is a *trace*, not a running program: it executes at compile
95 * time with placeholder references, so data-dependent JS control flow over
96 * step results is unsound — use `Sfn.when` / `Sfn.match` for branching.
97 * Yielding a real `Effect` is a type error by design.
98 */
99export const gen = <Y extends SfnEffect<any, any>, R, In = any>(
100 body: (input: Expr<In>) => Generator<Y, R, any>,
101): SfnEffect<UnwrapExpr<R>, [Y] extends [never] ? never : Error<Y>> =>
102 make({ kind: "gen", body: body as GenBody });
103
104type GenBody = (input: Expr<any>) => Generator<SfnEffect<any, any>, any, any>;