readiness-manager
Version:
👨💼 Define when your app is ready
209 lines (175 loc) • 5.97 kB
JavaScript
const {
ERRORS,
ActionExecutionError,
defaultErrorHandler
} = require('./error');
const { BeaconStatus } = require('./constants');
/**
* Sets of private attributes which should not be exposed / accessed by consumer.
* @type {Object<Symbol>}
*/
const Symbols = [
'beacons',
'callbacks',
'execute',
'trackBeaconUpdate',
'updateBeacon',
'errorHandler'
].reduce((acc, key) => ({ ...acc, [key]: Symbol() }), {});
/**
* The internal "ready" state to determine process readiness.
* @type {boolean}
*/
let readyState = false;
/**
* @type {ReadinessManager}
*/
class ReadinessManager {
constructor() {
this[Symbols.beacons] = {};
this[Symbols.callbacks] = [];
this[Symbols.errorHandler] = defaultErrorHandler;
this[Symbols.execute] = this[Symbols.execute].bind(this);
}
/**
* Returns true if current process state is considered as "ready".
* @return {boolean}
*/
get ready() {
return readyState;
}
/**
* Returns current registered actions status
* @return {ActionsStatus}
*/
status() {
return Object.values(this[Symbols.beacons])
.reduce((acc, { name, status }) => {
const current = acc[status] || [];
return Object.assign(acc, { [status]: [...current, name] });
}, {});
}
/**
* Registers a given name and action as beacon under manager.
* @param {string} name - The operation name to register with.
* @param {ReadyAction} action - The ready emitter to condition the process readiness with.
*/
register(name, action) {
if (this.ready) { return; }
if (this[Symbols.beacons][name]) { throw new Error(ERRORS.BEACON_ALREADY_EXISTS); }
const beacon = {
name,
action,
callbacks: [],
status: BeaconStatus.NOT_STARTED
};
Object.assign(this[Symbols.beacons], { [name]: beacon });
}
/**
* Runs current registered beacons execution to determine process "ready" state.
* @return {ReadinessManager}
*/
run() {
readyState = false;
Object.values(this[Symbols.beacons])
.forEach((beacon) => this[Symbols.execute](beacon));
return this;
}
/**
* Registers a given callback to be triggered once process is marked as "ready".
* @param {ReadyCallback} callback
* @return {ReadinessManager}
*/
onReady(callback) {
if (readyState) {
// Invokes immediately given callback if process is already ready.
callback();
return this;
}
this[Symbols.callbacks].push(callback);
return this;
}
/**
* Registers a given callback on a specific action resolved..
* @param {string} name - The name of the action the callback should be attached to.
* @param {ReadyCallback} callback - The callback to run upon beacon resolved.
* @return {ReadinessManager}
*/
onActionReady(name, callback) {
const beacon = this[Symbols.beacons][name];
if (!beacon) { throw new Error(ERRORS.BEACON_DOES_NOT_EXISTS); }
if (beacon.status === BeaconStatus.RESOLVED) {
// Invokes immediately given callback if beacon status is already resolved.
return callback();
}
this[Symbols.beacons][name].callbacks.push(callback);
return this;
}
/**
* Registers a given error handler under manager errors.
* @param {ActionErrorHandler} errorHandler - The handler to trigger upon action errors.
* @return {ReadinessManager}
*/
onError(errorHandler) {
Object.assign(this, { [Symbols.errorHandler]: errorHandler });
return this;
}
/**
* Tracks a given beacon action.
* @param {Beacon} beacon - The beacon to execute.
* @param {Number} attempt - The attempt number of current beacon execution.
* @throws {ActionExecutionError}
* @private
*/
async [Symbols.execute](beacon, attempt = 1) {
const { name, action } = beacon;
const update = (status) => this[Symbols.updateBeacon](name, status);
update(BeaconStatus.PENDING);
try {
// The actual beacon execution we want to keep track on.
const result = action();
if (result instanceof Promise) {
await result;
}
update(BeaconStatus.RESOLVED);
} catch (error) {
update(BeaconStatus.REJECTED);
// Invokes consumer error hook with a retry method and the beacon error.
this[Symbols.errorHandler](
new ActionExecutionError(name, attempt, error),
() => this[Symbols.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
*/
[Symbols.updateBeacon](name, status) {
Object.assign(
this[Symbols.beacons][name],
{ status }
);
// Resolves specific beacon ready listeners.
if (status === BeaconStatus.RESOLVED) {
this[Symbols.beacons][name].callbacks.map(
(callback) => callback()
);
}
this[Symbols.trackBeaconUpdate]();
}
/**
* Tracks an beacon update event, checks whether process is "ready".
* If so, will trigger registered `onReady` callbacks.
* @private
*/
[Symbols.trackBeaconUpdate]() {
if (readyState) { return; }
readyState = Object.values(this[Symbols.beacons])
.every((beacon) => beacon.status === BeaconStatus.RESOLVED);
readyState && this[Symbols.callbacks].map((callback) => callback());
}
}
module.exports = new ReadinessManager();