Initial import
This commit is contained in:
56
src/lifecycle/AppClass.ts
Normal file
56
src/lifecycle/AppClass.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {Application} from './Application';
|
||||
import {Container} from "@extollo/di";
|
||||
|
||||
/**
|
||||
* Base type for a class that supports binding methods by string.
|
||||
*/
|
||||
export interface Bindable {
|
||||
getBoundMethod(methodName: string): (...args: any[]) => any
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given object is bindable.
|
||||
* @param what
|
||||
* @return boolean
|
||||
*/
|
||||
export function isBindable(what: any): what is Bindable {
|
||||
return (
|
||||
what
|
||||
&& typeof what.getBoundMethod === 'function'
|
||||
&& what.getBoundMethod.length === 1
|
||||
&& typeof what.getBoundMethod('getBoundMethod') === 'function'
|
||||
)
|
||||
}
|
||||
|
||||
export class AppClass {
|
||||
private readonly appClassApplication!: Application;
|
||||
|
||||
constructor() {
|
||||
this.appClassApplication = Application.getApplication();
|
||||
}
|
||||
|
||||
protected app(): Application {
|
||||
return this.appClassApplication;
|
||||
}
|
||||
|
||||
protected container(): Container {
|
||||
return this.appClassApplication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the method with the given name from this class, bound to this class.
|
||||
* @param {string} methodName
|
||||
* @return function
|
||||
*/
|
||||
public getBoundMethod(methodName: string): (...args: any[]) => any {
|
||||
// @ts-ignore
|
||||
if ( typeof this[methodName] !== 'function' ) {
|
||||
throw new TypeError(`Attempt to get bound method for non-function type: ${methodName}`)
|
||||
}
|
||||
|
||||
return (...args: any[]): any => {
|
||||
// @ts-ignore
|
||||
return this[methodName](...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
198
src/lifecycle/Application.ts
Normal file
198
src/lifecycle/Application.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import {Container} from '@extollo/di';
|
||||
import {
|
||||
ErrorWithContext,
|
||||
globalRegistry,
|
||||
infer,
|
||||
isLoggingLevel,
|
||||
PathLike,
|
||||
StandardLogger,
|
||||
universalPath,
|
||||
UniversalPath
|
||||
} from '@extollo/util';
|
||||
|
||||
import {Logging} from '../service/Logging';
|
||||
import {RunLevelErrorHandler} from "./RunLevelErrorHandler";
|
||||
import {Unit, UnitStatus} from "./Unit";
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
export function env(key: string, defaultValue?: any): any {
|
||||
return Application.getApplication().env(key, defaultValue)
|
||||
}
|
||||
|
||||
export class Application extends Container {
|
||||
public static getContainer(): Container {
|
||||
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
|
||||
if ( !existing ) {
|
||||
const container = new Application()
|
||||
globalRegistry.setGlobal('extollo/injector', container)
|
||||
return container
|
||||
}
|
||||
|
||||
return existing as Container
|
||||
}
|
||||
|
||||
public static getApplication(): Application {
|
||||
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
|
||||
if ( existing && !(existing instanceof Application) ) {
|
||||
const app = new Application()
|
||||
existing.cloneTo(app)
|
||||
|
||||
globalRegistry.setGlobal('extollo/injector', app)
|
||||
return app
|
||||
} else if ( !existing ) {
|
||||
const app = new Application()
|
||||
globalRegistry.setGlobal('extollo/injector', app)
|
||||
return app
|
||||
}
|
||||
|
||||
return existing
|
||||
}
|
||||
|
||||
protected baseDir!: string
|
||||
protected basePath!: UniversalPath
|
||||
protected applicationUnits: (typeof Unit)[] = []
|
||||
protected instantiatedUnits: Unit[] = []
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
if ( !this.hasKey(Application) ) {
|
||||
this.register(Application)
|
||||
this.instances.push({
|
||||
key: Application,
|
||||
value: this,
|
||||
})
|
||||
}
|
||||
|
||||
if ( !this.hasKey('app') ) {
|
||||
this.registerSingleton('app', this)
|
||||
}
|
||||
}
|
||||
|
||||
get root() {
|
||||
return this.basePath.concat()
|
||||
}
|
||||
|
||||
get appRoot() {
|
||||
return this.basePath.concat('app')
|
||||
}
|
||||
|
||||
path(...parts: PathLike[]) {
|
||||
return this.basePath.concat(...parts)
|
||||
}
|
||||
|
||||
appPath(...parts: PathLike[]) {
|
||||
return this.basePath.concat('app', ...parts)
|
||||
}
|
||||
|
||||
get errorHandler() {
|
||||
const rleh: RunLevelErrorHandler = this.make<RunLevelErrorHandler>(RunLevelErrorHandler)
|
||||
return rleh.handle
|
||||
}
|
||||
|
||||
errorWrapContext(e: Error, context: {[key: string]: any}): ErrorWithContext {
|
||||
const rleh: RunLevelErrorHandler = this.make<RunLevelErrorHandler>(RunLevelErrorHandler)
|
||||
return rleh.wrapContext(e, context)
|
||||
}
|
||||
|
||||
scaffold(absolutePathToApplicationRoot: string, applicationUnits: (typeof Unit)[]) {
|
||||
this.baseDir = absolutePathToApplicationRoot
|
||||
this.basePath = universalPath(absolutePathToApplicationRoot)
|
||||
this.applicationUnits = applicationUnits
|
||||
|
||||
this.bootstrapEnvironment()
|
||||
this.setupLogging()
|
||||
|
||||
this.make<Logging>(Logging).debug(`Application root: ${this.baseDir}`)
|
||||
}
|
||||
|
||||
protected setupLogging() {
|
||||
const standard: StandardLogger = this.make<StandardLogger>(StandardLogger)
|
||||
const logging: Logging = this.make<Logging>(Logging)
|
||||
|
||||
logging.registerLogger(standard)
|
||||
|
||||
try {
|
||||
logging.verbose('Attempting to load logging level from the environment...')
|
||||
const envLevel = this.env('EXTOLLO_LOGGING_LEVEL')
|
||||
logging.verbose(`Read logging level: ${envLevel}`)
|
||||
|
||||
if ( isLoggingLevel(envLevel) ) {
|
||||
logging.verbose('Logging level is valid.')
|
||||
logging.level = envLevel
|
||||
logging.debug(`Set logging level from environment: ${envLevel}`)
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
protected bootstrapEnvironment() {
|
||||
dotenv.config({
|
||||
path: this.basePath.concat('.env').toLocal
|
||||
})
|
||||
}
|
||||
|
||||
public env(key: string, defaultValue?: any): any {
|
||||
return infer(process.env[key] ?? '') ?? defaultValue
|
||||
}
|
||||
|
||||
async run() {
|
||||
try {
|
||||
await this.up()
|
||||
await this.down()
|
||||
} catch (e) {
|
||||
this.errorHandler(e)
|
||||
}
|
||||
}
|
||||
|
||||
async up() {
|
||||
const logging: Logging = this.make<Logging>(Logging)
|
||||
|
||||
logging.info('Starting Extollo...', true)
|
||||
for ( const unitClass of this.applicationUnits ) {
|
||||
const unit: Unit = this.make<Unit>(unitClass)
|
||||
this.instantiatedUnits.push(unit)
|
||||
await this.startUnit(unit)
|
||||
}
|
||||
}
|
||||
|
||||
async down() {
|
||||
const logging: Logging = this.make<Logging>(Logging)
|
||||
|
||||
logging.info('Stopping Extollo...', true)
|
||||
for ( const unit of this.instantiatedUnits ) {
|
||||
if ( !unit ) continue
|
||||
await this.stopUnit(unit)
|
||||
}
|
||||
}
|
||||
|
||||
protected async startUnit(unit: Unit) {
|
||||
const logging: Logging = this.make<Logging>(Logging)
|
||||
|
||||
try {
|
||||
logging.debug(`Starting ${unit.constructor.name}...`)
|
||||
unit.status = UnitStatus.Starting
|
||||
await unit.up()
|
||||
unit.status = UnitStatus.Started
|
||||
logging.info(`Started ${unit.constructor.name}.`)
|
||||
} catch (e) {
|
||||
unit.status = UnitStatus.Error
|
||||
console.log(e)
|
||||
throw this.errorWrapContext(e, {unit_name: unit.constructor.name})
|
||||
}
|
||||
}
|
||||
|
||||
protected async stopUnit(unit: Unit) {
|
||||
const logging: Logging = this.make<Logging>(Logging)
|
||||
|
||||
try {
|
||||
logging.debug(`Stopping ${unit.constructor.name}...`)
|
||||
unit.status = UnitStatus.Stopping
|
||||
await unit.down()
|
||||
unit.status = UnitStatus.Stopped
|
||||
logging.info(`Stopped ${unit.constructor.name}.`)
|
||||
} catch (e) {
|
||||
unit.status = UnitStatus.Error
|
||||
throw this.errorWrapContext(e, {unit_name: unit.constructor.name})
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/lifecycle/RunLevelErrorHandler.ts
Normal file
74
src/lifecycle/RunLevelErrorHandler.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as color from 'colors/safe'
|
||||
import {Logging} from "../service/Logging";
|
||||
import {Inject} from "@extollo/di";
|
||||
import {ErrorWithContext} from "@extollo/util";
|
||||
|
||||
export class RunLevelErrorHandler {
|
||||
@Inject()
|
||||
protected logging!: Logging
|
||||
|
||||
/**
|
||||
* Get the error handler function.
|
||||
* @type (e: Error) => void
|
||||
*/
|
||||
get handle(): (e: Error) => void {
|
||||
return (e: Error) => {
|
||||
this.display(e)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
wrapContext(e: Error, context: {[key: string]: any}): ErrorWithContext {
|
||||
if ( e instanceof ErrorWithContext ) {
|
||||
e.context = {...e.context, ...context}
|
||||
return e
|
||||
}
|
||||
|
||||
const error = new ErrorWithContext(e.message)
|
||||
error.originalError = e
|
||||
error.context = context
|
||||
return error
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the error to the logger.
|
||||
* @param {Error} e
|
||||
*/
|
||||
display(e: Error) {
|
||||
let operativeError = e
|
||||
let context: {[key: string]: string} = {}
|
||||
if ( e instanceof ErrorWithContext ) {
|
||||
if ( e.originalError ) operativeError = e.originalError
|
||||
context = e.context
|
||||
}
|
||||
|
||||
const contextDisplay = Object.keys(context).map(key => ` - ${key}: ${context[key]}`).join('\n')
|
||||
|
||||
try {
|
||||
let errorString = `RunLevelErrorHandler invoked:
|
||||
|
||||
${color.bgRed(' ')}
|
||||
${color.bgRed(' UNCAUGHT RUN-LEVEL ERROR ')}
|
||||
${color.bgRed(' ')}
|
||||
|
||||
${e.constructor ? e.constructor.name : e.name}
|
||||
${color.red(`---------------------------------------------------`)}
|
||||
${e.stack}
|
||||
`
|
||||
|
||||
if ( contextDisplay ) {
|
||||
errorString += `
|
||||
With the following context:
|
||||
${contextDisplay}
|
||||
`
|
||||
}
|
||||
|
||||
this.logging.error(errorString, true)
|
||||
} catch (display_e) {
|
||||
// The error display encountered an error...
|
||||
// just throw the original so it makes it out
|
||||
console.error('RunLevelErrorHandler encountered an error:', display_e.message)
|
||||
throw operativeError
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/lifecycle/Unit.ts
Normal file
15
src/lifecycle/Unit.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import {AppClass} from './AppClass';
|
||||
|
||||
export enum UnitStatus {
|
||||
Starting,
|
||||
Started,
|
||||
Stopping,
|
||||
Stopped,
|
||||
Error,
|
||||
}
|
||||
|
||||
export abstract class Unit extends AppClass {
|
||||
public status: UnitStatus = UnitStatus.Stopped
|
||||
public up(): Promise<void> | void {}
|
||||
public down(): Promise<void> | void {}
|
||||
}
|
||||
Reference in New Issue
Block a user