Initial import

This commit is contained in:
2021-03-02 18:57:41 -06:00
commit be1f615858
23 changed files with 1092 additions and 0 deletions

56
src/lifecycle/AppClass.ts Normal file
View 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)
}
}
}

View 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})
}
}
}

View 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
View 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 {}
}