Skip to content

poll

Edit

Repeats an operation until its result satisfies a condition.

poll.ts
export type PollOptions<Value> = {
until: (
this: void,
value: Value,
attempt: number
) => boolean | PromiseLike<boolean>;
intervalMs?: number | ((this: void, attempt: number) => number);
maxAttempts?: number;
signal?: AbortSignal;
};
const abortError = (signal: AbortSignal): unknown => {
if (signal.reason !== undefined) return signal.reason;
const error = new Error("Operation aborted");
error.name = "AbortError";
return error;
};
const wait = (delayMs: number, signal?: AbortSignal): Promise<void> => {
if (signal?.aborted) return Promise.reject(abortError(signal));
return new Promise((resolve, reject) => {
const timer = setTimeout(done, delayMs);
function done() {
signal?.removeEventListener("abort", aborted);
resolve();
}
function aborted() {
clearTimeout(timer);
reject(abortError(signal as AbortSignal));
}
signal?.addEventListener("abort", aborted, { once: true });
});
};
/** Repeats an operation until its result passes a condition or attempts are exhausted. */
export const poll = async <Return>(
operation: (this: void, attempt: number) => Return,
{
until,
intervalMs = 0,
maxAttempts = Infinity,
signal,
}: PollOptions<NoInfer<Awaited<Return>>>
): Promise<Awaited<Return>> => {
if (
maxAttempts !== Infinity &&
(!Number.isSafeInteger(maxAttempts) || maxAttempts < 1)
) {
throw new RangeError("maxAttempts must be a positive safe integer or Infinity");
}
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
if (signal?.aborted) throw abortError(signal);
const value = await operation(attempt);
if (signal?.aborted) throw abortError(signal);
const completed = await until(value, attempt);
if (signal?.aborted) throw abortError(signal);
if (completed) return value;
if (attempt === maxAttempts) break;
const delay = typeof intervalMs === "function" ? intervalMs(attempt) : intervalMs;
if (!Number.isFinite(delay) || delay < 0) {
throw new RangeError("interval must be finite and non-negative");
}
await wait(delay, signal);
}
throw new Error(`Polling condition was not met after ${maxAttempts} attempts`);
};
Terminal
wget -O src/lib/poll.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/functions/poll.ts
poll.example.ts
import { poll } from "./poll";
const job = await poll(
() => fetch("/api/job").then((response) => response.json()),
{
until: (value) => value.status === "complete",
intervalMs: 1000,
maxAttempts: 30
}
);
  • operation — Receives a one-based attempt number.
  • until — Sync or async success predicate.
  • intervalMs — Fixed delay or callback; maxAttempts and signal are optional.
  • Aborting after an operation settles prevents the condition from running.