| 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 | */ |
| 12 | import type { NonEmptyReadonlyArray } from "./Array.ts" |
| 13 | import type * as Cause from "./Cause.ts" |
| 14 | import * as Context from "./Context.ts" |
| 15 | import type * as Duration from "./Duration.ts" |
| 16 | import type * as Effect from "./Effect.ts" |
| 17 | import { constant } from "./Function.ts" |
| 18 | import * as effect from "./internal/effect.ts" |
| 19 | import * as Layer from "./Layer.ts" |
| 20 | import type { Pipeable } from "./Pipeable.ts" |
| 21 | import { pipeArguments } from "./Pipeable.ts" |
| 22 | import * as Predicate from "./Predicate.ts" |
| 23 | import 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 | */ |
| 32 | export 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 | */ |
| 41 | export 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 | */ |
| 64 | export 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 | */ |
| 90 | export 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 | */ |
| 138 | export 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 | */ |
| 169 | export 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 | */ |
| 206 | export 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 | |
| 285 | const 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 | |
| 306 | const 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 | */ |
| 337 | export 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 | */ |
| 357 | export 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 | */ |
| 374 | export 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 | */ |
| 394 | export 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 | */ |
| 412 | export 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 | */ |
| 433 | export 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 | */ |
| 454 | export type Event<E> = AttemptStart | AttemptSuccess | AttemptFailure<E> |