Обзор

Retry

Retry a behavior with attempts, delay, cancellation, and local retry policy.

retryasyncresilience

Install

$
Terminal window
pnpm dlx shadcn@latest add https://archyn.net/r/retry.json

Copy the files below into the target paths shown in each tab.

ts@lib/flow/retry.ts
import {
type Behavior,
type RuntimeContext,
isRuntimeError,
resultFromSignal,
runBehavior,
} from "./core/runtime";
import { wait } from "./wait";
/**
* Decision returned by the internal retry policy.
*
* A retry decision may include a delay before the next attempt. A fail decision returns the current
* domain failure immediately.
*/
type RetryDecision = { kind: "retry"; afterMs?: number } | { kind: "fail" };
/**
* Internal policy function used by `retry`.
*
* @typeParam E - Domain error type inspected by the policy.
*/
type RetryPolicy<E> = (meta: { error: E; attempt: number; elapsedMs: number }) => RetryDecision;
/**
* Configuration for retrying expected domain failures.
*
* Runtime errors are not retried. Attempts are counted starting at one for the first execution.
*
* @typeParam E - Domain error type produced by the source behavior.
*/
export type RetryOptions<E> = {
attempts: number;
delay?: number | ((attempt: number, error: E) => number);
when?: (error: E, meta: { attempt: number; elapsedMs: number }) => boolean;
};
/**
* Retries a behavior when it returns an expected domain failure.
*
* The source is run at most `options.attempts` times. Runtime errors and cancellation return
* immediately. Cancellation during a retry delay returns `Cancelled`.
*
* @typeParam I - Source input type.
* @typeParam O - Source output type.
* @typeParam E - Source domain error type.
* @typeParam C - Custom context type.
* @param source - Behavior to retry.
* @param options - Retry attempts, optional delay, and optional predicate.
* @returns A behavior with the same success and domain error types as the source.
*
* @example
* ```ts
* const resilientLoad = retry(loadUser, {
* attempts: 3,
* delay: (attempt) => attempt * 100,
* when: (error) => error.kind === "rate-limited",
* });
* ```
*/
export function retry<I, O, E, C extends object>(
source: Behavior<I, O, E, C>,
options: RetryOptions<E>,
): Behavior<I, O, E, C> {
return retryBehavior(source, retryPolicy(options));
}
function retryBehavior<I, O, E, C extends object>(
source: Behavior<I, O, E, C>,
policy: RetryPolicy<E>,
): Behavior<I, O, E, C> {
return async (input: I, context: RuntimeContext<C>) => {
const startedAt = Date.now();
let attempt = 1;
while (true) {
if (context.signal.aborted) {
return resultFromSignal(context.signal);
}
const result = await runBehavior(source, input, context);
if (result.ok) {
return result;
}
if (isRuntimeError(result.error)) {
return result;
}
const decision = policy({
error: result.error,
attempt,
elapsedMs: Date.now() - startedAt,
});
if (decision.kind === "fail") {
return result;
}
const delay = decision.afterMs ?? 0;
const delayResult = await wait(delay, context.signal);
if (!delayResult.ok) {
return delayResult;
}
attempt += 1;
}
};
}
function retryPolicy<E>(options: RetryOptions<E>): RetryPolicy<E> {
return ({ attempt, error, elapsedMs }) => {
if (options.when?.(error, { attempt, elapsedMs }) === false) {
return { kind: "fail" };
}
if (attempt >= options.attempts) {
return { kind: "fail" };
}
const afterMs =
typeof options.delay === "function" ? options.delay(attempt, error) : options.delay;
if (afterMs === undefined) {
return { kind: "retry" };
}
return { kind: "retry", afterMs };
};
}

Update the import paths to match your project setup.

Retry a Behavior with a local policy for attempts, delay, and retryable domain failures.

Usage

retry re-runs a Behavior only when it returns a typed domain failure and the retry policy allows another attempt. Runtime errors and cancellation are not hidden.

Use it when:

  • Retry transient domain failures such as rate limits or stale reads.
  • Add fixed or computed backoff between attempts.
  • Stop retrying for errors that should surface immediately.
import { retry } from "@/lib/flow/retry";
import { task } from "@/lib/flow/task";
type ApiError = { kind: "rate-limited" } | { kind: "invalid" };
const loadProfile = task<string, { name: string }, ApiError>(async (id, context) => {
const response = await fetch(`/api/profile/${id}`);
if (response.status === 429) return context.fail({ kind: "rate-limited" });
if (!response.ok) return context.fail({ kind: "invalid" });
return response.json();
});
const resilientLoadProfile = retry(loadProfile, {
attempts: 3,
delay: (attempt) => attempt * 200,
when: (error) => error.kind === "rate-limited",
});

Behavior

The first run is attempt 1. After each domain failure, retry evaluates when, checks the attempt limit, waits for the configured delay, and tries again.

Only typed domain failures are eligible for retry. Runtime Thrown errors and Cancelled values return immediately, which keeps aborts and programming errors visible.

Types

The source output and domain error types are preserved; RetryOptions controls only execution policy.

Import from @/lib/flow/retry.

Composition Notes

Wrap the smallest behavior that owns the transient failure, then compose with timeout if the whole retry budget must be bounded.

Do not retry non-idempotent operations unless the operation has its own duplicate protection.

Source

ts@lib/flow/retry.ts
import {
type Behavior,
type RuntimeContext,
isRuntimeError,
resultFromSignal,
runBehavior,
} from "./core/runtime";
import { wait } from "./wait";
/**
* Decision returned by the internal retry policy.
*
* A retry decision may include a delay before the next attempt. A fail decision returns the current
* domain failure immediately.
*/
type RetryDecision = { kind: "retry"; afterMs?: number } | { kind: "fail" };
/**
* Internal policy function used by `retry`.
*
* @typeParam E - Domain error type inspected by the policy.
*/
type RetryPolicy<E> = (meta: { error: E; attempt: number; elapsedMs: number }) => RetryDecision;
/**
* Configuration for retrying expected domain failures.
*
* Runtime errors are not retried. Attempts are counted starting at one for the first execution.
*
* @typeParam E - Domain error type produced by the source behavior.
*/
export type RetryOptions<E> = {
attempts: number;
delay?: number | ((attempt: number, error: E) => number);
when?: (error: E, meta: { attempt: number; elapsedMs: number }) => boolean;
};
/**
* Retries a behavior when it returns an expected domain failure.
*
* The source is run at most `options.attempts` times. Runtime errors and cancellation return
* immediately. Cancellation during a retry delay returns `Cancelled`.
*
* @typeParam I - Source input type.
* @typeParam O - Source output type.
* @typeParam E - Source domain error type.
* @typeParam C - Custom context type.
* @param source - Behavior to retry.
* @param options - Retry attempts, optional delay, and optional predicate.
* @returns A behavior with the same success and domain error types as the source.
*
* @example
* ```ts
* const resilientLoad = retry(loadUser, {
* attempts: 3,
* delay: (attempt) => attempt * 100,
* when: (error) => error.kind === "rate-limited",
* });
* ```
*/
export function retry<I, O, E, C extends object>(
source: Behavior<I, O, E, C>,
options: RetryOptions<E>,
): Behavior<I, O, E, C> {
return retryBehavior(source, retryPolicy(options));
}
function retryBehavior<I, O, E, C extends object>(
source: Behavior<I, O, E, C>,
policy: RetryPolicy<E>,
): Behavior<I, O, E, C> {
return async (input: I, context: RuntimeContext<C>) => {
const startedAt = Date.now();
let attempt = 1;
while (true) {
if (context.signal.aborted) {
return resultFromSignal(context.signal);
}
const result = await runBehavior(source, input, context);
if (result.ok) {
return result;
}
if (isRuntimeError(result.error)) {
return result;
}
const decision = policy({
error: result.error,
attempt,
elapsedMs: Date.now() - startedAt,
});
if (decision.kind === "fail") {
return result;
}
const delay = decision.afterMs ?? 0;
const delayResult = await wait(delay, context.signal);
if (!delayResult.ok) {
return delayResult;
}
attempt += 1;
}
};
}
function retryPolicy<E>(options: RetryOptions<E>): RetryPolicy<E> {
return ({ attempt, error, elapsedMs }) => {
if (options.when?.(error, { attempt, elapsedMs }) === false) {
return { kind: "fail" };
}
if (attempt >= options.attempts) {
return { kind: "fail" };
}
const afterMs =
typeof options.delay === "function" ? options.delay(attempt, error) : options.delay;
if (afterMs === undefined) {
return { kind: "retry" };
}
return { kind: "retry", afterMs };
};
}