Source: muze-utils/src/lifecycle-manager/lifecycle-manager.js

import EVENT_LIST from './event-list';

/**
 * At first call it accepts one function
 * calling it second time accepts notification object.
 * notification object then called with first accepted function
 *
 * @param {Function} resolveFn a promise's resolve function
 * @return {Function} a function to accept notification object
 */
const resolver = resolveFn => notification => resolveFn(notification);

/**
 * Checks if the property string is present in the source object
 *
 * @param {Object} source source object whom you want to check the property
 * @param {string} property key name that need to be checked
 * @return {boolean} returns true if the property found in the source object
 */
const hasOwn = (source, property) => Object.prototype.hasOwnProperty.call(source, property);

/**
 * LifeCycleManager which goes as a dependendecy to a muze
 * it accepts notification from the entire library throughout
 * the exeecution life
 *
 * @export
 * @class LifeCycleManager
 */
export default class LifeCycleManager {

    /**
     * Creates an instance of LifeCycleManager.
     * @memberof LifeCycleManager
     */
    constructor () {
        this._eventList = EVENT_LIST;
        this._promises = new Map();
        this._notifiers = {};
        this._lifeCycles = {};
        this._unreleasedLifeCycles = {};
        this._boot();
    }

    /**
     * Fills all the notifiers with resolver callback
     *
     * @memberof LifeCycleManager
     */
    _boot () {
        this._notifiers = this._eventList.reduce((acc, name) => {
            acc[name] = resolver;
            return acc;
        }, {});
        this._preparePromises();
    }

    /**
     * Public method which gives the promise corresponding
     * to the event name
     *
     * @param {string} eventName name of the event like `canvas.drawn`
     * @return {promise} promise that is passed to the user
     * @memberof LifeCycleManager
     */
    retrieve (eventName) {
        return this._promises.get(eventName);
    }

    /**
     * Creates a promise that will be passed to user's callback
     *
     * @param {string} eventName name of the event like `canvas.drawn`
     * @return {promise} a pending promise waiting for resolve to be called
     * @memberof LifeCycleManager
     */
    _makeNotifierPromise (eventName) {
        return new Promise((resolve) => {
            this._notifiers[eventName] = this._notifiers[eventName](resolve);
        });
    }

    /**
     * Prepares the promise map with pending promises
     * all the promises are in a pending state where their's
     * resolve function waiting to be called with notification object
     * @memberof LifeCycleManager
     */
    _preparePromises () {
        this._eventList.forEach((eventName) => {
            this._promises.set(eventName, this._makeNotifierPromise(eventName));
        });
    }

    /**
     * Resolves the promise with notification object
     *
     * @param {string} eventName name of the event in the system
     * @param {Object} notification notification object
     * @param {string} notification.formalName name of the sender creating the notification
     * @param {Object} notification.client instance or array of instances sending the notification
     * @param {string} notification.action the stage name when the event happened like `beforedraw`
     *
     * @memberof LifeCycleManager
     */
    _resolvePromise (eventName, notification) {
        this._notifiers[eventName](notification);
    }

    /**
     * Public method which is being called by different
     * subcompoents of the system at different execution atages
     * @param {Object} notification notification object
     * @param {string} notification.formalName name of the sender creating the notification
     * @param {Object} notification.client instance or array of instances sending the notification
     * @param {string} notification.action the stage name when the event happened like `beforedraw`
     * @memberof LifeCycleManager
     */
    notify (notification) {
        // get the composition name from notification
        // or from static formalName() method
        const composition = notification.formalName || notification.client.constructor.formalName();
        notification.formalName = composition;
        this._releasePromisesFromCache();
        this._notify(composition, notification);
    }

    /**
     * Notify user the with promsie in a callback
     * if callback not found then cache it
     *
     * @param {string} composition
     * @param {Object} notification notification object
     * @param {string} notification.formalName name of the sender creating the notification
     * @param {Object} notification.client instance or array of instances sending the notification
     * @param {string} notification.action the stage name when the event happened like `beforedraw`
     * @memberof LifeCycleManager
     */
    _notify (composition, notification) {
        const stage = notification.action;
        const eventName = `${composition}.${stage}`;

        // resolves promise with the notification object
        this._resolvePromise(eventName, notification);

        // get the promise from the promise map
        const promise = this.retrieve(eventName);

        if (this._lifeCycles[composition]) {
            if (this._lifeCycles[composition][stage]) {
                // if user given call back exists, then call with promise
                this._callLifeCycleCallback(composition, stage, promise);
            } else {
                // otherwise cache the promise
                this._cachePromise(composition, stage, promise);
            }
        } else {
            this._cachePromise(composition, stage, promise);
        }
    }

    /**
     * Calls the user's callback with callback
     *
     * @param {string} composition name of the compositon like `canvas`
     * @param {string} stage the stage name when the event happened like `beforedraw`
     * @param {promise} promise promise that is passed to the user
     * @memberof LifeCycleManager
     */
    _callLifeCycleCallback (composition, stage, promise) {
        const eventName = `${composition}.${stage}`;
        this._lifeCycles[composition][stage](promise);
        this._resetTargetPromise(eventName);
    }

    /**
     * Try to clear the cached promsies once we have
     * macthed callbacks from the user
     *
     * @memberof LifeCycleManager
     */
    _releasePromisesFromCache () {
        const compositions = Object.keys(this._unreleasedLifeCycles);
        compositions.forEach((composition) => {
            const stages = Object.keys(this._unreleasedLifeCycles[composition]);
            stages.forEach((stage) => {
                if (this._lifeCycles[composition]) {
                    if (this._lifeCycles[composition][stage]) {
                        // take the promise from unreleased lifeCycles
                        const promise = this._unreleasedLifeCycles[composition][stage];
                        // call the user given callback with that promise
                        this._callLifeCycleCallback(composition, stage, promise);
                        // delete promise from unreleased lifeCycles
                        delete this._unreleasedLifeCycles[composition][stage];
                    }
                }
            });
        });
    }

    /**
     * Cache the promise in the map. This cache
     * then get consumed if we get user callbacks
     * @param {string} composition name of the compositon like `canvas`
     * @param {string} stage the stage name when the event happened like `beforedraw`
     * @param {promise} promise promise that is passed to the user
     * @memberof LifeCycleManager
     */
    _cachePromise (composition, stage, promise) {
        if (!hasOwn(this._unreleasedLifeCycles, composition)) {
            this._unreleasedLifeCycles[composition] = {};
        }

        if (!hasOwn(this._unreleasedLifeCycles[composition], stage)) {
            this._unreleasedLifeCycles[composition][stage] = {};
        }
        this._unreleasedLifeCycles[composition][stage] = promise;
    }

    /**
     * It register's the callbacks gievn the user
     *
     * @param {Object} [lifeCycles={}] a object with list of callbacks
     * @memberof LifeCycleManager
     */
    register (lifeCycles = {}) {
        this._lifeCycles = lifeCycles;
        // as we get all the callbacks, try to call them
        // from unreleased promise map
        this._releasePromisesFromCache();
    }

    /**
     * Reset the promise into pending state
     *
     * @param {string} eventName name of the event happening
     * @memberof LifeCycleManager
     */
    _resetTargetPromise (eventName) {
        setTimeout(() => {
            this._notifiers[eventName] = resolver;
            this._promises.set(eventName, this._makeNotifierPromise(eventName));
        }, 0);
    }
}