1/**
2 * Describes ordered fallback steps for running effects or streams.
3 *
4 * An `ExecutionPlan` contains one or more steps. Each step provides a `Context`
5 * or `Layer`, and may also define attempt limits, retry schedules, or
6 * predicates that decide whether to keep trying. The runtime tries steps in
7 * order until the workflow succeeds or the plan is exhausted. This module also
8 * supports merging plans and reading metadata for the active step and attempt.
9 *
10 * @since 3.16.0
11 */
12import type { NonEmptyReadonlyArray } from "./Array.ts"
13import type * as Cause from "./Cause.ts"
14import * as Context from "./Context.ts"
15import type * as Duration from "./Duration.ts"
16import type * as Effect from "./Effect.ts"
17import { constant } from "./Function.ts"
18import * as effect from "./internal/effect.ts"
19import * as Layer from "./Layer.ts"
20import type { Pipeable } from "./Pipeable.ts"
21import { pipeArguments } from "./Pipeable.ts"
22import * as Predicate from "./Predicate.ts"
23import type * as Schedule from "./Schedule.ts"
24
25/**
26 * String literal type used as the runtime type identifier for `ExecutionPlan`
27 * values.
28 *
29 * @category type IDs
30 * @since 3.16.0
31 */
32export type TypeId = "~effect/ExecutionPlan"
33
34/**
35 * Runtime type identifier attached to `ExecutionPlan` values and used by
36 * `isExecutionPlan`.
37 *
38 * @category type IDs
39 * @since 3.16.0
40 */
41export const TypeId: TypeId = "~effect/ExecutionPlan"
42
43/**
44 * Returns `true` if a value is an `ExecutionPlan` by checking for the
45 * `ExecutionPlan.TypeId` marker.
46 *
47 * **When to use**
48 *
49 * Use when accepting an unknown value and you need to narrow it to an
50 * `ExecutionPlan` before reading plan fields or passing it to plan-consuming
51 * APIs.
52 *
53 * **Gotchas**
54 *
55 * This is a structural marker check; it does not validate the marker value or
56 * the shape of the plan steps.
57 *
58 * @see {@link make} for constructing execution plans that satisfy this guard
59 * @see {@link TypeId} for the runtime marker checked by this guard
60 *
61 * @category guards
62 * @since 3.16.0
63 */
64export const isExecutionPlan = (u: unknown): u is ExecutionPlan<any> => Predicate.hasProperty(u, TypeId)
65
66/**
67 * A `ExecutionPlan` can be used with `Effect.withExecutionPlan` or `Stream.withExecutionPlan`, allowing you to provide different resources for each step of execution until the effect succeeds or the plan is exhausted.
68 *
69 * **Example** (Defining fallback execution steps)
70 *
71 * ```ts import.meta.vitest
72 * import { Context, ExecutionPlan } from "effect"
73 *
74 * const ThePlan = ExecutionPlan.make(
75 * {
76 * provide: Context.empty(),
77 * attempts: 2
78 * },
79 * {
80 * provide: Context.empty()
81 * }
82 * )
83 *
84 * ThePlan.steps.map((step) => step.attempts ?? 1) // => [2, 1]
85 * ```
86 *
87 * @category models
88 * @since 3.16.0
89 */
90export interface ExecutionPlan<
91 Config extends {
92 provides: any
93 input: any
94 error: any
95 requirements: any
96 }
97> extends Pipeable {
98 readonly [TypeId]: TypeId
99 readonly steps: NonEmptyReadonlyArray<{
100 readonly provide:
101 | Context.Context<Config["provides"]>
102 | Layer.Layer<Config["provides"], Config["error"], Config["requirements"]>
103 readonly attempts?: number | undefined
104 readonly while?:
105 | ((input: Config["input"]) => Effect.Effect<boolean, Config["error"], Config["requirements"]>)
106 | undefined
107 readonly schedule?: Schedule.Schedule<any, Config["input"], Config["requirements"]> | undefined
108 }>
109
110 /**
111 * Returns an equivalent `ExecutionPlan` with the requirements satisfied, using the current context.
112 */
113 readonly captureRequirements: Effect.Effect<
114 ExecutionPlan<{
115 provides: Config["provides"]
116 input: Config["input"]
117 error: Config["error"]
118 requirements: never
119 }>,
120 never,
121 Config["requirements"]
122 >
123}
124
125/**
126 * Base type-level configuration carried by an `ExecutionPlan`.
127 *
128 * **Details**
129 *
130 * `provides` tracks services supplied by plan steps, `input` tracks the error
131 * input consumed by schedules and `while` predicates, `error` tracks failures
132 * from plan layers or predicates, and `requirements` tracks services needed to
133 * build or run the plan.
134 *
135 * @category models
136 * @since 4.0.0
137 */
138export type ConfigBase = {
139 provides: any
140 input: any
141 error: any
142 requirements: any
143}
144
145/**
146 * Create an `ExecutionPlan`, which can be used with `Effect.withExecutionPlan` or `Stream.withExecutionPlan`, allowing you to provide different resources for each step of execution until the effect succeeds or the plan is exhausted.
147 *
148 * **Example** (Creating an execution plan)
149 *
150 * ```ts import.meta.vitest
151 * import { Context, ExecutionPlan } from "effect"
152 *
153 * const ThePlan = ExecutionPlan.make(
154 * {
155 * provide: Context.empty(),
156 * attempts: 2
157 * },
158 * {
159 * provide: Context.empty()
160 * }
161 * )
162 *
163 * ThePlan.steps.length // => 2
164 * ```
165 *
166 * @category constructors
167 * @since 3.16.0
168 */
169export const make = <const Steps extends NonEmptyReadonlyArray<make.Step>>(
170 ...steps: Steps & { [K in keyof Steps]: make.Step }
171): ExecutionPlan<{
172 provides: make.StepProvides<Steps>
173 input: make.StepInput<Steps>
174 error:
175 | (Steps[number]["provide"] extends Context.Context<infer _P> | Layer.Layer<infer _P, infer E, infer _R> ? E
176 : never)
177 | (Steps[number]["while"] extends (input: infer _I) => Effect.Effect<infer _A, infer _E, infer _R> ? _E : never)
178 requirements:
179 | (Steps[number]["provide"] extends Layer.Layer<infer _A, infer _E, infer R> ? R : never)
180 | (Steps[number]["while"] extends (input: infer _I) => Effect.Effect<infer _A, infer _E, infer R> ? R : never)
181 | (Steps[number]["schedule"] extends Schedule.Schedule<infer _O, infer _I, infer R> ? R : never)
182}> =>
183 makeProto(steps.map((options, i) => {
184 if (options.attempts !== undefined && options.attempts < 1) {
185 throw new Error(`ExecutionPlan.make: step[${i}].attempts must be greater than 0`)
186 }
187 return {
188 schedule: options.schedule,
189 attempts: options.attempts,
190 while: options.while
191 ? (input: any) =>
192 effect.suspend(() => {
193 const result = options.while!(input)
194 return typeof result === "boolean" ? effect.succeed(result) : result
195 })
196 : undefined,
197 provide: options.provide
198 }
199 }) as any)
200
201/**
202 * Namespace containing type helpers used by `ExecutionPlan.make`.
203 *
204 * @since 3.16.0
205 */
206export declare namespace make {
207 /**
208 * Input shape for a single execution-plan step.
209 *
210 * **Details**
211 *
212 * Each step provides a `Context` or `Layer` and may limit attempts, add a
213 * `while` predicate for retry decisions, or attach a `Schedule` for retry
214 * timing.
215 *
216 * @category models
217 * @since 3.16.0
218 */
219 export type Step = {
220 readonly provide: Context.Context<any> | Context.Context<never> | Layer.Any
221 readonly attempts?: number | undefined
222 readonly while?: ((input: any) => boolean | Effect.Effect<boolean, any, any>) | undefined
223 readonly schedule?: Schedule.Schedule<any, any, any> | undefined
224 }
225
226 /**
227 * Computes the intersection of services provided by a list of execution-plan
228 * steps.
229 *
230 * @category utility types
231 * @since 3.16.1
232 */
233 export type StepProvides<Steps extends ReadonlyArray<any>, Out = unknown> = Steps extends
234 readonly [infer Step, ...infer Rest] ? StepProvides<
235 Rest,
236 & Out
237 & (
238 (Step extends { readonly provide: Context.Context<infer P> | Layer.Layer<infer P, infer _E, infer _R> } ? P
239 : unknown)
240 )
241 > :
242 Out
243
244 /**
245 * Computes the intersection of services provided by a list of execution plans.
246 *
247 * @category utility types
248 * @since 3.16.1
249 */
250 export type PlanProvides<Plans extends ReadonlyArray<any>, Out = unknown> = Plans extends
251 readonly [infer Plan, ...infer Rest] ?
252 PlanProvides<Rest, Out & (Plan extends ExecutionPlan<infer T> ? T["provides"] : unknown)> :
253 Out
254
255 /**
256 * Computes the input type consumed by the `while` predicates and schedules in
257 * a list of execution-plan steps.
258 *
259 * @category utility types
260 * @since 3.16.0
261 */
262 export type StepInput<Steps extends ReadonlyArray<any>, Out = unknown> = Steps extends
263 readonly [infer Step, ...infer Rest] ? StepInput<
264 Rest,
265 & Out
266 & (
267 & (Step extends { readonly while: (input: infer I) => infer _ } ? I : unknown)
268 & (Step extends { readonly schedule: Schedule.Schedule<infer _O, infer I, infer _R> } ? I : unknown)
269 )
270 > :
271 Out
272
273 /**
274 * Computes the combined input type consumed by a list of execution plans.
275 *
276 * @category utility types
277 * @since 3.16.0
278 */
279 export type PlanInput<Plans extends ReadonlyArray<any>, Out = unknown> = Plans extends
280 readonly [infer Plan, ...infer Rest] ?
281 PlanInput<Rest, Out & (Plan extends ExecutionPlan<infer T> ? T["input"] : unknown)> :
282 Out
283}
284
285const Proto: Omit<ExecutionPlan<any>, "steps"> = {
286 [TypeId]: TypeId,
287 get captureRequirements() {
288 const self = this as any as ExecutionPlan<any>
289 return effect.contextWith((context: Context.Context<any>) =>
290 effect.succeed(makeProto(self.steps.map((step) => ({
291 ...step,
292 while: step.while
293 ? (input: any) => effect.provideContext(step.while!(input), context)
294 : undefined,
295 provide: Layer.isLayer(step.provide)
296 ? Layer.provide(step.provide, Layer.succeedContext(context))
297 : step.provide
298 })) as any))
299 )
300 },
301 pipe() {
302 return pipeArguments(this, arguments)
303 }
304}
305
306const makeProto = <Provides, In, PlanE, PlanR>(
307 steps: ExecutionPlan<{
308 provides: Provides
309 input: In
310 error: PlanE
311 requirements: PlanR
312 }>["steps"]
313) => {
314 const self = Object.create(Proto)
315 self.steps = steps
316 return self
317}
318
319/**
320 * Combines multiple execution plans by concatenating their steps in order.
321 *
322 * **When to use**
323 *
324 * Use to combine separately defined fallback plans into one ordered plan before
325 * applying it to an effect or stream.
326 *
327 * **Details**
328 *
329 * The resulting plan tries every step from the first plan, then every step from
330 * the next plan, and so on.
331 *
332 * @see {@link make} for building a plan from individual steps instead of combining existing plans
333 *
334 * @category combining
335 * @since 3.16.0
336 */
337export const merge = <const Plans extends NonEmptyReadonlyArray<ExecutionPlan<any>>>(
338 ...plans: Plans
339): ExecutionPlan<{
340 provides: make.PlanProvides<Plans>
341 input: make.PlanInput<Plans>
342 error: Plans[number] extends ExecutionPlan<infer T> ? T["error"] : never
343 requirements: Plans[number] extends ExecutionPlan<infer T> ? T["requirements"] : never
344}> => makeProto(plans.flatMap((plan) => plan.steps) as any)
345
346/**
347 * Metadata describing the currently running execution-plan attempt.
348 *
349 * **Details**
350 *
351 * `attempt` is the current 1-based attempt number, and `stepIndex` is the
352 * 0-based index of the plan step currently being evaluated.
353 *
354 * @category metadata
355 * @since 4.0.0
356 */
357export interface Metadata {
358 readonly attempt: number
359 readonly stepIndex: number
360}
361
362/**
363 * Context reference containing metadata for the currently running
364 * execution-plan attempt.
365 *
366 * **When to use**
367 *
368 * Use to read the active plan step and attempt while code is running under an
369 * execution plan.
370 *
371 * @category services
372 * @since 4.0.0
373 */
374export const CurrentMetadata = Context.Reference<Metadata>("effect/ExecutionPlan/CurrentMetadata", {
375 defaultValue: constant({
376 attempt: 0,
377 stepIndex: 0
378 })
379})
380
381/**
382 * Lifecycle event emitted before an execution-plan attempt runs.
383 *
384 * **Details**
385 *
386 * `attempt` is the cumulative 1-based attempt number across all steps and
387 * matches `CurrentMetadata.attempt` for the same attempt. `stepAttempt` is the
388 * 1-based attempt number within the current step, and `stepIndex` is the
389 * 0-based index of the step being attempted.
390 *
391 * @category models
392 * @since 4.0.0
393 */
394export interface AttemptStart {
395 readonly _tag: "AttemptStart"
396 readonly attempt: number
397 readonly stepAttempt: number
398 readonly stepIndex: number
399}
400
401/**
402 * Lifecycle event emitted when an execution-plan attempt succeeds.
403 *
404 * **Details**
405 *
406 * A successful attempt completes the plan, so this is always the final event.
407 * `duration` is the elapsed time of the attempt.
408 *
409 * @category models
410 * @since 4.0.0
411 */
412export interface AttemptSuccess {
413 readonly _tag: "AttemptSuccess"
414 readonly attempt: number
415 readonly stepAttempt: number
416 readonly stepIndex: number
417 readonly duration: Duration.Duration
418}
419
420/**
421 * Lifecycle event emitted when an execution-plan attempt fails.
422 *
423 * **Details**
424 *
425 * `cause` holds the full failure cause, so defects and interruption are
426 * reported as well as expected errors. Whether the plan retries or fails over
427 * afterwards is decided by the step's `attempts`, `while`, and `schedule`; a
428 * following `AttemptStart` indicates another attempt was made.
429 *
430 * @category models
431 * @since 4.0.0
432 */
433export interface AttemptFailure<E> {
434 readonly _tag: "AttemptFailure"
435 readonly attempt: number
436 readonly stepAttempt: number
437 readonly stepIndex: number
438 readonly duration: Duration.Duration
439 readonly cause: Cause.Cause<E>
440}
441
442/**
443 * Union of the lifecycle events emitted while an execution plan runs.
444 *
445 * **Details**
446 *
447 * Every `AttemptStart` is followed by exactly one terminal event, either
448 * `AttemptSuccess` or `AttemptFailure`. An interrupted attempt emits
449 * `AttemptFailure` with the interruption cause.
450 *
451 * @category models
452 * @since 4.0.0
453 */
454export type Event<E> = AttemptStart | AttemptSuccess | AttemptFailure<E>