Back to Delivery 4 — Collaborative and Tested Quality Utilities
delivery-04-collaborative-tested-utilities/src/polling.ts
import { QualityUtilityError } from "./errors.js";
import type { Clock, PollingResult } from "./models.js";
export interface PollingOptions<T> {
readonly operationName: string;
operation(): Promise<T>;
isComplete(value: T): boolean;
describe(value: T): string;
readonly timeoutMs: number;
readonly intervalMs: number;
readonly clock: Clock;
}
function validateTiming(timeoutMs: number, intervalMs: number): void {
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
throw new QualityUtilityError("invalid-input", "timeoutMs must be a non-negative finite number.", {
operation: "poll",
});
}
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
throw new QualityUtilityError("invalid-input", "intervalMs must be a positive finite number.", {
operation: "poll",
});
}
}
/** Polls a named operation until its explicit completion condition or timeout. */
export async function pollUntil<T>(options: PollingOptions<T>): Promise<PollingResult<T>> {
validateTiming(options.timeoutMs, options.intervalMs);
const startedAtMs = options.clock.now();
let attempts = 0;
let lastObservedState = "No operation result was observed.";
while (true) {
attempts += 1;
const value = await options.operation();
const elapsedMs = options.clock.now() - startedAtMs;
lastObservedState = options.describe(value);
if (options.isComplete(value)) {
return { value, attempts, elapsedMs };
}
if (elapsedMs >= options.timeoutMs) {
throw new QualityUtilityError(
"timeout",
`Timed out waiting for ${options.operationName} after ${elapsedMs} ms. Last observation: ${lastObservedState}`,
{
operation: options.operationName,
attempt: attempts,
elapsedMs,
observedState: lastObservedState,
},
);
}
await options.clock.sleep(Math.min(options.intervalMs, options.timeoutMs - elapsedMs));
}
}