API reference › @evolu/common › Schedule › Schedule

type Schedule<Output, Input = unknown> = (
  deps: ScheduleDeps,
) => (input: Input) => NextResult<readonly [Output, Millis]>;

Defined in: packages/common/src/Schedule.ts:113

Composable scheduling strategies for retry, repeat, rate limiting, and more.

A Schedule uses the State pattern: calling schedule(deps) creates a step function with internal state captured in closures. Each call to step(input) advances that state and returns Ok([Output, Millis]) or Err(Done<void>) to stop. Multiple calls to schedule(deps) create independent state instances.

Err(Done<void>) is terminal. After a step returns it, every subsequent call to that step must also return Err(Done<void>).

With retry and repeat, the initial Task execution happens before the first schedule step. Schedule outputs therefore describe recurrences, not the initial execution. Time-based schedules establish their time origin on the first step call, not when schedule(deps) creates the step.

They measure elapsed time on Time.performance, so a system clock adjustment cannot distort it. On platforms where that clock stops while the device sleeps, a suspended interval measures as little or no elapsed time, so a schedule does not see the gap the wall clock saw. A time box outlives the wall-clock deadline it was given, as during and maxElapsed do. resetScheduleAfter does not treat the sleep as inactivity, and compensate, fixed and windowed do not treat it as time to catch up on.

Composing a retry policy

import {
  assertOk,
  err,
  exponential,
  jitter,
  maxDelay,
  ok,
  retry,
  take,
  testCreateRun,
  type RandomNumber,
  type Task,
} from "@evolu/common";

let attempts = 0;
const fetchData: Task<string, { readonly type: "FetchError" }> = () => {
  attempts++;
  return attempts < 2 ? err({ type: "FetchError" }) : ok("data");
};

const fetchWithRetry = retry(
  fetchData,
  // A jittered, capped, limited exponential backoff.
  jitter("100%")(maxDelay("20s")(take(2)(exponential("100ms")))),
);

await using run = testCreateRun({
  random: { next: () => 0 as RandomNumber },
});
assertOk(await run(fetchWithRetry), "data");

Or use a preset:

import { ok, retry, retryStrategyAws, type Task } from "@evolu/common";

const fetchData: Task<string> = () => ok("data");
const _fetchWithRetry = retry(fetchData, retryStrategyAws);