readiness-manager
Version:
👨💼 Define when your app is ready
231 lines (193 loc) • 7.42 kB
text/typescript
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 };