UNPKG

readiness-manager

Version:

👨‍💼 Define when your app is ready

231 lines (193 loc) 7.42 kB
import { logger } from '@fiverr-private/obs'; import { ERRORS, ActionExecutionError, defaultErrorHandler, ActionErrorHandler } from './error'; import type { ActionStatus, ReadyAction, ReadyCallback, ActionsStatus, BeaconsMap, Beacon } from './types'; /** * The internal "ready" state to determine process readiness. */ let readyState = false; export interface ReadinessManager { register: (name: string, action: ReadyAction, debug?: boolean) => void; run: () => ReadinessManager; onReady: (callback: ReadyCallback) => ReadinessManager; onActionReady: (name: string, callback: ReadyCallback) => ReadinessManager; onError: (errorHandler: ActionErrorHandler) => ReadinessManager; status: () => ActionsStatus; ready: boolean; } /** * The internal implementation of the ReadinessManager. * @example * const readinessManager = new ReadinessManager(); * readinessManager.register('database', () => connectToDB()); * readinessManager.register('redis', () => connectToRedis()); * readinessManager.run(); * readinessManager.onReady(() => console.log('App is ready! o/')); * @returns {ReadinessManager} */ class ReadinessManagerImpl implements ReadinessManager { #beacons: BeaconsMap; #callbacks: ReadyCallback[]; #errorHandler: ActionErrorHandler; constructor() { this.#beacons = {}; this.#callbacks = []; this.#errorHandler = defaultErrorHandler; } /** * Returns `true` if all the registered actions has been ran without exceptions/rejections. */ get ready(): boolean { return readyState; } /** * Returns current registered actions status */ status(): ActionsStatus { return Object.values(this.#beacons).reduce((acc, { name, status }) => { const current = acc[status] || []; return Object.assign(acc, { [status]: [...current, name] }); }, {} as ActionsStatus); } /** * Registers a given name and action as beacon under manager. * registered actions will be observed once ran in order to determine the process readiness state. * @param name - The operation name to register with. * @param action - The ready emitter to condition the process readiness with. * @param debug - (Optional) flag to enable timing logs (default: `false`). * @throws {Error} If the action name is already registered. */ register(name: string, action: ReadyAction, debug = false): void { if (this.ready) { return; } if (this.#beacons[name]) { throw new Error(ERRORS.BEACON_ALREADY_EXISTS); } const beacon: Beacon = { name, action, callbacks: [], status: 'not_started', debug, }; Object.assign(this.#beacons, { [name]: beacon }); } /** * * Runs all registered actions. Once all actions are resolved successfully, your app state will be determined as `ready`. * @returns {ReadinessManager} */ run(): ReadinessManager { readyState = false; Object.values(this.#beacons).forEach((beacon) => this.#execute(beacon)); return this; } /** * Registers a given callback on the global manager ready event. * @param callback - The callback to run upon manager ready event. * @returns {ReadinessManager} */ onReady(callback: ReadyCallback): ReadinessManager { if (readyState) { // Invokes immediately given callback if process is already ready. callback(); return this; } this.#callbacks.push(callback); return this; } /** * Registers a given callback on a specific action resolved. * @param name - The name of the action the callback should be attached to. * @param callback - The callback to run upon beacon resolved. * @returns {ReadinessManager} * @throws {Error} If the action name does not exists. */ onActionReady(name: string, callback: ReadyCallback): ReadinessManager { const beacon = this.#beacons[name]; if (!beacon) { throw new Error(ERRORS.BEACON_DOES_NOT_EXISTS); } if (beacon.status === 'resolved') { // Invokes immediately given callback if beacon status is already resolved. callback(); return this; } this.#beacons[name].callbacks.push(callback); return this; } /** * Registers a given error handler under manager errors. * @param errorHandler - The handler to trigger upon action errors. */ onError(errorHandler: ActionErrorHandler): ReadinessManager { this.#errorHandler = errorHandler; return this; } /** * Tracks a given beacon action. * @param beacon - The beacon to execute. * @param attempt - The attempt number of current beacon execution. * @throws {ActionExecutionError} * @private */ async #execute(beacon: Beacon, attempt = 1): Promise<void> { const { name, action, debug } = beacon; const update = (status: ActionStatus) => this.#updateBeacon(name, status); update('pending'); const startTime = debug ? Date.now() : null; try { // The actual beacon execution we want to keep track on. const result = action(); if (result instanceof Promise) { await result; } if (debug && startTime !== null) { const duration = Date.now() - startTime; logger.debug(`[ReadinessManager] Action "${name}" completed in ${duration}ms`); } update('resolved'); } catch (error) { if (debug && startTime !== null) { const duration = Date.now() - startTime; logger.debug(`[ReadinessManager] Action "${name}" failed after ${duration}ms`); } update('rejected'); // Invokes consumer error hook with a retry method and the beacon error. this.#errorHandler(new ActionExecutionError(name, attempt, error as Error), () => this.#execute(beacon, attempt + 1) ); } } /** * Updates a given beacon name with a given status. * @param name - The beacon name to update. * @param status - The beacon status to set. * @private */ #updateBeacon(name: string, status: ActionStatus): void { Object.assign(this.#beacons[name], { status }); // Resolves specific beacon ready listeners. if (status === 'resolved') { this.#beacons[name].callbacks.forEach((callback) => callback()); } this.#trackBeaconUpdate(); } /** * Tracks a beacon update event, checks whether process is "ready". * If so, will trigger registered `onReady` callbacks. * @private */ #trackBeaconUpdate(): void { if (readyState) { return; } readyState = Object.values(this.#beacons).every((beacon) => beacon.status === 'resolved'); if (readyState) { this.#callbacks.forEach((callback) => callback()); } } } const manager: ReadinessManager = new ReadinessManagerImpl(); export default manager; export { ActionExecutionError, ActionErrorHandler };