Warning: Effeck is v0. The language will change in major, breaking ways. It is not suitable for production use.

Choosing and combining schedules

Schedule works in Effeck exactly as in Effect. Start with the timing rule you need, add limits to it, and combine separate rules only when the job needs them. The consumer determines whether schedule input is a successful result (repeat) or an expected error (retry).

Choosing a starting rule

const immediate = Schedule.recurs(4)
const afterCompletion = Schedule.spaced("20 seconds")
const cadence = Schedule.fixed("20 seconds")
const backoff = Schedule.exponential("300 millis")
const calendar = Schedule.cron("15 4 * * *", "UTC")

These declarations build reusable values without running work. Choose a rule according to the requirement:

RequirementRuleWhen it continues
Four additional runsrecurs(4)Immediately, up to four times
Pause between actionsspacedAfter a full delay following completion
Keep a regular cadencefixedAt the next interval boundary
Increase retry delaysexponentialWith a growing wait, doubling by default
Follow a calendarcronAt the next matching date in the given zone

Schedule.fibonacci provides another growing backoff. None of the interval or backoff rules above stops by itself.

Understanding schedule steps

function runChecks() {
  repeat(Schedule.spaced("6 seconds").upTo({ times: 2 })) {
    logInfo("checking")
    sleep("2 seconds")
  }
}

With repeat, the initial action runs before the schedule advances. This example has three action starts roughly at zero, eight, and sixteen seconds. Replacing spaced with fixed gives starts roughly at zero, eight, and fourteen seconds in this version: the fixed schedule anchors its cadence at its first step, after the initial two-second action. Later starts follow that cadence instead of adding a full delay after every action; overruns do not replay every missed interval.

A schedule step receives input and produces output plus timing metadata. In retry, each input is the latest error; in repeat, it is the latest success. Constructing one schedule does not share a running loop's step state with another loop.

Limiting duration and adjusting delays

const bounded = Schedule.exponential("300 millis")
  .modifyDelay(({ duration }) => duration.min(Duration.seconds(8)))
  .jittered()
  .upTo({ times: 6, duration: "40 seconds" })

const firstThree = Schedule.while(
  Schedule.spaced("1 second"),
  ({ attempt }) => attempt <= 3
)

upTo stops when either configured limit is reached. Its duration limit is checked at schedule steps; it does not interrupt a running attempt or wait, so use Effect.timeout around the whole operation for a deadline. modifyDelay replaces the selected wait, jittered spreads retries across time, and while can stop based on metadata.

Requiring every timing rule

const policy = Schedule.max([
    Schedule.exponential("400 millis"),
    Schedule.spaced("2 seconds")
  ]).upTo({ times: 4 })

max continues only while every input schedule can continue and chooses the largest proposed delay. Here the first waits are two seconds, two seconds, two seconds, and 3.2 seconds. The separate limit permits four recurrences.

Accepting any timing rule

const startupAndSteady = Schedule.min([
  Schedule.spaced("500 millis").upTo({ times: 2 }),
  Schedule.fixed("12 seconds")
])

min continues while at least one rule remains active and chooses the shortest active delay. Both rules advance from the start, so the two quick probes do not restart the twelve second cadence. The combined schedule stops only when every input has stopped.

Switching rules in phases

const reconnect = Schedule.spaced("150 millis").upTo({ times: 2 }).concat(Schedule.spaced("3 seconds").upTo({ times: 5 }))

concat exhausts the first rule before starting the second. With retry, this permits an initial attempt, two fast retries, then five slow retries. Schedule.concatResult keeps the outputs of the two phases distinguishable when an observer needs to identify the active phase.