/** * `SfnEffect` — the yieldable, pipeable Step Functions program type. * * It mirrors Effect's `` channels (there is deliberately no `R` * channel: an ASL program cannot require services — its "requirements" are * the resources its Task states reference, collected structurally at * compile). It is **not** an `Effect`: it is a shallow-embedded AST that * *feels* like Effect (generator syntax, `catchTag` narrowing `E`) but * constructs a data structure that `compile.ts` turns into ASL JSON. * * Yielding a real `Effect` inside `Sfn.gen` is a **type error** — only * `SfnEffect`s (branded with {@link SfnTypeId}) are yieldable. */ import { pipeArguments } from "effect/Pipeable"; import type { Pipeable } from "effect/Pipeable"; import type { Expr, UnwrapExpr } from "./Jsonata.ts"; import type { AslNode } from "./Node.ts"; /** Brand distinguishing `SfnEffect` from `Effect` (and everything else). */ export const SfnTypeId = Symbol.for("alchemy/AWS/StepFunctions/SfnEffect"); export type SfnTypeId = typeof SfnTypeId; /** * A typed Step Functions program producing `A` or failing with a tagged * error `E`. Build with `Sfn.gen` and the `Sfn.*` combinators; compile with * `StateMachine.fromProgram` (or `compileProgram`); run locally with * `simulate`. */ export interface SfnEffect extends Pipeable { readonly [SfnTypeId]: { readonly _A: (_: never) => A; readonly _E: (_: never) => E; }; /** The program AST node this effect wraps. */ readonly node: AslNode; /** * `yield*` protocol for `Sfn.gen`: yields this program once and resumes * with a typed {@link Expr} reference to its result. */ [Symbol.iterator](): Generator, Expr, any>; } /** The success type of an {@link SfnEffect}. */ export type Success = T extends SfnEffect ? A : never; /** The error type of an {@link SfnEffect}. */ export type Error = T extends SfnEffect ? E : never; const variance = { _A: undefined as never, _E: undefined as never, }; class SfnEffectImpl implements SfnEffect { readonly [SfnTypeId] = variance; constructor(readonly node: AslNode) {} pipe() { // eslint-disable-next-line prefer-rest-params return pipeArguments(this, arguments); } *[Symbol.iterator](): Generator, Expr, any> { // yield this program once; the tracer (compile/simulate) resumes the // generator with a typed variable reference to its result return yield this; } } /** Construct an {@link SfnEffect} from an AST node. */ export const make = (node: AslNode): SfnEffect => new SfnEffectImpl(node) as SfnEffect; /** Runtime guard for {@link SfnEffect} values. */ export const isSfnEffect = (value: unknown): value is SfnEffect => (typeof value === "object" || typeof value === "function") && value !== null && SfnTypeId in value; /** * Write a Step Functions program with generator syntax — the `Sfn` * counterpart of `Effect.gen`. Each `yield*` of an `SfnEffect` resumes with * a **typed reference** ({@link Expr}) to that step's result, usable in * later payloads and conditions; the compiler turns the sequence into * `Next`-chained ASL states. * * The body receives a typed reference to the execution input. Annotate the * parameter to type it: * * ```typescript * const program = Sfn.gen(function* (input: Sfn.Expr<{ orderId: string }>) { * const order = yield* Sfn.invoke(getOrder, { id: input.orderId }); * return { total: order.total }; * }); * ``` * * The generator is a *trace*, not a running program: it executes at compile * time with placeholder references, so data-dependent JS control flow over * step results is unsound — use `Sfn.when` / `Sfn.match` for branching. * Yielding a real `Effect` is a type error by design. */ export const gen = , R, In = any>( body: (input: Expr) => Generator, ): SfnEffect, [Y] extends [never] ? never : Error> => make({ kind: "gen", body: body as GenBody }); type GenBody = (input: Expr) => Generator, any, any>;