EdgeAIG's picture
download
raw
208 kB
/**
* Describes policies for retrying, repeating, and pacing Effect programs.
*
* A `Schedule<Output, Input, Error, Env>` 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.
*
* @since 2.0.0
*/
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 type { LazyArg } from "./Function.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 } from "./Types.ts"
const TypeId = "~effect/Schedule"
const randomNext: Effect<number> = 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 { Console, Data, Effect, Schedule } from "effect"
*
* class NetworkError extends Data.TaggedError("NetworkError")<{
* readonly attempt: number
* }> {}
*
* // Basic retry schedule - retry up to 3 times with exponential backoff
* const retrySchedule = Schedule.exponential("100 millis").pipe(
* Schedule.both(Schedule.recurs(3))
* )
*
* // Basic repeat schedule - repeat every 30 seconds forever
* const repeatSchedule: Schedule.Schedule<number, unknown, never> = Schedule
* .spaced("30 seconds")
*
* const program = Effect.gen(function*() {
* let attempts = 0
*
* const result1 = yield* Effect.retry(
* Effect.gen(function*() {
* attempts++
* if (attempts < 3) {
* return yield* Effect.fail(new NetworkError({ attempt: attempts }))
* }
* return "Success"
* }),
* retrySchedule
* )
* console.log(result1) // "Success"
*
* yield* Console.log("heartbeat").pipe(
* Effect.repeat(repeatSchedule.pipe(Schedule.take(5)))
* )
* })
* ```
*
* @category models
* @since 2.0.0
*/
export interface Schedule<out Output, in Input = unknown, out Error = never, out Env = never>
extends Schedule.Variance<Output, Input, Error, Env>, Pipeable
{}
/**
* Metadata provided to schedule functions containing timing and input information.
*
* **Example** (Reading schedule input metadata)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Custom schedule that uses input metadata
* const metadataAwareSchedule = Schedule.spaced("1 second").pipe(
* Schedule.collectWhile((metadata) =>
* Effect.succeed(metadata.attempt <= 5 && metadata.elapsed < 10000)
* )
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Console.log("Task execution"),
* metadataAwareSchedule
* )
* })
* ```
*
* @category metadata
* @since 4.0.0
*/
export interface InputMetadata<Input> {
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.
*
* **Example** (Logging schedule output metadata)
*
* ```ts
* import { Console, Duration, Effect, Schedule } from "effect"
*
* // Custom schedule that logs metadata and output for each recurrence
* const loggingSchedule = Schedule.unfold(0, (n) => Effect.succeed(n + 1)).pipe(
* Schedule.addDelay(() => Effect.succeed(Duration.millis(100))),
* Schedule.collectWhile((metadata) =>
* Console.log(
* `Output: ${metadata.output}, attempt: ${metadata.attempt}, elapsed: ${metadata.elapsed}ms`
* ).pipe(Effect.as(metadata.attempt <= 3))
* )
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.succeed("task completed"),
* loggingSchedule.pipe(Schedule.take(3))
* )
* })
*
* // Output logs will show:
* // Output: 0, attempt: 1, elapsed: 0ms
* // Output: 1, attempt: 2, elapsed: 100ms
* // Output: 2, attempt: 3, elapsed: 200ms
* ```
*
* @category metadata
* @since 4.0.0
*/
export interface Metadata<Output = unknown, Input = unknown> extends InputMetadata<Input> {
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.
*
* @category metadata
* @since 4.0.0
*/
export const CurrentMetadata = Context.Reference<Metadata>("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.
*
* **Example** (Creating custom schedules with the namespace)
*
* ```ts
* import { Duration, Effect, Schedule } from "effect"
*
* // Usage of the Schedule namespace for creating schedules
*
* // Create custom schedule with metadata
* const customSchedule = Schedule.unfold(0, (n) => Effect.succeed(n + 1)).pipe(
* Schedule.addDelay((n) => Effect.succeed(Duration.millis(n * 100)))
* )
*
* const program = Effect.gen(function*() {
* let attempt = 0
*
* yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* if (attempt < 3) {
* return yield* Effect.fail(`Attempt ${attempt} failed`)
* }
* return `Success on attempt ${attempt}`
* }),
* customSchedule.pipe(Schedule.take(5))
* )
* })
* ```
*
* @since 2.0.0
*/
export declare namespace Schedule {
/**
* Variance interface that defines the type parameter relationships for Schedule.
*
* **Example** (Understanding schedule variance)
*
* ```ts
* import { Effect, Schedule } from "effect"
*
* // Understanding Schedule variance:
* // - Output: covariant (can be a subtype)
* // - Input: contravariant (can accept supertypes)
* // - Error: covariant (can be a subtype)
* // - Env: covariant (can be a subtype)
*
* // Schedule that produces strings, accepts any input
* const stringSchedule = Schedule.spaced("1 second").pipe(
* Schedule.map(() => Effect.succeed("tick"))
* )
*
* // Schedule that only accepts Error inputs
* const errorSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.take(5)
* )
*
* // Schedule requiring a service environment
* const serviceSchedule = Schedule.spaced("5 seconds")
* ```
*
* @category models
* @since 2.0.0
*/
export interface Variance<out Output, in Input, out Error, out Env> {
readonly [TypeId]: VarianceStruct<Output, Input, Error, Env>
}
/**
* 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<out Output, in Input, out Error, out Env> {
readonly _Out: Covariant<Output>
readonly _In: Contravariant<Input>
readonly _Error: Covariant<Error>
readonly _Env: Covariant<Env>
}
}
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 { Schedule } from "effect"
*
* const schedule = Schedule.exponential("100 millis")
* const notSchedule = { foo: "bar" }
*
* console.log(Schedule.isSchedule(schedule)) // true
* console.log(Schedule.isSchedule(notSchedule)) // false
* console.log(Schedule.isSchedule(null)) // false
* console.log(Schedule.isSchedule(undefined)) // false
* ```
*
* @category guards
* @since 2.0.0
*/
export const isSchedule = (u: unknown): u is Schedule<unknown, never, unknown, unknown> => 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 { 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])
* }
* }))
* ```
*
* @category constructors
* @since 4.0.0
*/
export const fromStep = <Input, Output, EnvX, Error, ErrorX, Env>(
step: Effect<
(now: number, input: Input) => Pull.Pull<[Output, Duration.Duration], ErrorX, Output, EnvX>,
Error,
Env
>
): Schedule<Output, Input, Error | Pull.ExcludeDone<ErrorX>, 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 <In>(now: number, input: In): InputMetadata<In> => {
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 { Cause, Duration, Effect, Schedule } from "effect"
*
* const firstThreeInputs = Schedule.fromStepWithMetadata(Effect.succeed((metadata: Schedule.InputMetadata<string>) => {
* if (metadata.attempt > 3) {
* return Cause.done("finished")
* }
*
* return Effect.succeed([
* `attempt ${metadata.attempt}: ${metadata.input}`,
* Duration.millis(250)
* ] as [string, Duration.Duration])
* }))
* ```
*
* @category constructors
* @since 4.0.0
*/
export const fromStepWithMetadata = <Input, Output, EnvX, ErrorX, Error, Env>(
step: Effect<
(options: InputMetadata<Input>) => Pull.Pull<[Output, Duration.Duration], ErrorX, Output, EnvX>,
Error,
Env
>
): Schedule<Output, Input, Error | Pull.ExcludeDone<ErrorX>, 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 { Effect, Schedule } from "effect"
*
* // Extract step function from an existing schedule
* const schedule = Schedule.exponential("100 millis").pipe(Schedule.take(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
* const result = yield* stepFn(now, "input")
*
* console.log(`Step result: ${result}`)
* })
* ```
*
* @category destructors
* @since 4.0.0
*/
export const toStep = <Output, Input, Error, Env>(
schedule: Schedule<Output, Input, Error, Env>
): 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
*
* @category destructors
* @since 4.0.0
*/
export const toStepWithMetadata = <Output, Input, Error, Env>(
schedule: Schedule<Output, Input, Error, Env>
): Effect<
(input: Input) => Pull.Pull<Metadata<Output, Input>, 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<Metadata<Output, Input>>
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 { Effect, Schedule } from "effect"
*
* // Convert schedule to step function with automatic sleeping
* const schedule = Schedule.spaced("1 second").pipe(Schedule.take(3))
*
* const program = Effect.gen(function*() {
* const stepWithSleep = yield* Schedule.toStepWithSleep(schedule)
*
* // Each call will automatically sleep for the scheduled delay
* console.log("Starting...")
* const result1 = yield* stepWithSleep("first")
* console.log(`First result: ${result1}`)
*
* const result2 = yield* stepWithSleep("second")
* console.log(`Second result: ${result2}`)
*
* const result3 = yield* stepWithSleep("third")
* console.log(`Third result: ${result3}`)
* })
* ```
*
* @category destructors
* @since 4.0.0
*/
export const toStepWithSleep = <Output, Input, Error, Env>(
schedule: Schedule<Output, Input, Error, Env>
): Effect<
(input: Input) => Pull.Pull<Output, Error, Output, Env>,
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 { Console, Data, Duration, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Add a deterministic extra delay based on the schedule output
* const delayedSchedule = Schedule.addDelay(
* Schedule.exponential("100 millis").pipe(Schedule.take(5)),
* (output) =>
* Effect.succeed(Duration.millis(Duration.toMillis(output) * 0.25))
* )
*
* const repeatProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.succeed("delayed task"),
* delayedSchedule.pipe(
* Schedule.tapOutput((delay) =>
* Console.log(`Base delay: ${delay}`)
* )
* )
* )
* })
*
* // Add adaptive delay based on execution count
* const adaptiveSchedule = Schedule.addDelay(
* Schedule.recurs(6),
* (executionCount) =>
* // Increase delay as execution count grows
* Effect.succeed(Duration.millis(executionCount * 200))
* )
*
* const adaptiveProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Adaptive delay task")
* return "adaptive"
* }),
* adaptiveSchedule.pipe(
* Schedule.tapOutput((count) =>
* Console.log(`Execution ${count + 1} with adaptive delay`)
* )
* )
* )
* })
*
* // Add effectful delay computation from deterministic service data
* const loadByExecution = [1, 3, 2, 4] as const
*
* const dynamicSchedule = Schedule.addDelay(
* Schedule.spaced("1 second").pipe(Schedule.take(4)),
* (executionNumber) => {
* const load = loadByExecution[executionNumber] ?? 1
* return Effect.succeed(Duration.millis(load * 100))
* }
* )
*
* const dynamicProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Dynamic delay task")
* return "dynamic"
* }),
* dynamicSchedule
* )
* })
*
* // Combine with retry for progressive backoff
* const progressiveRetrySchedule = Schedule.addDelay(
* Schedule.exponential("50 millis").pipe(Schedule.take(4)),
* () => Effect.succeed(Duration.millis(100)) // Fixed additional delay
* )
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* if (attempt < 5) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
* return `Success on attempt ${attempt}`
* }),
* progressiveRetrySchedule
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
* ```
*
* @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 { Console, Data, Duration, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Add a deterministic extra delay based on the schedule output
* const delayedSchedule = Schedule.addDelay(
* Schedule.exponential("100 millis").pipe(Schedule.take(5)),
* (output) =>
* Effect.succeed(Duration.millis(Duration.toMillis(output) * 0.25))
* )
*
* const repeatProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.succeed("delayed task"),
* delayedSchedule.pipe(
* Schedule.tapOutput((delay) =>
* Console.log(`Base delay: ${delay}`)
* )
* )
* )
* })
*
* // Add adaptive delay based on execution count
* const adaptiveSchedule = Schedule.addDelay(
* Schedule.recurs(6),
* (executionCount) =>
* // Increase delay as execution count grows
* Effect.succeed(Duration.millis(executionCount * 200))
* )
*
* const adaptiveProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Adaptive delay task")
* return "adaptive"
* }),
* adaptiveSchedule.pipe(
* Schedule.tapOutput((count) =>
* Console.log(`Execution ${count + 1} with adaptive delay`)
* )
* )
* )
* })
*
* // Add effectful delay computation from deterministic service data
* const loadByExecution = [1, 3, 2, 4] as const
*
* const dynamicSchedule = Schedule.addDelay(
* Schedule.spaced("1 second").pipe(Schedule.take(4)),
* (executionNumber) => {
* const load = loadByExecution[executionNumber] ?? 1
* return Effect.succeed(Duration.millis(load * 100))
* }
* )
*
* const dynamicProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Dynamic delay task")
* return "dynamic"
* }),
* dynamicSchedule
* )
* })
*
* // Combine with retry for progressive backoff
* const progressiveRetrySchedule = Schedule.addDelay(
* Schedule.exponential("50 millis").pipe(Schedule.take(4)),
* () => Effect.succeed(Duration.millis(100)) // Fixed additional delay
* )
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* if (attempt < 5) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
* return `Success on attempt ${attempt}`
* }),
* progressiveRetrySchedule
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
* ```
*
* @category delays & timeouts
* @since 2.0.0
*/
<Output, Error2 = never, Env2 = never>(f: (output: Output) => Effect<Duration.Input, Error2, Env2>): <Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error | Error2, Env | Env2>
/**
* 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 { Console, Data, Duration, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Add a deterministic extra delay based on the schedule output
* const delayedSchedule = Schedule.addDelay(
* Schedule.exponential("100 millis").pipe(Schedule.take(5)),
* (output) =>
* Effect.succeed(Duration.millis(Duration.toMillis(output) * 0.25))
* )
*
* const repeatProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.succeed("delayed task"),
* delayedSchedule.pipe(
* Schedule.tapOutput((delay) =>
* Console.log(`Base delay: ${delay}`)
* )
* )
* )
* })
*
* // Add adaptive delay based on execution count
* const adaptiveSchedule = Schedule.addDelay(
* Schedule.recurs(6),
* (executionCount) =>
* // Increase delay as execution count grows
* Effect.succeed(Duration.millis(executionCount * 200))
* )
*
* const adaptiveProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Adaptive delay task")
* return "adaptive"
* }),
* adaptiveSchedule.pipe(
* Schedule.tapOutput((count) =>
* Console.log(`Execution ${count + 1} with adaptive delay`)
* )
* )
* )
* })
*
* // Add effectful delay computation from deterministic service data
* const loadByExecution = [1, 3, 2, 4] as const
*
* const dynamicSchedule = Schedule.addDelay(
* Schedule.spaced("1 second").pipe(Schedule.take(4)),
* (executionNumber) => {
* const load = loadByExecution[executionNumber] ?? 1
* return Effect.succeed(Duration.millis(load * 100))
* }
* )
*
* const dynamicProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Dynamic delay task")
* return "dynamic"
* }),
* dynamicSchedule
* )
* })
*
* // Combine with retry for progressive backoff
* const progressiveRetrySchedule = Schedule.addDelay(
* Schedule.exponential("50 millis").pipe(Schedule.take(4)),
* () => Effect.succeed(Duration.millis(100)) // Fixed additional delay
* )
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* if (attempt < 5) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
* return `Success on attempt ${attempt}`
* }),
* progressiveRetrySchedule
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
* ```
*
* @category delays & timeouts
* @since 2.0.0
*/
<Output, Input, Error, Env, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
f: (output: Output) => Effect<Duration.Input, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
f: (output: Output) => Effect<Duration.Input, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2> =>
modifyDelay(
self,
(output, delay) =>
effect.map(f(output), (d) => Duration.sum(Duration.fromInputUnsafe(d), Duration.fromInputUnsafe(delay)))
))
/**
* Returns a schedule that runs `self` to completion, then runs `other`, and
* merges their outputs.
*
* **Example** (Sequencing quick and slow retries)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // First retry 3 times quickly, then switch to slower retries
* const quickRetries = Schedule.exponential("100 millis").pipe(
* Schedule.take(3)
* )
* const slowRetries = Schedule.exponential("1 second").pipe(
* Schedule.take(2)
* )
*
* const combinedRetries = Schedule.andThen(quickRetries, slowRetries)
*
* const program = Effect.gen(function*() {
* let attempt = 0
* yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Attempt ${attempt}`)
* if (attempt < 6) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Failure ${attempt}` }))
* }
* return `Success on attempt ${attempt}`
* }),
* combinedRetries
* )
* })
* ```
*
* @category sequencing
* @since 2.0.0
*/
export const andThen: {
/**
* Returns a schedule that runs `self` to completion, then runs `other`, and
* merges their outputs.
*
* **Example** (Sequencing quick and slow retries)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // First retry 3 times quickly, then switch to slower retries
* const quickRetries = Schedule.exponential("100 millis").pipe(
* Schedule.take(3)
* )
* const slowRetries = Schedule.exponential("1 second").pipe(
* Schedule.take(2)
* )
*
* const combinedRetries = Schedule.andThen(quickRetries, slowRetries)
*
* const program = Effect.gen(function*() {
* let attempt = 0
* yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Attempt ${attempt}`)
* if (attempt < 6) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Failure ${attempt}` }))
* }
* return `Success on attempt ${attempt}`
* }),
* combinedRetries
* )
* })
* ```
*
* @category sequencing
* @since 2.0.0
*/
<Output2, Input2, Error2, Env2>(other: Schedule<Output2, Input2, Error2, Env2>): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output | Output2, Input & Input2, Error | Error2, Env | Env2>
/**
* Returns a schedule that runs `self` to completion, then runs `other`, and
* merges their outputs.
*
* **Example** (Sequencing quick and slow retries)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // First retry 3 times quickly, then switch to slower retries
* const quickRetries = Schedule.exponential("100 millis").pipe(
* Schedule.take(3)
* )
* const slowRetries = Schedule.exponential("1 second").pipe(
* Schedule.take(2)
* )
*
* const combinedRetries = Schedule.andThen(quickRetries, slowRetries)
*
* const program = Effect.gen(function*() {
* let attempt = 0
* yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Attempt ${attempt}`)
* if (attempt < 6) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Failure ${attempt}` }))
* }
* return `Success on attempt ${attempt}`
* }),
* combinedRetries
* )
* })
* ```
*
* @category sequencing
* @since 2.0.0
*/
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output | Output2, Input & Input2, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output | Output2, Input & Input2, Error | Error2, Env | Env2> =>
map(andThenResult(self, other), (result) => effect.succeed(Result.merge(result))))
/**
* 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 { Console, Effect, Result, Schedule } from "effect"
*
* // Track which phase of the schedule we're in
* const phaseTracker = Schedule.andThenResult(
* Schedule.exponential("100 millis").pipe(Schedule.take(2)),
* Schedule.spaced("500 millis").pipe(Schedule.take(2))
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* phaseTracker.pipe(
* Schedule.tapOutput((result) =>
* Result.match(result, {
* onFailure: (phase1Output) => Console.log(`Phase 1: ${phase1Output}`),
* onSuccess: (phase2Output) => Console.log(`Phase 2: ${phase2Output}`)
* })
* )
* )
* )
* })
* ```
*
* @category sequencing
* @since 4.0.0
*/
export const andThenResult: {
/**
* 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 { Console, Effect, Result, Schedule } from "effect"
*
* // Track which phase of the schedule we're in
* const phaseTracker = Schedule.andThenResult(
* Schedule.exponential("100 millis").pipe(Schedule.take(2)),
* Schedule.spaced("500 millis").pipe(Schedule.take(2))
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* phaseTracker.pipe(
* Schedule.tapOutput((result) =>
* Result.match(result, {
* onFailure: (phase1Output) => Console.log(`Phase 1: ${phase1Output}`),
* onSuccess: (phase2Output) => Console.log(`Phase 2: ${phase2Output}`)
* })
* )
* )
* )
* })
* ```
*
* @category sequencing
* @since 4.0.0
*/
<Output2, Input2, Error2, Env2>(other: Schedule<Output2, Input2, Error2, Env2>): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Result.Result<Output2, Output>, 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 { Console, Effect, Result, Schedule } from "effect"
*
* // Track which phase of the schedule we're in
* const phaseTracker = Schedule.andThenResult(
* Schedule.exponential("100 millis").pipe(Schedule.take(2)),
* Schedule.spaced("500 millis").pipe(Schedule.take(2))
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* phaseTracker.pipe(
* Schedule.tapOutput((result) =>
* Result.match(result, {
* onFailure: (phase1Output) => Console.log(`Phase 1: ${phase1Output}`),
* onSuccess: (phase2Output) => Console.log(`Phase 2: ${phase2Output}`)
* })
* )
* )
* )
* })
* ```
*
* @category sequencing
* @since 4.0.0
*/
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Result.Result<Output2, Output>, Input & Input2, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Result.Result<Output, Output2>, Input & Input2, Error | Error2, Env | Env2> =>
fromStep(effect.sync(() => {
let currentSide = 0
let currentStep:
| undefined
| ((now: number, input: Input & Input2) => Pull.Pull<
[Result.Result<Output, Output2>, Duration.Duration],
Error | Error2,
Result.Result<Output, Output2>,
Env | Env2
>)
const left = map(self, Result.succeed)
const right = map(other, Result.fail)
return function recur(
now,
input
): Pull.Pull<
[Result.Result<Output, Output2>, Duration.Duration],
Error | Error2,
Result.Result<Output, Output2>,
Env | Env2
> {
if (currentStep) return currentStep(now, input)
return toStep<
/**
* 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 { Console, Effect, Result, Schedule } from "effect"
*
* // Track which phase of the schedule we're in
* const phaseTracker = Schedule.andThenResult(
* Schedule.exponential("100 millis").pipe(Schedule.take(2)),
* Schedule.spaced("500 millis").pipe(Schedule.take(2))
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* phaseTracker.pipe(
* Schedule.tapOutput((result) =>
* Result.match(result, {
* onFailure: (phase1Output) => Console.log(`Phase 1: ${phase1Output}`),
* onSuccess: (phase2Output) => Console.log(`Phase 2: ${phase2Output}`)
* })
* )
* )
* )
* })
* ```
*
* @category sequencing
* @since 4.0.0
*/
Result.Result<Output, Output2>,
/**
* 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 { Console, Effect, Result, Schedule } from "effect"
*
* // Track which phase of the schedule we're in
* const phaseTracker = Schedule.andThenResult(
* Schedule.exponential("100 millis").pipe(Schedule.take(2)),
* Schedule.spaced("500 millis").pipe(Schedule.take(2))
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* phaseTracker.pipe(
* Schedule.tapOutput((result) =>
* Result.match(result, {
* onFailure: (phase1Output) => Console.log(`Phase 1: ${phase1Output}`),
* onSuccess: (phase2Output) => Console.log(`Phase 2: ${phase2Output}`)
* })
* )
* )
* )
* })
* ```
*
* @category sequencing
* @since 4.0.0
*/
Input & Input2,
/**
* 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 { Console, Effect, Result, Schedule } from "effect"
*
* // Track which phase of the schedule we're in
* const phaseTracker = Schedule.andThenResult(
* Schedule.exponential("100 millis").pipe(Schedule.take(2)),
* Schedule.spaced("500 millis").pipe(Schedule.take(2))
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* phaseTracker.pipe(
* Schedule.tapOutput((result) =>
* Result.match(result, {
* onFailure: (phase1Output) => Console.log(`Phase 1: ${phase1Output}`),
* onSuccess: (phase2Output) => Console.log(`Phase 2: ${phase2Output}`)
* })
* )
* )
* )
* })
* ```
*
* @category sequencing
* @since 4.0.0
*/
Error | Error2,
/**
* 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 { Console, Effect, Result, Schedule } from "effect"
*
* // Track which phase of the schedule we're in
* const phaseTracker = Schedule.andThenResult(
* Schedule.exponential("100 millis").pipe(Schedule.take(2)),
* Schedule.spaced("500 millis").pipe(Schedule.take(2))
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* phaseTracker.pipe(
* Schedule.tapOutput((result) =>
* Result.match(result, {
* onFailure: (phase1Output) => Console.log(`Phase 1: ${phase1Output}`),
* onSuccess: (phase2Output) => Console.log(`Phase 2: ${phase2Output}`)
* })
* )
* )
* )
* })
* ```
*
* @category sequencing
* @since 4.0.0
*/
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 two `Schedule`s by recurring if both of the two schedules want
* to recur, using the maximum of the two durations between recurrences and
* outputting a tuple of the outputs of both schedules.
*
* **When to use**
*
* Use when the combined schedule should continue only while both schedules still recur.
*
* **Example** (Combining time and attempt limits)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Both schedules must want to continue for the combined schedule to continue
* const timeLimit = Schedule.spaced("1 second").pipe(Schedule.take(5)) // max 5 times
* const attemptLimit = Schedule.recurs(3) // max 3 attempts
*
* // Continues only while BOTH schedules want to continue (intersection/AND logic)
* const bothSchedule = Schedule.both(timeLimit, attemptLimit)
* // Outputs: [time_result, attempt_count] tuple
*
* const program = Effect.gen(function*() {
* const results = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task completed"
* }),
* bothSchedule.pipe(
* Schedule.tapOutput(([timeResult, attemptResult]) =>
* Console.log(`Time: ${timeResult}, Attempts: ${attemptResult}`)
* )
* )
* )
*
* yield* Console.log("Completed all executions")
* })
*
* // Both with different delay strategies - uses maximum delay
* const fastSchedule = Schedule.fixed("500 millis").pipe(Schedule.take(4))
* const slowSchedule = Schedule.spaced("2 seconds").pipe(Schedule.take(6))
*
* // Will use the slower (maximum) delay and stop when first schedule exhausts
* const conservativeSchedule = Schedule.both(fastSchedule, slowSchedule)
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Retry attempt ${attempt}`)
*
* if (attempt < 3) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
*
* return `Success on attempt ${attempt}`
* }),
* conservativeSchedule
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Both provides intersection semantics (AND logic)
* // Compare with either which provides union semantics (OR logic)
* ```
*
* @see {@link either} for continuing while either schedule still recurs
*
* @category combining
* @since 2.0.0
*/
export const both: {
/**
* Combines two `Schedule`s by recurring if both of the two schedules want
* to recur, using the maximum of the two durations between recurrences and
* outputting a tuple of the outputs of both schedules.
*
* **When to use**
*
* Use when the combined schedule should continue only while both schedules still recur.
*
* **Example** (Combining time and attempt limits)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Both schedules must want to continue for the combined schedule to continue
* const timeLimit = Schedule.spaced("1 second").pipe(Schedule.take(5)) // max 5 times
* const attemptLimit = Schedule.recurs(3) // max 3 attempts
*
* // Continues only while BOTH schedules want to continue (intersection/AND logic)
* const bothSchedule = Schedule.both(timeLimit, attemptLimit)
* // Outputs: [time_result, attempt_count] tuple
*
* const program = Effect.gen(function*() {
* const results = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task completed"
* }),
* bothSchedule.pipe(
* Schedule.tapOutput(([timeResult, attemptResult]) =>
* Console.log(`Time: ${timeResult}, Attempts: ${attemptResult}`)
* )
* )
* )
*
* yield* Console.log("Completed all executions")
* })
*
* // Both with different delay strategies - uses maximum delay
* const fastSchedule = Schedule.fixed("500 millis").pipe(Schedule.take(4))
* const slowSchedule = Schedule.spaced("2 seconds").pipe(Schedule.take(6))
*
* // Will use the slower (maximum) delay and stop when first schedule exhausts
* const conservativeSchedule = Schedule.both(fastSchedule, slowSchedule)
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Retry attempt ${attempt}`)
*
* if (attempt < 3) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
*
* return `Success on attempt ${attempt}`
* }),
* conservativeSchedule
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Both provides intersection semantics (AND logic)
* // Compare with either which provides union semantics (OR logic)
* ```
*
* @see {@link either} for continuing while either schedule still recurs
*
* @category combining
* @since 2.0.0
*/
<Output2, Input2, Error2, Env2, Output>(other: Schedule<Output2, Input2, Error2, Env2>): <Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2>
/**
* Combines two `Schedule`s by recurring if both of the two schedules want
* to recur, using the maximum of the two durations between recurrences and
* outputting a tuple of the outputs of both schedules.
*
* **When to use**
*
* Use when the combined schedule should continue only while both schedules still recur.
*
* **Example** (Combining time and attempt limits)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Both schedules must want to continue for the combined schedule to continue
* const timeLimit = Schedule.spaced("1 second").pipe(Schedule.take(5)) // max 5 times
* const attemptLimit = Schedule.recurs(3) // max 3 attempts
*
* // Continues only while BOTH schedules want to continue (intersection/AND logic)
* const bothSchedule = Schedule.both(timeLimit, attemptLimit)
* // Outputs: [time_result, attempt_count] tuple
*
* const program = Effect.gen(function*() {
* const results = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task completed"
* }),
* bothSchedule.pipe(
* Schedule.tapOutput(([timeResult, attemptResult]) =>
* Console.log(`Time: ${timeResult}, Attempts: ${attemptResult}`)
* )
* )
* )
*
* yield* Console.log("Completed all executions")
* })
*
* // Both with different delay strategies - uses maximum delay
* const fastSchedule = Schedule.fixed("500 millis").pipe(Schedule.take(4))
* const slowSchedule = Schedule.spaced("2 seconds").pipe(Schedule.take(6))
*
* // Will use the slower (maximum) delay and stop when first schedule exhausts
* const conservativeSchedule = Schedule.both(fastSchedule, slowSchedule)
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Retry attempt ${attempt}`)
*
* if (attempt < 3) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
*
* return `Success on attempt ${attempt}`
* }),
* conservativeSchedule
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Both provides intersection semantics (AND logic)
* // Compare with either which provides union semantics (OR logic)
* ```
*
* @see {@link either} for continuing while either schedule still recurs
*
* @category combining
* @since 2.0.0
*/
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2> =>
bothWith(self, other, (left, right) => [left, right]))
/**
* Combines two `Schedule`s by recurring if both of the two schedules want
* to recur, using the maximum of the two durations between recurrences and
* outputting the result of the left schedule (i.e. `self`).
*
* **When to use**
*
* Use when two schedules must both allow recurrence and only the left schedule's
* output is needed.
*
* **Example** (Combining schedules and keeping the left output)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules, keeping left output
* const leftSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("left-result"))
* )
* const rightSchedule = Schedule.spaced("50 millis")
*
* const combined = Schedule.bothLeft(leftSchedule, rightSchedule)
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-done"
* }),
* combined.pipe(Schedule.take(3))
* )
* })
* ```
*
* @category combining
* @since 2.0.0
*/
export const bothLeft: {
/**
* Combines two `Schedule`s by recurring if both of the two schedules want
* to recur, using the maximum of the two durations between recurrences and
* outputting the result of the left schedule (i.e. `self`).
*
* **When to use**
*
* Use when two schedules must both allow recurrence and only the left schedule's
* output is needed.
*
* **Example** (Combining schedules and keeping the left output)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules, keeping left output
* const leftSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("left-result"))
* )
* const rightSchedule = Schedule.spaced("50 millis")
*
* const combined = Schedule.bothLeft(leftSchedule, rightSchedule)
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-done"
* }),
* combined.pipe(Schedule.take(3))
* )
* })
* ```
*
* @category combining
* @since 2.0.0
*/
<Output2, Input2, Error2, Env2>(other: Schedule<Output2, Input2, Error2, Env2>): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input & Input2, Error | Error2, Env | Env2>
/**
* Combines two `Schedule`s by recurring if both of the two schedules want
* to recur, using the maximum of the two durations between recurrences and
* outputting the result of the left schedule (i.e. `self`).
*
* **When to use**
*
* Use when two schedules must both allow recurrence and only the left schedule's
* output is needed.
*
* **Example** (Combining schedules and keeping the left output)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules, keeping left output
* const leftSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("left-result"))
* )
* const rightSchedule = Schedule.spaced("50 millis")
*
* const combined = Schedule.bothLeft(leftSchedule, rightSchedule)
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-done"
* }),
* combined.pipe(Schedule.take(3))
* )
* })
* ```
*
* @category combining
* @since 2.0.0
*/
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output, Input & Input2, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output, Input & Input2, Error | Error2, Env | Env2> => bothWith(self, other, (output) => output))
/**
* Combines two `Schedule`s by recurring if both of the two schedules want
* to recur, using the maximum of the two durations between recurrences and
* outputting the result of the right schedule (i.e. `other`).
*
* **When to use**
*
* Use when two schedules must both allow recurrence and only the right
* schedule's output is needed.
*
* **Example** (Combining schedules and keeping the right output)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules, keeping right output
* const leftSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("left-result"))
* )
* const rightSchedule = Schedule.spaced("50 millis").pipe(
* Schedule.map(() => Effect.succeed("right-result"))
* )
*
* const combined = Schedule.bothRight(leftSchedule, rightSchedule)
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-done"
* }),
* combined.pipe(Schedule.take(3))
* )
* })
* ```
*
* @category combining
* @since 2.0.0
*/
export const bothRight: {
/**
* Combines two `Schedule`s by recurring if both of the two schedules want
* to recur, using the maximum of the two durations between recurrences and
* outputting the result of the right schedule (i.e. `other`).
*
* **When to use**
*
* Use when two schedules must both allow recurrence and only the right
* schedule's output is needed.
*
* **Example** (Combining schedules and keeping the right output)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules, keeping right output
* const leftSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("left-result"))
* )
* const rightSchedule = Schedule.spaced("50 millis").pipe(
* Schedule.map(() => Effect.succeed("right-result"))
* )
*
* const combined = Schedule.bothRight(leftSchedule, rightSchedule)
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-done"
* }),
* combined.pipe(Schedule.take(3))
* )
* })
* ```
*
* @category combining
* @since 2.0.0
*/
<Output2, Input2, Error2, Env2>(other: Schedule<Output2, Input2, Error2, Env2>): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input & Input2, Error | Error2, Env | Env2>
/**
* Combines two `Schedule`s by recurring if both of the two schedules want
* to recur, using the maximum of the two durations between recurrences and
* outputting the result of the right schedule (i.e. `other`).
*
* **When to use**
*
* Use when two schedules must both allow recurrence and only the right
* schedule's output is needed.
*
* **Example** (Combining schedules and keeping the right output)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules, keeping right output
* const leftSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("left-result"))
* )
* const rightSchedule = Schedule.spaced("50 millis").pipe(
* Schedule.map(() => Effect.succeed("right-result"))
* )
*
* const combined = Schedule.bothRight(leftSchedule, rightSchedule)
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-done"
* }),
* combined.pipe(Schedule.take(3))
* )
* })
* ```
*
* @category combining
* @since 2.0.0
*/
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output, Input & Input2, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output2, Input & Input2, Error | Error2, Env | Env2> => bothWith(self, other, (_, output) => output))
/**
* Combines two `Schedule`s by recurring if both of the two schedules want
* to recur, using the maximum of the two durations between recurrences and
* outputting the result of the combination of both schedule outputs using the
* specified `combine` function.
*
* **When to use**
*
* Use when two schedules must both allow recurrence and their outputs should be
* combined into a custom value.
*
* **Example** (Combining schedule outputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules with custom output combination
* const leftSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("left"))
* )
* const rightSchedule = Schedule.spaced("50 millis").pipe(
* Schedule.map(() => Effect.succeed("right"))
* )
*
* const combined = Schedule.bothWith(
* leftSchedule,
* rightSchedule,
* (left, right) => `${left}-${right}`
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* combined.pipe(Schedule.take(3))
* )
* })
* ```
*
* @category combining
* @since 2.0.0
*/
export const bothWith: {
/**
* Combines two `Schedule`s by recurring if both of the two schedules want
* to recur, using the maximum of the two durations between recurrences and
* outputting the result of the combination of both schedule outputs using the
* specified `combine` function.
*
* **When to use**
*
* Use when two schedules must both allow recurrence and their outputs should be
* combined into a custom value.
*
* **Example** (Combining schedule outputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules with custom output combination
* const leftSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("left"))
* )
* const rightSchedule = Schedule.spaced("50 millis").pipe(
* Schedule.map(() => Effect.succeed("right"))
* )
*
* const combined = Schedule.bothWith(
* leftSchedule,
* rightSchedule,
* (left, right) => `${left}-${right}`
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* combined.pipe(Schedule.take(3))
* )
* })
* ```
*
* @category combining
* @since 2.0.0
*/
<Output2, Input2, Error2, Env2, Output, Output3>(
other: Schedule<Output2, Input2, Error2, Env2>,
combine: (selfOutput: Output, otherOutput: Output2) => Output3
): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output3, Input & Input2, Error | Error2, Env | Env2>
/**
* Combines two `Schedule`s by recurring if both of the two schedules want
* to recur, using the maximum of the two durations between recurrences and
* outputting the result of the combination of both schedule outputs using the
* specified `combine` function.
*
* **When to use**
*
* Use when two schedules must both allow recurrence and their outputs should be
* combined into a custom value.
*
* **Example** (Combining schedule outputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules with custom output combination
* const leftSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("left"))
* )
* const rightSchedule = Schedule.spaced("50 millis").pipe(
* Schedule.map(() => Effect.succeed("right"))
* )
*
* const combined = Schedule.bothWith(
* leftSchedule,
* rightSchedule,
* (left, right) => `${left}-${right}`
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* combined.pipe(Schedule.take(3))
* )
* })
* ```
*
* @category combining
* @since 2.0.0
*/
<Output, Input, Error, Env, Output2, Input2, Error2, Env2, Output3>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>,
combine: (selfOutput: Output, otherOutput: Output2) => Output3
): Schedule<Output3, Input & Input2, Error | Error2, Env | Env2>
} = dual(3, <Output, Input, Error, Env, Output2, Input2, Error2, Env2, Output3>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>,
combine: (selfOutput: Output, otherOutput: Output2) => Output3
): Schedule<Output3, Input & Input2, Error | Error2, Env | Env2> =>
fromStep(effect.map(
effect.zip(toStep(self), toStep(other)),
([stepLeft, stepRight]) => (now, input) =>
Pull.matchEffect(stepLeft(now, input as Input), {
onSuccess: (leftResult) =>
stepRight(now, input as Input2).pipe(
effect.map((rightResult) =>
[
combine(leftResult[0], rightResult[0]),
Duration.max(leftResult[1], rightResult[1])
] as [Output3, Duration.Duration]
),
Pull.catchDone((rightDone) => Cause.done(combine(leftResult[0], rightDone as Output2)))
),
onDone: (leftDone) =>
stepRight(now, input as Input2).pipe(
effect.flatMap((rightResult) => Cause.done(combine(leftDone, rightResult[0]))),
Pull.catchDone((rightDone) => Cause.done(combine(leftDone, rightDone as Output2)))
),
onFailure: effect.failCause
})
)))
/**
* Returns a new `Schedule` that follows `self` and outputs the inputs seen so
* far as an array.
*
* **Details**
*
* This does not make the schedule run forever. The collected schedule stops
* when `self` stops and fails when `self` fails.
*
* **Example** (Collecting schedule inputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Collect all inputs passed to the schedule
* const inputCollector = Schedule.collectInputs(
* Schedule.spaced("100 millis")
* )
*
* const program = Effect.gen(function*() {
* let counter = 0
* yield* Effect.repeat(
* Effect.gen(function*() {
* counter++
* yield* Console.log(`Iteration ${counter}`)
* return `result-${counter}`
* }),
* inputCollector.pipe(Schedule.take(4))
* )
* })
* ```
*
* @category collecting
* @since 4.0.0
*/
export const collectInputs = <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
): Schedule<Array<Input>, Input, Error, Env> => collectWhile(passthrough(self), () => effect.succeed(true))
/**
* Returns a new `Schedule` that follows `self` and outputs the schedule outputs
* seen so far as an array.
*
* **Details**
*
* This does not make the schedule run forever. The collected schedule stops
* when `self` stops and fails when `self` fails.
*
* **Example** (Collecting schedule outputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Collect all outputs from the schedule
* const outputCollector = Schedule.collectOutputs(
* Schedule.recurs(4)
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* outputCollector.pipe(Schedule.take(4))
* )
* })
* ```
*
* @category collecting
* @since 4.0.0
*/
export const collectOutputs = <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
): Schedule<Array<Output>, Input, Error, Env> => collectWhile(self, () => effect.succeed(true))
/**
* Returns a new `Schedule` that recurs as long as the specified `predicate`
* returns `true`, collecting all outputs of the schedule into an array.
*
* **Example** (Collecting outputs while a condition holds)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Collect outputs while condition is met
* const collectWhileSmall = Schedule.collectWhile(
* Schedule.exponential("100 millis"),
* (metadata) =>
* Effect.succeed(metadata.attempt <= 5 && metadata.elapsed < 2000)
* )
*
* const conditionalProgram = Effect.gen(function*() {
* let attempt = 0
*
* const attempts = yield* Effect.repeat(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Retry attempt ${attempt}`)
* return `attempt-${attempt}`
* }),
* collectWhileSmall
* )
*
* yield* Console.log(`Collected attempts: [${attempts.join(", ")}]`)
* })
*
* // Collect with effectful predicate
* const collectWithCheck = Schedule.collectWhile(
* Schedule.fixed("1 second"),
* (metadata) =>
* Effect.gen(function*() {
* const shouldContinue = metadata.attempt < 5
* yield* Console.log(
* `Check ${metadata.attempt}: continue = ${shouldContinue}`
* )
* return shouldContinue
* })
* )
*
* const effectfulProgram = Effect.gen(function*() {
* const results = yield* Effect.repeat(
* Effect.succeed("checked"),
* collectWithCheck
* )
*
* yield* Console.log(`Final collection: ${results.length} items`)
* })
*
* // Collect samples with condition
* const samples = [12, 18, 24, 30, 36]
*
* const collectSamples = Schedule.collectWhile(
* Schedule.spaced("200 millis"),
* (metadata) =>
* Effect.succeed(metadata.attempt <= 5 && metadata.elapsed < 2000)
* )
*
* const samplingProgram = Effect.gen(function*() {
* let index = 0
* const collected = yield* Effect.repeat(
* Effect.gen(function*() {
* const sample = samples[index++]
* yield* Console.log(`Sample: ${sample}`)
* return sample
* }),
* collectSamples
* )
*
* const average = collected.reduce((sum, s) => sum + s, 0) / collected.length
* yield* Console.log(
* `Collected ${collected.length} samples, average: ${average.toFixed(1)}`
* )
* })
* ```
*
* @category collecting
* @since 2.0.0
*/
export const collectWhile: {
/**
* Returns a new `Schedule` that recurs as long as the specified `predicate`
* returns `true`, collecting all outputs of the schedule into an array.
*
* **Example** (Collecting outputs while a condition holds)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Collect outputs while condition is met
* const collectWhileSmall = Schedule.collectWhile(
* Schedule.exponential("100 millis"),
* (metadata) =>
* Effect.succeed(metadata.attempt <= 5 && metadata.elapsed < 2000)
* )
*
* const conditionalProgram = Effect.gen(function*() {
* let attempt = 0
*
* const attempts = yield* Effect.repeat(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Retry attempt ${attempt}`)
* return `attempt-${attempt}`
* }),
* collectWhileSmall
* )
*
* yield* Console.log(`Collected attempts: [${attempts.join(", ")}]`)
* })
*
* // Collect with effectful predicate
* const collectWithCheck = Schedule.collectWhile(
* Schedule.fixed("1 second"),
* (metadata) =>
* Effect.gen(function*() {
* const shouldContinue = metadata.attempt < 5
* yield* Console.log(
* `Check ${metadata.attempt}: continue = ${shouldContinue}`
* )
* return shouldContinue
* })
* )
*
* const effectfulProgram = Effect.gen(function*() {
* const results = yield* Effect.repeat(
* Effect.succeed("checked"),
* collectWithCheck
* )
*
* yield* Console.log(`Final collection: ${results.length} items`)
* })
*
* // Collect samples with condition
* const samples = [12, 18, 24, 30, 36]
*
* const collectSamples = Schedule.collectWhile(
* Schedule.spaced("200 millis"),
* (metadata) =>
* Effect.succeed(metadata.attempt <= 5 && metadata.elapsed < 2000)
* )
*
* const samplingProgram = Effect.gen(function*() {
* let index = 0
* const collected = yield* Effect.repeat(
* Effect.gen(function*() {
* const sample = samples[index++]
* yield* Console.log(`Sample: ${sample}`)
* return sample
* }),
* collectSamples
* )
*
* const average = collected.reduce((sum, s) => sum + s, 0) / collected.length
* yield* Console.log(
* `Collected ${collected.length} samples, average: ${average.toFixed(1)}`
* )
* })
* ```
*
* @category collecting
* @since 2.0.0
*/
<Input, Output, Error2 = never, Env2 = never>(
predicate: (
metadata: Metadata<Output, Input>
) => boolean | Effect<boolean, Error2, Env2>
): <Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Array<Output>, Input, Error | Error2, Env | Env2>
/**
* Returns a new `Schedule` that recurs as long as the specified `predicate`
* returns `true`, collecting all outputs of the schedule into an array.
*
* **Example** (Collecting outputs while a condition holds)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Collect outputs while condition is met
* const collectWhileSmall = Schedule.collectWhile(
* Schedule.exponential("100 millis"),
* (metadata) =>
* Effect.succeed(metadata.attempt <= 5 && metadata.elapsed < 2000)
* )
*
* const conditionalProgram = Effect.gen(function*() {
* let attempt = 0
*
* const attempts = yield* Effect.repeat(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Retry attempt ${attempt}`)
* return `attempt-${attempt}`
* }),
* collectWhileSmall
* )
*
* yield* Console.log(`Collected attempts: [${attempts.join(", ")}]`)
* })
*
* // Collect with effectful predicate
* const collectWithCheck = Schedule.collectWhile(
* Schedule.fixed("1 second"),
* (metadata) =>
* Effect.gen(function*() {
* const shouldContinue = metadata.attempt < 5
* yield* Console.log(
* `Check ${metadata.attempt}: continue = ${shouldContinue}`
* )
* return shouldContinue
* })
* )
*
* const effectfulProgram = Effect.gen(function*() {
* const results = yield* Effect.repeat(
* Effect.succeed("checked"),
* collectWithCheck
* )
*
* yield* Console.log(`Final collection: ${results.length} items`)
* })
*
* // Collect samples with condition
* const samples = [12, 18, 24, 30, 36]
*
* const collectSamples = Schedule.collectWhile(
* Schedule.spaced("200 millis"),
* (metadata) =>
* Effect.succeed(metadata.attempt <= 5 && metadata.elapsed < 2000)
* )
*
* const samplingProgram = Effect.gen(function*() {
* let index = 0
* const collected = yield* Effect.repeat(
* Effect.gen(function*() {
* const sample = samples[index++]
* yield* Console.log(`Sample: ${sample}`)
* return sample
* }),
* collectSamples
* )
*
* const average = collected.reduce((sum, s) => sum + s, 0) / collected.length
* yield* Console.log(
* `Collected ${collected.length} samples, average: ${average.toFixed(1)}`
* )
* })
* ```
*
* @category collecting
* @since 2.0.0
*/
<Output, Input, Error, Env, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
predicate: (
metadata: Metadata<Output, Input>
) => boolean | Effect<boolean, Error2, Env2>
): Schedule<Array<Output>, Input, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
predicate: (
metadata: Metadata<Output, Input>
) => boolean | Effect<boolean, Error2, Env2>
): Schedule<Array<Output>, Input, Error | Error2, Env | Env2> =>
reduce(while_(self, predicate), () => [] as Array<Output>, (outputs, output) => {
outputs.push(output)
return outputs
}))
/**
* 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 { Console, Data, Effect, Schedule } from "effect"
*
* class ScheduledTaskError extends Data.TaggedError("ScheduledTaskError")<{ readonly message: string }> {}
*
* // Run every minute
* const everyMinute = Schedule.cron("* * * * *")
*
* const minutelyProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Running minutely task")
* return "minute"
* }),
* everyMinute.pipe(
* Schedule.take(3), // Run only 3 times for demo
* Schedule.tapOutput((duration) =>
* Console.log(`Next execution in: ${duration}`)
* )
* )
* )
* })
*
* // Run every day at 2:30 AM
* const dailyBackup = Schedule.cron("30 2 * * *")
*
* const backupProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Running daily backup...")
* // Simulate backup process
* yield* Effect.sleep("2 seconds")
* yield* Console.log("Backup completed")
* return "backup-done"
* }),
* dailyBackup.pipe(
* Schedule.take(2) // Run 2 times for demo
* )
* )
* })
*
* // Run every Monday at 9:00 AM with timezone
* const weeklyReport = Schedule.cron("0 9 * * 1", "America/New_York")
*
* const reportProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Generating weekly report...")
* const report = {
* week: 42,
* status: "ready" as const
* }
* yield* Console.log(`Report generated: ${JSON.stringify(report)}`)
* return report
* }),
* weeklyReport.pipe(Schedule.take(1))
* )
* })
*
* // Run every 15 minutes during business hours (9 AM - 5 PM)
* const businessHoursCheck = Schedule.cron("0,15,30,45 9-17 * * 1-5")
*
* const businessProgram = Effect.gen(function*() {
* const statuses = ["healthy", "healthy", "degraded", "healthy"] as const
* let index = 0
*
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Business hours health check...")
* const status = statuses[index++]
* yield* Console.log(`System status: ${status}`)
* return status
* }),
* businessHoursCheck.pipe(
* Schedule.take(4) // Demo with 4 checks
* )
* )
* })
*
* // Run on specific days of the month
* const monthlyInvoice = Schedule.cron("0 10 1,15 * *") // 1st and 15th at 10 AM
*
* const invoiceProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Processing monthly invoices...")
* const invoiceCount = 72
* yield* Console.log(`Processed ${invoiceCount} invoices`)
* return { count: invoiceCount, batch: "2024-01-a" }
* }),
* monthlyInvoice.pipe(Schedule.take(1))
* )
* })
*
* // Complex cron with error handling
* const complexCron = Schedule.cron("0 2,4,6 * * *").pipe(
* Schedule.tapOutput((duration) =>
* Console.log(`Scheduled to run again in ${duration}`)
* )
* )
*
* const robustProgram = Effect.gen(function*() {
* let attempt = 0
*
* yield* Effect.repeat(
* Effect.gen(function*() {
* attempt++
* yield* Console.log("Complex scheduled task...")
* if (attempt === 1) {
* return yield* Effect.fail(new ScheduledTaskError({ message: "Scheduled task failed" }))
* }
* return "success"
* }),
* complexCron.pipe(Schedule.take(3))
* ).pipe(
* Effect.catch((error: unknown) =>
* Console.log(`Cron task error: ${String(error)}`)
* )
* )
* })
* ```
*
* @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 { Console, Data, Effect, Schedule } from "effect"
*
* class ScheduledTaskError extends Data.TaggedError("ScheduledTaskError")<{ readonly message: string }> {}
*
* // Run every minute
* const everyMinute = Schedule.cron("* * * * *")
*
* const minutelyProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Running minutely task")
* return "minute"
* }),
* everyMinute.pipe(
* Schedule.take(3), // Run only 3 times for demo
* Schedule.tapOutput((duration) =>
* Console.log(`Next execution in: ${duration}`)
* )
* )
* )
* })
*
* // Run every day at 2:30 AM
* const dailyBackup = Schedule.cron("30 2 * * *")
*
* const backupProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Running daily backup...")
* // Simulate backup process
* yield* Effect.sleep("2 seconds")
* yield* Console.log("Backup completed")
* return "backup-done"
* }),
* dailyBackup.pipe(
* Schedule.take(2) // Run 2 times for demo
* )
* )
* })
*
* // Run every Monday at 9:00 AM with timezone
* const weeklyReport = Schedule.cron("0 9 * * 1", "America/New_York")
*
* const reportProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Generating weekly report...")
* const report = {
* week: 42,
* status: "ready" as const
* }
* yield* Console.log(`Report generated: ${JSON.stringify(report)}`)
* return report
* }),
* weeklyReport.pipe(Schedule.take(1))
* )
* })
*
* // Run every 15 minutes during business hours (9 AM - 5 PM)
* const businessHoursCheck = Schedule.cron("0,15,30,45 9-17 * * 1-5")
*
* const businessProgram = Effect.gen(function*() {
* const statuses = ["healthy", "healthy", "degraded", "healthy"] as const
* let index = 0
*
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Business hours health check...")
* const status = statuses[index++]
* yield* Console.log(`System status: ${status}`)
* return status
* }),
* businessHoursCheck.pipe(
* Schedule.take(4) // Demo with 4 checks
* )
* )
* })
*
* // Run on specific days of the month
* const monthlyInvoice = Schedule.cron("0 10 1,15 * *") // 1st and 15th at 10 AM
*
* const invoiceProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Processing monthly invoices...")
* const invoiceCount = 72
* yield* Console.log(`Processed ${invoiceCount} invoices`)
* return { count: invoiceCount, batch: "2024-01-a" }
* }),
* monthlyInvoice.pipe(Schedule.take(1))
* )
* })
*
* // Complex cron with error handling
* const complexCron = Schedule.cron("0 2,4,6 * * *").pipe(
* Schedule.tapOutput((duration) =>
* Console.log(`Scheduled to run again in ${duration}`)
* )
* )
*
* const robustProgram = Effect.gen(function*() {
* let attempt = 0
*
* yield* Effect.repeat(
* Effect.gen(function*() {
* attempt++
* yield* Console.log("Complex scheduled task...")
* if (attempt === 1) {
* return yield* Effect.fail(new ScheduledTaskError({ message: "Scheduled task failed" }))
* }
* return "success"
* }),
* complexCron.pipe(Schedule.take(3))
* ).pipe(
* Effect.catch((error: unknown) =>
* Console.log(`Cron task error: ${String(error)}`)
* )
* )
* })
* ```
*
* @category constructors
* @since 2.0.0
*/
(expression: Cron.Cron): Schedule<Duration.Duration, unknown, Cron.CronParseError>
/**
* 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 { Console, Data, Effect, Schedule } from "effect"
*
* class ScheduledTaskError extends Data.TaggedError("ScheduledTaskError")<{ readonly message: string }> {}
*
* // Run every minute
* const everyMinute = Schedule.cron("* * * * *")
*
* const minutelyProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Running minutely task")
* return "minute"
* }),
* everyMinute.pipe(
* Schedule.take(3), // Run only 3 times for demo
* Schedule.tapOutput((duration) =>
* Console.log(`Next execution in: ${duration}`)
* )
* )
* )
* })
*
* // Run every day at 2:30 AM
* const dailyBackup = Schedule.cron("30 2 * * *")
*
* const backupProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Running daily backup...")
* // Simulate backup process
* yield* Effect.sleep("2 seconds")
* yield* Console.log("Backup completed")
* return "backup-done"
* }),
* dailyBackup.pipe(
* Schedule.take(2) // Run 2 times for demo
* )
* )
* })
*
* // Run every Monday at 9:00 AM with timezone
* const weeklyReport = Schedule.cron("0 9 * * 1", "America/New_York")
*
* const reportProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Generating weekly report...")
* const report = {
* week: 42,
* status: "ready" as const
* }
* yield* Console.log(`Report generated: ${JSON.stringify(report)}`)
* return report
* }),
* weeklyReport.pipe(Schedule.take(1))
* )
* })
*
* // Run every 15 minutes during business hours (9 AM - 5 PM)
* const businessHoursCheck = Schedule.cron("0,15,30,45 9-17 * * 1-5")
*
* const businessProgram = Effect.gen(function*() {
* const statuses = ["healthy", "healthy", "degraded", "healthy"] as const
* let index = 0
*
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Business hours health check...")
* const status = statuses[index++]
* yield* Console.log(`System status: ${status}`)
* return status
* }),
* businessHoursCheck.pipe(
* Schedule.take(4) // Demo with 4 checks
* )
* )
* })
*
* // Run on specific days of the month
* const monthlyInvoice = Schedule.cron("0 10 1,15 * *") // 1st and 15th at 10 AM
*
* const invoiceProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Processing monthly invoices...")
* const invoiceCount = 72
* yield* Console.log(`Processed ${invoiceCount} invoices`)
* return { count: invoiceCount, batch: "2024-01-a" }
* }),
* monthlyInvoice.pipe(Schedule.take(1))
* )
* })
*
* // Complex cron with error handling
* const complexCron = Schedule.cron("0 2,4,6 * * *").pipe(
* Schedule.tapOutput((duration) =>
* Console.log(`Scheduled to run again in ${duration}`)
* )
* )
*
* const robustProgram = Effect.gen(function*() {
* let attempt = 0
*
* yield* Effect.repeat(
* Effect.gen(function*() {
* attempt++
* yield* Console.log("Complex scheduled task...")
* if (attempt === 1) {
* return yield* Effect.fail(new ScheduledTaskError({ message: "Scheduled task failed" }))
* }
* return "success"
* }),
* complexCron.pipe(Schedule.take(3))
* ).pipe(
* Effect.catch((error: unknown) =>
* Console.log(`Cron task error: ${String(error)}`)
* )
* )
* })
* ```
*
* @category constructors
* @since 2.0.0
*/
(expression: string, tz?: string | DateTime.TimeZone): Schedule<Duration.Duration, unknown, Cron.CronParseError>
} = (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, _) =>
effect.sync(() => {
const next = Cron.next(cron, now).getTime()
const duration = Duration.millis(next - now)
return [duration, duration]
})))
}
/**
* Returns a new schedule that outputs the delay between each occurrence.
*
* **Example** (Extracting schedule delays)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Extract delays from an exponential backoff schedule
* const exponentialDelays = Schedule.delays(
* Schedule.exponential("100 millis").pipe(Schedule.take(5))
* )
*
* const delayProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task result"
* }),
* exponentialDelays.pipe(
* Schedule.tapOutput((delay) =>
* Console.log(`Waiting ${delay} before next execution`)
* )
* )
* )
* })
*
* // Monitor delays from a Fibonacci schedule
* const fibonacciDelays = Schedule.delays(
* Schedule.fibonacci("200 millis").pipe(Schedule.take(8))
* )
*
* const fibDelayProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Console.log("Fibonacci task"),
* fibonacciDelays.pipe(
* Schedule.tapOutput((delay) => Console.log(`Fibonacci delay: ${delay}`))
* )
* )
* })
*
* // Extract delays for analysis or logging
* const analyzeDelays = Schedule.delays(
* Schedule.spaced("1 second").pipe(Schedule.take(3))
* ).pipe(
* Schedule.tapOutput((delay) =>
* Effect.gen(function*() {
* yield* Console.log(`Recorded delay: ${delay}`)
* // In real applications, might send to metrics system
* })
* )
* )
*
* // Combine delays with other schedules for complex timing
* const adaptiveSchedule = Schedule.unfold(100, (delay) => Effect.succeed(delay * 1.5)).pipe(
* Schedule.take(6)
* )
*
* const adaptiveDelays = Schedule.delays(adaptiveSchedule)
*
* const adaptiveProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Adaptive task execution")
* return "completed"
* }),
* adaptiveDelays.pipe(
* Schedule.tapOutput((delay) => Console.log(`Adaptive delay: ${delay}`))
* )
* )
* })
*
* // Use delays to implement custom timing logic
* const customTimingSchedule = Schedule.delays(
* Schedule.exponential("50 millis").pipe(Schedule.take(4))
* ).pipe(
* Schedule.map((delay) => Effect.succeed(`Next execution in ${delay}`)),
* Schedule.tapOutput((message) => Console.log(message))
* )
* ```
*
* @category constructors
* @since 2.0.0
*/
export const delays = <Out, In, E, R>(self: Schedule<Out, In, E, R>): Schedule<Duration.Duration, In, E, R> =>
fromStep(
effect.map(
toStep(self),
(step) => (now, input) =>
Pull.catchDone(
effect.map(step(now, input), ([_, duration]) => [duration, duration]),
(_) => Cause.done(Duration.zero)
)
)
)
/**
* 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 { Console, Effect, Schedule } from "effect"
*
* const program = Effect.repeat(
* Console.log("runs again after one second"),
* Schedule.duration("1 second")
* )
* ```
*
* @see {@link during} for recurring until a duration has elapsed
*
* @category constructors
* @since 2.0.0
*/
export const duration = (durationInput: Duration.Input): Schedule<Duration.Duration> => {
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 { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Run a task for exactly 5 seconds, regardless of how many iterations
* const fiveSecondSchedule = Schedule.during("5 seconds")
*
* const timedProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed inside the time window")
* yield* Effect.sleep("500 millis") // Each task takes 500ms
* return "task done"
* }),
* fiveSecondSchedule.pipe(
* Schedule.tapOutput((elapsedDuration) =>
* Console.log(`Total elapsed: ${elapsedDuration}`)
* )
* )
* )
*
* yield* Console.log("Time limit reached!")
* })
*
* // Combine with other schedules for time-bounded execution
* const timeAndCountLimited = Schedule.spaced("1 second").pipe(
* Schedule.both(Schedule.during("10 seconds")), // Stop after 10 seconds OR
* Schedule.both(Schedule.recurs(15)) // 15 attempts, whichever comes first
* )
*
* // Burst execution within time window
* const burstWindow = Schedule.during("3 seconds")
*
* const burstProgram = Effect.gen(function*() {
* yield* Console.log("Starting burst execution...")
*
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Burst task")
* return "burst"
* }),
* burstWindow
* )
*
* yield* Console.log("Burst window completed")
* })
*
* // Timed retry window - retry for up to 30 seconds
* const timedRetry = Schedule.exponential("200 millis").pipe(
* Schedule.both(Schedule.during("30 seconds"))
* )
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Retry attempt ${attempt}`)
*
* if (attempt < 4) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
*
* return `Success on attempt ${attempt}`
* }),
* timedRetry
* )
*
* yield* Console.log(`Result: ${result}`)
* }).pipe(
* Effect.catch((error: unknown) => Console.log(`Timed out: ${String(error)}`))
* )
* ```
*
* @see {@link duration} for one delayed recurrence
*
* @category constructors
* @since 4.0.0
*/
export const during = (duration: Duration.Input): Schedule<Duration.Duration> =>
while_(
elapsed,
({ output }) => effect.succeed(Duration.isLessThanOrEqualTo(output, Duration.fromInputUnsafe(duration)))
)
/**
* Combines two `Schedule`s by recurring if either of the two schedules wants
* to recur, using the minimum of the two durations between recurrences and
* outputting a tuple of the outputs of both schedules.
*
* **When to use**
*
* Use when the combined schedule should continue while at least one schedule still recurs.
*
* **Example** (Combining schedules with either semantics)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Either continues as long as at least one schedule wants to continue
* const timeBasedSchedule = Schedule.spaced("2 seconds").pipe(Schedule.take(3))
* const countBasedSchedule = Schedule.recurs(5)
*
* // Continues until both schedules are exhausted (either still wants to recur)
* const eitherSchedule = Schedule.either(timeBasedSchedule, countBasedSchedule)
* // Outputs: [time_result, count_result] tuple
*
* const program = Effect.gen(function*() {
* const results = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task completed"
* }),
* eitherSchedule.pipe(
* Schedule.tapOutput(([timeResult, countResult]) =>
* Console.log(`Time: ${timeResult}, Count: ${countResult}`)
* )
* )
* )
*
* yield* Console.log(`Total executions: ${results.length}`)
* })
*
* // Either with different delay strategies
* const aggressiveRetry = Schedule.exponential("100 millis").pipe(
* Schedule.take(3)
* )
* const fallbackRetry = Schedule.fixed("5 seconds").pipe(Schedule.take(2))
*
* // Will use the more aggressive retry until it's exhausted, then fallback
* const combinedRetry = Schedule.either(aggressiveRetry, fallbackRetry)
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Retry attempt ${attempt}`)
*
* if (attempt < 6) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
*
* return `Success on attempt ${attempt}`
* }),
* combinedRetry
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Either provides union semantics (OR logic)
* // Compare with both, which provides intersection semantics (AND logic)
* ```
*
* @see {@link both} for continuing only while both schedules still recur
*
* @category combining
* @since 2.0.0
*/
export const either: {
/**
* Combines two `Schedule`s by recurring if either of the two schedules wants
* to recur, using the minimum of the two durations between recurrences and
* outputting a tuple of the outputs of both schedules.
*
* **When to use**
*
* Use when the combined schedule should continue while at least one schedule still recurs.
*
* **Example** (Combining schedules with either semantics)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Either continues as long as at least one schedule wants to continue
* const timeBasedSchedule = Schedule.spaced("2 seconds").pipe(Schedule.take(3))
* const countBasedSchedule = Schedule.recurs(5)
*
* // Continues until both schedules are exhausted (either still wants to recur)
* const eitherSchedule = Schedule.either(timeBasedSchedule, countBasedSchedule)
* // Outputs: [time_result, count_result] tuple
*
* const program = Effect.gen(function*() {
* const results = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task completed"
* }),
* eitherSchedule.pipe(
* Schedule.tapOutput(([timeResult, countResult]) =>
* Console.log(`Time: ${timeResult}, Count: ${countResult}`)
* )
* )
* )
*
* yield* Console.log(`Total executions: ${results.length}`)
* })
*
* // Either with different delay strategies
* const aggressiveRetry = Schedule.exponential("100 millis").pipe(
* Schedule.take(3)
* )
* const fallbackRetry = Schedule.fixed("5 seconds").pipe(Schedule.take(2))
*
* // Will use the more aggressive retry until it's exhausted, then fallback
* const combinedRetry = Schedule.either(aggressiveRetry, fallbackRetry)
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Retry attempt ${attempt}`)
*
* if (attempt < 6) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
*
* return `Success on attempt ${attempt}`
* }),
* combinedRetry
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Either provides union semantics (OR logic)
* // Compare with both, which provides intersection semantics (AND logic)
* ```
*
* @see {@link both} for continuing only while both schedules still recur
*
* @category combining
* @since 2.0.0
*/
<Output2, Input2, Error2, Env2>(other: Schedule<Output2, Input2, Error2, Env2>): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2>
/**
* Combines two `Schedule`s by recurring if either of the two schedules wants
* to recur, using the minimum of the two durations between recurrences and
* outputting a tuple of the outputs of both schedules.
*
* **When to use**
*
* Use when the combined schedule should continue while at least one schedule still recurs.
*
* **Example** (Combining schedules with either semantics)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Either continues as long as at least one schedule wants to continue
* const timeBasedSchedule = Schedule.spaced("2 seconds").pipe(Schedule.take(3))
* const countBasedSchedule = Schedule.recurs(5)
*
* // Continues until both schedules are exhausted (either still wants to recur)
* const eitherSchedule = Schedule.either(timeBasedSchedule, countBasedSchedule)
* // Outputs: [time_result, count_result] tuple
*
* const program = Effect.gen(function*() {
* const results = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task completed"
* }),
* eitherSchedule.pipe(
* Schedule.tapOutput(([timeResult, countResult]) =>
* Console.log(`Time: ${timeResult}, Count: ${countResult}`)
* )
* )
* )
*
* yield* Console.log(`Total executions: ${results.length}`)
* })
*
* // Either with different delay strategies
* const aggressiveRetry = Schedule.exponential("100 millis").pipe(
* Schedule.take(3)
* )
* const fallbackRetry = Schedule.fixed("5 seconds").pipe(Schedule.take(2))
*
* // Will use the more aggressive retry until it's exhausted, then fallback
* const combinedRetry = Schedule.either(aggressiveRetry, fallbackRetry)
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Retry attempt ${attempt}`)
*
* if (attempt < 6) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
*
* return `Success on attempt ${attempt}`
* }),
* combinedRetry
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Either provides union semantics (OR logic)
* // Compare with both, which provides intersection semantics (AND logic)
* ```
*
* @see {@link both} for continuing only while both schedules still recur
*
* @category combining
* @since 2.0.0
*/
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<[Output, Output2], Input & Input2, Error | Error2, Env | Env2> =>
eitherWith(self, other, (left, right) => [left, right]))
/**
* Combines two `Schedule`s by recurring if either of the two schedules wants
* to recur, using the minimum of the two durations between recurrences and
* outputting the result of the left schedule (i.e. `self`).
*
* **When to use**
*
* Use when either schedule may keep recurrence going and only the left
* schedule's output is needed.
*
* **Example** (Combining either schedules and keeping the left output)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules with either semantics, keeping left output
* const primarySchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("primary-result")),
* Schedule.take(2)
* )
* const backupSchedule = Schedule.spaced("500 millis").pipe(
* Schedule.map(() => Effect.succeed("backup-result"))
* )
*
* const combined = Schedule.eitherLeft(primarySchedule, backupSchedule)
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-done"
* }),
* combined.pipe(Schedule.take(5))
* )
* })
* ```
*
* @category combining
* @since 4.0.0
*/
export const eitherLeft: {
/**
* Combines two `Schedule`s by recurring if either of the two schedules wants
* to recur, using the minimum of the two durations between recurrences and
* outputting the result of the left schedule (i.e. `self`).
*
* **When to use**
*
* Use when either schedule may keep recurrence going and only the left
* schedule's output is needed.
*
* **Example** (Combining either schedules and keeping the left output)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules with either semantics, keeping left output
* const primarySchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("primary-result")),
* Schedule.take(2)
* )
* const backupSchedule = Schedule.spaced("500 millis").pipe(
* Schedule.map(() => Effect.succeed("backup-result"))
* )
*
* const combined = Schedule.eitherLeft(primarySchedule, backupSchedule)
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-done"
* }),
* combined.pipe(Schedule.take(5))
* )
* })
* ```
*
* @category combining
* @since 4.0.0
*/
<Output2, Input2, Error2, Env2>(other: Schedule<Output2, Input2, Error2, Env2>): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input & Input2, Error | Error2, Env | Env2>
/**
* Combines two `Schedule`s by recurring if either of the two schedules wants
* to recur, using the minimum of the two durations between recurrences and
* outputting the result of the left schedule (i.e. `self`).
*
* **When to use**
*
* Use when either schedule may keep recurrence going and only the left
* schedule's output is needed.
*
* **Example** (Combining either schedules and keeping the left output)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules with either semantics, keeping left output
* const primarySchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("primary-result")),
* Schedule.take(2)
* )
* const backupSchedule = Schedule.spaced("500 millis").pipe(
* Schedule.map(() => Effect.succeed("backup-result"))
* )
*
* const combined = Schedule.eitherLeft(primarySchedule, backupSchedule)
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-done"
* }),
* combined.pipe(Schedule.take(5))
* )
* })
* ```
*
* @category combining
* @since 4.0.0
*/
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output, Input & Input2, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output, Input & Input2, Error | Error2, Env | Env2> => eitherWith(self, other, (output) => output))
/**
* Combines two `Schedule`s by recurring if either of the two schedules wants
* to recur, using the minimum of the two durations between recurrences and
* outputting the result of the right schedule (i.e. `other`).
*
* **When to use**
*
* Use when either schedule may keep recurrence going and only the right
* schedule's output is needed.
*
* **Example** (Combining either schedules and keeping the right output)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules with either semantics, keeping right output
* const primarySchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("primary-result")),
* Schedule.take(2)
* )
* const backupSchedule = Schedule.spaced("500 millis").pipe(
* Schedule.map(() => Effect.succeed("backup-result"))
* )
*
* const combined = Schedule.eitherRight(primarySchedule, backupSchedule)
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-done"
* }),
* combined.pipe(Schedule.take(5))
* )
* })
* ```
*
* @category combining
* @since 4.0.0
*/
export const eitherRight: {
/**
* Combines two `Schedule`s by recurring if either of the two schedules wants
* to recur, using the minimum of the two durations between recurrences and
* outputting the result of the right schedule (i.e. `other`).
*
* **When to use**
*
* Use when either schedule may keep recurrence going and only the right
* schedule's output is needed.
*
* **Example** (Combining either schedules and keeping the right output)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules with either semantics, keeping right output
* const primarySchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("primary-result")),
* Schedule.take(2)
* )
* const backupSchedule = Schedule.spaced("500 millis").pipe(
* Schedule.map(() => Effect.succeed("backup-result"))
* )
*
* const combined = Schedule.eitherRight(primarySchedule, backupSchedule)
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-done"
* }),
* combined.pipe(Schedule.take(5))
* )
* })
* ```
*
* @category combining
* @since 4.0.0
*/
<Output2, Input2, Error2, Env2>(other: Schedule<Output2, Input2, Error2, Env2>): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output2, Input & Input2, Error | Error2, Env | Env2>
/**
* Combines two `Schedule`s by recurring if either of the two schedules wants
* to recur, using the minimum of the two durations between recurrences and
* outputting the result of the right schedule (i.e. `other`).
*
* **When to use**
*
* Use when either schedule may keep recurrence going and only the right
* schedule's output is needed.
*
* **Example** (Combining either schedules and keeping the right output)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine two schedules with either semantics, keeping right output
* const primarySchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("primary-result")),
* Schedule.take(2)
* )
* const backupSchedule = Schedule.spaced("500 millis").pipe(
* Schedule.map(() => Effect.succeed("backup-result"))
* )
*
* const combined = Schedule.eitherRight(primarySchedule, backupSchedule)
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-done"
* }),
* combined.pipe(Schedule.take(5))
* )
* })
* ```
*
* @category combining
* @since 4.0.0
*/
<Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output2, Input & Input2, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, Output2, Input2, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>
): Schedule<Output2, Input & Input2, Error | Error2, Env | Env2> => eitherWith(self, other, (_, output) => output))
/**
* Combines two `Schedule`s by recurring if either of the two schedules wants
* to recur, using the minimum of the two durations between recurrences and
* outputting the result of the combination of both schedule outputs using the
* specified `combine` function.
*
* **When to use**
*
* Use when either schedule may keep recurrence going and their outputs should be
* combined into a custom value.
*
* **Example** (Combining either schedule outputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine schedules with either semantics and custom combination
* const primarySchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("primary")),
* Schedule.take(2)
* )
* const fallbackSchedule = Schedule.spaced("500 millis").pipe(
* Schedule.map(() => Effect.succeed("fallback"))
* )
*
* const combined = Schedule.eitherWith(
* primarySchedule,
* fallbackSchedule,
* (primary, fallback) => `${primary}+${fallback}`
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* combined.pipe(Schedule.take(5))
* )
* })
* ```
*
* @category combining
* @since 2.0.0
*/
export const eitherWith: {
/**
* Combines two `Schedule`s by recurring if either of the two schedules wants
* to recur, using the minimum of the two durations between recurrences and
* outputting the result of the combination of both schedule outputs using the
* specified `combine` function.
*
* **When to use**
*
* Use when either schedule may keep recurrence going and their outputs should be
* combined into a custom value.
*
* **Example** (Combining either schedule outputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine schedules with either semantics and custom combination
* const primarySchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("primary")),
* Schedule.take(2)
* )
* const fallbackSchedule = Schedule.spaced("500 millis").pipe(
* Schedule.map(() => Effect.succeed("fallback"))
* )
*
* const combined = Schedule.eitherWith(
* primarySchedule,
* fallbackSchedule,
* (primary, fallback) => `${primary}+${fallback}`
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* combined.pipe(Schedule.take(5))
* )
* })
* ```
*
* @category combining
* @since 2.0.0
*/
<Output2, Input2, Error2, Env2, Output, Output3>(
other: Schedule<Output2, Input2, Error2, Env2>,
combine: (selfOutput: Output, otherOutput: Output2) => Output3
): <Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output3, Input & Input2, Error | Error2, Env | Env2>
/**
* Combines two `Schedule`s by recurring if either of the two schedules wants
* to recur, using the minimum of the two durations between recurrences and
* outputting the result of the combination of both schedule outputs using the
* specified `combine` function.
*
* **When to use**
*
* Use when either schedule may keep recurrence going and their outputs should be
* combined into a custom value.
*
* **Example** (Combining either schedule outputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine schedules with either semantics and custom combination
* const primarySchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("primary")),
* Schedule.take(2)
* )
* const fallbackSchedule = Schedule.spaced("500 millis").pipe(
* Schedule.map(() => Effect.succeed("fallback"))
* )
*
* const combined = Schedule.eitherWith(
* primarySchedule,
* fallbackSchedule,
* (primary, fallback) => `${primary}+${fallback}`
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* combined.pipe(Schedule.take(5))
* )
* })
* ```
*
* @category combining
* @since 2.0.0
*/
<Output, Input, Error, Env, Output2, Input2, Error2, Env2, Output3>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>,
combine: (selfOutput: Output, otherOutput: Output2) => Output3
): Schedule<Output3, Input & Input2, Error | Error2, Env | Env2>
} = dual(3, <Output, Input, Error, Env, Output2, Input2, Error2, Env2, Output3>(
self: Schedule<Output, Input, Error, Env>,
other: Schedule<Output2, Input2, Error2, Env2>,
combine: (selfOutput: Output, otherOutput: Output2) => Output3
): Schedule<Output3, Input & Input2, Error | Error2, Env | Env2> =>
fromStep(effect.map(
effect.zip(toStep(self), toStep(other)),
([stepLeft, stepRight]) => (now, input) =>
Pull.matchEffect(stepLeft(now, input as Input), {
onSuccess: (leftResult) =>
stepRight(now, input as Input2).pipe(
effect.map((rightResult) =>
[combine(leftResult[0], rightResult[0]), Duration.min(leftResult[1], rightResult[1])] as [
Output3,
Duration.Duration
]
),
Pull.catchDone((rightDone) =>
effect.succeed</**
* Combines two `Schedule`s by recurring if either of the two schedules wants
* to recur, using the minimum of the two durations between recurrences and
* outputting the result of the combination of both schedule outputs using the
* specified `combine` function.
*
* **When to use**
*
* Use when either schedule may keep recurrence going and their outputs should be
* combined into a custom value.
*
* **Example** (Combining either schedule outputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Combine schedules with either semantics and custom combination
* const primarySchedule = Schedule.exponential("100 millis").pipe(
* Schedule.map(() => Effect.succeed("primary")),
* Schedule.take(2)
* )
* const fallbackSchedule = Schedule.spaced("500 millis").pipe(
* Schedule.map(() => Effect.succeed("fallback"))
* )
*
* const combined = Schedule.eitherWith(
* primarySchedule,
* fallbackSchedule,
* (primary, fallback) => `${primary}+${fallback}`
* )
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task-result"
* }),
* combined.pipe(Schedule.take(5))
* )
* })
* ```
*
* @category combining
* @since 2.0.0
*/
[Output3, Duration.Duration]>([
combine(leftResult[0], rightDone as Output2),
leftResult[1]
])
)
),
onFailure: effect.failCause,
onDone: (leftDone) =>
stepRight(now, input as Input2).pipe(
effect.map((rightResult) =>
[combine(leftDone, rightResult[0]), rightResult[1]] as [
Output3,
Duration.Duration
]
),
Pull.catchDone((rightDone) => Cause.done(combine(leftDone, rightDone as Output2)))
)
})
)))
/**
* Schedule that always recurs and returns the total elapsed duration since the
* first recurrence.
*
* **Details**
*
* This schedule never stops and outputs the cumulative time that has passed since the schedule
* started executing. Useful for tracking execution time or implementing time-based logic.
*
* **Example** (Measuring elapsed schedule time)
*
* ```ts
* import { Console, Duration, Effect, Schedule } from "effect"
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Console.log("Running task..."),
* Schedule.spaced("1 second").pipe(
* Schedule.both(Schedule.elapsed),
* Schedule.tapOutput(([count, duration]) =>
* Console.log(`Run ${count}, elapsed: ${Duration.toMillis(duration)}ms`)
* ),
* Schedule.take(5)
* )
* )
* })
* ```
*
* @category constructors
* @since 2.0.0
*/
export const elapsed: Schedule<Duration.Duration> = fromStepWithMetadata(
effect.succeed((meta) => effect.succeed([Duration.millis(meta.elapsed), Duration.zero] as const))
)
/**
* 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 { Console, Data, Effect, Schedule } from "effect"
*
* class RetryFailure extends Data.TaggedError("RetryFailure")<{ readonly message: string }> {}
*
* // Basic exponential backoff with default factor of 2
* const basicExponential = Schedule.exponential("100 millis")
* // Delays: 100ms, 200ms, 400ms, 800ms, 1600ms, ...
*
* // Custom exponential backoff with factor 1.5
* const gentleExponential = Schedule.exponential("200 millis", 1.5)
* // Delays: 200ms, 300ms, 450ms, 675ms, 1012ms, ...
*
* // Retry with exponential backoff (limited to 5 attempts)
* const retryPolicy = Schedule.exponential("50 millis").pipe(
* Schedule.both(Schedule.recurs(5))
* )
*
* const program = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* if (attempt < 4) {
* yield* Console.log(`Attempt ${attempt} failed, retrying...`)
* return yield* Effect.fail(new RetryFailure({ message: `Failure ${attempt}` }))
* }
* return `Success on attempt ${attempt}`
* }),
* retryPolicy
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Will retry with delays: 50ms, 100ms, 200ms before success
* ```
*
* @category constructors
* @since 2.0.0
*/
export const exponential = (
base: Duration.Input,
factor: number = 2
): Schedule<Duration.Duration> => {
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 { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Basic Fibonacci schedule starting with 100ms
* const fibSchedule = Schedule.fibonacci("100 millis")
* // Delays: 100ms, 100ms, 200ms, 300ms, 500ms, 800ms, 1300ms, ...
*
* // Retry with Fibonacci backoff for gradual increase
* const retryWithFib = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Attempt ${attempt}`)
*
* if (attempt < 5) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
*
* return `Success on attempt ${attempt}`
* }),
* Schedule.fibonacci("50 millis").pipe(
* Schedule.both(Schedule.recurs(6)), // Maximum 6 retries
* Schedule.tapOutput((delay) => Console.log(`Next retry in ${delay}`))
* )
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Heartbeat with Fibonacci intervals (starts fast, gets slower)
* const adaptiveHeartbeat = Effect.gen(function*() {
* yield* Console.log("Heartbeat")
* return "pulse"
* }).pipe(
* Effect.repeat(
* Schedule.fibonacci("200 millis").pipe(
* Schedule.take(8) // First 8 heartbeats
* )
* )
* )
*
* // Fibonacci vs exponential comparison
* const compareSchedules = Effect.gen(function*() {
* yield* Console.log("=== Fibonacci Delays ===")
* // 100ms, 100ms, 200ms, 300ms, 500ms, 800ms
*
* yield* Console.log("=== Exponential Delays ===")
* // 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms
*
* // Fibonacci grows more slowly than exponential
* })
* ```
*
* @category constructors
* @since 2.0.0
*/
export const fibonacci = (one: Duration.Input): Schedule<Duration.Duration> => {
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 { Console, Effect, Schedule } from "effect"
*
* // Fixed interval schedule - recurs on a one-second cadence
* const everySecond = Schedule.fixed("1 second")
*
* // Health check that runs at fixed intervals
* const healthCheck = Effect.gen(function*() {
* yield* Console.log("Health check")
* yield* Effect.sleep("200 millis") // simulate health check work
* return "healthy"
* }).pipe(
* Effect.repeat(Schedule.fixed("2 seconds").pipe(Schedule.take(5)))
* )
*
* // Difference between fixed and spaced:
* // - fixed: maintains constant rate regardless of action duration
* // - spaced: waits for the duration AFTER each action completes
*
* const longRunningTask = Effect.gen(function*() {
* yield* Console.log("Task started")
* yield* Effect.sleep("1.5 seconds") // Longer than interval
* yield* Console.log("Task completed")
* return "done"
* })
*
* // Fixed schedule: if task takes 1.5s but interval is 1s,
* // next execution happens immediately (no pile-up)
* const fixedSchedule = longRunningTask.pipe(
* Effect.repeat(Schedule.fixed("1 second").pipe(Schedule.take(3)))
* )
*
* // Comparing with spaced (waits 1s AFTER each task)
* const spacedSchedule = longRunningTask.pipe(
* Effect.repeat(Schedule.spaced("1 second").pipe(Schedule.take(3)))
* )
*
* const program = Effect.gen(function*() {
* yield* Console.log("=== Fixed Schedule Demo ===")
* yield* fixedSchedule
*
* yield* Console.log("=== Spaced Schedule Demo ===")
* yield* spacedSchedule
* })
* ```
*
* @see {@link spaced} for delaying after each action completes
*
* @category constructors
* @since 2.0.0
*/
export const fixed = (interval: Duration.Input): Schedule<number> => {
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 the output of this schedule using the
* specified function.
*
* **Example** (Mapping schedule outputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Transform schedule output from number to string
* const countSchedule = Schedule.recurs(5).pipe(
* Schedule.map((count) => Effect.succeed(`Execution #${count + 1}`))
* )
*
* // Map schedule delays to human-readable format
* const readableDelays = Schedule.exponential("100 millis").pipe(
* Schedule.map((duration) => Effect.succeed(`Next retry in ${duration}`))
* )
*
* // Transform numeric output to structured data
* const structuredSchedule = Schedule.spaced("1 second").pipe(
* Schedule.map((recurrence) => Effect.succeed({
* iteration: recurrence + 1,
* phase: recurrence < 5 ? "warmup" as const : "steady" as const
* }))
* )
*
* const program = Effect.gen(function*() {
* const results = yield* Effect.repeat(
* Effect.succeed("task completed"),
* structuredSchedule.pipe(
* Schedule.take(8),
* Schedule.tapOutput((info) =>
* Console.log(
* `${info.phase} phase - iteration ${info.iteration}`
* )
* )
* )
* )
*
* yield* Console.log(`Completed iterations`)
* })
*
* // Map with effectful transformation
* const effectfulMap = Schedule.fixed("2 seconds").pipe(
* Schedule.map((count) =>
* Effect.gen(function*() {
* yield* Console.log(`Processing count: ${count}`)
* return count * 10
* })
* )
* )
*
* // Combine mapping with other schedule operations
* const complexSchedule = Schedule.fibonacci("100 millis").pipe(
* Schedule.map((delay) => Effect.succeed(`Delay: ${delay}`))
* )
* ```
*
* @category mapping
* @since 2.0.0
*/
export const map: {
/**
* Returns a new `Schedule` that maps the output of this schedule using the
* specified function.
*
* **Example** (Mapping schedule outputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Transform schedule output from number to string
* const countSchedule = Schedule.recurs(5).pipe(
* Schedule.map((count) => Effect.succeed(`Execution #${count + 1}`))
* )
*
* // Map schedule delays to human-readable format
* const readableDelays = Schedule.exponential("100 millis").pipe(
* Schedule.map((duration) => Effect.succeed(`Next retry in ${duration}`))
* )
*
* // Transform numeric output to structured data
* const structuredSchedule = Schedule.spaced("1 second").pipe(
* Schedule.map((recurrence) => Effect.succeed({
* iteration: recurrence + 1,
* phase: recurrence < 5 ? "warmup" as const : "steady" as const
* }))
* )
*
* const program = Effect.gen(function*() {
* const results = yield* Effect.repeat(
* Effect.succeed("task completed"),
* structuredSchedule.pipe(
* Schedule.take(8),
* Schedule.tapOutput((info) =>
* Console.log(
* `${info.phase} phase - iteration ${info.iteration}`
* )
* )
* )
* )
*
* yield* Console.log(`Completed iterations`)
* })
*
* // Map with effectful transformation
* const effectfulMap = Schedule.fixed("2 seconds").pipe(
* Schedule.map((count) =>
* Effect.gen(function*() {
* yield* Console.log(`Processing count: ${count}`)
* return count * 10
* })
* )
* )
*
* // Combine mapping with other schedule operations
* const complexSchedule = Schedule.fibonacci("100 millis").pipe(
* Schedule.map((delay) => Effect.succeed(`Delay: ${delay}`))
* )
* ```
*
* @category mapping
* @since 2.0.0
*/
<Output, Output2, Error2 = never, Env2 = never>(f: (output: Output) => Output2 | Effect<Output2, Error2, Env2>): <Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output2, Input, Error | Error2, Env | Env2>
/**
* Returns a new `Schedule` that maps the output of this schedule using the
* specified function.
*
* **Example** (Mapping schedule outputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Transform schedule output from number to string
* const countSchedule = Schedule.recurs(5).pipe(
* Schedule.map((count) => Effect.succeed(`Execution #${count + 1}`))
* )
*
* // Map schedule delays to human-readable format
* const readableDelays = Schedule.exponential("100 millis").pipe(
* Schedule.map((duration) => Effect.succeed(`Next retry in ${duration}`))
* )
*
* // Transform numeric output to structured data
* const structuredSchedule = Schedule.spaced("1 second").pipe(
* Schedule.map((recurrence) => Effect.succeed({
* iteration: recurrence + 1,
* phase: recurrence < 5 ? "warmup" as const : "steady" as const
* }))
* )
*
* const program = Effect.gen(function*() {
* const results = yield* Effect.repeat(
* Effect.succeed("task completed"),
* structuredSchedule.pipe(
* Schedule.take(8),
* Schedule.tapOutput((info) =>
* Console.log(
* `${info.phase} phase - iteration ${info.iteration}`
* )
* )
* )
* )
*
* yield* Console.log(`Completed iterations`)
* })
*
* // Map with effectful transformation
* const effectfulMap = Schedule.fixed("2 seconds").pipe(
* Schedule.map((count) =>
* Effect.gen(function*() {
* yield* Console.log(`Processing count: ${count}`)
* return count * 10
* })
* )
* )
*
* // Combine mapping with other schedule operations
* const complexSchedule = Schedule.fibonacci("100 millis").pipe(
* Schedule.map((delay) => Effect.succeed(`Delay: ${delay}`))
* )
* ```
*
* @category mapping
* @since 2.0.0
*/
<Output, Input, Error, Env, Output2, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
f: (output: Output) => Output2 | Effect<Output2, Error2, Env2>
): Schedule<Output2, Input, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, Output2, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
f: (output: Output) => Output2 | Effect<Output2, Error2, Env2>
): Schedule<Output2, Input, Error | Error2, Env | Env2> => {
const handle = Pull.matchEffect({
onSuccess: ([output, duration]: [Output, Duration.Duration]) => {
const result = f(output)
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<Error>,
onDone: (output: Output) => {
const result = f(output)
if (!isEffect(result)) return Cause.done(result as Output2)
return effect.flatMap(result, Cause.done)
}
})
return fromStep(effect.map(toStep(self), (step) => (now, input) => handle(step(now, input))))
})
/**
* 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 output)
*
* ```ts
* import { Console, Duration, Effect, Schedule } from "effect"
*
* // Modify delays based on output - increase delay on high iteration counts
* const adaptiveDelay = Schedule.recurs(10).pipe(
* Schedule.modifyDelay((output, delay) => {
* // Double the delay if we're seeing high iteration counts
* return Effect.succeed(output > 5 ? Duration.times(delay, 2) : delay)
* })
* )
*
* const program = Effect.gen(function*() {
* let counter = 0
* yield* Effect.repeat(
* Effect.gen(function*() {
* counter++
* yield* Console.log(`Attempt ${counter}`)
* return counter
* }),
* adaptiveDelay.pipe(Schedule.take(8))
* )
* })
* ```
*
* @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 output)
*
* ```ts
* import { Console, Duration, Effect, Schedule } from "effect"
*
* // Modify delays based on output - increase delay on high iteration counts
* const adaptiveDelay = Schedule.recurs(10).pipe(
* Schedule.modifyDelay((output, delay) => {
* // Double the delay if we're seeing high iteration counts
* return Effect.succeed(output > 5 ? Duration.times(delay, 2) : delay)
* })
* )
*
* const program = Effect.gen(function*() {
* let counter = 0
* yield* Effect.repeat(
* Effect.gen(function*() {
* counter++
* yield* Console.log(`Attempt ${counter}`)
* return counter
* }),
* adaptiveDelay.pipe(Schedule.take(8))
* )
* })
* ```
*
* @category delays & timeouts
* @since 2.0.0
*/
<Output, Error2 = never, Env2 = never>(
f: (
output: Output,
delay: Duration.Duration
) => Effect<Duration.Input, Error2, Env2>
): <Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error | Error2, Env | Env2>
/**
* 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 output)
*
* ```ts
* import { Console, Duration, Effect, Schedule } from "effect"
*
* // Modify delays based on output - increase delay on high iteration counts
* const adaptiveDelay = Schedule.recurs(10).pipe(
* Schedule.modifyDelay((output, delay) => {
* // Double the delay if we're seeing high iteration counts
* return Effect.succeed(output > 5 ? Duration.times(delay, 2) : delay)
* })
* )
*
* const program = Effect.gen(function*() {
* let counter = 0
* yield* Effect.repeat(
* Effect.gen(function*() {
* counter++
* yield* Console.log(`Attempt ${counter}`)
* return counter
* }),
* adaptiveDelay.pipe(Schedule.take(8))
* )
* })
* ```
*
* @category delays & timeouts
* @since 2.0.0
*/
<Output, Input, Error, Env, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
f: (
output: Output,
delay: Duration.Input
) => Effect<Duration.Input, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
f: (
output: Output,
delay: Duration.Input
) => Effect<Duration.Input, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2> =>
fromStep(effect.map(toStep(self), (step) => (now, input) =>
effect.flatMap(
step(now, input),
([output, delay]) => effect.map(f(output, delay), (delay) => [output, Duration.fromInputUnsafe(delay)])
))))
/**
* 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
*
* @category delays & timeouts
* @since 2.0.0
*/
export const jittered = <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
): Schedule<Output, Input, Error, Env> =>
modifyDelay(self, (_, delay) =>
effect.map(randomNext, (random) => {
const millis = Duration.toMillis(Duration.fromInputUnsafe(delay))
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 { Console, Effect, Schedule } from "effect"
*
* // Create a schedule that outputs the inputs instead of original outputs
* const inputSchedule = Schedule.passthrough(
* Schedule.exponential("100 millis").pipe(Schedule.take(3))
* )
*
* const program = Effect.gen(function*() {
* let counter = 0
* yield* Effect.repeat(
* Effect.gen(function*() {
* counter++
* yield* Console.log(`Task ${counter} executed`)
* return `result-${counter}`
* }),
* inputSchedule
* )
* })
* ```
*
* @category mapping
* @since 2.0.0
*/
export const passthrough = <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
): Schedule<Input, Input, Error, Env> =>
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 { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Basic recurs - retry at most 3 times
* const maxThreeAttempts = Schedule.recurs(3)
*
* // Retry a failing operation at most 5 times
* const program = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Attempt ${attempt}`)
*
* if (attempt < 4) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
*
* return `Success on attempt ${attempt}`
* }),
* Schedule.recurs(5) // Will retry up to 5 times
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Combining recurs with other schedules for sophisticated retry logic
* const complexRetry = Schedule.exponential("100 millis").pipe(
* Schedule.both(Schedule.recurs(3)) // At most 3 retries
* )
*
* // Allow ten recurrences after the initial run
* const tenRecurrences = Effect.gen(function*() {
* yield* Console.log("Executing task...")
* return "completed"
* }).pipe(
* Effect.repeat(Schedule.recurs(10))
* )
*
* // The schedule outputs the current recurrence count (0-based)
* const countingSchedule = Schedule.recurs(3).pipe(
* Schedule.tapOutput((count) => Console.log(`Execution #${count + 1}`))
* )
* ```
*
* @see {@link take} for limiting an existing schedule
*
* @category constructors
* @since 2.0.0
*/
export const recurs = (times: number): Schedule<number> =>
while_(forever, ({ attempt }) => effect.succeed(attempt <= times))
/**
* Returns a new `Schedule` that combines the outputs of the provided schedule
* using the specified effectful `combine` function and starting from the
* specified `initial` state.
*
* **Example** (Reducing schedule outputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Sum up execution counts from a counter schedule
* const sumSchedule = Schedule.reduce(
* Schedule.recurs(5),
* () => 0, // Initial sum
* (sum, count) => Effect.succeed(sum + count) // Add each count to the sum
* )
*
* const sumProgram = Effect.gen(function*() {
* const finalSum = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task"
* }),
* sumSchedule.pipe(
* Schedule.tapOutput((sum) => Console.log(`Running sum: ${sum}`))
* )
* )
*
* yield* Console.log(`Final sum: ${finalSum}`)
* })
*
* // Build a history of execution counts
* const historySchedule = Schedule.reduce(
* Schedule.spaced("1 second").pipe(Schedule.take(4)),
* () => [] as Array<number>, // Initial empty array
* (history, executionNumber) => Effect.succeed([...history, executionNumber])
* )
*
* const historyProgram = Effect.gen(function*() {
* const timeline = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Recording execution...")
* return "recorded"
* }),
* historySchedule
* )
*
* yield* Console.log(
* `Execution timeline: ${timeline.join(", ")}`
* )
* })
*
* // Accumulate metrics with effectful combination
* const metricsAccumulator = Schedule.reduce(
* Schedule.recurs(6),
* () => ({ total: 0, count: 0, max: 0 }),
* (metrics, executionCount) => Effect.succeed({
* total: metrics.total + executionCount + 1,
* count: metrics.count + 1,
* max: Math.max(metrics.max, executionCount + 1)
* })
* )
*
* const metricsProgram = Effect.gen(function*() {
* const finalMetrics = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Processing...")
* return "processed"
* }),
* metricsAccumulator
* )
*
* const average = finalMetrics.total / finalMetrics.count
* yield* Console.log(`Final metrics: ${finalMetrics.count} executions`)
* yield* Console.log(
* `Average delay: ${average.toFixed(1)}ms, Max delay: ${finalMetrics.max}ms`
* )
* })
*
* // Build configuration state over time
* const configBuilder = Schedule.reduce(
* Schedule.fixed("500 millis").pipe(Schedule.take(3)),
* () => ({ retries: 1, timeout: 1000, backoff: 100 }),
* (config, executionNumber) => Effect.succeed({
* retries: config.retries + 1,
* timeout: config.timeout * 1.5,
* backoff: Math.min(config.backoff * 2, 5000)
* })
* )
*
* const configProgram = Effect.gen(function*() {
* const finalConfig = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Updating configuration...")
* return "updated"
* }),
* configBuilder.pipe(
* Schedule.tapOutput((config) =>
* Console.log(
* `Config: retries=${config.retries}, timeout=${config.timeout}ms`
* )
* )
* )
* )
*
* yield* Console.log(`Final config: ${JSON.stringify(finalConfig)}`)
* })
* ```
*
* @category folding
* @since 2.0.0
*/
export const reduce: {
/**
* Returns a new `Schedule` that combines the outputs of the provided schedule
* using the specified effectful `combine` function and starting from the
* specified `initial` state.
*
* **Example** (Reducing schedule outputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Sum up execution counts from a counter schedule
* const sumSchedule = Schedule.reduce(
* Schedule.recurs(5),
* () => 0, // Initial sum
* (sum, count) => Effect.succeed(sum + count) // Add each count to the sum
* )
*
* const sumProgram = Effect.gen(function*() {
* const finalSum = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task"
* }),
* sumSchedule.pipe(
* Schedule.tapOutput((sum) => Console.log(`Running sum: ${sum}`))
* )
* )
*
* yield* Console.log(`Final sum: ${finalSum}`)
* })
*
* // Build a history of execution counts
* const historySchedule = Schedule.reduce(
* Schedule.spaced("1 second").pipe(Schedule.take(4)),
* () => [] as Array<number>, // Initial empty array
* (history, executionNumber) => Effect.succeed([...history, executionNumber])
* )
*
* const historyProgram = Effect.gen(function*() {
* const timeline = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Recording execution...")
* return "recorded"
* }),
* historySchedule
* )
*
* yield* Console.log(
* `Execution timeline: ${timeline.join(", ")}`
* )
* })
*
* // Accumulate metrics with effectful combination
* const metricsAccumulator = Schedule.reduce(
* Schedule.recurs(6),
* () => ({ total: 0, count: 0, max: 0 }),
* (metrics, executionCount) => Effect.succeed({
* total: metrics.total + executionCount + 1,
* count: metrics.count + 1,
* max: Math.max(metrics.max, executionCount + 1)
* })
* )
*
* const metricsProgram = Effect.gen(function*() {
* const finalMetrics = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Processing...")
* return "processed"
* }),
* metricsAccumulator
* )
*
* const average = finalMetrics.total / finalMetrics.count
* yield* Console.log(`Final metrics: ${finalMetrics.count} executions`)
* yield* Console.log(
* `Average delay: ${average.toFixed(1)}ms, Max delay: ${finalMetrics.max}ms`
* )
* })
*
* // Build configuration state over time
* const configBuilder = Schedule.reduce(
* Schedule.fixed("500 millis").pipe(Schedule.take(3)),
* () => ({ retries: 1, timeout: 1000, backoff: 100 }),
* (config, executionNumber) => Effect.succeed({
* retries: config.retries + 1,
* timeout: config.timeout * 1.5,
* backoff: Math.min(config.backoff * 2, 5000)
* })
* )
*
* const configProgram = Effect.gen(function*() {
* const finalConfig = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Updating configuration...")
* return "updated"
* }),
* configBuilder.pipe(
* Schedule.tapOutput((config) =>
* Console.log(
* `Config: retries=${config.retries}, timeout=${config.timeout}ms`
* )
* )
* )
* )
*
* yield* Console.log(`Final config: ${JSON.stringify(finalConfig)}`)
* })
* ```
*
* @category folding
* @since 2.0.0
*/
<State, Output, Error2 = never, Env2 = never>(
initial: LazyArg<State>,
combine: (state: State, output: Output) => State | Effect<State, Error2, Env2>
): <Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<State, Input, Error | Error2, Env | Env2>
/**
* Returns a new `Schedule` that combines the outputs of the provided schedule
* using the specified effectful `combine` function and starting from the
* specified `initial` state.
*
* **Example** (Reducing schedule outputs)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Sum up execution counts from a counter schedule
* const sumSchedule = Schedule.reduce(
* Schedule.recurs(5),
* () => 0, // Initial sum
* (sum, count) => Effect.succeed(sum + count) // Add each count to the sum
* )
*
* const sumProgram = Effect.gen(function*() {
* const finalSum = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "task"
* }),
* sumSchedule.pipe(
* Schedule.tapOutput((sum) => Console.log(`Running sum: ${sum}`))
* )
* )
*
* yield* Console.log(`Final sum: ${finalSum}`)
* })
*
* // Build a history of execution counts
* const historySchedule = Schedule.reduce(
* Schedule.spaced("1 second").pipe(Schedule.take(4)),
* () => [] as Array<number>, // Initial empty array
* (history, executionNumber) => Effect.succeed([...history, executionNumber])
* )
*
* const historyProgram = Effect.gen(function*() {
* const timeline = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Recording execution...")
* return "recorded"
* }),
* historySchedule
* )
*
* yield* Console.log(
* `Execution timeline: ${timeline.join(", ")}`
* )
* })
*
* // Accumulate metrics with effectful combination
* const metricsAccumulator = Schedule.reduce(
* Schedule.recurs(6),
* () => ({ total: 0, count: 0, max: 0 }),
* (metrics, executionCount) => Effect.succeed({
* total: metrics.total + executionCount + 1,
* count: metrics.count + 1,
* max: Math.max(metrics.max, executionCount + 1)
* })
* )
*
* const metricsProgram = Effect.gen(function*() {
* const finalMetrics = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Processing...")
* return "processed"
* }),
* metricsAccumulator
* )
*
* const average = finalMetrics.total / finalMetrics.count
* yield* Console.log(`Final metrics: ${finalMetrics.count} executions`)
* yield* Console.log(
* `Average delay: ${average.toFixed(1)}ms, Max delay: ${finalMetrics.max}ms`
* )
* })
*
* // Build configuration state over time
* const configBuilder = Schedule.reduce(
* Schedule.fixed("500 millis").pipe(Schedule.take(3)),
* () => ({ retries: 1, timeout: 1000, backoff: 100 }),
* (config, executionNumber) => Effect.succeed({
* retries: config.retries + 1,
* timeout: config.timeout * 1.5,
* backoff: Math.min(config.backoff * 2, 5000)
* })
* )
*
* const configProgram = Effect.gen(function*() {
* const finalConfig = yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Updating configuration...")
* return "updated"
* }),
* configBuilder.pipe(
* Schedule.tapOutput((config) =>
* Console.log(
* `Config: retries=${config.retries}, timeout=${config.timeout}ms`
* )
* )
* )
* )
*
* yield* Console.log(`Final config: ${JSON.stringify(finalConfig)}`)
* })
* ```
*
* @category folding
* @since 2.0.0
*/
<Output, Input, Error, Env, State, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
initial: LazyArg<State>,
combine: (state: State, output: Output) => State | Effect<State, Error2, Env2>
): Schedule<State, Input, Error | Error2, Env | Env2>
} = dual(3, <Output, Input, Error, Env, State, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
initial: LazyArg<State>,
combine: (state: State, output: Output) => State | Effect<State, Error2, Env2>
): Schedule<State, Input, Error | Error2, Env | Env2> =>
fromStep(effect.map(toStep(self), (step) => {
let state = initial()
return (now, input) =>
Pull.matchEffect(step(now, input), {
onSuccess([output, delay]) {
const next = combine(state, output)
if (!isEffect(next)) {
state = next
return effect.succeed([next, delay] as [State, Duration.Duration])
}
return effect.map(next, (nextState) => {
state = nextState
return [nextState, delay]
})
},
onFailure: effect.failCause,
onDone(output) {
const next = combine(state, output)
return isEffect(next) ? effect.flatMap(next, Cause.done) : Cause.done(next)
}
})
})))
/**
* 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 { Console, Effect, Schedule } from "effect"
*
* // Basic spaced schedule - runs every 2 seconds
* const everyTwoSeconds = Schedule.spaced("2 seconds")
*
* // Heartbeat that runs indefinitely with fixed spacing
* const heartbeat = Effect.gen(function*() {
* yield* Console.log("Heartbeat")
* }).pipe(
* Effect.repeat(everyTwoSeconds)
* )
*
* // Limited repeat - run only 5 times with 1-second spacing
* const limitedTask = Effect.gen(function*() {
* yield* Console.log("Executing scheduled task...")
* yield* Effect.sleep("500 millis") // simulate work
* return "Task completed"
* }).pipe(
* Effect.repeat(
* Schedule.spaced("1 second").pipe(Schedule.take(5))
* )
* )
*
* // Simple spaced schedule with limited repetitions
* const limitedSpaced = Schedule.spaced("100 millis").pipe(
* Schedule.both(Schedule.recurs(5)) // at most 5 times
* )
*
* const program = Effect.gen(function*() {
* yield* Console.log("Starting spaced execution...")
*
* yield* Effect.repeat(
* Effect.succeed("work item"),
* limitedSpaced
* )
*
* yield* Console.log("Completed executions")
* })
* ```
*
* @see {@link fixed} for recurrence aligned to a regular cadence
*
* @category constructors
* @since 2.0.0
*/
export const spaced = (duration: Duration.Input): Schedule<number> => {
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 { Console, Effect, Schedule } from "effect"
*
* const monitoredSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.take(5),
* Schedule.tap((metadata) =>
* Console.log(
* `Attempt ${metadata.attempt} produced ${metadata.output} ` +
* `after ${metadata.elapsed}ms; next delay is ${metadata.duration}`
* )
* )
* )
*
* const program = Effect.retry(
* Effect.fail("transient error"),
* monitoredSchedule
* )
* ```
*
* @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 { Console, Effect, Schedule } from "effect"
*
* const monitoredSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.take(5),
* Schedule.tap((metadata) =>
* Console.log(
* `Attempt ${metadata.attempt} produced ${metadata.output} ` +
* `after ${metadata.elapsed}ms; next delay is ${metadata.duration}`
* )
* )
* )
*
* const program = Effect.retry(
* Effect.fail("transient error"),
* monitoredSchedule
* )
* ```
*
* @category sequencing
* @since 4.0.0
*/
<Output, Input, X, Error2, Env2>(f: (metadata: Metadata<Output, Input>) => Effect<X, Error2, Env2>): <Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error | Error2, Env | Env2>
/**
* 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 { Console, Effect, Schedule } from "effect"
*
* const monitoredSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.take(5),
* Schedule.tap((metadata) =>
* Console.log(
* `Attempt ${metadata.attempt} produced ${metadata.output} ` +
* `after ${metadata.elapsed}ms; next delay is ${metadata.duration}`
* )
* )
* )
*
* const program = Effect.retry(
* Effect.fail("transient error"),
* monitoredSchedule
* )
* ```
*
* @category sequencing
* @since 4.0.0
*/
<Output, Input, Error, Env, X, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
f: (metadata: Metadata<Output, Input>) => Effect<X, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, X, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
f: (metadata: Metadata<Output, Input>) => Effect<X, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2> =>
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 allows execution of an effectful function for
* every input to the schedule, but does not alter the inputs and outputs of
* the schedule.
*
* **Example** (Tapping retry inputs)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryError extends Data.TaggedError("RetryError")<{ readonly message: string }> {}
*
* // Log retry errors for debugging
* const errorLoggingSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.take(3),
* Schedule.tapInput((error: RetryError) =>
* Console.log(`Retry triggered by error: ${String(error)}`)
* )
* )
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* if (attempt < 4) {
* return yield* Effect.fail(new RetryError({ message: `Network timeout on attempt ${attempt}` }))
* }
* return `Success on attempt ${attempt}`
* }),
* errorLoggingSchedule
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Monitor input frequency for metrics
* const inputMonitoringSchedule = Schedule.spaced("1 second").pipe(
* Schedule.take(5),
* Schedule.tapInput((input: unknown) =>
* Effect.gen(function*() {
* yield* Console.log(`Input type: ${typeof input}`)
* // In real applications, might send metrics to monitoring system
* })
* )
* )
*
* // Input validation with side effects
* const validatingSchedule = Schedule.fixed("500 millis").pipe(
* Schedule.take(4),
* Schedule.tapInput((input: any) =>
* Effect.gen(function*() {
* if (typeof input === "object" && input !== null) {
* yield* Console.log(`Valid object input: ${JSON.stringify(input)}`)
* } else {
* yield* Console.log(`Warning: Non-object input received: ${input}`)
* }
* })
* )
* )
*
* const validationProgram = Effect.gen(function*() {
* let count = 0
*
* yield* Effect.repeat(
* Effect.gen(function*() {
* count++
* yield* Console.log("Task with validation")
* return { data: `sample-${count}` }
* }),
* validatingSchedule
* )
* })
*
* // Conditional alerting based on input
* const alertingSchedule = Schedule.exponential("200 millis").pipe(
* Schedule.take(6),
* Schedule.tapInput((error: RetryError) =>
* Effect.gen(function*() {
* if (String(error).includes("critical")) {
* yield* Console.log(`Critical error: ${String(error)}`)
* // In real applications, might trigger alerts or notifications
* } else {
* yield* Console.log(`Regular error: ${String(error)}`)
* }
* })
* )
* )
*
* const alertProgram = Effect.gen(function*() {
* let attempt = 0
*
* yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* const isCritical = attempt === 3
* const errorType = isCritical
* ? "critical database failure"
* : "temporary network issue"
* return yield* Effect.fail(new RetryError({ message: errorType }))
* }),
* alertingSchedule
* ).pipe(
* Effect.catch((error: unknown) =>
* Console.log(`All retries exhausted: ${String(error)}`)
* )
* )
* })
*
* // Chain multiple input taps for different purposes
* const comprehensiveSchedule = Schedule.fibonacci("100 millis").pipe(
* Schedule.take(5),
* Schedule.tapInput((error: RetryError) =>
* Console.log(`Error occurred: ${error._tag}`)
* ),
* Schedule.tapInput((error: RetryError) =>
* String(error).length > 20
* ? Console.log("Long error message detected")
* : Effect.void
* )
* )
* ```
*
* @category sequencing
* @since 2.0.0
*/
export const tapInput: {
/**
* Returns a new `Schedule` that allows execution of an effectful function for
* every input to the schedule, but does not alter the inputs and outputs of
* the schedule.
*
* **Example** (Tapping retry inputs)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryError extends Data.TaggedError("RetryError")<{ readonly message: string }> {}
*
* // Log retry errors for debugging
* const errorLoggingSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.take(3),
* Schedule.tapInput((error: RetryError) =>
* Console.log(`Retry triggered by error: ${String(error)}`)
* )
* )
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* if (attempt < 4) {
* return yield* Effect.fail(new RetryError({ message: `Network timeout on attempt ${attempt}` }))
* }
* return `Success on attempt ${attempt}`
* }),
* errorLoggingSchedule
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Monitor input frequency for metrics
* const inputMonitoringSchedule = Schedule.spaced("1 second").pipe(
* Schedule.take(5),
* Schedule.tapInput((input: unknown) =>
* Effect.gen(function*() {
* yield* Console.log(`Input type: ${typeof input}`)
* // In real applications, might send metrics to monitoring system
* })
* )
* )
*
* // Input validation with side effects
* const validatingSchedule = Schedule.fixed("500 millis").pipe(
* Schedule.take(4),
* Schedule.tapInput((input: any) =>
* Effect.gen(function*() {
* if (typeof input === "object" && input !== null) {
* yield* Console.log(`Valid object input: ${JSON.stringify(input)}`)
* } else {
* yield* Console.log(`Warning: Non-object input received: ${input}`)
* }
* })
* )
* )
*
* const validationProgram = Effect.gen(function*() {
* let count = 0
*
* yield* Effect.repeat(
* Effect.gen(function*() {
* count++
* yield* Console.log("Task with validation")
* return { data: `sample-${count}` }
* }),
* validatingSchedule
* )
* })
*
* // Conditional alerting based on input
* const alertingSchedule = Schedule.exponential("200 millis").pipe(
* Schedule.take(6),
* Schedule.tapInput((error: RetryError) =>
* Effect.gen(function*() {
* if (String(error).includes("critical")) {
* yield* Console.log(`Critical error: ${String(error)}`)
* // In real applications, might trigger alerts or notifications
* } else {
* yield* Console.log(`Regular error: ${String(error)}`)
* }
* })
* )
* )
*
* const alertProgram = Effect.gen(function*() {
* let attempt = 0
*
* yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* const isCritical = attempt === 3
* const errorType = isCritical
* ? "critical database failure"
* : "temporary network issue"
* return yield* Effect.fail(new RetryError({ message: errorType }))
* }),
* alertingSchedule
* ).pipe(
* Effect.catch((error: unknown) =>
* Console.log(`All retries exhausted: ${String(error)}`)
* )
* )
* })
*
* // Chain multiple input taps for different purposes
* const comprehensiveSchedule = Schedule.fibonacci("100 millis").pipe(
* Schedule.take(5),
* Schedule.tapInput((error: RetryError) =>
* Console.log(`Error occurred: ${error._tag}`)
* ),
* Schedule.tapInput((error: RetryError) =>
* String(error).length > 20
* ? Console.log("Long error message detected")
* : Effect.void
* )
* )
* ```
*
* @category sequencing
* @since 2.0.0
*/
<Input, X, Error2, Env2>(f: (input: Input) => Effect<X, Error2, Env2>): <Output, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error | Error2, Env | Env2>
/**
* Returns a new `Schedule` that allows execution of an effectful function for
* every input to the schedule, but does not alter the inputs and outputs of
* the schedule.
*
* **Example** (Tapping retry inputs)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryError extends Data.TaggedError("RetryError")<{ readonly message: string }> {}
*
* // Log retry errors for debugging
* const errorLoggingSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.take(3),
* Schedule.tapInput((error: RetryError) =>
* Console.log(`Retry triggered by error: ${String(error)}`)
* )
* )
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* if (attempt < 4) {
* return yield* Effect.fail(new RetryError({ message: `Network timeout on attempt ${attempt}` }))
* }
* return `Success on attempt ${attempt}`
* }),
* errorLoggingSchedule
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Monitor input frequency for metrics
* const inputMonitoringSchedule = Schedule.spaced("1 second").pipe(
* Schedule.take(5),
* Schedule.tapInput((input: unknown) =>
* Effect.gen(function*() {
* yield* Console.log(`Input type: ${typeof input}`)
* // In real applications, might send metrics to monitoring system
* })
* )
* )
*
* // Input validation with side effects
* const validatingSchedule = Schedule.fixed("500 millis").pipe(
* Schedule.take(4),
* Schedule.tapInput((input: any) =>
* Effect.gen(function*() {
* if (typeof input === "object" && input !== null) {
* yield* Console.log(`Valid object input: ${JSON.stringify(input)}`)
* } else {
* yield* Console.log(`Warning: Non-object input received: ${input}`)
* }
* })
* )
* )
*
* const validationProgram = Effect.gen(function*() {
* let count = 0
*
* yield* Effect.repeat(
* Effect.gen(function*() {
* count++
* yield* Console.log("Task with validation")
* return { data: `sample-${count}` }
* }),
* validatingSchedule
* )
* })
*
* // Conditional alerting based on input
* const alertingSchedule = Schedule.exponential("200 millis").pipe(
* Schedule.take(6),
* Schedule.tapInput((error: RetryError) =>
* Effect.gen(function*() {
* if (String(error).includes("critical")) {
* yield* Console.log(`Critical error: ${String(error)}`)
* // In real applications, might trigger alerts or notifications
* } else {
* yield* Console.log(`Regular error: ${String(error)}`)
* }
* })
* )
* )
*
* const alertProgram = Effect.gen(function*() {
* let attempt = 0
*
* yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* const isCritical = attempt === 3
* const errorType = isCritical
* ? "critical database failure"
* : "temporary network issue"
* return yield* Effect.fail(new RetryError({ message: errorType }))
* }),
* alertingSchedule
* ).pipe(
* Effect.catch((error: unknown) =>
* Console.log(`All retries exhausted: ${String(error)}`)
* )
* )
* })
*
* // Chain multiple input taps for different purposes
* const comprehensiveSchedule = Schedule.fibonacci("100 millis").pipe(
* Schedule.take(5),
* Schedule.tapInput((error: RetryError) =>
* Console.log(`Error occurred: ${error._tag}`)
* ),
* Schedule.tapInput((error: RetryError) =>
* String(error).length > 20
* ? Console.log("Long error message detected")
* : Effect.void
* )
* )
* ```
*
* @category sequencing
* @since 2.0.0
*/
<Output, Input, Error, Env, X, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
f: (input: Input) => Effect<X, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, X, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
f: (input: Input) => Effect<X, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2> =>
fromStep(effect.map(
toStep(self),
(step) => (now, input) => effect.andThen(f(input), step(now, input))
)))
/**
* Returns a new `Schedule` that allows execution of an effectful function for
* every output of the schedule, but does not alter the inputs and outputs of
* the schedule.
*
* **Example** (Tapping schedule outputs)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Log schedule outputs for debugging/monitoring
* const monitoredSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.take(5),
* Schedule.tapOutput((delay) => Console.log(`Next delay will be: ${delay}`))
* )
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* if (attempt < 4) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
* return `Success on attempt ${attempt}`
* }),
* monitoredSchedule
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Tap output for metrics collection
* const metricsSchedule = Schedule.spaced("1 second").pipe(
* Schedule.take(10),
* Schedule.tapOutput((executionCount) =>
* Effect.gen(function*() {
* // Simulate metrics collection
* yield* Console.log(`Recording metric: execution_count=${executionCount}`)
* // In real code, this might send to monitoring system
* })
* )
* )
*
* // Tap output with conditional side effects
* const alertingSchedule = Schedule.fibonacci("200 millis").pipe(
* Schedule.take(8),
* Schedule.tapOutput((delay) =>
* Effect.gen(function*() {
* const delayMs = delay.toString()
* if (delayMs.includes("1000")) { // Alert on delays >= 1 second
* yield* Console.log(`High delay detected: ${delay}`)
* }
* })
* )
* )
*
* const healthCheckProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Performing health check...")
* return "healthy"
* }),
* alertingSchedule
* )
* })
*
* // Chain multiple taps for different purposes
* const comprehensiveSchedule = Schedule.fixed("500 millis").pipe(
* Schedule.take(6),
* Schedule.tapOutput((count) => Console.log(`Execution ${count + 1}`)),
* Schedule.tapOutput((count) =>
* count % 3 === 0
* ? Console.log("Checkpoint reached")
* : Effect.void
* )
* )
* ```
*
* @category sequencing
* @since 2.0.0
*/
export const tapOutput: {
/**
* Returns a new `Schedule` that allows execution of an effectful function for
* every output of the schedule, but does not alter the inputs and outputs of
* the schedule.
*
* **Example** (Tapping schedule outputs)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Log schedule outputs for debugging/monitoring
* const monitoredSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.take(5),
* Schedule.tapOutput((delay) => Console.log(`Next delay will be: ${delay}`))
* )
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* if (attempt < 4) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
* return `Success on attempt ${attempt}`
* }),
* monitoredSchedule
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Tap output for metrics collection
* const metricsSchedule = Schedule.spaced("1 second").pipe(
* Schedule.take(10),
* Schedule.tapOutput((executionCount) =>
* Effect.gen(function*() {
* // Simulate metrics collection
* yield* Console.log(`Recording metric: execution_count=${executionCount}`)
* // In real code, this might send to monitoring system
* })
* )
* )
*
* // Tap output with conditional side effects
* const alertingSchedule = Schedule.fibonacci("200 millis").pipe(
* Schedule.take(8),
* Schedule.tapOutput((delay) =>
* Effect.gen(function*() {
* const delayMs = delay.toString()
* if (delayMs.includes("1000")) { // Alert on delays >= 1 second
* yield* Console.log(`High delay detected: ${delay}`)
* }
* })
* )
* )
*
* const healthCheckProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Performing health check...")
* return "healthy"
* }),
* alertingSchedule
* )
* })
*
* // Chain multiple taps for different purposes
* const comprehensiveSchedule = Schedule.fixed("500 millis").pipe(
* Schedule.take(6),
* Schedule.tapOutput((count) => Console.log(`Execution ${count + 1}`)),
* Schedule.tapOutput((count) =>
* count % 3 === 0
* ? Console.log("Checkpoint reached")
* : Effect.void
* )
* )
* ```
*
* @category sequencing
* @since 2.0.0
*/
<Output, X, Error2, Env2>(f: (output: Output) => Effect<X, Error2, Env2>): <Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error | Error2, Env | Env2>
/**
* Returns a new `Schedule` that allows execution of an effectful function for
* every output of the schedule, but does not alter the inputs and outputs of
* the schedule.
*
* **Example** (Tapping schedule outputs)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Log schedule outputs for debugging/monitoring
* const monitoredSchedule = Schedule.exponential("100 millis").pipe(
* Schedule.take(5),
* Schedule.tapOutput((delay) => Console.log(`Next delay will be: ${delay}`))
* )
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* if (attempt < 4) {
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
* return `Success on attempt ${attempt}`
* }),
* monitoredSchedule
* )
*
* yield* Console.log(`Final result: ${result}`)
* })
*
* // Tap output for metrics collection
* const metricsSchedule = Schedule.spaced("1 second").pipe(
* Schedule.take(10),
* Schedule.tapOutput((executionCount) =>
* Effect.gen(function*() {
* // Simulate metrics collection
* yield* Console.log(`Recording metric: execution_count=${executionCount}`)
* // In real code, this might send to monitoring system
* })
* )
* )
*
* // Tap output with conditional side effects
* const alertingSchedule = Schedule.fibonacci("200 millis").pipe(
* Schedule.take(8),
* Schedule.tapOutput((delay) =>
* Effect.gen(function*() {
* const delayMs = delay.toString()
* if (delayMs.includes("1000")) { // Alert on delays >= 1 second
* yield* Console.log(`High delay detected: ${delay}`)
* }
* })
* )
* )
*
* const healthCheckProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Performing health check...")
* return "healthy"
* }),
* alertingSchedule
* )
* })
*
* // Chain multiple taps for different purposes
* const comprehensiveSchedule = Schedule.fixed("500 millis").pipe(
* Schedule.take(6),
* Schedule.tapOutput((count) => Console.log(`Execution ${count + 1}`)),
* Schedule.tapOutput((count) =>
* count % 3 === 0
* ? Console.log("Checkpoint reached")
* : Effect.void
* )
* )
* ```
*
* @category sequencing
* @since 2.0.0
*/
<Output, Input, Error, Env, X, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
f: (output: Output) => Effect<X, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, X, Error2, Env2>(
self: Schedule<Output, Input, Error, Env>,
f: (output: Output) => Effect<X, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2> =>
fromStep(effect.map(
toStep(self),
(step) => (now, input) => effect.tap(step(now, input), ([output]) => f(output))
)))
/**
* Returns a new `Schedule` that takes at most the specified number of outputs
* from the schedule. Once the specified number of outputs is reached, the
* schedule will stop.
*
* **When to use**
*
* Use to limit an existing schedule while preserving its output and delay behavior.
*
* **Gotchas**
*
* `take(n)` 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 number of outputs taken.
*
* **Example** (Taking a limited number of recurrences)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Limit an infinite schedule to five recurrences
* const limitedHeartbeat = Schedule.spaced("1 second").pipe(
* Schedule.take(5) // Will stop after 5 schedule outputs
* )
*
* const heartbeatProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Heartbeat")
* return "pulse"
* }),
* limitedHeartbeat
* )
*
* yield* Console.log("Heartbeat sequence completed")
* })
*
* // Limit retry attempts to a specific number
* const limitedRetry = Schedule.exponential("100 millis").pipe(
* Schedule.take(3) // At most 3 retry attempts
* )
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Attempt ${attempt}`)
*
* if (attempt < 5) { // Will fail more than 3 times
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
*
* return `Success on attempt ${attempt}`
* }),
* limitedRetry
* )
*
* yield* Console.log(`Result: ${result}`)
* }).pipe(
* Effect.catch((error: unknown) =>
* Console.log(`Failed after limited retries: ${String(error)}`)
* )
* )
*
* // Combine take with other schedule operations
* const samplingSchedule = Schedule.fixed("500 millis").pipe(
* Schedule.take(10), // Take at most 10 schedule outputs
* Schedule.map((count) => Effect.succeed(`Sample #${count + 1}`))
* )
*
* const samplingProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* const value = "sample"
* yield* Console.log(`Sampled value: ${value}`)
* return value
* }),
* samplingSchedule.pipe(
* Schedule.tapOutput((label) => Console.log(`Completed: ${label}`))
* )
* )
* })
* ```
*
* @see {@link recurs} for creating a count-limited schedule
*
* @category taking
* @since 4.0.0
*/
export const take: {
/**
* Returns a new `Schedule` that takes at most the specified number of outputs
* from the schedule. Once the specified number of outputs is reached, the
* schedule will stop.
*
* **When to use**
*
* Use to limit an existing schedule while preserving its output and delay behavior.
*
* **Gotchas**
*
* `take(n)` 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 number of outputs taken.
*
* **Example** (Taking a limited number of recurrences)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Limit an infinite schedule to five recurrences
* const limitedHeartbeat = Schedule.spaced("1 second").pipe(
* Schedule.take(5) // Will stop after 5 schedule outputs
* )
*
* const heartbeatProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Heartbeat")
* return "pulse"
* }),
* limitedHeartbeat
* )
*
* yield* Console.log("Heartbeat sequence completed")
* })
*
* // Limit retry attempts to a specific number
* const limitedRetry = Schedule.exponential("100 millis").pipe(
* Schedule.take(3) // At most 3 retry attempts
* )
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Attempt ${attempt}`)
*
* if (attempt < 5) { // Will fail more than 3 times
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
*
* return `Success on attempt ${attempt}`
* }),
* limitedRetry
* )
*
* yield* Console.log(`Result: ${result}`)
* }).pipe(
* Effect.catch((error: unknown) =>
* Console.log(`Failed after limited retries: ${String(error)}`)
* )
* )
*
* // Combine take with other schedule operations
* const samplingSchedule = Schedule.fixed("500 millis").pipe(
* Schedule.take(10), // Take at most 10 schedule outputs
* Schedule.map((count) => Effect.succeed(`Sample #${count + 1}`))
* )
*
* const samplingProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* const value = "sample"
* yield* Console.log(`Sampled value: ${value}`)
* return value
* }),
* samplingSchedule.pipe(
* Schedule.tapOutput((label) => Console.log(`Completed: ${label}`))
* )
* )
* })
* ```
*
* @see {@link recurs} for creating a count-limited schedule
*
* @category taking
* @since 4.0.0
*/
(n: number): <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error, Env>
/**
* Returns a new `Schedule` that takes at most the specified number of outputs
* from the schedule. Once the specified number of outputs is reached, the
* schedule will stop.
*
* **When to use**
*
* Use to limit an existing schedule while preserving its output and delay behavior.
*
* **Gotchas**
*
* `take(n)` 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 number of outputs taken.
*
* **Example** (Taking a limited number of recurrences)
*
* ```ts
* import { Console, Data, Effect, Schedule } from "effect"
*
* class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {}
*
* // Limit an infinite schedule to five recurrences
* const limitedHeartbeat = Schedule.spaced("1 second").pipe(
* Schedule.take(5) // Will stop after 5 schedule outputs
* )
*
* const heartbeatProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Heartbeat")
* return "pulse"
* }),
* limitedHeartbeat
* )
*
* yield* Console.log("Heartbeat sequence completed")
* })
*
* // Limit retry attempts to a specific number
* const limitedRetry = Schedule.exponential("100 millis").pipe(
* Schedule.take(3) // At most 3 retry attempts
* )
*
* const retryProgram = Effect.gen(function*() {
* let attempt = 0
*
* const result = yield* Effect.retry(
* Effect.gen(function*() {
* attempt++
* yield* Console.log(`Attempt ${attempt}`)
*
* if (attempt < 5) { // Will fail more than 3 times
* return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` }))
* }
*
* return `Success on attempt ${attempt}`
* }),
* limitedRetry
* )
*
* yield* Console.log(`Result: ${result}`)
* }).pipe(
* Effect.catch((error: unknown) =>
* Console.log(`Failed after limited retries: ${String(error)}`)
* )
* )
*
* // Combine take with other schedule operations
* const samplingSchedule = Schedule.fixed("500 millis").pipe(
* Schedule.take(10), // Take at most 10 schedule outputs
* Schedule.map((count) => Effect.succeed(`Sample #${count + 1}`))
* )
*
* const samplingProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* const value = "sample"
* yield* Console.log(`Sampled value: ${value}`)
* return value
* }),
* samplingSchedule.pipe(
* Schedule.tapOutput((label) => Console.log(`Completed: ${label}`))
* )
* )
* })
* ```
*
* @see {@link recurs} for creating a count-limited schedule
*
* @category taking
* @since 4.0.0
*/
<Output, Input, Error, Env>(self: Schedule<Output, Input, Error, Env>, n: number): Schedule<Output, Input, Error, Env>
} = dual(2, <Output, Input, Error, Env>(
self: Schedule<Output, Input, Error, Env>,
n: number
): Schedule<Output, Input, Error, Env> => while_(self, ({ attempt }) => effect.succeed(attempt <= n)))
/**
* Creates a schedule that unfolds a state by repeatedly applying a function,
* outputting the current state and computing the next state.
*
* **Example** (Unfolding schedule state)
*
* ```ts
* import { Console, Effect, Schedule } from "effect"
*
* // Counter schedule that increments by 1 each time
* const counterSchedule = Schedule.unfold(0, (n) => Effect.succeed(n + 1))
* // Outputs: 0, 1, 2, 3, 4, 5, ...
*
* const countingProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Task executed")
* return "done"
* }),
* counterSchedule.pipe(
* Schedule.take(5),
* Schedule.tapOutput((count) => Console.log(`Count: ${count}`))
* )
* )
* })
*
* // Fibonacci sequence schedule
* const fibonacciSchedule = Schedule.unfold(
* [0, 1] as [number, number],
* ([a, b]) => Effect.succeed([b, a + b] as [number, number])
* )
* // Outputs: [0,1], [1,1], [1,2], [2,3], [3,5], [5,8], ...
*
* const fibProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Console.log("Fibonacci step"),
* fibonacciSchedule.pipe(
* Schedule.take(8),
* Schedule.tapOutput(([a, b]) => Console.log(`Fib: ${a}, next: ${b}`))
* )
* )
* })
*
* // Effectful unfold - exponential backoff with state
* const exponentialState = Schedule.unfold(
* 100,
* (delayMs) =>
* Effect.gen(function*() {
* yield* Console.log(`Current delay: ${delayMs}ms`)
* return Math.min(delayMs * 2, 5000) // Cap at 5 seconds
* })
* )
*
* // Deterministic delay adjustment schedule
* const adjustedDelaySchedule = Schedule.unfold(
* { delay: 1000, adjustment: 100 },
* ({ delay, adjustment }) =>
* Effect.gen(function*() {
* const nextDelay = Math.max(100, delay + adjustment)
* yield* Console.log(`Adjusted delay: ${nextDelay}ms`)
* return { delay: nextDelay, adjustment: adjustment * -1 }
* })
* )
*
* // State machine schedule
* type State = "init" | "warming" | "active" | "cooling"
* const stateMachineSchedule = Schedule.unfold("init" as State, (state) => {
* switch (state) {
* case "init":
* return Effect.succeed("warming" as State)
* case "warming":
* return Effect.succeed("active" as State)
* case "active":
* return Effect.succeed("cooling" as State)
* case "cooling":
* return Effect.succeed("active" as State)
* }
* })
*
* const stateMachineProgram = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("State machine step")
* return "step"
* }),
* stateMachineSchedule.pipe(
* Schedule.take(10),
* Schedule.tapOutput((state) => Console.log(`State: ${state}`))
* )
* )
* })
* ```
*
* @category constructors
* @since 2.0.0
*/
export const unfold = <State, Error = never, Env = never>(
initial: State,
next: (state: State) => Effect<State, Error, Env>
): Schedule<State, unknown, Error, Env> =>
fromStep(effect.sync(() => {
let state = initial
return constant(effect.map(
effect.suspend(() => next(state)),
(nextState) => {
const prev = state
state = nextState
return [prev, Duration.zero] as const
}
))
}))
const while_: {
<Input, Output, Error2 = never, Env2 = never>(
predicate: (
metadata: Metadata<Output, Input>
) => boolean | Effect<boolean, Error2, Env2>
): <Error, Env>(
self: Schedule<Output, Input, Error, Env>
) => Schedule<Output, Input, Error | Error2, Env | Env2>
<Output, Input, Error, Env, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
predicate: (
metadata: Metadata<Output, Input>
) => boolean | Effect<boolean, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2>
} = dual(2, <Output, Input, Error, Env, Error2 = never, Env2 = never>(
self: Schedule<Output, Input, Error, Env>,
predicate: (
metadata: Metadata<Output, Input>
) => boolean | Effect<boolean, Error2, Env2>
): Schedule<Output, Input, Error | Error2, Env | Env2> =>
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<boolean, ...>`, preserves the output and delay when it returns
* `true`, and stops the schedule when it returns `false`.
*
* @see {@link collectWhile} for collecting outputs while using the same predicate
* @see {@link take} for stopping after a fixed number of schedule outputs
*
* @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 { Console, Effect, Schedule } from "effect"
*
* // Execute tasks at regular intervals aligned to window boundaries
* const windowSchedule = Schedule.windowed("5 seconds")
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Window task executed")
* return "window-task"
* }),
* windowSchedule.pipe(Schedule.take(4))
* )
* })
* ```
*
* @category constructors
* @since 2.0.0
*/
export const windowed = (interval: Duration.Input): Schedule<number> => {
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 { Console, Effect, Schedule } from "effect"
*
* // A schedule that runs forever with no delay
* const infiniteSchedule = Schedule.forever
*
* const program = Effect.gen(function*() {
* yield* Effect.repeat(
* Effect.gen(function*() {
* yield* Console.log("Running forever...")
* return "continuous-task"
* }),
* infiniteSchedule.pipe(Schedule.take(5)) // Limit for demo
* )
* })
* ```
*
* @category constructors
* @since 2.0.0
*/
export const forever: Schedule<number> = spaced(Duration.zero)
const constIdentity = fromStep(
effect.succeed((_now, input: unknown) => effect.succeed([input, Duration.zero] as [unknown, Duration.Duration]))
)
const identity_ = <A>(): Schedule<A, A> => constIdentity as Schedule<A, A>
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
* @category constructors
* @since 2.0.0
*/
identity_ as identity
}
/**
* Ensures that a schedule's input type extends a given type `T`.
*
* **When to use**
*
* Use when you need a generic helper to prove that an existing schedule can
* consume a required input type without changing runtime behavior.
*
* **Example** (Constraining schedule input types)
*
* ```ts
* import { Schedule } from "effect"
*
* declare const StringInputSchedule: Schedule.Schedule<number, string>
* declare const NumberInputSchedule: Schedule.Schedule<number, number>
*
* const satisfiesStringInput = Schedule.satisfiesInputType<string>()
*
* // This works because the schedule input type is string.
* const validSchedule = satisfiesStringInput(StringInputSchedule)
*
* // This would cause a TypeScript compilation error:
* // const invalidSchedule = satisfiesStringInput(NumberInputSchedule)
* ```
*
* @see {@link setInputType} for adapting an input-agnostic schedule
*
* @category utility types
* @since 4.0.0
*/
export const satisfiesInputType = <T>() =>
<Input extends T, Output = never, Error = never, Env = never>(
self: Schedule<Output, Input, Error, Env>
): Schedule<Output, Input, Error, Env> => self
/**
* 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 { Schedule } from "effect"
*
* const schedule = Schedule.recurs(3).pipe(
* Schedule.setInputType<string>()
* )
* ```
*
* @see {@link satisfiesInputType} for checking an existing input type
*
* @category utility types
* @since 4.0.0
*/
export const setInputType =
<T>() => <Output, Error, Env>(self: Schedule<Output, T, Error, Env>): Schedule<Output, T, Error, Env> => self
/**
* Ensures that a schedule's output type extends a given type `T`.
*
* **Details**
*
* This helper is checked at compile time and does not change the schedule's
* runtime behavior.
*
* **Example** (Constraining schedule output types)
*
* ```ts
* import { Schedule } from "effect"
*
* declare const StringOutputSchedule: Schedule.Schedule<string>
* declare const NumberOutputSchedule: Schedule.Schedule<number>
*
* const satisfiesStringOutput = Schedule.satisfiesOutputType<string>()
*
* // This works because the schedule output type is string.
* const validSchedule = satisfiesStringOutput(StringOutputSchedule)
*
* // This would cause a TypeScript compilation error:
* // const invalidSchedule = satisfiesStringOutput(NumberOutputSchedule)
* ```
*
* @category utility types
* @since 4.0.0
*/
export const satisfiesOutputType = <T>() =>
<Output extends T, Error = never, Input = unknown, Env = never>(
self: Schedule<Output, Input, Error, Env>
): Schedule<Output, Input, Error, Env> => self
/**
* Ensures that a schedule's error type extends a given type `T`.
*
* **Details**
*
* This helper is checked at compile time and does not change the schedule's
* runtime behavior.
*
* **Example** (Constraining schedule error types)
*
* ```ts
* import { Data, Schedule } from "effect"
*
* // Create a custom error using Data.TaggedError
* class CustomError extends Data.TaggedError("CustomError")<{
* message: string
* }> {}
*
* declare const CustomErrorSchedule: Schedule.Schedule<number, unknown, CustomError>
* declare const StringErrorSchedule: Schedule.Schedule<number, unknown, string>
*
* const satisfiesCustomError = Schedule.satisfiesErrorType<CustomError>()
*
* // This works because the schedule error type is CustomError.
* const validSchedule = satisfiesCustomError(CustomErrorSchedule)
*
* // This would cause a TypeScript compilation error:
* // const invalidSchedule = satisfiesCustomError(StringErrorSchedule)
* ```
*
* @category utility types
* @since 4.0.0
*/
export const satisfiesErrorType = <T>() =>
<Error extends T, Output = never, Input = unknown, Env = never>(
self: Schedule<Output, Input, Error, Env>
): Schedule<Output, Input, Error, Env> => self
/**
* Ensures that a schedule's context type extends a given type `T`.
*
* **Details**
*
* This helper is checked at compile time and does not change the schedule's
* runtime behavior.
*
* **Example** (Constraining schedule service types)
*
* ```ts
* import { Schedule } from "effect"
*
* interface Logger {
* readonly log: (message: string) => void
* }
*
* declare const LoggerSchedule: Schedule.Schedule<number, unknown, never, Logger>
* declare const NumberSchedule: Schedule.Schedule<number, unknown, never, number>
*
* const satisfiesLogger = Schedule.satisfiesServicesType<Logger>()
*
* // This works because the schedule context type is Logger.
* const validSchedule = satisfiesLogger(LoggerSchedule)
*
* // This would cause a TypeScript compilation error:
* // const invalidSchedule = satisfiesLogger(NumberSchedule)
* ```
*
* @category utility types
* @since 4.0.0
*/
export const satisfiesServicesType = <T>() =>
<Env extends T, Output = never, Input = unknown, Error = never>(
self: Schedule<Output, Input, Error, Env>
): Schedule<Output, Input, Error, Env> => self

Xet Storage Details

Size:
208 kB
·
Xet hash:
6b61eec8945d61d6bdb188dee89ad0c167eb42ebe2ff13f062af5d5eb762aab5

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.