@portable/timers
Functions
Timers.backoff
Checks delay and returns a function that takes the attempt number, starting at 1, and the
error that ended that attempt, and returns the wait in ms before the next attempt. retry and
poll use it, and so do the reconnecting clients in @portable/websockets and @portable/sse.
function backoff(delay: Delay): (attempt: Attempt) => number;Throws
RangeErrorWhen a wait,initialormaxis negative, orfactorisn't a positive finite number.
import * as Timers from '@portable/timers';
const next = Timers.backoff({
initial: 1000,
max: 30_000,
jitter: false,
});
next({ attempt: 1 }); // 1000
next({ attempt: 3 }); // 4000Timers.debounce
Wraps fn so that a burst of calls invokes it once, ms after the last call, with the last call's
arguments. Every call in the burst resolves with that invocation's result.
function debounce<args extends unknown[], result, self = unknown>(
fn: (this: self, ...args: args) => result,
ms: number,
options: debounce.Options = {},
): Debounced<args, result, self>;| Option | Type | Default | Description |
|---|---|---|---|
leading | boolean | false | Invoke on the first call of a burst. |
trailing | boolean | true | Invoke ms after the last call of a burst, with that call's arguments. |
maxWait | number | Longest time, in ms, that a call waits before an invocation, however often calls arrive. | |
signal | AbortSignal | Cancels the waiting invocation, as cancel() does but rejecting with signal.reason, and disables the function. |
import * as Timers from '@portable/timers';
const save = Timers.debounce(
(text: string) => fetch('/draft', { method: 'PUT', body: text }),
500,
);
editor.addEventListener('input', () => save(editor.value));
const response = await save(editor.value); // the response of the save that this call sharesTimers.poll
Calls fn until it returns a value other than undefined, waiting delay between calls, and
resolves with that value. An error thrown by fn rejects the poll without another call.
async function poll<value>(
fn: (
context: Context,
) => value | undefined | PromiseLike<value | undefined>,
options: poll.Options = {},
): Promise<value>;| Option | Type | Default | Description |
|---|---|---|---|
delay | Delay | 1000 | Wait after each check that returns undefined. |
signal | AbortSignal | Stops polling and rejects with signal.reason, also while a check is running. Pass AbortSignal.timeout(ms) to limit the whole poll. |
import * as Timers from '@portable/timers';
const order = await Timers.poll(
async ({ signal }) => {
const order = await (
await fetch(`/orders/${id}`, { signal })
).json();
return order.status === 'paid' ? order : undefined;
},
{ delay: 2000, signal: AbortSignal.timeout(120_000) },
);Timers.retry
Calls fn until it returns without throwing, waiting between attempts. When every attempt fails,
rejects with the last error unchanged.
async function retry<value>(
fn: (context: Context) => value | PromiseLike<value>,
options: retry.Options = {},
): Promise<value>;| Option | Type | Default | Description |
|---|---|---|---|
attempts | number | 3 | Total number of attempts, including the first. Infinity retries until success or abort. |
delay | Delay | { initial: 200, max: 10_000 } | Wait before each retry. |
retryIf | ((error: unknown, retry: { attempt: number; }) => boolean | PromiseLike<boolean>) | () => true | Called before each retry with the error and { attempt }, the number of the attempt that threw it. Return false to stop and throw that error. It isn't called after the last attempt. |
onRetry | ((error: unknown, retry: retry.Retry) => void | PromiseLike<void>) | Called after retryIf decides to retry and before the wait, with the error and { attempt, delay }: the number of the attempt that threw it, and the wait in ms. Use it to log retries. If onRetry returns a promise, the next attempt waits for it. If onRetry throws, retry rejects with that error. | |
timeout | number | Infinity | Time limit for each attempt, in ms. When it passes, the attempt's signal aborts with a TimeoutError DOMException, and the attempt fails with it. |
signal | AbortSignal | Stops retrying and rejects with signal.reason, also while an attempt is running. |
import * as Timers from '@portable/timers';
const response = await Timers.retry(
async ({ signal }) => {
const response = await fetch('https://example.com/api', { signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response;
},
{
attempts: 5,
timeout: 10_000,
onRetry: (error, { attempt, delay }) =>
console.warn(attempt, delay, error),
},
);Timers.sleep
Resolves after ms milliseconds. Uses scheduler.wait where the runtime has it, and setTimeout
otherwise. Delays longer than setTimeout's limit of about 24.8 days, including
Infinity, are waited out in steps, because setTimeout would fire such a timer at once.
async function sleep(
ms: number,
options: sleep.Options = {},
): Promise<void>;| Option | Type | Default | Description |
|---|---|---|---|
signal | AbortSignal | Rejects the returned promise with signal.reason and clears the timer when aborted. |
import * as Timers from '@portable/timers';
await Timers.sleep(1000);
await Timers.sleep(5000, { signal: AbortSignal.timeout(1000) }); // rejects with a TimeoutErrorTimers.throttle
Wraps fn so that it runs at most once every ms milliseconds, however often it is called.
Calls that share an invocation all resolve with its result.
function throttle<args extends unknown[], result, self = unknown>(
fn: (this: self, ...args: args) => result,
ms: number,
options: throttle.Options = {},
): Debounced<args, result, self>;| Option | Type | Default | Description |
|---|---|---|---|
leading | boolean | true | Invoke on the first call of a burst. |
trailing | boolean | true | Invoke once more at the end of each ms window that had calls, with the last call's arguments. |
signal | AbortSignal | Cancels the waiting invocation, as cancel() does but rejecting with signal.reason, and disables the function. |
import * as Timers from '@portable/timers';
const report = Timers.throttle(
() => fetch('/position', { method: 'POST', body: String(scrollY) }),
1000,
);
addEventListener('scroll', () => report());Types
Timers.Attempt
What a delay function receives.
type Attempt = {
attempt: number;
error?: unknown;
};Timers.Backoff
Exponential backoff. Attempt n waits up to min(max, initial * factor ** (n - 1)) ms. With
jitter (the default), the wait is a random value between 0 and that cap ("full jitter").
type Backoff = {
initial: number;
max: number;
factor?: number | undefined;
jitter?: boolean | undefined;
};Timers.Context
What retry and poll pass to fn.
type Context = {
attempt: number;
signal: AbortSignal;
};Timers.Delay
How long to wait before the next attempt: a fixed number of milliseconds, exponential backoff, or a
function that takes { attempt, error }: the number of the attempt that just finished and, in
retry, the error it threw.
type Delay = number | Backoff | ((attempt: Attempt) => number);Timers.Instant
A point in time that an option such as now or expiresAt accepts: a Date, or any object
with epochMilliseconds, such as a Temporal.Instant or Temporal.ZonedDateTime.
type Instant =
| Date
| {
readonly epochMilliseconds: number;
};Timers.Debounced
A debounced or throttled function. Calls that arrive while an invocation is waiting share it,
and each one returns a promise of that invocation's result. Where the runtime has
Symbol.dispose, [Symbol.dispose]() calls cancel(), so a function declared with using
drops its waiting invocation when the scope ends.
type Debounced<
args extends unknown[],
result,
self = unknown,
> = DisposeMethod & {
(this: self, ...args: args): Promise<Awaited<result>>;
cancel(): void;
flush(): Promise<Awaited<result> | undefined>;
readonly pending: boolean;
};