/**
* @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