libflitter/app/FlitterApp.js

/**
 * @module libflitter/app/FlitterApp
 */

const express = require('express')
const Context = require('../Context')
const FatalError = require('../errors/FatalError')
const StopError = require('../errors/StopError')
const SoftError = require('../errors/SoftError')

/**
 * This class is the core of the Flitter initialization system. It
 * contains the underlying Express app. The up method links Flitter
 * units together to form the initialization stack, which starts the
 * application server.
 * 
 * @class
 */
class FlitterApp {

    /**
     * An ordered collection of Units to be launched by the application. These should be in key-value pairs such that
     * the key is the string name of the unit, and the value is some instance of {@link module:libflitter/Unit~Unit}.
     * These units are loaded IN ORDER by the FlitterApp class when it is launched.
     * 
     * @typedef {Object} FlitterApp~UnitList
     */

    /**
     * Instantiate the class. Create a new Express app, as well as a global context and bind the essentials to it.
     * Creates the global "_flitter" variable which has access to the global context.
     * @param {module:libflitter/app/FlitterApp~FlitterApp~UnitList} unit_classes - collection of units to be loaded by the app
     */
    constructor(unit_classes){

        /**
         * The underlying Express application.
         * @type {Express}
         */
        this.express = express()

        /**
         * The units to be loaded by the application.
         * @type {module:libflitter/app/FlitterApp~FlitterApp~UnitList}
         */
        this.units = unit_classes

        /**
         * String list of services present in the application. This is populated as the application is started.
         * @type {String[]}
         */
        this.services = []

        /**
         * Collection of directories provided by the units in the application. This is populated as the application
         * is started, and the directories are in key-value pairs such that the key is the agnostic name of the
         * directory which can be used to access it programmatically and the value is the fully-qualified path
         * to the directory.
         * @type {Object}
         */
        this.directories = {}

        /**
         * The Flitter daemon. This is a collection of {@link module:libflitter/Context~Context} instances populated
         * by each unit. The collection is a set of key-value pairs such that each key is the name of a unit, and the
         * value is an Object obtained by calling the {@link module:libflitter/Context~Context#serialize} method on
         * each unit's context.
         * @type {Object}
         */
        this.d = {}

        /**
         * The global context. This can be accessed by all units and is used to expose functionality to the app-space
         * code in the controllers/etc. It is usually mapped to the global "_flitter" variable.
         * @type {module:libflitter/Context~Context}
         */
        this.global = new Context(this)
        this.global.bind('directories', this.directories)
        this.global.bind('services', this.services)
        this.global.bind('has', this.has)
        this.global.bind('daemon', this.d)
        this.global.bind('stop', this.stop)
        
        global['_flitter'] = this.global.serialize()
    }

    /**
     * Launch the application. For each unit in {@link module:libflitter/app/FlitterApp~FlitterApp#units}, call the
     * {@link module:libflitter/Unit~Unit#go} method. The unit's context is then stored in the
     * daemon ({@link module:libflitter/app/FlitterApp~FlitterApp#d}. Once all the units have been loaded, re-iterate
     * over them and call the {@link module:libflitter/Unit~Unit#cleanup} method if it exists.
     * @returns {Promise<void>}
     */
    async up(){
        
        /*
         * Chain the units together by passing the next
         * unit to the previous unit as a callback. We
         * use arrow functions to inject the Express app
         * and the next unit into each call.
         */
        for (let unit_name in this.units){
            const unit = this.units[unit_name]
            const unit_context = new Context(this)
            
            this.services.push(unit.name())
            
            this.directories = {...this.directories, ...unit.directories()}
            this.global.bind('directories', this.directories)
            
            try {
                await unit.go(this, unit_context)
            }
            catch (error){
                console.log(error)
        
                // if fatal error, exit immediately
                if ( error instanceof FatalError ){
                    process.exit(1)
                }
                // if stop error, try to stop gracefully
                else if ( error instanceof StopError ){
                    process.exitCode = 1
                    break
                }
                // if soft error, continue
                else if ( error instanceof SoftError ){
                    process.exitCode = 1
                    continue
                }
        
                break
            }
            
            this.d[unit.name() ? unit.name() : unit_name] = unit_context.serialize()
        }
        
        await this.down()
    }

    /**
     * End the process. Calls this.down() and passes the status code to process.exit(). This is not the recommended
     * way of exiting Flitter. {@link module:libflitter/app/FlitterApp~FlitterApp#down} is preferred.
     * @returns {Promise<void>}
     */
    async stop(){
        process.exit(await this.down(false))
    }

    /**
     * Clean up and prepare to exit. Check each Unit in {@link module:libflitter/app/FlitterApp~FlitterApp#units} for a
     * "cleanup" method. If it exists, call it. If no errors occur, end the process with exit code 0. If an error
     * occurs during any cleanup process, print the error and when all units have finished, exit with code 1.
     * In most cases, this is all that is needed to end the Flitter process. You shouldn't really need to call 
     * {@link module:libflitter/app/FlitterApp~FlitterApp#stop}.
     * @returns {Promise<number>}
     */
    async down(set = true){
        const unit_names = Object.keys(this.units).reverse()
        let code = 0

        for ( let name_key in unit_names ){
            const unit = this.units[unit_names[name_key]]

            if ( typeof unit.cleanup === 'function' ){
                try {
                    await unit.cleanup(this)
                }
                catch (error){                    
                    console.log(error)
                    code = 1
                    
                    // if fatal error, exit immediately
                    if ( error instanceof FatalError ){
                        process.exit(1)
                    }
                }
            }
        }
        
        if ( set ){
            process.exitCode = (process.exitCode !== 0) ? process.exitCode : code
        }
        
        return code
    }

    /**
     * Helper function that checks if a unit with the specified name has been loaded.
     * This is usually bound to the {@link module:libflitter/app/FlitterApp~FlitterApp#global} context.
     * @param {string} service_name - the name of the unit ti check
     * @returns {boolean}
     */
    has(service_name){
        if ( Array.isArray(service_name) ){
            for (let key in service_name){
                if ( !this.services.includes(service_name[key]) ) return false
            }
            
            return true
        }
        
        return this.services.includes(service_name)
    }
}

module.exports = exports = FlitterApp