src/instantiators/PrototypeWrapper.js

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * @typedef Object PrototypeWrap
 * @property {Object} targetPrototype The proxied and wired up prototype to use
 * @property {Array} originalChain The original prototype chain to use later to restore the prototype
 */

/**
 * The Prototype Wrapper will resolve dependencies up in the prototype tree.
 * If you require a module which inherits other prototypes, this wrapper will insert proxies in
 * between the inheritance steps which then will handle the dependency injections in the parent
 * prototypes. You can still call super() with arguments in the constructor, the supplied arguments
 * will then be appended to the injected properties in the constructor of the parent prototype.
 *
 * @author Wolfgang Felbermeier (@f3lang)
 */
class PrototypeWrapper {

    constructor(objectManager) {
        this.objectManager = objectManager;
    }

    /**
     * Will wrap the node require and inject prototype proxies for the dependency injection.
     * @param {String} module The path of the module to require
     * @param {String} root The identifier of the tree to work in
     * @param {String} requestId The id of the current injection request
     * @return {PrototypeWrap} An Object containing the resolved prototype and the original chain
     */
    require(module, root, requestId) {
        let mod = require(module);
        let prototypeChain = this.buildPrototypeChain(mod);
        if (prototypeChain.length === 1) {
            // no chain detected so we can continue with a regular require()
            return {targetPrototype: mod, originalChain: null};
        } else if (prototypeChain.length === 2) {
            // chain with one level of inheritance detected
            let targetPrototype = prototypeChain[0];
            let wrappedParentPrototype = this.wrapPrototype(prototypeChain[1], root, requestId + prototypeChain[1].name);
            Object.setPrototypeOf(targetPrototype, wrappedParentPrototype);
            return {targetPrototype, originalChain: prototypeChain};
        }
        // more complex chain detected with multiple levels of inheritance
        let originalChain = prototypeChain.slice();
        let i = prototypeChain.length;
        let targetPrototype = prototypeChain[i - 2];
        while (i-- && i > 1) {
            let upperPrototype = prototypeChain[i];
            let proxiedUpperPrototype = this.wrapPrototype(upperPrototype, root, requestId + upperPrototype.name);
            Object.setPrototypeOf(targetPrototype, proxiedUpperPrototype);
            prototypeChain[i - 1] = targetPrototype;
            targetPrototype = prototypeChain[i - 2];
        }
        Object.setPrototypeOf(targetPrototype, prototypeChain[1]);
        return {targetPrototype, originalChain};
    }

    /**
     * Will traverse the prototype chain recursively up to the top and convert
     * it to an array.
     * @param {Object} prototype The prototype to build a chain for
     * @return {Array} An array with all prototypes of the chain ordered from bottom to top.
     */
    buildPrototypeChain(prototype) {
        let chain = [];
        chain.push(prototype);
        if (Object.getPrototypeOf(prototype) !== Object.getPrototypeOf(class {
            })) {
            chain.push(...this.buildPrototypeChain(Object.getPrototypeOf(prototype)));
        }
        return chain;
    }

    /**
     * Will wrap a prototype in a proxy, that will inject the dependencies into the
     * original prototype constructor.
     * @param {Object} prototype The prototype to wrap
     * @param {String} root The identifier of the tree to work in
     * @param {String} requestId The id of the current injection request
     * @return {Object} The wrapped prototype
     */
    wrapPrototype(prototype, root, requestId) {
        let injectConfiguration = this.objectManager.getInjectionConfiguration(prototype.name);
        prototype.tree = root;
        if (!injectConfiguration || injectConfiguration.config.injectMap.length === 0) {
            return prototype;
        }
        let params = this.objectManager.getModuleParams(injectConfiguration.config.injectMap, root, requestId);
        return class extends prototype {
            constructor(...args) {
                module.exports.tree = root;
                super(...params, ...args);
            }
        };
    }

    /**
     * After instantiation of the module it is important to restore the original
     * prototype tree, since node will somehow cache it, which will lead to problems
     * when traversing the tree again.
     * @param {Array} originalChain The original prototype chain as an Array
     */
    restoreOriginalPrototypeChain(originalChain) {
        if (originalChain.length === 2) {
            Object.setPrototypeOf(originalChain[0], originalChain[1]);
            return;
        }
        let i = originalChain.length - 1;
        while (i--) {
            Object.setPrototypeOf(originalChain[i], originalChain[i + 1]);
        }
    }

}

module.exports = PrototypeWrapper;
module.exports.inject = ['ObjectManager'];