timeout-flow
Version:
Fluent, composable, pauseable JavaScript timers and time control flows — plus RAF utilities for frame-based logic.
101 lines (85 loc) • 2.82 kB
JavaScript
// ./raf/waitForRaf.js
import { parseDuration } from '../parseDuration.js';
import { attachAbort, createAbortError } from '../abort.js';
/**
* Waits for a condition to return truthy, polling each frame.
*
* Timing model:
* - Evaluates `condition()` once per animation frame until it returns truthy.
* - If `timeout` is provided, timeout is measured in RAF time (frame timestamps).
*
* @param {Function} condition - Function returning truthy when complete
* @param {Object} [options]
* @param {string|number} [options.timeout] - Optional max time to wait
* @param {AbortSignal} [options.signal] - Optional AbortSignal to cancel waiting
* @param {boolean} [options.immediate=false] - If true, evaluate condition immediately before first frame
* @returns {Promise<void>} Resolves when condition is met, rejects on timeout/abort
*/
export function waitForRaf(condition, { timeout, signal, immediate = false } = {}) {
return new Promise((resolve, reject) => {
if (typeof condition !== 'function') {
reject(new TypeError(`waitForRaf: condition must be a function, got: ${typeof condition}`));
return;
}
if (signal?.aborted) {
reject(createAbortError());
return;
}
const timeoutMs = timeout != null ? Math.max(0, parseDuration(timeout)) : null;
let rafId = 0;
let startedAt = null; // RAF timestamp of first tick (or immediate tick reference)
const cleanup = () => {
if (rafId) cancelAnimationFrame(rafId);
rafId = 0;
cleanupAbort();
};
const onAbort = () => {
cleanup();
reject(createAbortError());
};
const cleanupAbort = attachAbort(signal, onAbort);
const evaluateCondition = () => {
try {
return !!condition();
} catch (err) {
cleanup();
reject(err);
return null;
}
};
// Optional immediate evaluation (before any frames)
if (immediate) {
const ok = evaluateCondition();
if (ok === null) return;
if (ok) {
cleanup();
resolve();
return;
}
// If timeout is explicitly 0, and immediate failed, we can fail fast.
if (timeoutMs === 0) {
cleanup();
reject(new Error('waitForRaf timed out'));
return;
}
}
const tick = (ts) => {
// Initialize start time using the same clock as `ts`
if (startedAt == null) startedAt = ts;
const ok = evaluateCondition();
if (ok === null) return;
if (ok) {
cleanup();
resolve();
return;
}
if (timeoutMs != null && ts - startedAt >= timeoutMs) {
cleanup();
reject(new Error('waitForRaf timed out'));
return;
}
rafId = requestAnimationFrame(tick);
};
rafId = requestAnimationFrame(tick);
});
}