1import * as Context from "effect/Context";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4
5/**
6 * Retry policy for a durable step. Maps onto the Durable Execution SDK's
7 * `retryStrategy` — the delay grows per the configured backoff between
8 * attempts, and the step fails the execution once `limit` retries are
9 * exhausted.
10 */
11export interface DurableRetryPolicy {
12 /**
13 * Maximum number of RETRIES after the first attempt (a step with
14 * `limit: 2` runs at most 3 times).
15 */
16 limit: number;
17 /**
18 * Delay before the first retry.
19 * @default 1 second
20 */
21 delay?: Duration.Input;
22 /**
23 * Backoff shape applied to `delay` across attempts.
24 * @default "exponential"
25 */
26 backoff?: "constant" | "linear" | "exponential";
27 /**
28 * Upper bound on the computed delay.
29 */
30 maxDelay?: Duration.Input;
31}
32
33/**
34 * Configuration for a durable {@link step}.
35 */
36export interface DurableStepConfig {
37 /**
38 * Retry policy applied when the step's effect dies. Omit for the SDK's
39 * default behavior.
40 */
41 retry?: DurableRetryPolicy;
42 /**
43 * Checkpoint semantics for the step body.
44 *
45 * - `"at-least-once"` (default) — the body may re-run if the invocation is
46 * interrupted before the checkpoint lands; the body must be idempotent.
47 * - `"at-most-once"` — the step is checkpointed as started before the body
48 * runs; an interruption surfaces as a `StepInterruptedError` instead of a
49 * silent re-run.
50 */
51 semantics?: "at-least-once" | "at-most-once";
52}
53
54/**
55 * Configuration for {@link waitForCallback}.
56 */
57export interface DurableCallbackConfig {
58 /**
59 * How long to wait for the external system to complete the callback before
60 * the operation fails.
61 */
62 timeout?: Duration.Input;
63 /**
64 * Maximum silence between `SendDurableExecutionCallbackHeartbeat` calls
65 * before the callback is considered lost.
66 */
67 heartbeatTimeout?: Duration.Input;
68}
69
70/**
71 * Internal descriptor passed from {@link step} to the bridge. Bundles the
72 * step name and (fully-provided) Effect together with the step config.
73 */
74export interface DurableStepOptions<
75 Output = unknown,
76> extends DurableStepConfig {
77 name: string;
78 effect: Effect.Effect<Output>;
79}
80
81/**
82 * Internal descriptor passed from {@link waitForCallback} to the bridge.
83 */
84export interface DurableCallbackOptions extends DurableCallbackConfig {
85 name: string;
86 /**
87 * Runs exactly once (checkpointed) with the callback id; hand the id to the
88 * external system that will complete the callback via
89 * `SendDurableExecutionCallbackSuccess`/`Failure`.
90 */
91 submitter: (callbackId: string) => Effect.Effect<void>;
92}
93
94/**
95 * Internal service that wraps the AWS Durable Execution SDK's
96 * `DurableContext`. Not accessed directly by users — use {@link step},
97 * {@link sleep}, and {@link waitForCallback} instead.
98 */
99export class DurableStep extends Context.Service<
100 DurableStep,
101 {
102 step<T>(options: DurableStepOptions<T>): Effect.Effect<T>;
103 wait(name: string, duration: Duration.Input): Effect.Effect<void>;
104 waitForCallback<T>(options: DurableCallbackOptions): Effect.Effect<T>;
105 }
106>()("AWS.Lambda.DurableStep") {}
107
108/**
109 * Runtime information about the current durable execution.
110 * `yield* DurableExecutionContext` inside a durable function body.
111 */
112export class DurableExecutionContext extends Context.Service<
113 DurableExecutionContext,
114 {
115 /**
116 * ARN uniquely identifying this durable execution instance. Stable across
117 * every suspend/resume invocation of the same execution.
118 */
119 executionArn: string;
120 }
121>()("AWS.Lambda.DurableExecutionContext") {}
122
123/**
124 * Execute a named, durable step. The effect runs inside the AWS Durable
125 * Execution checkpoint protocol: its result is persisted after first
126 * completion and replayed (without re-executing) on every subsequent
127 * invocation of the execution.
128 *
129 * Any services the inner effect requires (e.g. binding clients resolved in
130 * the durable function's init phase) are threaded through automatically by
131 * capturing the surrounding body's context and providing it to the inner
132 * effect before it is handed to the SDK.
133 *
134 * Determinism law: all effectful/non-deterministic work (time, randomness,
135 * I/O, SDK calls) must live INSIDE a step — code between steps re-runs on
136 * every replay and must be a pure function of the input and prior step
137 * results.
138 */
139export function step<T, R = never>(
140 name: string,
141 effect: Effect.Effect<T, never, R>,
142 config?: DurableStepConfig,
143): Effect.Effect<T, never, DurableStep | R> {
144 return Effect.gen(function* () {
145 const durable = yield* DurableStep;
146 const context = yield* Effect.context<R>();
147 return yield* durable.step({
148 ...config,
149 name,
150 effect: effect.pipe(Effect.provide(context)),
151 });
152 });
153}
154
155/**
156 * Pause the durable execution for the given duration. The invocation is
157 * checkpointed and terminated — no compute is billed while suspended — and
158 * Lambda re-invokes the function when the timer fires.
159 */
160export const sleep = (
161 name: string,
162 duration: Duration.Input,
163): Effect.Effect<void, never, DurableStep> =>
164 Effect.gen(function* () {
165 const durable = yield* DurableStep;
166 yield* durable.wait(name, duration);
167 });
168
169/**
170 * Suspend the durable execution until an external system completes the
171 * callback via `SendDurableExecutionCallbackSuccess` (or fails it via
172 * `...Failure`). The `submitter` effect runs exactly once (checkpointed) and
173 * receives the callback id to hand to the external system.
174 */
175export const waitForCallback = <T = unknown, R = never>(
176 name: string,
177 submitter: (callbackId: string) => Effect.Effect<void, never, R>,
178 config?: DurableCallbackConfig,
179): Effect.Effect<T, never, DurableStep | R> =>
180 Effect.gen(function* () {
181 const durable = yield* DurableStep;
182 const context = yield* Effect.context<R>();
183 return yield* durable.waitForCallback<T>({
184 ...config,
185 name,
186 submitter: (callbackId) =>
187 submitter(callbackId).pipe(Effect.provide(context)),
188 });
189 });