/** Return type of a readiness check function */ export type ReadinessFunctionReturn = { /** Status of the readiness check */ status: ReadinessStatus; /** Optional message providing additional information about the readiness check */ message?: string; }; /** Function that performs a readiness check */ export type ReadinessFunction = (check: ReadinessCheck) => Promise; /** Status of a readiness check */ export type ReadinessStatus = 'ok' | 'error' | 'degraded'; const aggregateStatus = (statuses: ReadinessStatus[]): ReadinessStatus => { if (statuses.includes('error')) return 'error'; if (statuses.includes('degraded')) return 'degraded'; return 'ok'; }; /** Represents a readiness check with an optional timeout */ export type ReadinessCheck = { /** Name of the readiness check */ name: string; /** Function that performs the readiness check */ fn: ReadinessFunction; /** Timeout in milliseconds for the readiness check (default: 5000) */ timeout?: number; }; /** Result of a system readiness check */ export type ReadinessResult = { /** * Status of the system readiness check, aggregated as the worst status * among individual checks. 'unknown' is a special case indicating that * no checks were performed, used by ScheduledReadiness before the first run. * */ status: ReadinessStatus | 'unknown'; /** Start time of the system readiness check in milliseconds since the Unix epoch */ start: number; /** Duration of the system readiness check in milliseconds */ duration: number; /** Details of individual readiness checks */ details: ReadinessDetail[]; }; /** Detail of an individual readiness check */ export type ReadinessDetail = { /** Name of the readiness check */ name: string; /** Status of the readiness check */ status: ReadinessStatus; /** Duration of the readiness check in milliseconds */ duration: number; /** Message providing additional information about the readiness check */ message?: string; }; /** * Performs a readiness check by executing the provided readiness functions. * @param checks - An array of readiness functions to execute. * @returns A Promise that resolves to a ReadinessResult object. */ export const readiness = async (checks: ReadinessCheck[]): Promise => { const start = Date.now(); const t0 = performance.now(); const details: ReadinessDetail[] = []; for (const check of checks) { const checkt0 = performance.now(); try { const result = await withTimeout( check.fn(check), check.timeout ?? 5000, `Readiness check '${check.name}' timed out after ${check.timeout ?? 5000} ms`, ); details.push({ name: check.name, status: result.status, message: result.message, duration: performance.now() - checkt0, }); } catch (err) { details.push({ name: check.name, status: 'error', message: err instanceof Error ? err.message : String(err), duration: performance.now() - checkt0, }); } } const duration = performance.now() - t0; return { status: aggregateStatus(details.map((d) => d.status)), start, duration, details, }; }; /** * Creates a handler function for readiness HTTP requests. Warning: this runs all * checks on each request and may be slow. * @param checks - An array of readiness functions to execute. * @returns A function that returns a Response object with ReadinessResult in JSON format. */ export const createReadinessHandler = (checks: ReadinessCheck[]): (() => Promise) => { return async () => { const result = await readiness(checks); return respondWithResult(result); }; }; /** * Class that schedules periodic readiness checks. */ export class ScheduledReadiness { private checks: ReadinessCheck[]; private interval: number; private started: boolean = false; private timer: NodeJS.Timeout | null = null; private latestResult: ReadinessResult | null = null; private nextResult: Promise | null = null; /** * Creates an instance of ScheduledReadiness. * @param checks - An array of readiness functions to execute. * @param interval - Interval in milliseconds between readiness checks. */ constructor(checks: ReadinessCheck[], interval: number) { this.checks = checks; this.interval = interval; } /** Starts the scheduled readiness checks */ async start() { if (this.started) return; // Already started this.started = true; const runCheck = async () => { // Prevent concurrent runs if (this.nextResult) return; this.nextResult = readiness(this.checks); try { this.latestResult = await this.nextResult; } finally { this.nextResult = null; } }; await runCheck(); // Initial run this.timer = setInterval(runCheck, this.interval); } /** Stops the scheduled readiness checks */ stop() { if (this.timer) { clearInterval(this.timer); this.timer = null; } this.started = false; } /** Gets the next readiness result, waiting if a check is in progress */ async getNextResult(): Promise { if (this.nextResult) { return await this.nextResult; } return this.latestResult; } /** Gets the latest readiness result without waiting */ getResult(): ReadinessResult | null { return this.latestResult; } /** * Sets the interval for readiness checks and restarts the timer if already started. * @param ms - Interval in milliseconds. */ setInterval(ms: number) { this.interval = ms; if (this.timer) { this.stop(); this.start(); } } /** * Creates a handler function for readiness HTTP requests using the latest scheduled result. * Scheduled handler always returns the most recent result, or 'unknown' if no checks have run yet. * @returns A function that returns a Response object with ReadinessResult in JSON format. */ createHandler(): () => Promise { return async () => { const result = await this.getNextResult(); if (!result) { return new Response( JSON.stringify({ status: 'unknown', start: Date.now(), duration: 0, details: [], } as ReadinessResult), { status: httpStatusFromReadiness('unknown'), headers: { 'Content-Type': 'application/json' }, }, ); } return respondWithResult(result); }; } } /** Returns a Response object with the given ReadinessResult in JSON format */ const respondWithResult = (result: ReadinessResult) => { return new Response(JSON.stringify(result), { status: httpStatusFromReadiness(result.status), headers: { 'Content-Type': 'application/json' }, }); }; /** Returns the HTTP status code corresponding to a given readiness status */ const httpStatusFromReadiness = (status: ReadinessStatus | 'unknown'): number => { if (status === 'ok') return 200; if (status === 'degraded') return 200; // 206 could also be suitable, but let's avoid false alarms if (status === 'error') return 503; return 200; // unknown, treat as ok to avoid false alarms }; const withTimeout = async ( promise: Promise, ms: number, timeoutMessage: string, ): Promise => { let timeoutHandle: NodeJS.Timeout; const timeoutPromise = new Promise((_, reject) => { timeoutHandle = setTimeout(() => { reject(new Error(timeoutMessage)); }, ms); }); return Promise.race([promise, timeoutPromise]).finally(() => { clearTimeout(timeoutHandle); }); };