/** * Describes policies for retrying, repeating, and pacing Effect programs. * * A `Schedule` is stepped with an input value. Each * step either stops or produces an output together with the delay before the * next step. Schedules are used by retry, repeat, stream, and channel APIs to * decide when work should continue, how long to wait, and when to stop. * * @stability stable * @since 2.0.0 */ import type { NonEmptyReadonlyArray } from "./Array.ts" import * as Cause from "./Cause.ts" import * as Context from "./Context.ts" import * as Cron from "./Cron.ts" import type * as DateTime from "./DateTime.ts" import * as Duration from "./Duration.ts" import type { Effect } from "./Effect.ts" import { constant, dual, identity } from "./Function.ts" import { isEffect } from "./internal/core.ts" import * as effect from "./internal/effect.ts" import * as random from "./internal/random.ts" import { type Pipeable, pipeArguments } from "./Pipeable.ts" import { hasProperty } from "./Predicate.ts" import * as Pull from "./Pull.ts" import * as Result from "./Result.ts" import type { Contravariant, Covariant, Mutable, UnionToIntersection } from "./Types.ts" const TypeId = "~effect/Schedule" const randomNext: Effect = random.Random.useSync((random) => random.nextDoubleUnsafe()) /** * A Schedule defines a strategy for repeating or retrying effects based on some policy. * * **Example** (Defining retry and repeat schedules) * * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" * import { TestClock } from "effect/testing" * * const executions: Array = [] * const program = Effect.sync(() => executions.push(executions.length + 1)).pipe( * Effect.repeat(Schedule.recurs(2)), * Effect.as(executions) * ) * * await Effect.runPromise(Effect.provide(program, TestClock.layer())) // => [1, 2, 3] * ``` * * @stability stable * @category models * @since 2.0.0 */ export interface Schedule extends Schedule.Variance, Pipeable {} /** * Metadata provided to schedule functions containing timing and input information. * * @stability stable * @category metadata * @since 4.0.0 */ export interface InputMetadata { readonly input: Input readonly attempt: number readonly start: number readonly now: number readonly elapsed: number readonly elapsedSincePrevious: number } /** * Extended metadata that includes both input metadata and the output value from the schedule. * * @stability stable * @category metadata * @since 4.0.0 */ export interface Metadata extends InputMetadata { readonly output: Output readonly duration: Duration.Duration } /** * Context reference containing metadata for the currently running schedule step. * * **Details** * * Repeat, retry, stream, and channel scheduling operations provide this service * to effects run between schedule steps. The default value contains undefined * input and output values, zero duration, and zeroed timing fields before any * schedule step has produced metadata. * * @stability stable * @category services * @since 4.0.0 */ export const CurrentMetadata = Context.Reference("effect/Schedule/CurrentMetadata", { defaultValue: constant({ input: undefined, output: undefined, duration: Duration.zero, attempt: 0, start: 0, now: 0, elapsed: 0, elapsedSincePrevious: 0 }) }) /** * The Schedule namespace contains types and utilities for working with schedules. * * @stability stable * @since 2.0.0 */ export declare namespace Schedule { /** * Variance interface that defines the type parameter relationships for Schedule. * * **Example** (Understanding schedule variance) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * const schedule: Schedule.Schedule = Schedule.recurs(2) * Schedule.isSchedule(schedule) // => true * ``` * * @category models * @since 2.0.0 */ export interface Variance { readonly [TypeId]: VarianceStruct } /** * Type-level marker used by `Schedule.Variance` to record the variance of * `Schedule` type parameters. * * **Details** * * This interface exists for TypeScript inference and assignability. Users * normally do not construct or inspect it directly. * * @category models * @since 4.0.0 */ export interface VarianceStruct { readonly _Out: Covariant readonly _In: Contravariant readonly _Error: Covariant readonly _Env: Covariant } } /** * Extracts the output type from a `Schedule`. * * @stability stable * @category utility types * @since 4.0.0 */ export type Output = S extends Schedule ? Output : never /** * Extracts the input type from a `Schedule`. * * @stability stable * @category utility types * @since 4.0.0 */ export type Input = S extends Schedule ? Input : never /** * Extracts the error type from a `Schedule`. * * @stability stable * @category utility types * @since 4.0.0 */ export type Error = S extends Schedule ? Error : never /** * Extracts the service requirements from a `Schedule`. * * @stability stable * @category utility types * @since 4.0.0 */ export type Env = S extends Schedule ? Env : never const ScheduleProto = { [TypeId]: { _Out: identity, _In: identity, _Env: identity }, pipe() { return pipeArguments(this, arguments) } } /** * Type guard that checks if a value is a Schedule. * * **Example** (Checking for schedules) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * const schedule = Schedule.exponential("100 millis") * const notSchedule = { foo: "bar" } * * Schedule.isSchedule(schedule) // => true * Schedule.isSchedule(notSchedule) // => false * Schedule.isSchedule(null) // => false * Schedule.isSchedule(undefined) // => false * ``` * * @stability stable * @category guards * @since 2.0.0 */ export const isSchedule = (u: unknown): u is Schedule => hasProperty(u, TypeId) /** * Creates a Schedule from a step function that returns a Pull. * * **Example** (Creating a custom schedule from a step function) * * ```ts import.meta.vitest * import { Cause, Duration, Effect, Schedule } from "effect" * * const schedule = Schedule.fromStep(Effect.sync(() => { * let count = 0 * * return (_now: number, _input: string) => { * if (count >= 3) { * return Cause.done(count) * } * return Effect.succeed([count++, Duration.millis(100)] as [number, Duration.Duration]) * } * })) * * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(schedule) * const [output] = yield* step(0, "input") * return output * }) * * await Effect.runPromise(program) // => 0 * ``` * * @stability stable * @category constructors * @since 4.0.0 */ export const fromStep = ( step: Effect< (now: number, input: Input) => Pull.Pull<[Output, Duration.Duration], ErrorX, Output, EnvX>, Error, Env > ): Schedule, Env | EnvX> => { const self = Object.create(ScheduleProto) self.step = step return self } const metadataFn = () => { let n = 0 let previous: number | undefined let start: number | undefined return (now: number, input: In): InputMetadata => { if (start === undefined) start = now const elapsed = now - start const elapsedSincePrevious = previous === undefined ? 0 : now - previous previous = now return { input, attempt: ++n, start, now, elapsed, elapsedSincePrevious } } } /** * Creates a Schedule from a step function that receives metadata about the schedule's execution. * * **Example** (Creating a metadata-aware schedule) * * ```ts import.meta.vitest * import { Cause, Duration, Effect, Schedule } from "effect" * * const firstThreeInputs = Schedule.fromStepWithMetadata(Effect.succeed((metadata: Schedule.InputMetadata) => { * if (metadata.attempt > 3) { * return Cause.done("finished") * } * * return Effect.succeed([ * `attempt ${metadata.attempt}: ${metadata.input}`, * Duration.millis(250) * ] as [string, Duration.Duration]) * })) * * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(firstThreeInputs) * const [output] = yield* step(0, "input") * return output * }) * * await Effect.runPromise(program) // => "attempt 1: input" * ``` * * @stability stable * @category constructors * @since 4.0.0 */ export const fromStepWithMetadata = ( step: Effect< (options: InputMetadata) => Pull.Pull<[Output, Duration.Duration], ErrorX, Output, EnvX>, Error, Env > ): Schedule, Env | EnvX> => fromStep(effect.map(step, (f) => { const meta = metadataFn() return (now, input) => f(meta(now, input)) })) /** * Extracts the step function from a Schedule. * * **Example** (Extracting a schedule step function) * * ```ts import.meta.vitest * import { Duration, Effect, Schedule } from "effect" * * // Extract step function from an existing schedule * const schedule = Schedule.exponential("100 millis").pipe(Schedule.upTo({ times: 3 })) * * const program = Effect.gen(function*() { * const stepFn = yield* Schedule.toStep(schedule) * * // Use the step function directly for custom logic. The timestamp is * // supplied by the caller, so tests can pass a deterministic value. * const now = 0 * return yield* stepFn(now, "input") * }) * * await Effect.runPromise(program) // => [Duration.millis(100), Duration.millis(100)] * ``` * * @stability stable * @category destructors * @since 4.0.0 */ export const toStep = ( schedule: Schedule ): Effect< (now: number, input: Input) => Pull.Pull<[Output, Duration.Duration], Error, Output, Env>, never, Env > => effect.catchCause( (schedule as any).step, (cause) => effect.succeed(() => effect.failCause(cause) as any) ) /** * Extracts a step function from a `Schedule` that sleeps for each computed * delay and returns metadata for the completed step. * * **When to use** * * Use to drive a schedule manually while preserving the computed output, * delay, input, attempt, and elapsed timing metadata for each step. * * **Details** * * The returned step reads the current time from `Clock` when invoked, calls the * schedule step with that timestamp and input, sleeps for the returned * duration, and then yields `Metadata`. * * @see {@link toStep} for manually supplying the timestamp and handling the returned delay yourself * @see {@link toStepWithSleep} for the same automatic sleeping behavior when only the schedule output is needed * * @stability stable * @category destructors * @since 4.0.0 */ export const toStepWithMetadata = ( schedule: Schedule ): Effect< (input: Input) => Pull.Pull, Error, Output, Env>, never, Env > => effect.clockWith((clock) => effect.map( toStep(schedule), (step) => { const metaFn = metadataFn() return (input) => effect.suspend(() => { const now = clock.currentTimeMillisUnsafe() return effect.flatMap( step(now, input), ([output, duration]) => { const meta = metaFn(now, input) as Mutable> meta.output = output meta.duration = duration return effect.as(effect.sleep(duration), meta) } ) }) } ) ) /** * Extracts a step function from a Schedule that automatically handles sleep delays. * * **Example** (Extracting a sleeping step function) * * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" * import { TestClock } from "effect/testing" * * const schedule = Schedule.recurs(3) * * const program = Effect.gen(function*() { * const stepWithSleep = yield* Schedule.toStepWithSleep(schedule) * * return [yield* stepWithSleep("first"), yield* stepWithSleep("second")] * }) * * await Effect.runPromise(Effect.provide(program, TestClock.layer())) // => [0, 1] * ``` * * @stability stable * @category destructors * @since 4.0.0 */ export const toStepWithSleep = ( schedule: Schedule ): Effect< (input: Input) => Pull.Pull, never, Env > => effect.map( toStepWithMetadata(schedule), (step) => (input) => effect.map(step(input), (meta) => meta.output) ) /** * Returns a new `Schedule` that adds the delay computed by the specified * effectful function to the next recurrence of the schedule. * * **Example** (Adding extra delay to a schedule) * * ```ts import.meta.vitest * import { Duration, Effect, Schedule } from "effect" * * const schedule = Schedule.recurs(1).pipe( * Schedule.addDelay(() => Effect.succeed("25 millis")) * ) * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(schedule) * const [, delay] = yield* step(0, undefined) * return delay * }) * * await Effect.runPromise(program) // => Duration.millis(25) * ``` * * @stability stable * @category delays & timeouts * @since 2.0.0 */ export const addDelay: { /** * Returns a new `Schedule` that adds the delay computed by the specified * effectful function to the next recurrence of the schedule. * * **Example** (Adding extra delay to a schedule) * * ```ts import.meta.vitest * import { Duration, Effect, Schedule } from "effect" * * const schedule = Schedule.recurs(1).pipe( * Schedule.addDelay(() => Effect.succeed("25 millis")) * ) * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(schedule) * const [, delay] = yield* step(0, undefined) * return delay * }) * * await Effect.runPromise(program) // => Duration.millis(25) * ``` * * @stability stable * @category delays & timeouts * @since 2.0.0 */ ( f: (metadata: Metadata) => Effect ): ( self: Schedule ) => Schedule /** * Returns a new `Schedule` that adds the delay computed by the specified * effectful function to the next recurrence of the schedule. * * **Example** (Adding extra delay to a schedule) * * ```ts import.meta.vitest * import { Duration, Effect, Schedule } from "effect" * * const schedule = Schedule.recurs(1).pipe( * Schedule.addDelay(() => Effect.succeed("25 millis")) * ) * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(schedule) * const [, delay] = yield* step(0, undefined) * return delay * }) * * await Effect.runPromise(program) // => Duration.millis(25) * ``` * * @stability stable * @category delays & timeouts * @since 2.0.0 */ ( self: Schedule, f: (metadata: Metadata) => Effect ): Schedule } = dual(2, ( self: Schedule, f: (metadata: Metadata) => Effect ): Schedule => modifyDelay( self, (metadata) => effect.map(f(metadata), (d) => Duration.sum(Duration.fromInputUnsafe(d), metadata.duration)) )) /** * Returns a schedule that runs `self` to completion, then runs `other`, and * merges their outputs. * * **Example** (Sequencing quick and slow retries) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * const schedule = Schedule.concat(Schedule.recurs(1), Schedule.recurs(2)) * Schedule.isSchedule(schedule) // => true * ``` * * @stability stable * @category sequencing * @since 2.0.0 */ export const concat: { /** * Returns a schedule that runs `self` to completion, then runs `other`, and * merges their outputs. * * **Example** (Sequencing quick and slow retries) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * const schedule = Schedule.concat(Schedule.recurs(1), Schedule.recurs(2)) * Schedule.isSchedule(schedule) // => true * ``` * * @stability stable * @category sequencing * @since 2.0.0 */ (other: Schedule): ( self: Schedule ) => Schedule /** * Returns a schedule that runs `self` to completion, then runs `other`, and * merges their outputs. * * **Example** (Sequencing quick and slow retries) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * const schedule = Schedule.concat(Schedule.recurs(1), Schedule.recurs(2)) * Schedule.isSchedule(schedule) // => true * ``` * * @stability stable * @category sequencing * @since 2.0.0 */ ( self: Schedule, other: Schedule ): Schedule } = dual(2, ( self: Schedule, other: Schedule ): Schedule => map(concatResult(self, other), ({ output }) => effect.succeed(Result.merge(output)))) /** * Returns a schedule that runs `self` to completion, then runs `other`, and * preserves which schedule produced each output. * * **Details** * * The resulting schedule emits a `Result` to indicate which phase produced * each output: outputs from `self` are emitted as `Failure`, and outputs from * `other` are emitted as `Success`. * * **Example** (Tracking sequential schedule phases) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * const schedule = Schedule.concatResult(Schedule.recurs(1), Schedule.recurs(2)) * Schedule.isSchedule(schedule) // => true * ``` * * @stability stable * @category sequencing * @since 4.0.0 */ export const concatResult: { /** * Returns a schedule that runs `self` to completion, then runs `other`, and * preserves which schedule produced each output. * * **Details** * * The resulting schedule emits a `Result` to indicate which phase produced * each output: outputs from `self` are emitted as `Failure`, and outputs from * `other` are emitted as `Success`. * * **Example** (Tracking sequential schedule phases) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * const schedule = Schedule.concatResult(Schedule.recurs(1), Schedule.recurs(2)) * Schedule.isSchedule(schedule) // => true * ``` * * @stability stable * @category sequencing * @since 4.0.0 */ (other: Schedule): ( self: Schedule ) => Schedule, Input & Input2, Error | Error2, Env | Env2> /** * Returns a schedule that runs `self` to completion, then runs `other`, and * preserves which schedule produced each output. * * **Details** * * The resulting schedule emits a `Result` to indicate which phase produced * each output: outputs from `self` are emitted as `Failure`, and outputs from * `other` are emitted as `Success`. * * **Example** (Tracking sequential schedule phases) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * const schedule = Schedule.concatResult(Schedule.recurs(1), Schedule.recurs(2)) * Schedule.isSchedule(schedule) // => true * ``` * * @stability stable * @category sequencing * @since 4.0.0 */ ( self: Schedule, other: Schedule ): Schedule, Input & Input2, Error | Error2, Env | Env2> } = dual(2, ( self: Schedule, other: Schedule ): Schedule, Input & Input2, Error | Error2, Env | Env2> => fromStep(effect.sync(() => { let currentSide = 0 let currentStep: | undefined | ((now: number, input: Input & Input2) => Pull.Pull< [Result.Result, Duration.Duration], Error | Error2, Result.Result, Env | Env2 >) const left = map(self, ({ output }) => Result.fail(output)) const right = map(other, ({ output }) => Result.succeed(output)) return function recur( now, input ): Pull.Pull< [Result.Result, Duration.Duration], Error | Error2, Result.Result, Env | Env2 > { if (currentStep) return currentStep(now, input) return toStep< Result.Result, Input & Input2, Error | Error2, Env | Env2 >(currentSide === 0 ? left : right).pipe( effect.flatMap((step) => { currentSide++ if (currentSide === 1) { currentStep = (now, input) => Pull.catchDone(step(now, input), (_) => { currentStep = undefined return recur(now, input) }) return currentStep(now, input) } currentStep = step return currentStep(now, input) }) ) } }))) /** * Combines schedules by recurring while all schedules want to recur, using the * maximum delay between recurrences and outputting that maximum delay. * * **When to use** * * Use when a combined policy should continue only while every schedule still * recurs, and should wait for the slowest schedule between recurrences. * * **Example** (Combining retry schedules by their maximum delay) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * const schedule = Schedule.max([Schedule.fixed("5 seconds"), Schedule.spaced("10 seconds")]) * Schedule.isSchedule(schedule) // => true * ``` * * @stability stable * @category combining * @since 4.0.0 */ export const max = < const Schedules extends NonEmptyReadonlyArray< Schedule > >( schedules: Schedules ): Schedule< Duration.Duration, UnionToIntersection< Input >, Error, Env > => fromStep(effect.map( effect.all(schedules.map(toStep)), (steps) => (now, input) => effect.flatMap( effect.forEach(steps, (step) => Pull.matchEffect(step(now, input as never), { onSuccess: (result) => effect.succeed(result[1]), onDone: () => effect.undefined, onFailure: effect.failCause })), (results) => { const duration = maxDuration(results) if (duration === undefined) { return Cause.done(Duration.zero) } return effect.succeed([duration, duration] as [Duration.Duration, Duration.Duration]) } ) )) const maxDuration = (results: ReadonlyArray): Duration.Duration | undefined => { let max = results[0] for (let i = 1; i < results.length; i++) { max = results[i] && max && Duration.max(max, results[i]!) if (max === undefined) break } return max } /** * Returns a new `Schedule` that recurs on the specified `Cron` schedule and * outputs the duration between recurrences. * * **Example** (Scheduling work with cron expressions) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * const everyMinute = Schedule.cron("* * * * *") * Schedule.isSchedule(everyMinute) // => true * ``` * * @stability stable * @category constructors * @since 2.0.0 */ export const cron: { /** * Returns a new `Schedule` that recurs on the specified `Cron` schedule and * outputs the duration between recurrences. * * **Example** (Scheduling work with cron expressions) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * const everyMinute = Schedule.cron("* * * * *") * Schedule.isSchedule(everyMinute) // => true * ``` * * @stability stable * @category constructors * @since 2.0.0 */ (expression: Cron.Cron): Schedule /** * Returns a new `Schedule` that recurs on the specified `Cron` schedule and * outputs the duration between recurrences. * * **Example** (Scheduling work with cron expressions) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * const everyMinute = Schedule.cron("* * * * *") * Schedule.isSchedule(everyMinute) // => true * ``` * * @stability stable * @category constructors * @since 2.0.0 */ (expression: string, tz?: string | DateTime.TimeZone): Schedule } = (expression: string | Cron.Cron, tz?: string | DateTime.TimeZone) => { const parsed = Cron.isCron(expression) ? Result.succeed(expression) : Cron.parse(expression, tz) return fromStep(effect.map(effect.fromResult(parsed), (cron) => (now, _) => { if (now === Number.POSITIVE_INFINITY) { return Cause.done(Duration.zero) } return effect.sync(() => { const next = Cron.next(cron, now).getTime() const duration = Duration.millis(next - now) return [duration, duration] }) })) } /** * Returns a schedule that recurs once after the specified duration. * * **When to use** * * Use when you need a schedule that recurs once after a fixed delay. * * **Details** * * The schedule outputs the configured duration for its first recurrence and * then completes. * * **Example** (Recurring once after a duration) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * Schedule.isSchedule(Schedule.duration("1 second")) // => true * ``` * * @see {@link during} for recurring until a duration has elapsed * * @stability stable * @category constructors * @since 2.0.0 */ export const duration = (durationInput: Duration.Input): Schedule => { const duration = Duration.fromInputUnsafe(durationInput) return fromStepWithMetadata(effect.succeed((meta) => meta.attempt === 1 ? effect.succeed([duration, duration]) : Cause.done(Duration.zero) )) } /** * Returns a new `Schedule` that will always recur, but only during the * specified `duration` of time. * * **When to use** * * Use to bound a repeating or retrying schedule by elapsed time. * * **Example** (Repeating work during a duration) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * Schedule.isSchedule(Schedule.during("5 seconds")) // => true * ``` * * @see {@link duration} for one delayed recurrence * * @stability stable * @category constructors * @since 4.0.0 */ export const during = (duration: Duration.Input): Schedule => { const durationMillis = Duration.toMillis(duration) return fromStepWithMetadata( effect.succeed((meta) => { const elapsed = Duration.millis(meta.elapsed) return meta.elapsed > durationMillis ? Cause.done(elapsed) : effect.succeed([elapsed, Duration.zero]) }) ) } /** * Combines schedules by recurring while at least one schedule wants to recur, * using the minimum delay between recurrences and outputting that minimum delay. * * **When to use** * * Use when a combined policy should continue while any schedule still recurs, * and should wait for the fastest schedule between recurrences. * * **Example** (Combining retry schedules by their minimum delay) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * const schedule = Schedule.min([Schedule.fixed("5 seconds"), Schedule.spaced("10 seconds")]) * Schedule.isSchedule(schedule) // => true * ``` * * @stability stable * @category combining * @since 4.0.0 */ export const min = < const Schedules extends NonEmptyReadonlyArray< Schedule > >( schedules: Schedules ): Schedule< Duration.Duration, UnionToIntersection< Input >, Error, Env > => fromStep(effect.map( effect.all(schedules.map(toStep)), (steps) => (now, input) => effect.flatMap( effect.forEach(steps, (step) => Pull.matchEffect(step(now, input as never), { onSuccess: (result) => effect.succeed(result[1]), onDone: () => effect.undefined, onFailure: effect.failCause })), (results) => { const duration = minDuration(results) if (duration === undefined) { return Cause.done(Duration.zero) } return effect.succeed([duration, duration] as [Duration.Duration, Duration.Duration]) } ) )) const minDuration = (results: ReadonlyArray): Duration.Duration | undefined => { let min: Duration.Duration | undefined = undefined for (let i = 0; i < results.length; i++) { const duration = results[i] if (duration !== undefined) { min = min === undefined ? duration : Duration.min(min, duration) } } return min } /** * Schedule that always recurs, but will wait a certain amount between * repetitions, given by `base * factor.pow(n)`, where `n` is the number of * repetitions so far. Returns the current duration between recurrences. * * **Example** (Retrying with exponential backoff) * * ```ts import.meta.vitest * import { Duration, Effect, Schedule } from "effect" * * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(Schedule.exponential("100 millis")) * return yield* step(0, undefined) * }) * * await Effect.runPromise(program) // => [Duration.millis(100), Duration.millis(100)] * ``` * * @stability stable * @category constructors * @since 2.0.0 */ export const exponential = ( base: Duration.Input, factor: number = 2 ): Schedule => { const baseMillis = Duration.toMillis(Duration.fromInputUnsafe(base)) return fromStepWithMetadata(effect.succeed((meta) => { const duration = Duration.millis(baseMillis * Math.pow(factor, meta.attempt - 1)) return effect.succeed([duration, duration]) })) } /** * Schedule that always recurs, increasing delays by summing the preceding * two delays (similar to the Fibonacci sequence). Returns the current * duration between recurrences. * * **Example** (Retrying with Fibonacci backoff) * * ```ts import.meta.vitest * import { Duration, Effect, Schedule } from "effect" * * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(Schedule.fibonacci("100 millis")) * return yield* step(0, undefined) * }) * * await Effect.runPromise(program) // => [Duration.millis(100), Duration.millis(100)] * ``` * * @stability stable * @category constructors * @since 2.0.0 */ export const fibonacci = (one: Duration.Input): Schedule => { const oneMillis = Duration.toMillis(Duration.fromInputUnsafe(one)) return fromStep(effect.sync(() => { let a = 0 let b = oneMillis return constant(effect.sync(() => { const next = a + b a = b b = next const duration = Duration.millis(next) return [duration, duration] })) })) } /** * Returns a `Schedule` that recurs on the specified fixed `interval` and * outputs the number of repetitions of the schedule so far. * * **When to use** * * Use when recurrences should stay aligned to a regular cadence. * * **Gotchas** * * If the action run between recurrences takes longer than the interval, the * next recurrence happens immediately, but missed intervals are not replayed. * * ```text * |-----interval-----|-----interval-----|-----interval-----| * |---------action--------||action|-----|action|-----------| * ``` * * **Example** (Repeating on fixed intervals) * * ```ts import.meta.vitest * import { Duration, Effect, Schedule } from "effect" * * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(Schedule.fixed("1 second")) * return yield* step(0, undefined) * }) * * await Effect.runPromise(program) // => [0, Duration.seconds(1)] * ``` * * @see {@link spaced} for delaying after each action completes * * @stability stable * @category constructors * @since 2.0.0 */ export const fixed = (interval: Duration.Input): Schedule => { const window = Duration.toMillis(Duration.fromInputUnsafe(interval)) return fromStepWithMetadata(effect.sync(() => { let start = 0 let lastRun = 0 return (meta) => effect.sync(() => { if (window === 0) { return [meta.attempt - 1, Duration.zero] as const } if (meta.attempt === 1) { start = meta.now lastRun = meta.now + window return [0, Duration.millis(window)] as const } const runningBehind = meta.now > (lastRun + window) const boundary = window - ((meta.now - start) % window) const delay = runningBehind ? 0 : boundary === 0 ? window : boundary lastRun = runningBehind ? meta.now : meta.now + delay return [meta.attempt - 1, Duration.millis(delay)] as const }) })) } /** * Returns a new `Schedule` that maps each schedule decision to a new output * using the full schedule metadata. * * **Details** * * The callback receives the schedule input, output, selected delay duration, * current attempt, and elapsed timing information. Return either a plain value * or an `Effect` that produces the new output. * * **Example** (Mapping schedule outputs) * * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" * * const countSchedule = Schedule.recurs(5).pipe( * Schedule.map(({ output: count }) => Effect.succeed(`Execution #${count + 1}`)) * ) * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(countSchedule) * const [output] = yield* step(0, undefined) * return output * }) * * await Effect.runPromise(program) // => "Execution #1" * ``` * * @stability stable * @category mapping * @since 2.0.0 */ export const map: { /** * Returns a new `Schedule` that maps each schedule decision to a new output * using the full schedule metadata. * * **Details** * * The callback receives the schedule input, output, selected delay duration, * current attempt, and elapsed timing information. Return either a plain value * or an `Effect` that produces the new output. * * **Example** (Mapping schedule outputs) * * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" * * const countSchedule = Schedule.recurs(5).pipe( * Schedule.map(({ output: count }) => Effect.succeed(`Execution #${count + 1}`)) * ) * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(countSchedule) * const [output] = yield* step(0, undefined) * return output * }) * * await Effect.runPromise(program) // => "Execution #1" * ``` * * @stability stable * @category mapping * @since 2.0.0 */ ( f: (metadata: Metadata) => Output2 | Effect ): ( self: Schedule ) => Schedule /** * Returns a new `Schedule` that maps each schedule decision to a new output * using the full schedule metadata. * * **Details** * * The callback receives the schedule input, output, selected delay duration, * current attempt, and elapsed timing information. Return either a plain value * or an `Effect` that produces the new output. * * **Example** (Mapping schedule outputs) * * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" * * const countSchedule = Schedule.recurs(5).pipe( * Schedule.map(({ output: count }) => Effect.succeed(`Execution #${count + 1}`)) * ) * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(countSchedule) * const [output] = yield* step(0, undefined) * return output * }) * * await Effect.runPromise(program) // => "Execution #1" * ``` * * @stability stable * @category mapping * @since 2.0.0 */ ( self: Schedule, f: (metadata: Metadata) => Output2 | Effect ): Schedule } = dual(2, ( self: Schedule, f: (metadata: Metadata) => Output2 | Effect ): Schedule => fromStep(effect.map(toStep(self), (step) => { const meta = metadataFn() return (now, input) => Pull.matchEffect(step(now, input), { onSuccess: ([output, duration]) => { const result = f({ ...meta(now, input), output, duration }) if (!isEffect(result)) return effect.succeed([result, duration] as [Output2, Duration.Duration]) return effect.map(result, (output) => [output, duration] as [Output2, Duration.Duration]) }, onFailure: effect.failCause, onDone: (output) => { const result = f({ ...meta(now, input), output, duration: Duration.zero }) if (!isEffect(result)) return Cause.done(result as Output2) return effect.flatMap(result, Cause.done) } }) }))) /** * Returns a new `Schedule` that modifies the delay of the next recurrence * of the schedule using the specified effectful function. * * **Example** (Modifying delays from schedule metadata) * * ```ts import.meta.vitest * import { Duration, Effect, Schedule } from "effect" * * const schedule = Schedule.spaced("10 millis").pipe( * Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.times(duration, 2))) * ) * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(schedule) * const [, delay] = yield* step(0, undefined) * return delay * }) * * await Effect.runPromise(program) // => Duration.millis(20) * ``` * * @stability stable * @category delays & timeouts * @since 2.0.0 */ export const modifyDelay: { /** * Returns a new `Schedule` that modifies the delay of the next recurrence * of the schedule using the specified effectful function. * * **Example** (Modifying delays from schedule metadata) * * ```ts import.meta.vitest * import { Duration, Effect, Schedule } from "effect" * * const schedule = Schedule.spaced("10 millis").pipe( * Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.times(duration, 2))) * ) * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(schedule) * const [, delay] = yield* step(0, undefined) * return delay * }) * * await Effect.runPromise(program) // => Duration.millis(20) * ``` * * @stability stable * @category delays & timeouts * @since 2.0.0 */ ( f: ( metadata: Metadata ) => Effect ): ( self: Schedule ) => Schedule /** * Returns a new `Schedule` that modifies the delay of the next recurrence * of the schedule using the specified effectful function. * * **Example** (Modifying delays from schedule metadata) * * ```ts import.meta.vitest * import { Duration, Effect, Schedule } from "effect" * * const schedule = Schedule.spaced("10 millis").pipe( * Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.times(duration, 2))) * ) * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(schedule) * const [, delay] = yield* step(0, undefined) * return delay * }) * * await Effect.runPromise(program) // => Duration.millis(20) * ``` * * @stability stable * @category delays & timeouts * @since 2.0.0 */ ( self: Schedule, f: ( metadata: Metadata ) => Effect ): Schedule } = dual(2, ( self: Schedule, f: ( metadata: Metadata ) => Effect ): Schedule => fromStep(effect.map(toStep(self), (step) => { const meta = metadataFn() return (now, input) => effect.flatMap( step(now, input), ([output, duration]) => effect.map(f({ ...meta(now, input), output, duration }), (replacement) => [ output, Duration.fromInputUnsafe(replacement) ]) ) }))) /** * Returns a new `Schedule` that randomly adjusts each recurrence delay. * * **When to use** * * Use to add random variation to an existing schedule's recurrence delays while * preserving its output and completion behavior. * * **Details** * * Each recurrence delay is scaled by a random factor between `0.8` and `1.2`. * * @see {@link modifyDelay} for replacing recurrence delays with a custom effectful transformation * * @stability stable * @category delays & timeouts * @since 2.0.0 */ export const jittered = ( self: Schedule ): Schedule => modifyDelay(self, ({ duration }) => effect.map(randomNext, (random) => { const millis = Duration.toMillis(duration) return Duration.millis(millis * 0.8 * (1 - random) + millis * 1.2 * random) })) /** * Returns a new `Schedule` that outputs the inputs of the specified schedule. * * **Example** (Passing inputs through as outputs) * * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" * * const inputSchedule = Schedule.passthrough( * Schedule.exponential("100 millis").pipe(Schedule.upTo({ times: 3 })) * ) * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(inputSchedule) * const [output] = yield* step(0, "input") * return output * }) * * await Effect.runPromise(program) // => "input" * ``` * * @stability stable * @category mapping * @since 2.0.0 */ export const passthrough = ( self: Schedule ): Schedule => fromStep(effect.map(toStep(self), (step) => (now, input) => Pull.matchEffect(step(now, input), { onSuccess: (result) => effect.succeed([input, result[1]]), onFailure: effect.failCause, onDone: () => Cause.done(input) }))) /** * Returns a `Schedule` which can only be stepped the specified number of * `times` before it terminates. * * **When to use** * * Use when you need a counter schedule with no additional delay. * * **Gotchas** * * `recurs(n)` counts schedule recurrences, not the first evaluation of the * effect being repeated or retried. For retrying, this means one initial * attempt plus at most `n` retries. * * **Example** (Limiting recurrences) * * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" * import { TestClock } from "effect/testing" * * const executions: Array = [] * const program = Effect.sync(() => executions.push(executions.length + 1)).pipe( * Effect.repeat(Schedule.recurs(3)), * Effect.as(executions) * ) * * await Effect.runPromise(Effect.provide(program, TestClock.layer())) // => [1, 2, 3, 4] * ``` * * @see {@link upTo} for limiting an existing schedule * * @stability stable * @category constructors * @since 2.0.0 */ export const recurs = (times: number): Schedule => while_(forever, ({ attempt }) => effect.succeed(attempt <= times)) /** * Returns a schedule that recurs continuously, each repetition spaced the * specified duration from the last run. * * **When to use** * * Use when each delay should start after the previous action completes. * * **Example** (Repeating with fixed spacing) * * ```ts import.meta.vitest * import { Duration, Effect, Schedule } from "effect" * * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(Schedule.spaced("2 seconds")) * return yield* step(0, undefined) * }) * * await Effect.runPromise(program) // => [0, Duration.seconds(2)] * ``` * * @see {@link fixed} for recurrence aligned to a regular cadence * * @stability stable * @category constructors * @since 2.0.0 */ export const spaced = (duration: Duration.Input): Schedule => { const decoded = Duration.fromInputUnsafe(duration) return fromStepWithMetadata(effect.succeed((meta) => effect.succeed([meta.attempt - 1, decoded]))) } /** * Returns a new `Schedule` that allows execution of an effectful function for * every decision of the schedule, but does not alter the inputs and outputs of * the schedule. * * **Details** * * The callback receives the full schedule metadata, including the input, output, * computed delay duration, current attempt, and elapsed timing information. * * **Example** (Tapping schedule metadata) * * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" * * const attempts: Array = [] * const monitoredSchedule = Schedule.recurs(2).pipe( * Schedule.tap((metadata) => Effect.sync(() => attempts.push(metadata.attempt))) * ) * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(monitoredSchedule) * const [output] = yield* step(0, undefined) * return { attempts, output } * }) * * await Effect.runPromise(program) // => { attempts: [1], output: 0 } * ``` * * @stability stable * @category sequencing * @since 4.0.0 */ export const tap: { /** * Returns a new `Schedule` that allows execution of an effectful function for * every decision of the schedule, but does not alter the inputs and outputs of * the schedule. * * **Details** * * The callback receives the full schedule metadata, including the input, output, * computed delay duration, current attempt, and elapsed timing information. * * **Example** (Tapping schedule metadata) * * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" * * const attempts: Array = [] * const monitoredSchedule = Schedule.recurs(2).pipe( * Schedule.tap((metadata) => Effect.sync(() => attempts.push(metadata.attempt))) * ) * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(monitoredSchedule) * const [output] = yield* step(0, undefined) * return { attempts, output } * }) * * await Effect.runPromise(program) // => { attempts: [1], output: 0 } * ``` * * @stability stable * @category sequencing * @since 4.0.0 */ (f: (metadata: Metadata) => Effect): ( self: Schedule ) => Schedule /** * Returns a new `Schedule` that allows execution of an effectful function for * every decision of the schedule, but does not alter the inputs and outputs of * the schedule. * * **Details** * * The callback receives the full schedule metadata, including the input, output, * computed delay duration, current attempt, and elapsed timing information. * * **Example** (Tapping schedule metadata) * * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" * * const attempts: Array = [] * const monitoredSchedule = Schedule.recurs(2).pipe( * Schedule.tap((metadata) => Effect.sync(() => attempts.push(metadata.attempt))) * ) * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(monitoredSchedule) * const [output] = yield* step(0, undefined) * return { attempts, output } * }) * * await Effect.runPromise(program) // => { attempts: [1], output: 0 } * ``` * * @stability stable * @category sequencing * @since 4.0.0 */ ( self: Schedule, f: (metadata: Metadata) => Effect ): Schedule } = dual(2, ( self: Schedule, f: (metadata: Metadata) => Effect ): Schedule => fromStep(effect.map(toStep(self), (step) => { const meta = metadataFn() return (now, input) => effect.tap(step(now, input), ([output, duration]) => f({ ...meta(now, input), output, duration })) }))) /** * Returns a new `Schedule` that limits an existing schedule by elapsed * duration, number of outputs, or both. * * **When to use** * * Use to bound an existing schedule while preserving its output and delay * behavior. When both `duration` and `times` are specified, the schedule * stops as soon as either limit is reached. * * **Gotchas** * * The `times` option limits schedule outputs. When used with repeat or retry, * the effect is evaluated once before the schedule is stepped, so the total * number of evaluations can be one greater than the configured number of * outputs. * * The `duration` option is based on the elapsed time observed by the schedule * step. Long-running effects can cause the duration limit to be detected on the * following schedule step. * * **Example** (Limiting by duration and recurrence count) * * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" * import { TestClock } from "effect/testing" * * const executions: Array = [] * const schedule = Schedule.forever.pipe(Schedule.upTo({ times: 2 })) * const program = Effect.sync(() => executions.push(executions.length + 1)).pipe( * Effect.repeat(schedule), * Effect.as(executions) * ) * * await Effect.runPromise(Effect.provide(program, TestClock.layer())) // => [1, 2, 3] * ``` * * @stability stable * @category filtering * @since 4.0.0 */ export const upTo: { /** * Returns a new `Schedule` that limits an existing schedule by elapsed * duration, number of outputs, or both. * * **When to use** * * Use to bound an existing schedule while preserving its output and delay * behavior. When both `duration` and `times` are specified, the schedule * stops as soon as either limit is reached. * * **Gotchas** * * The `times` option limits schedule outputs. When used with repeat or retry, * the effect is evaluated once before the schedule is stepped, so the total * number of evaluations can be one greater than the configured number of * outputs. * * The `duration` option is based on the elapsed time observed by the schedule * step. Long-running effects can cause the duration limit to be detected on the * following schedule step. * * **Example** (Limiting by duration and recurrence count) * * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" * import { TestClock } from "effect/testing" * * const executions: Array = [] * const schedule = Schedule.forever.pipe(Schedule.upTo({ times: 2 })) * const program = Effect.sync(() => executions.push(executions.length + 1)).pipe( * Effect.repeat(schedule), * Effect.as(executions) * ) * * await Effect.runPromise(Effect.provide(program, TestClock.layer())) // => [1, 2, 3] * ``` * * @stability stable * @category filtering * @since 4.0.0 */ ( options: { readonly duration?: Duration.Input | undefined readonly times?: number | undefined } ): ( self: Schedule ) => Schedule /** * Returns a new `Schedule` that limits an existing schedule by elapsed * duration, number of outputs, or both. * * **When to use** * * Use to bound an existing schedule while preserving its output and delay * behavior. When both `duration` and `times` are specified, the schedule * stops as soon as either limit is reached. * * **Gotchas** * * The `times` option limits schedule outputs. When used with repeat or retry, * the effect is evaluated once before the schedule is stepped, so the total * number of evaluations can be one greater than the configured number of * outputs. * * The `duration` option is based on the elapsed time observed by the schedule * step. Long-running effects can cause the duration limit to be detected on the * following schedule step. * * **Example** (Limiting by duration and recurrence count) * * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" * import { TestClock } from "effect/testing" * * const executions: Array = [] * const schedule = Schedule.forever.pipe(Schedule.upTo({ times: 2 })) * const program = Effect.sync(() => executions.push(executions.length + 1)).pipe( * Effect.repeat(schedule), * Effect.as(executions) * ) * * await Effect.runPromise(Effect.provide(program, TestClock.layer())) // => [1, 2, 3] * ``` * * @stability stable * @category filtering * @since 4.0.0 */ ( self: Schedule, options: { readonly duration?: Duration.Input | undefined readonly times?: number | undefined } ): Schedule } = dual(2, ( self: Schedule, options: { readonly duration?: Duration.Input | undefined readonly times?: number | undefined } ): Schedule => { const duration = options.duration === undefined ? undefined : Duration.fromInputUnsafe(options.duration) return while_(self, ({ attempt, elapsed }) => effect.succeed( (options.times === undefined || attempt <= options.times) && (duration === undefined || Duration.isLessThanOrEqualTo(Duration.millis(elapsed), duration)) )) }) const while_: { >( predicate: ( metadata: Metadata ) => metadata is Meta ): ( self: Schedule ) => Schedule ( predicate: ( metadata: Metadata ) => boolean | Effect ): ( self: Schedule ) => Schedule >( self: Schedule, predicate: ( metadata: Metadata ) => metadata is Meta ): Schedule ( self: Schedule, predicate: ( metadata: Metadata ) => boolean | Effect ): Schedule } = dual(2, ( self: Schedule, predicate: ( metadata: Metadata ) => boolean | Effect ): Schedule => fromStep(effect.map(toStep(self), (step) => { const meta = metadataFn() return (now, input) => effect.flatMap(step(now, input), (result) => { const [output, duration] = result const eff = predicate({ ...meta(now, input), output, duration }) return effect.flatMap( isEffect(eff) ? eff : effect.succeed(eff), (check) => (check ? effect.succeed(result) : Cause.done(output)) ) }) }))) export { /** * Returns a new schedule that continues while the predicate returns `true`. * * **When to use** * * Use to stop an existing schedule based on its full metadata, such as the * current input, output, attempt, delay, or elapsed time. * * **Details** * * The predicate receives `Metadata`, may return `boolean` or an * `Effect`, preserves the output and delay when it returns * `true`, and stops the schedule when it returns `false`. * * @see {@link upTo} for stopping after a fixed number of schedule outputs * * @stability stable * @category filtering * @since 4.0.0 */ while_ as while } /** * Schedule that divides the timeline to `interval`-long windows, and sleeps * until the nearest window boundary every time it recurs. * * **Details** * * For example, `Schedule.windowed("10 seconds")` would produce a schedule as * follows: * * ```text * 10s 10s 10s 10s * |----------|----------|----------|----------| * |action------|sleep---|act|-sleep|action----| * ``` * * **Example** (Repeating on aligned windows) * * ```ts import.meta.vitest * import { Duration, Effect, Schedule } from "effect" * * const program = Effect.gen(function*() { * const step = yield* Schedule.toStep(Schedule.windowed("5 seconds")) * return yield* step(0, undefined) * }) * * await Effect.runPromise(program) // => [0, Duration.seconds(5)] * ``` * * @stability stable * @category constructors * @since 2.0.0 */ export const windowed = (interval: Duration.Input): Schedule => { const window = Duration.toMillis(Duration.fromInputUnsafe(interval)) return fromStepWithMetadata(effect.succeed((meta) => effect.sync(() => [ meta.attempt - 1, window === 0 ? Duration.zero : Duration.millis(window - (meta.elapsed % window)) ]) )) } /** * Returns a new `Schedule` that will recur forever. * * **Details** * * The output of the schedule is the current count of its repetitions thus far * (i.e. `0, 1, 2, ...`). * * **Example** (Repeating forever) * * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" * import { TestClock } from "effect/testing" * * const executions: Array = [] * const schedule = Schedule.forever.pipe(Schedule.upTo({ times: 2 })) * const program = Effect.sync(() => executions.push(executions.length + 1)).pipe( * Effect.repeat(schedule), * Effect.as(executions) * ) * * await Effect.runPromise(Effect.provide(program, TestClock.layer())) // => [1, 2, 3] * ``` * * @stability stable * @category constructors * @since 2.0.0 */ export const forever: Schedule = spaced(Duration.zero) /** * Returns a schedule that recurs immediately once, then completes. * * **Details** * * The output of both the recurrence and completion is `void`. When used with * `Effect.repeat`, the effect runs twice in total: once initially and once * after the schedule recurs. * * @stability stable * @category constructors * @since 4.0.0 */ export const once: Schedule = map(recurs(1), () => void 0) const constIdentity = fromStep( effect.succeed((_now, input: unknown) => effect.succeed([input, Duration.zero] as [unknown, Duration.Duration])) ) const identity_ = (): Schedule => constIdentity as Schedule export { /** * Creates a schedule that always recurs, passing inputs directly as outputs. * * **When to use** * * Use when you need an infinite schedule that preserves input values as * outputs. * * **Details** * * This schedule runs indefinitely, returning each input value as its output * without modification. It effectively acts as a pass-through that simply * echoes its input values at each step. * * @see {@link forever} for an infinite schedule that returns incrementing step counts * @stability stable * @category constructors * @since 2.0.0 */ identity_ as identity } /** * Sets the input type of the provided schedule without altering its behavior. * * **When to use** * * Use to adapt a schedule that does not depend on its input values. * * **Details** * * This helper is checked at compile time and does not change the schedule's * runtime behavior. * * **Example** (Setting a schedule input type) * * ```ts import.meta.vitest * import { Schedule } from "effect" * * const schedule = Schedule.recurs(3).pipe( * Schedule.setInputType() * ) * Schedule.isSchedule(schedule) // => true * ``` * * @stability stable * @category utility types * @since 4.0.0 */ export const setInputType = () => (self: Schedule): Schedule => self