component-manager.js

"use strict";

const DepGraph = require("dependency-graph").DepGraph;
const DefaultLogger = require("component-logger-console");
const fs = require("fs");
const path = require("path");

var componentList = new Map();
var typeList = new Map();

/**
 * A component manager is a collection of components.
 * The ComponentManager is responsible for managing their lifecycle.
 */
class ComponentManager {
    constructor() {
        this.componentList = componentList;
        this.typeList = typeList;
        this.logger = null;
        this.settings = {};

        // retister default types
        require("./types")(this); // eslint-disable-line global-require
    }

    /**
     * Registers a new type
     * @param  {String} typeName     The name of the new type to register
     * @param  {Function} validationFn The function that will be used to validate components that claim to be this type
     */
    registerType(typeName, validationFn) {
        if (typeof typeName !== "string") {
            throw new TypeError("expected 'typeName' to be string; got " + typeof typeName);
        }

        if (typeof validationFn !== "function") {
            throw new TypeError("expected 'validationFn' to be function; got " + typeof validationFn);
        }

        // console.log (`registering type: "${typeName}"`);

        this.typeList.set(typeName, validationFn);
    }

    /**
     * Looks up a type by its name
     * @param  {String} typeName The name of the type to be retreived
     * @return {Object}          The type Object that was found, or undefined if no matching type was found
     */
    getType(typeName) {
        if (typeof typeName !== "string") {
            throw new TypeError("expected 'typeName' to be string; got " + typeof typeName);
        }

        return this.typeList.get(typeName);
    }

    /**
     * Registers a new component
     * @param  {String} name       [description]
     * @param  {String} type       [description]
     * @param  {Object|Function} objectOrFn [description]
     * @throws {TypeError} If any parameters were of the wrong type
     * @throws {Error} If the specified type was not found, or the objectOrFn was not of the right type
     */
    register(name, type, objectOrFn) {
        if (typeof name !== "string") {
            throw new TypeError("expected 'name' to be string; got " + typeof name);
        }

        if (typeof type !== "string") {
            throw new TypeError("expected 'type' to be string; got " + typeof type);
        }

        if (typeof objectOrFn !== "object" &&
            typeof objectOrFn !== "function") {
            throw new TypeError("expected 'objectOrFn' to be object or function; got " + typeof objectOrFn);
        }

        // console.log ("registering", name, "as", type);

        var validator = this.getType(type);
        if (validator === undefined) {
            throw new Error("type not found: " + type);
        }

        if (!validator(objectOrFn)) {
            throw new Error("object not a valid type: " + type);
        }

        this.componentList.set(name, objectOrFn);
        // console.log (name, "registered");
    }

    /**
     * Gets a component by its name
     * @param  {String} name The name of the component to retreive
     * @return {Object|Function}      The compontent Object or Function, or undefined if the component was not found
     */
    get(name) {
        if (typeof name !== "string") {
            throw new TypeError("expected 'name' to be string; got " + typeof name);
        }

        return this.componentList.get(name);
    }

    // TODO: getByType()

    /**
     * Removes all components and types
     */
    clear() {
        componentList.clear();
        typeList.clear();
    }

    /**
     * Sets the directory that components should use for storing data.
     * Path will be resolved to a real path with no symbolic links.
     * @param {String} dir The directory that will be used for storing data
     * @throws {TypeError} If the dir parameter is not a string
     * @throws {Error} If the directory does not exist
     */
    setDataDir(dir) {
        if (typeof dir !== "string") {
            throw new TypeError("expected 'dir' to be string, got " + typeof dir);
        }

        this.dataDir = fs.realpathSync(path.resolve(dir));
    }

    /**
     * Configures the named component. The behavior of this is largely defined by the component that
     * and the feature of that component that is being configured.
     * @param  {String} name    The name of the component to configure
     * @param  {String} feature The feature of the component to configure
     * @param  {Any} [value]   The specified value for the feature, or `undefined` if not needed
     * @return {Any}         The value returned by the feature that was configured.
     */
    config(name, feature, value) {
        if (typeof name !== "string") {
            throw new TypeError("expected 'name' to be string; got " + typeof name);
        }

        if (typeof feature !== "string") {
            throw new TypeError("expected 'feature' to be string; got " + typeof feature);
        }

        var component = this.get(name);
        // console.log("component", component);
        if (component === undefined) {
            throw new TypeError(name + " is not a valid component name");
        }

        if (typeof component.config !== "function") {
            throw new Error("configuration of '" + name + "' not allowed");
        }

        if (typeof component.features === "function") {
            let featureList = component.features();
            // console.log("featureList", featureList);
            if (!featureList || featureList.filter((f) => f.name === feature).length !== 1) {
                throw new TypeError("feature not found for component: " + feature);
            }
        }

        return component.config(feature, value);
    }

    /**
     * Initializes all registered components
     * @return {Promise<undefined>} A Promise that resolves when all the component initializations have completed
     */
    init() {
        var dg = new DepGraph();

        // make sure we have a logger
        if (!this.get("logger")) {
            this.register("logger", "logger", new DefaultLogger(this));
        }
        this.log = this.get("logger").create("ComponentManager");

        // add dependency nodes
        this.componentList.forEach((component, name) => {
            this.log.silly("adding node:", name);
            dg.addNode(name);
        });

        // add dependencies
        this.componentList.forEach((component, name) => {
            if (typeof component.dependencies !== "function") {
                return;
            }

            var deps = component.dependencies();
            for (let depObj of deps) {
                var dep = depObj.name;
                this.log.silly("adding dependency:", name, "->", dep);
                try {
                    dg.addDependency(name, dep);
                } catch (err) {
                    // err.message = "foo";
                    var match = err.message.match(/Node does not exist: (.*)$/);
                    if (!match) throw err;
                    throw new Error(`'${name}' cannot find dependency '${dep}'`);
                }
            }
        });

        // build the ordered list of dependency loads
        var loadOrder = dg.overallOrder();

        // initialize components in the right order
        var pacc = Promise.resolve();
        for (let i = 0; i < loadOrder.length; i++) {
            let componentName = loadOrder[i];
            let component = this.get(componentName);
            if (typeof component.init === "function") {
                pacc = pacc.then(() => {
                    this.log.debug("initializing component:", componentName, "...");
                    let res = component.init();
                    // if (!(res instanceof Promise)) res = Promise.resolve(res);
                    return res;
                });
            }
        }

        return pacc;
    }

    /**
     * In theory this shuts down components
     * @todo Not implemented
     */
    shutdown() {
        // this.log.debug("shutdown");
    }
}

module.exports = ComponentManager;