nw-skeleton

Source: app-wrapper/js/lib/base.js

/**
 * @fileOverview Base class file
 * @author Dino Ivankov <dinoivankov@gmail.com>
 * @version 1.3.1
 */

const _ = require('lodash');
const path = require('path');
const eventEmitter = require('events');

let _appWrapper;
let appState;

/**
 * Base class for extending when creating other classes
 *
 * @class
 * @memberOf appWrapper
 * @property {Object}   manifest            Object containing manifest (package.json) file data
 * @property {Object}   config              Object containing configuration data
 * @property {Boolean}  forceUserMessages   Flag to force user message output
 * @property {Boolean}  forceDebug          Flag to force debug message output
 * @property {Object}   boundMethods        Object to hold bound method references for event listeners
 * @property {Object}   timeouts            Object that holds references to this class instance timeouts
 * @property {Object}   intervals           Object that holds references to this class instance intervals
 * @property {Boolean}  needsConfig         Flag to indicate whether class instance needs config, triggering warnings if config is not available for the class
 */
class BaseClass extends eventEmitter {

    /**
     * Creates class instance, setting basic properties, and returning the instance itself
     *
     * @constructor
     * @return {BaseClass} Instance of current class
     */
    constructor () {
        super();

        if (window && window.getAppWrapper && _.isFunction(window.getAppWrapper)){
            _appWrapper = window.getAppWrapper();
            appState = _appWrapper.getAppState();
        }
        this.manifest = null;
        this.config = null;
        this.forceUserMessages = false;
        this.forceDebug = false;
        this.boundMethods = {};
        this.needsConfig = true;
        this.timeouts = {};
        this.intervals = {};

        return this;
    }

    /**
     * Initializes current class instance, setting up logging and
     * bound methods to be used in event listeners
     *
     * @async
     * @param {BaseInitializationOptions} options Initialization options
     * @return {BaseClass} Instance of current class
     */
    async initialize (options){
        if (options && options.manifest) {
            this.manifest = options.manifest;
        }
        if (options && options.config) {
            this.config = options.config;
        }

        await this.initializeLogging(options);
        this.addBoundMethods();
        this.addEventListeners();
        if (!(options && options.silent)){
            let className = this.constructor.name;
            this.log('Initialized object "{1}"', 'debug', [className]);
        }
        return this;
    }

    /**
     * Adds event listeners for this object
     *
     * @return {undefined}
     */
    addEventListeners () {
        return;
    }

    /**
     * Removes event listeners for this object
     *
     * @return {undefined}
     */
    removeEventListeners () {
        return;
    }

    /**
     * Finalizes current class instance, setting up any additional properties
     * etc. Entire app structure, including frontend app is available here
     *
     * @async
     * @return {Boolean} Finalizing result
     */
    async finalize () {
        return true;
    }

    /**
     * Determines whether logging for this class is regulated through
     * configuration, setting the logging by it (or warning if there
     * are no configuration settings for this class)
     *
     * @async
     * @return {BaseClass}      Instance of the current class
     */
    async initializeLogging() {
        return this;
    }

    /**
     * Helper method to get appWrapper instance
     *
     * @return {AppWrapper} An instance of AppWrapper class
     */
    getAppWrapper () {
        if (!_appWrapper){
            if (window && window.getAppWrapper && _.isFunction(window.getAppWrapper)){
                _appWrapper = window.getAppWrapper();
            }
        }
        return _appWrapper;
    }

    /**
     * Helper method to get appState object
     *
     * @return {Object} Current appState object
     */
    getAppState () {
        if (!appState){
            let aw = this.getAppWrapper();
            if (aw){
                appState = aw.getAppState();
            }
        }
        return appState;
    }

    /**
     * Method that sets up this.boundMethods property by binding this objects
     * functions to itself to be used as event listener handlers
     *
     * @return {undefined}
     */
    addBoundMethods () {
        if (this.boundMethods){
            let keys = _.keys(this.boundMethods);
            for (let i=0; i<keys.length; i++){
                if (this[keys[i]] && _.isFunction(this[keys[i]]) && this[keys[i]].bind && _.isFunction(this[keys[i]].bind)){
                    this.boundMethods[keys[i]] = this[keys[i]].bind(this);
                }
            }
        }
    }

    /**
     * Method that cleans up this.boundMethods property
     * set in this.addBoundMethods method
     *
     * @return {undefined}
     */
    removeBoundMethods () {
        let keys = _.keys(this.boundMethods);
        for (let i=0; i<keys.length; i++){
            this.boundMethods[keys[i]] = null;
        }
        this.boundMethods = {};
    }

    /**
     * Destructor method - cleans up references for this instance
     * freeing memory upon object destruction
     *
     * @return {undefined}
     */
    destroy () {
        this.removeEventListeners();
        this.removeBoundMethods();
    }

    /**
     * Returns appState var value
     *
     * @param  {string} varPath      String representing path to requested var (i.e. 'appData.appMainData.cancelable')
     * @param  {mixed} defaultValue  Default value to be returned if appState var is not found
     * @return {mixed}               appState var value
     */
    getStateVar (varPath, defaultValue){
        let varValue;
        if (appState){
            varValue = _.get(appState, varPath, defaultValue);
        }
        if (_.isUndefined(varValue) && !_.isUndefined(defaultValue)){
            varValue = defaultValue;
        }
        return varValue;
    }

    /**
     * Returns instance of helper object based on passed parameter (or false if helper can't be found)
     *
     * @param  {string} name       Name of the helper
     * @return {Object}            Instance of the helper object (or false if helper can't be found)
     */
    getHelper(name){
        return _appWrapper.getHelper(name);
    }

    /**
     * Returns configuration var value
     *
     * @param  {string} name         String representing path to requested var (i.e. 'appConfig.appInfo.name')
     * @param  {mixed} defaultValue  Default value to be returned if configuration var is not found
     * @return {mixed}               Configuration var value
     */
    getConfig (name, defaultValue){
        let path = name;
        let value;
        if (!path.match(/^config\./)){
            path = 'config.' + name;
        }
        value = _.get(appState, path);
        if (_.isUndefined(value)){
            path = name;
            if (!path.match(/^appWrapperConfig\./)){
                path = 'appWrapperConfig.' + name;
            }
            value = _.get(appState.u, path);
        }
        if (_.isUndefined(value)){
            path = name;
            if (!path.match(/^userConfig\./)){
                path = 'userConfig.' + name;
            }
            value = _.get(appState.u, path);
        }
        if (_.isUndefined(value) && !_.isUndefined(defaultValue)){
            value = defaultValue;
        }
        return value;
    }

    /**
     * Helper method for getting call stack array for debug or user message objects
     *
     * @return {array} An array of objects with properties 'function', 'file', 'line' and 'column', representing stack calls.
     */
    _getStack () {
        let stackArray;
        try {
            throw new Error();
        } catch (e) {
            let stackMessages = _.filter(e.stack.split('\n'), (msg) => {
                return msg.match(/^\s+at\s/);
            });
            // stackMessages = _.drop(_.dropRight(stackMessages));
            stackMessages = _.drop(stackMessages, 3);

            stackArray = _.map(stackMessages, (msg) => {
                let stackData = _.drop(_.trim(msg).split(' '));
                let returnValue = {
                    function: null,
                    file: null,
                    line: null,
                    column: null
                };
                if (stackData && _.isArray(stackData)){
                    if (stackData[0]){
                        returnValue.function = stackData[0];
                    }
                    if (stackData[1]){
                        stackData[1] = stackData[1].replace(/^\(/, '').replace(/\)$/, '');
                        if (stackData[1].match(/chrome-extension:\/\//)){
                            stackData[1] = stackData[1].replace(/^[^:]+:\/\/[^/]+\//, appState.appRootDir);
                        }
                        let callerData = stackData[1].split(':');
                        if (callerData && _.isArray(callerData) && callerData.length){

                            let fileName = callerData[0];
                            if (appState && appState.appRootDir){
                                fileName = path.relative(appState.appRootDir, fileName);
                            }
                            returnValue.file = fileName;

                            if (callerData[1]){
                                returnValue.line = parseInt(callerData[1], 10);
                            }
                            if (callerData[2]){
                                returnValue.column = parseInt(callerData[2], 10);
                            }
                        }
                    }
                }
                return returnValue;
            });
        }
        stackArray = _.filter(stackArray, (item) => {
            return item.function ? true : false;
        });
        return stackArray;
    }

    /**
     * Clears all timeouts bound to this AppWrapper instance
     *
     * @return {undefined}
     */
    clearTimeouts (){
        for (let name in this.timeouts){
            clearTimeout(this.timeouts[name]);
        }
    }

    /**
     * Clears all intervals bound to this AppWrapper instance
     *
     * @return {undefined}
     */
    clearIntervals (){
        for (let name in this.intervals){
            clearInterval(this.intervals[name]);
        }
    }
}
exports.BaseClass = BaseClass;

/**
 * BaseInitializationOptions Object that contains initialization options for BaseClass
 * @typedef  {Object}   BaseInitializationOptions
 *
 * @property {boolean}  silent          Flag to prevent initialization log messages
 * @property {Object}   manifest        Object with manifest file (package.json) data
 * @property {Object}   config          Object with configuration data
 */