1/**
2 * The `Sfn.*` combinators — Effect-mirroring constructors over the ASL AST.
3 *
4 * Names and semantics track their `Effect` counterparts as closely as the
5 * target language allows: `Sfn.gen`, `Sfn.retry`, `Sfn.catchTag`,
6 * `Sfn.catchAll`, `Sfn.all`, `Sfn.forEach`, `Sfn.sleep`, `Sfn.when`,
7 * `Sfn.match`, `Sfn.succeed`, `Sfn.fail`. Divergences are deliberate:
8 * `retry` takes an option bag mirroring the `Schedule.exponential`
9 * vocabulary (a real `Schedule` is opaque and cannot be introspected into
10 * ASL's `BackoffRate`), and branching goes through `when`/`match` because a
11 * compiled program cannot branch on runtime JS.
12 */
13import * as Duration from "effect/Duration";
14import { aslSeconds } from "./compile.ts";
15import type { Expr, UnwrapExpr } from "./Jsonata.ts";
16import type {
17 ErrorOutput,
18 ForEachOptions,
19 IntegrationOptions,
20 InvokableFunction,
21 RetryOptions,
22} from "./Node.ts";
23import {
24 isSfnEffect as isSfn,
25 make,
26 type Error as ErrorOf,
27 type SfnEffect,
28 type Success,
29} from "./Program.ts";
30
31/**
32 * Invoke a Lambda function as a Task state
33 * (`arn:aws:states:::lambda:invoke`). The payload may embed typed `Expr`
34 * references and resource `Output`s; the result reference is the function's
35 * response payload.
36 *
37 * Compiling the program emits the `lambda:InvokeFunction` policy statement
38 * for the function's exact ARN (and its qualified variants) into the
39 * collected `policyStatements`.
40 */
41export const invoke = <A = unknown, E = never>(
42 fn: InvokableFunction,
43 payload?: unknown,
44): SfnEffect<A, E> => make({ kind: "invoke", fn, payload });
45
46/**
47 * Call an optimized service integration as a Task state, e.g.
48 * `arn:aws:states:::sqs:sendMessage`. Provide the exact-ARN policy
49 * statements the execution role needs.
50 */
51export const integrate = <A = unknown, E = never>(
52 options: IntegrationOptions,
53): SfnEffect<A, E> =>
54 make({ kind: "integrate", options, waitForTaskToken: false });
55
56/**
57 * Call a service integration with the `.waitForTaskToken` callback pattern:
58 * the state parks until `SendTaskSuccess`/`SendTaskFailure` is called with
59 * the token. Embed {@link taskToken} (re-exported as `Sfn.taskToken`) in the
60 * arguments to hand the token to the callback side; the result is the JSON
61 * given to `SendTaskSuccess`.
62 */
63export const waitForTaskToken = <A = unknown, E = never>(
64 options: IntegrationOptions,
65): SfnEffect<A, E> =>
66 make({ kind: "integrate", options, waitForTaskToken: true });
67
68/**
69 * Run programs in parallel (an ASL `Parallel` state) — mirrors
70 * `Effect.all`. The result is the tuple of branch results.
71 */
72export const all = <const T extends readonly SfnEffect<any, any>[]>(
73 branches: T,
74): SfnEffect<{ [K in keyof T]: Success<T[K]> }, ErrorOf<T[number]>> =>
75 make({ kind: "all", branches });
76
77/**
78 * Apply `body` to every element of `items` (an ASL inline `Map` state) —
79 * mirrors `Effect.forEach`, including the `concurrency` option
80 * (`MaxConcurrency`). The result is the array of iteration results.
81 */
82export const forEach = <Item, A, E>(
83 items: Expr<readonly Item[]> | Expr<Item[]>,
84 body: (item: Expr<Item>) => SfnEffect<A, E>,
85 options?: ForEachOptions,
86): SfnEffect<A[], E> =>
87 make({
88 kind: "forEach",
89 items: items as Expr<any>,
90 body: body as (item: Expr<any>) => SfnEffect<any, any>,
91 options: options ?? {},
92 });
93
94/**
95 * Pause the workflow (an ASL `Wait` state) — mirrors `Effect.sleep`.
96 * Durations round up to whole seconds (ASL's granularity).
97 */
98export const sleep = (duration: Duration.Input): SfnEffect<void> =>
99 make({
100 kind: "sleep",
101 seconds: Math.max(1, Math.ceil(Duration.toSeconds(duration))),
102 });
103
104/**
105 * Branch on a typed condition (an ASL `Choice` state) — mirrors
106 * `Effect.if`. Build conditions with the typed comparators (`Sfn.eq`,
107 * `Sfn.gt`, `Sfn.and`, …). When `onFalse` is omitted the false branch
108 * produces `null`.
109 */
110export const when = <A, E, B = null, E2 = never>(
111 condition: Expr<boolean>,
112 onTrue: SfnEffect<A, E>,
113 onFalse?: SfnEffect<B, E2>,
114): SfnEffect<A | B, E | E2> =>
115 make({ kind: "when", condition, onTrue, onFalse });
116
117/**
118 * Branch on a value's literal cases (an ASL `Choice` state with one rule
119 * per case) — mirrors `Effect.match`-style dispatch. With no `otherwise`
120 * and no matching case the execution fails with `States.NoChoiceMatched`.
121 */
122export const match = <
123 T extends string | number | boolean,
124 const Cases extends Record<string, SfnEffect<any, any>>,
125 B = never,
126 E2 = never,
127>(
128 value: Expr<T>,
129 cases: Cases,
130 otherwise?: SfnEffect<B, E2>,
131): SfnEffect<
132 Success<Cases[keyof Cases]> | B,
133 ErrorOf<Cases[keyof Cases]> | E2
134> => make({ kind: "match", value, cases, otherwise });
135
136/**
137 * Produce a value (an ASL `Pass` state) — mirrors `Effect.succeed`. The
138 * value may embed typed `Expr` references from earlier steps.
139 */
140export const succeed = <const T>(value: T): SfnEffect<UnwrapExpr<T>> =>
141 make({ kind: "succeed", value });
142
143/**
144 * Fail the workflow with a tagged error (an ASL `Fail` state) — mirrors
145 * `Effect.fail`. The error's `_tag` becomes the ASL `Error` name, so
146 * `Sfn.catchTag(program, tag, …)` and `Sfn.retry({ while: [tag] })` line up
147 * with it end-to-end.
148 */
149export const fail = <E extends { readonly _tag: string }>(
150 error: E,
151 cause?: string,
152): SfnEffect<never, E> =>
153 make({
154 kind: "fail",
155 error: error._tag,
156 cause: cause ?? (error as { message?: string }).message ?? error._tag,
157 failure: error,
158 });
159
160/**
161 * Retry a program on failure (ASL `Retry`) — mirrors `Effect.retry`, with
162 * an option bag in the `Schedule.exponential` vocabulary (`initial`,
163 * `backoff`, `maxAttempts`, `maxDelay`, `jitter`; `while` takes error
164 * tags). Multi-state programs are wrapped in a single-branch `Parallel`
165 * state so the whole program re-runs, matching `Effect.retry` semantics.
166 */
167export const retry: {
168 (options: RetryOptions): <A, E>(self: SfnEffect<A, E>) => SfnEffect<A, E>;
169 <A, E>(self: SfnEffect<A, E>, options: RetryOptions): SfnEffect<A, E>;
170} = ((
171 selfOrOptions: SfnEffect<any, any> | RetryOptions,
172 options?: RetryOptions,
173) =>
174 isSfn(selfOrOptions)
175 ? make({ kind: "retry", inner: selfOrOptions, options: options ?? {} })
176 : (self: SfnEffect<any, any>) =>
177 make({ kind: "retry", inner: self, options: selfOrOptions })) as any;
178
179/**
180 * Catch failures by tag (ASL `Catch` with `ErrorEquals`) — mirrors
181 * `Effect.catchTag`, narrowing `E` exactly the same way. The handler
182 * receives a typed reference to the ASL error output (`{ Error, Cause }`).
183 */
184export const catchTag: {
185 <Tag extends string, A2, E2>(
186 tag: Tag | readonly Tag[],
187 handler: (error: Expr<ErrorOutput>) => SfnEffect<A2, E2>,
188 ): <A, E>(
189 self: SfnEffect<A, E>,
190 ) => SfnEffect<A | A2, Exclude<E, { _tag: Tag }> | E2>;
191 <A, E, Tag extends string, A2, E2>(
192 self: SfnEffect<A, E>,
193 tag: Tag | readonly Tag[],
194 handler: (error: Expr<ErrorOutput>) => SfnEffect<A2, E2>,
195 ): SfnEffect<A | A2, Exclude<E, { _tag: Tag }> | E2>;
196} = ((...args: any[]) =>
197 isSfn(args[0])
198 ? make({
199 kind: "catch",
200 inner: args[0],
201 tags: Array.isArray(args[1]) ? args[1] : [args[1]],
202 handler: args[2],
203 })
204 : (self: SfnEffect<any, any>) =>
205 make({
206 kind: "catch",
207 inner: self,
208 tags: Array.isArray(args[0]) ? args[0] : [args[0]],
209 handler: args[1],
210 })) as any;
211
212/**
213 * Catch every failure (ASL `Catch` with `States.ALL`) — mirrors
214 * `Effect.catchAll`.
215 */
216export const catchAll: {
217 <A2, E2>(
218 handler: (error: Expr<ErrorOutput>) => SfnEffect<A2, E2>,
219 ): <A, E>(self: SfnEffect<A, E>) => SfnEffect<A | A2, E2>;
220 <A, E, A2, E2>(
221 self: SfnEffect<A, E>,
222 handler: (error: Expr<ErrorOutput>) => SfnEffect<A2, E2>,
223 ): SfnEffect<A | A2, E2>;
224} = ((...args: any[]) =>
225 isSfn(args[0])
226 ? make({
227 kind: "catch",
228 inner: args[0],
229 tags: ["States.ALL"],
230 handler: args[1],
231 })
232 : (self: SfnEffect<any, any>) =>
233 make({
234 kind: "catch",
235 inner: self,
236 tags: ["States.ALL"],
237 handler: args[0],
238 })) as any;
239
240/** The `States.*` built-in error names, for `retry`/`catchTag`. */
241export const Errors = {
242 ALL: "States.ALL",
243 Timeout: "States.Timeout",
244 TaskFailed: "States.TaskFailed",
245 Permissions: "States.Permissions",
246 BranchFailed: "States.BranchFailed",
247 NoChoiceMatched: "States.NoChoiceMatched",
248 IntrinsicFailure: "States.IntrinsicFailure",
249 ExceedToleratedFailureThreshold: "States.ExceedToleratedFailureThreshold",
250 ItemReaderFailed: "States.ItemReaderFailed",
251 ResultWriterFailed: "States.ResultWriterFailed",
252 HeartbeatTimeout: "States.HeartbeatTimeout",
253 QueryEvaluationError: "States.QueryEvaluationError",
254 Runtime: "States.Runtime",
255} as const;