/** * The `Sfn.*` combinators — Effect-mirroring constructors over the ASL AST. * * Names and semantics track their `Effect` counterparts as closely as the * target language allows: `Sfn.gen`, `Sfn.retry`, `Sfn.catchTag`, * `Sfn.catchAll`, `Sfn.all`, `Sfn.forEach`, `Sfn.sleep`, `Sfn.when`, * `Sfn.match`, `Sfn.succeed`, `Sfn.fail`. Divergences are deliberate: * `retry` takes an option bag mirroring the `Schedule.exponential` * vocabulary (a real `Schedule` is opaque and cannot be introspected into * ASL's `BackoffRate`), and branching goes through `when`/`match` because a * compiled program cannot branch on runtime JS. */ import * as Duration from "effect/Duration"; import { aslSeconds } from "./compile.ts"; import type { Expr, UnwrapExpr } from "./Jsonata.ts"; import type { ErrorOutput, ForEachOptions, IntegrationOptions, InvokableFunction, RetryOptions, } from "./Node.ts"; import { isSfnEffect as isSfn, make, type Error as ErrorOf, type SfnEffect, type Success, } from "./Program.ts"; /** * Invoke a Lambda function as a Task state * (`arn:aws:states:::lambda:invoke`). The payload may embed typed `Expr` * references and resource `Output`s; the result reference is the function's * response payload. * * Compiling the program emits the `lambda:InvokeFunction` policy statement * for the function's exact ARN (and its qualified variants) into the * collected `policyStatements`. */ export const invoke = ( fn: InvokableFunction, payload?: unknown, ): SfnEffect => make({ kind: "invoke", fn, payload }); /** * Call an optimized service integration as a Task state, e.g. * `arn:aws:states:::sqs:sendMessage`. Provide the exact-ARN policy * statements the execution role needs. */ export const integrate = ( options: IntegrationOptions, ): SfnEffect => make({ kind: "integrate", options, waitForTaskToken: false }); /** * Call a service integration with the `.waitForTaskToken` callback pattern: * the state parks until `SendTaskSuccess`/`SendTaskFailure` is called with * the token. Embed {@link taskToken} (re-exported as `Sfn.taskToken`) in the * arguments to hand the token to the callback side; the result is the JSON * given to `SendTaskSuccess`. */ export const waitForTaskToken = ( options: IntegrationOptions, ): SfnEffect => make({ kind: "integrate", options, waitForTaskToken: true }); /** * Run programs in parallel (an ASL `Parallel` state) — mirrors * `Effect.all`. The result is the tuple of branch results. */ export const all = []>( branches: T, ): SfnEffect<{ [K in keyof T]: Success }, ErrorOf> => make({ kind: "all", branches }); /** * Apply `body` to every element of `items` (an ASL inline `Map` state) — * mirrors `Effect.forEach`, including the `concurrency` option * (`MaxConcurrency`). The result is the array of iteration results. */ export const forEach = ( items: Expr | Expr, body: (item: Expr) => SfnEffect, options?: ForEachOptions, ): SfnEffect => make({ kind: "forEach", items: items as Expr, body: body as (item: Expr) => SfnEffect, options: options ?? {}, }); /** * Pause the workflow (an ASL `Wait` state) — mirrors `Effect.sleep`. * Durations round up to whole seconds (ASL's granularity). */ export const sleep = (duration: Duration.Input): SfnEffect => make({ kind: "sleep", seconds: Math.max(1, Math.ceil(Duration.toSeconds(duration))), }); /** * Branch on a typed condition (an ASL `Choice` state) — mirrors * `Effect.if`. Build conditions with the typed comparators (`Sfn.eq`, * `Sfn.gt`, `Sfn.and`, …). When `onFalse` is omitted the false branch * produces `null`. */ export const when = ( condition: Expr, onTrue: SfnEffect, onFalse?: SfnEffect, ): SfnEffect => make({ kind: "when", condition, onTrue, onFalse }); /** * Branch on a value's literal cases (an ASL `Choice` state with one rule * per case) — mirrors `Effect.match`-style dispatch. With no `otherwise` * and no matching case the execution fails with `States.NoChoiceMatched`. */ export const match = < T extends string | number | boolean, const Cases extends Record>, B = never, E2 = never, >( value: Expr, cases: Cases, otherwise?: SfnEffect, ): SfnEffect< Success | B, ErrorOf | E2 > => make({ kind: "match", value, cases, otherwise }); /** * Produce a value (an ASL `Pass` state) — mirrors `Effect.succeed`. The * value may embed typed `Expr` references from earlier steps. */ export const succeed = (value: T): SfnEffect> => make({ kind: "succeed", value }); /** * Fail the workflow with a tagged error (an ASL `Fail` state) — mirrors * `Effect.fail`. The error's `_tag` becomes the ASL `Error` name, so * `Sfn.catchTag(program, tag, …)` and `Sfn.retry({ while: [tag] })` line up * with it end-to-end. */ export const fail = ( error: E, cause?: string, ): SfnEffect => make({ kind: "fail", error: error._tag, cause: cause ?? (error as { message?: string }).message ?? error._tag, failure: error, }); /** * Retry a program on failure (ASL `Retry`) — mirrors `Effect.retry`, with * an option bag in the `Schedule.exponential` vocabulary (`initial`, * `backoff`, `maxAttempts`, `maxDelay`, `jitter`; `while` takes error * tags). Multi-state programs are wrapped in a single-branch `Parallel` * state so the whole program re-runs, matching `Effect.retry` semantics. */ export const retry: { (options: RetryOptions): (self: SfnEffect) => SfnEffect; (self: SfnEffect, options: RetryOptions): SfnEffect; } = (( selfOrOptions: SfnEffect | RetryOptions, options?: RetryOptions, ) => isSfn(selfOrOptions) ? make({ kind: "retry", inner: selfOrOptions, options: options ?? {} }) : (self: SfnEffect) => make({ kind: "retry", inner: self, options: selfOrOptions })) as any; /** * Catch failures by tag (ASL `Catch` with `ErrorEquals`) — mirrors * `Effect.catchTag`, narrowing `E` exactly the same way. The handler * receives a typed reference to the ASL error output (`{ Error, Cause }`). */ export const catchTag: { ( tag: Tag | readonly Tag[], handler: (error: Expr) => SfnEffect, ): ( self: SfnEffect, ) => SfnEffect | E2>; ( self: SfnEffect, tag: Tag | readonly Tag[], handler: (error: Expr) => SfnEffect, ): SfnEffect | E2>; } = ((...args: any[]) => isSfn(args[0]) ? make({ kind: "catch", inner: args[0], tags: Array.isArray(args[1]) ? args[1] : [args[1]], handler: args[2], }) : (self: SfnEffect) => make({ kind: "catch", inner: self, tags: Array.isArray(args[0]) ? args[0] : [args[0]], handler: args[1], })) as any; /** * Catch every failure (ASL `Catch` with `States.ALL`) — mirrors * `Effect.catchAll`. */ export const catchAll: { ( handler: (error: Expr) => SfnEffect, ): (self: SfnEffect) => SfnEffect; ( self: SfnEffect, handler: (error: Expr) => SfnEffect, ): SfnEffect; } = ((...args: any[]) => isSfn(args[0]) ? make({ kind: "catch", inner: args[0], tags: ["States.ALL"], handler: args[1], }) : (self: SfnEffect) => make({ kind: "catch", inner: self, tags: ["States.ALL"], handler: args[0], })) as any; /** The `States.*` built-in error names, for `retry`/`catchTag`. */ export const Errors = { ALL: "States.ALL", Timeout: "States.Timeout", TaskFailed: "States.TaskFailed", Permissions: "States.Permissions", BranchFailed: "States.BranchFailed", NoChoiceMatched: "States.NoChoiceMatched", IntrinsicFailure: "States.IntrinsicFailure", ExceedToleratedFailureThreshold: "States.ExceedToleratedFailureThreshold", ItemReaderFailed: "States.ItemReaderFailed", ResultWriterFailed: "States.ResultWriterFailed", HeartbeatTimeout: "States.HeartbeatTimeout", QueryEvaluationError: "States.QueryEvaluationError", Runtime: "States.Runtime", } as const;