UNPKG

veloze

Version:

A modern and fast express-like webserver for the web

131 lines (119 loc) 2.97 kB
import { logger } from './logger.js' const log = logger(':readiness') export const nap = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms).unref()) /** * @template T * @param {Promise<T>} promise * @param {number} [ms] * @returns {Promise<T>} */ export const abortablePromise = (promise, ms = 1000) => { const p = Promise.withResolvers() p.promise.finally(() => clearTimeout(timeoutId)) let timeoutId = setTimeout(() => { p.reject(new Error('TimeoutError')) }, ms).unref() promise.then(p.resolve).catch(p.reject) return p.promise } /** * @typedef {Object} Check * @property {() => Promise<boolean>} asyncFn * @property {boolean} result * @property {Date} checkAt */ /** * Run readiness checks at regular intervals */ export class Readiness { /** @type {Map<string, Check>} */ _map = new Map() /** @type {boolean} */ _isRunning = false /** * @param {{ * name?: string, * intervalMs?: number, * abortTimeoutMs?: number, * }} options */ constructor(options) { this._options = { name: 'readiness', intervalMs: 5000, abortTimeoutMs: 5000, ...options } } /** * register a readiness check * @param {string} name * @param {() => Promise<boolean> } asyncFn * @param {boolean} [initialResult=false] */ register(name, asyncFn, initialResult = false) { this._map.set(name, { asyncFn, result: initialResult, checkAt: new Date(0) }) this.start() } /** * @returns {{statusCode: number, results: {}|Record<string, {result: boolean, checkAt: Date}>}} */ getResults() { const results = {} let statusCode = 200 for (const [name, check] of this._map.entries()) { results[name] = { result: check.result, checkAt: check.checkAt } if (!check.result) { statusCode = 500 } } return { statusCode, results } } async start() { if (this._isRunning) return this._isRunning = true while (this._isRunning) { await Promise.allSettled([ this._runAllChecks(), nap(this._options.intervalMs) ]) } } stop() { this._isRunning = false } /** * @private * @param {string} name * @param {Check} check */ async _runCheck(name, check) { try { // allow aborting long running checks const result = await abortablePromise( check.asyncFn(), this._options.abortTimeoutMs ) check.result = result === true log.debug(`%s: check=%s finished=%s`, this._options.name, name, result) } catch (/** @type {any} */ err) { check.result = false log.warn('%s: check=%s failed=%s', this._options.name, name, err.message) } check.checkAt = new Date() } async _runAllChecks() { const checks = Array.from(this._map.entries()).map(([name, check]) => this._runCheck(name, check) ) await Promise.allSettled(checks) } }