| 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 | */ |
| 14 | import { pipeArguments } from "effect/Pipeable"; |
| 15 | import type { Pipeable } from "effect/Pipeable"; |
| 16 | import type { Expr, UnwrapExpr } from "./Jsonata.ts"; |
| 17 | import type { AslNode } from "./Node.ts"; |
| 18 | |
| 19 | /** Brand distinguishing `SfnEffect` from `Effect` (and everything else). */ |
| 20 | export const SfnTypeId = Symbol.for("alchemy/AWS/StepFunctions/SfnEffect"); |
| 21 | export 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 | */ |
| 29 | export 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}. */ |
| 44 | export type Success<T> = T extends SfnEffect<infer A, any> ? A : never; |
| 45 | /** The error type of an {@link SfnEffect}. */ |
| 46 | export type Error<T> = T extends SfnEffect<any, infer E> ? E : never; |
| 47 | |
| 48 | const variance = { |
| 49 | _A: undefined as never, |
| 50 | _E: undefined as never, |
| 51 | }; |
| 52 | |
| 53 | class 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. */ |
| 68 | export 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. */ |
| 72 | export 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 | */ |
| 99 | export 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 | |
| 104 | type GenBody = (input: Expr<any>) => Generator<SfnEffect<any, any>, any, any>; |