libflitter/app/FlitterApp.js

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

const { Container, DependencyInjector } = require('flitter-di')
const Service = require('../NamedService')
const output = new (require('../utility/services/Output.service'))()
const Unit = require('../Unit')
const Analyzer = require('./Analyzer')
const SoftError = require('../errors/SoftError')
const FatalError = require('../errors/FatalError')
const UnitRuntimeDependencyError = require('../errors/UnitRuntimeDependencyError')
const RunLevelErrorHandler = require('../errors/RunLevelErrorHandler')
const error_context = require('../errors/error_context.fn')

/**
 * The Flitter application.
 * @extends module:flitter-di/src/Service~Service
 */
class FlitterApp extends Service {
    /**
     * Get the name of this app's service: 'app'
     * @returns {string} - 'app'
     */
    static get name() {
        return 'app'
    }

    /**
     * If true, the {@link module:libflitter/app/Analyzer~Analyzer} will check
     * the application dependencies before run.
     * @type {boolean}
     */
    enable_analyzer = true

    /**
     * Instantiate the application.
     * @param {object} units - mapping of names to static Unit CLASS definitions
     */
    constructor(units) {
        super()

        /**
         * The app's units.
         * @type {Array<module:libflitter/Unit~Unit>}
         */
        this.__units = Array.isArray(units) ? units : Object.values(units)

        /**
         * Instance of the output service.
         * @type {module:libflitter/utility/services/Output~Output}
         */
        this.output = output

        /**
         * Array of unit names.
         * @type {Array<string>}
         * @private
         */
        this.__unit_array = []

        /**
         * Mapping of directory names to paths.
         * @type {object}
         */
        this.directories = {}

        this.__init_dependency_injector()

        /**
         * The underlying Express.js app.
         * @type {express}
         */
        this.express = require('express')()

        /**
         * Analyzer used to process various aspects of the application.
         * @type {module:libflitter/app/Analyzer~Analyzer}
         */
        this.analyzer = new Analyzer(this, units)
    }

    /**
     * Inject dependencies into the static class.
     * @deprecated
     * @param {module:flitter-di/src/Injectable~Injectable} Class - the static CLASS
     * @returns {*} - the injected CLASS
     */
    make(Class) {
        this.output.warn(`Use of FlitterApp.make is deprecated and will be removed. Prefer FlitterApp.di().make() or FlitterApp.di().inject().`)
        return this.__di.make(Class)
    }

    /**
     * Get the DI.
     * @returns {module:flitter-di/src/DependencyInjector~DependencyInjector}
     */
    di() {
        return this.__di
    }

    /**
     * Run the application by starting, then cleanly stopping all units.
     * @returns {Promise<void>}
     */
    async run() {
        try {
            await this.up()
        } catch (e) {
            if ( !e instanceof FatalError ) await this.down()
            throw e
        }

        await this.down()
    }

    /**
     * Handle an application-level error. This will cause Flitter to
     * terminate. If the error passed is an instance of
     * {module:libflitter/error/FatalError~FatalError}, it will terminate
     * immediately. Otherwise, it will try to shut down cleanly.
     * @param {Error} e
     * @returns {Promise<void>}
     */
    async app_error(e) {
        const rleh = new RunLevelErrorHandler()
        if ( !(e instanceof FatalError) ) await this.down()
        rleh.handle(e)
    }

    /**
     * Initialize the application by starting all units.
     * @returns {Promise<void>}
     */
    async up() {
        this.output.message('Starting Flitter...', 0)

        this.output.info('Checking Unit dependency order...')
        if ( this.enable_analyzer ) this.analyzer.check_dependencies()

        // Start the units
        for ( const unit_name of this.__unit_array ) {
            const unit = this.__di.get(unit_name)
            try {
                // Make sure the unit's dependencies are satisfied
                const missing_deps = this.analyzer.get_missing_dependencies(unit)
                if ( missing_deps.length > 0 ) {
                    throw new UnitRuntimeDependencyError(unit_name, missing_deps)
                }

                await this.__init_unit(unit)
            } catch (e) {
                // If the error was a SoftError, try to continue loading
                if ( e instanceof SoftError ) {
                    this.output.error(`SoftError encountered while starting unit: ${unit_name}. Will attempt to continue loading.`)
                    this.output.error(`    ${e.constructor.name}: ${e.message}`)
                    unit.status(Unit.STATUS_ERROR)
                } else {
                    // otherwise, bail!
                    throw error_context(e, {
                        unit_name,
                    })
                }
            }
        }
    }

    /**
     * Stop the application cleanly by stopping all units.
     * @returns {Promise<void>}
     */
    async down() {
        this.output.message('Stopping Flitter...', 0)

        for ( const unit_name of this.__unit_array.reverse() ) {
            const unit = this.__di.get(unit_name)

            if ( unit.status() === Unit.STATUS_RUNNING ) {
                await this.__stop_unit(unit)
            }
        }
    }

    /**
     * Start a single unit.
     * @param {module:libflitter/Unit~Unit} unit
     * @returns {Promise<*>}
     * @private
     */
    async __init_unit(unit) {
        this.output.info(`Starting ${unit.name()}...`)

        try {
            unit.status(Unit.STATUS_STARTING)

            // register the unit's directories
            const directories = typeof unit.directories === 'function' ? unit.directories() : unit.directories
            this.directories = {...this.directories, ...directories}

            const go = await unit.go(this)
            unit.status(Unit.STATUS_RUNNING)
            return go
        } catch (e) {
            this.output.error(`Error encountered while starting ${unit.name()}!`)
            unit.status(Unit.STATUS_ERROR)
            throw error_context(e, {
                unit_service_name: unit.name(),
            })
        }
    }

    /**
     * Stop a single unit.
     * @param {module:libflitter/Unit~Unit} unit
     * @returns {Promise<void>}
     * @private
     */
    async __stop_unit(unit) {
        this.output.info(`Stopping ${unit.name()}...`)

        try {
            unit.status(Unit.STATUS_STOPPING)
            const cleanup = await unit.cleanup(this)
            unit.status(Unit.STATUS_STOPPED)
            return cleanup
        } catch (e) {
            this.output.error(`Error encountered while stopping ${unit.name()}.`)
            unit.status(Unit.STATUS_ERROR)
            throw error_context(e, {
                unit_service_name: unit.name(),
            })
        }
    }

    /**
     * Initialize the dependency injector for this app, adding the configured
     * units to the container as services.
     * @private
     */
    __init_dependency_injector() {
        const service_definitions = {
            app: this.constructor
        }

        for ( const unit of this.__units ) {
            service_definitions[unit.name] = unit
            this.__unit_array.push(unit.name)
        }

        this.__service_container = new Container(service_definitions)
        this.__service_container.register_singleton('app', this)
        this.__di = new DependencyInjector(this.__service_container)
        this.__service_container.register_singleton('injector', this.__di)
    }
}

module.exports = exports = FlitterApp