|
|
@ -1,156 +1,202 @@ |
|
|
|
import { InternalService } from "./InternalService"; |
|
|
|
import assert from "assert"; |
|
|
|
import EventEmitter from "events"; |
|
|
|
import { IDispatchableMethods, InternalService } from "./InternalService"; |
|
|
|
import { MicroserviceState } from "./schema"; |
|
|
|
import { InternalServiceConflictError, InternalServiceNotFoundError } from "./errors"; |
|
|
|
|
|
|
|
/** |
|
|
|
* Application InternalService map |
|
|
|
* The InternalService constructor type |
|
|
|
*/ |
|
|
|
interface InternalServiceMap { |
|
|
|
[name: string]: InternalService |
|
|
|
} |
|
|
|
type InternalServiceConstructor<T extends Microservice = Microservice> = new (microservice: T) => InternalService<T>; |
|
|
|
|
|
|
|
/** |
|
|
|
* The InternalService constructor type |
|
|
|
* Declare EventEmitter types |
|
|
|
*/ |
|
|
|
type InternalServiceConstructor<T extends Microservice = Microservice> = new (app: T) => InternalService<T>; |
|
|
|
interface Events { |
|
|
|
"ready": () => void, |
|
|
|
"finished": () => void |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Microservice states |
|
|
|
* Torrent IPC events |
|
|
|
*/ |
|
|
|
export enum MicroserviceState { |
|
|
|
Idling, |
|
|
|
Booting, |
|
|
|
Starting, |
|
|
|
Running, |
|
|
|
Quitting, |
|
|
|
Finished |
|
|
|
export declare interface Microservice { |
|
|
|
on<U extends keyof Events>(event: U, listener: Events[U]): this, |
|
|
|
emit<U extends keyof Events>(event: U, ...args: Parameters<Events[U]>): boolean |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* The main application class |
|
|
|
*/ |
|
|
|
export class Microservice |
|
|
|
export class Microservice extends EventEmitter |
|
|
|
{ |
|
|
|
/** |
|
|
|
* Maintain a static reference to the application instance |
|
|
|
* The exec promise used to wait for quit event |
|
|
|
*/ |
|
|
|
private static __instance: Microservice; |
|
|
|
#execPromise?: Promise<number>; |
|
|
|
|
|
|
|
/** |
|
|
|
* A handler function to quit the microservice application |
|
|
|
*/ |
|
|
|
private __quitHandler!: (value: number | PromiseLike<number>) => void; |
|
|
|
#quitHandler?: (value: number | PromiseLike<number>) => void; |
|
|
|
|
|
|
|
/** |
|
|
|
* All available services |
|
|
|
*/ |
|
|
|
protected services: InternalServiceMap = {}; |
|
|
|
#services = new Map<ThisType<this>, InternalService<any>>(); |
|
|
|
|
|
|
|
/** |
|
|
|
* The current state of the microservice |
|
|
|
*/ |
|
|
|
protected state: MicroserviceState; |
|
|
|
#state: MicroserviceState = MicroserviceState.Idling; |
|
|
|
|
|
|
|
// Event Handling ------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
/** |
|
|
|
* Return the current application instance |
|
|
|
* Invoked when the application has finished booting |
|
|
|
*/ |
|
|
|
public static instance() { |
|
|
|
return this.__instance; |
|
|
|
protected onStateChange(state: MicroserviceState): void|Promise<void> { |
|
|
|
switch(state) { |
|
|
|
case MicroserviceState.Running: |
|
|
|
this.emit("ready"); |
|
|
|
break; |
|
|
|
case MicroserviceState.Finished: |
|
|
|
this.emit("finished"); |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Microservice Management ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
/** |
|
|
|
* Create a new application instance |
|
|
|
* Run the application |
|
|
|
*/ |
|
|
|
public constructor() { |
|
|
|
Microservice.__instance = this; |
|
|
|
this.state = MicroserviceState.Idling; |
|
|
|
} |
|
|
|
public async exec() { |
|
|
|
// Exit if not in an idling state
|
|
|
|
if (this.state !== MicroserviceState.Idling) { |
|
|
|
console.error("Cannot exec an already-started microservice"); |
|
|
|
return 1; |
|
|
|
} |
|
|
|
// Run the microservice application
|
|
|
|
let exitCode = await (async () => { |
|
|
|
// Create the microservice execution promise to listen for quit events
|
|
|
|
let hasQuit = false; |
|
|
|
this.#execPromise = new Promise<number>(resolve => this.#quitHandler = (exitCode) => { |
|
|
|
resolve(exitCode); |
|
|
|
console.log("Quit has been invoked"); |
|
|
|
hasQuit = true; |
|
|
|
}); |
|
|
|
|
|
|
|
// Boot the microservice and internal services
|
|
|
|
if (!await this.boot()) { // no need to check for hasQuit
|
|
|
|
console.error("Failed to boot the microservice"); |
|
|
|
return 1; |
|
|
|
} |
|
|
|
// Start the internal services
|
|
|
|
if (!hasQuit && !await this.start()) { |
|
|
|
console.error("Failed to start the microservice"); |
|
|
|
return 2; |
|
|
|
} |
|
|
|
// If the application has not quit, we can run the app
|
|
|
|
let exitCode: number; |
|
|
|
if (!hasQuit) { |
|
|
|
exitCode = await this.run(); |
|
|
|
} else { |
|
|
|
exitCode = await this.#execPromise; |
|
|
|
} |
|
|
|
|
|
|
|
// Overridable --------------------------------------------------------------------------------
|
|
|
|
// Shutdown the microservice
|
|
|
|
if (!await this.shutdown()) { |
|
|
|
console.error("Failed to shutdown the microservice"); |
|
|
|
return 3; |
|
|
|
} |
|
|
|
return exitCode; |
|
|
|
})(); |
|
|
|
this.state = MicroserviceState.Finished; |
|
|
|
return exitCode; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Invoked when the application has finished booting |
|
|
|
* Quit the application |
|
|
|
*/ |
|
|
|
protected onStart(): void|Promise<void> {} |
|
|
|
public async quit(code: number = 0) { |
|
|
|
if (this.state === MicroserviceState.Idling) { |
|
|
|
this.state = MicroserviceState.Finished; |
|
|
|
return; |
|
|
|
} |
|
|
|
if (this.state > MicroserviceState.Running || this.#quitHandler === undefined) { |
|
|
|
return; |
|
|
|
} |
|
|
|
this.#quitHandler(code); |
|
|
|
} |
|
|
|
|
|
|
|
// Application Management ----------------------------------------------------------------------
|
|
|
|
// Microservice Internal Handling --------------------------------------------------------------
|
|
|
|
|
|
|
|
/** |
|
|
|
* Boot the application and all of the services |
|
|
|
* Dispatch a method call to the services, returning a boolean indicating the result |
|
|
|
*/ |
|
|
|
protected async boot() { |
|
|
|
let InternalServices = Object.values(this.services); |
|
|
|
return Promise.all(InternalServices.map(InternalService => InternalService.boot())); |
|
|
|
protected dispatch<T extends Microservice>(this: T, method: keyof IDispatchableMethods, |
|
|
|
onFail: (service: InternalService<T>, error: Error|string) => void) |
|
|
|
{ |
|
|
|
return new Promise<boolean>(resolve => { |
|
|
|
let services = Array.from(this.#services.values()); |
|
|
|
Promise.all(services.map(service => (async () => { |
|
|
|
try { |
|
|
|
await service[method](); |
|
|
|
} catch(e) { |
|
|
|
onFail(service, e); |
|
|
|
throw e; |
|
|
|
} |
|
|
|
})())) |
|
|
|
.then(() => resolve(true)) |
|
|
|
.catch(() => resolve(false)); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Shutdown the application |
|
|
|
* Boot the application and all of the services |
|
|
|
*/ |
|
|
|
protected shutdown() { |
|
|
|
let InternalServices = Object.values(this.services); |
|
|
|
return Promise.all(InternalServices.map(InternalService => InternalService.shutdown())); |
|
|
|
protected boot() { |
|
|
|
console.log("Booting..."); |
|
|
|
this.state = MicroserviceState.Booting; |
|
|
|
return this.dispatch("boot", (service, error) => { |
|
|
|
console.error(`Failed to boot service: ${service.NAME}\n`, error); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Start the application |
|
|
|
* Start the application servvices |
|
|
|
*/ |
|
|
|
public async exec() { |
|
|
|
// Exit if not in an idling state
|
|
|
|
if (this.state !== MicroserviceState.Idling) { |
|
|
|
return -1; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
// Boot the microservice
|
|
|
|
console.log("Booting services..."); |
|
|
|
this.state = MicroserviceState.Booting; |
|
|
|
await this.boot(); |
|
|
|
|
|
|
|
// Linking the internal services
|
|
|
|
console.log("Linking services..."); |
|
|
|
for (let service of Object.values(this.services)) { |
|
|
|
service.link(this); |
|
|
|
} |
|
|
|
|
|
|
|
// Start the microservice
|
|
|
|
console.log("Starting services..."); |
|
|
|
this.state = MicroserviceState.Starting |
|
|
|
await this.onStart(); |
|
|
|
for (let service of Object.values(this.services)) { |
|
|
|
service.start(); |
|
|
|
} |
|
|
|
} catch(e) { |
|
|
|
console.error("Failed to start the microservice:", e); |
|
|
|
return 1; |
|
|
|
} |
|
|
|
protected start() { |
|
|
|
console.log("Starting..."); |
|
|
|
this.state = MicroserviceState.Starting; |
|
|
|
return this.dispatch("start", (service, error) => { |
|
|
|
console.error(`Failed to start service: ${service.NAME}\n`, error); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// Run the microservice
|
|
|
|
console.log("Running"); |
|
|
|
/** |
|
|
|
* Run the application and wait for shutdown |
|
|
|
*/ |
|
|
|
protected run() { |
|
|
|
console.log("Running."); |
|
|
|
this.state = MicroserviceState.Running; |
|
|
|
process.on("SIGINT", this.quit.bind(this)); |
|
|
|
let exitCode = await new Promise<number>((resolve) => this.__quitHandler = resolve); |
|
|
|
|
|
|
|
// Shutdown the microservice
|
|
|
|
console.log("Shutting down..."); |
|
|
|
await this.shutdown().catch(() => { |
|
|
|
console.log("Error ocurred during shutdown..."); |
|
|
|
exitCode = 1; |
|
|
|
}); |
|
|
|
|
|
|
|
// Return the exit code
|
|
|
|
return exitCode; |
|
|
|
return this.#execPromise!; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Quit the application |
|
|
|
* Shutdown the application |
|
|
|
*/ |
|
|
|
public async quit(code: number = 0) { |
|
|
|
if (this.state !== MicroserviceState.Running) { |
|
|
|
return; |
|
|
|
} |
|
|
|
this.__quitHandler(code); |
|
|
|
protected shutdown() { |
|
|
|
console.log("Shutting down..."); |
|
|
|
this.state = MicroserviceState.Quitting; |
|
|
|
process.off("SIGINT", this.quit.bind(this)); |
|
|
|
return this.dispatch("shutdown", (service, error) => { |
|
|
|
console.error(`Failed to shutdown service: ${service.NAME}\n`, error); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// InternalService Management --------------------------------------------------------------------------
|
|
|
|
// Internal Service Management -----------------------------------------------------------------
|
|
|
|
|
|
|
|
/** |
|
|
|
* Install InternalServices into the application |
|
|
@ -165,25 +211,36 @@ export class Microservice |
|
|
|
* Install a InternalService into the application |
|
|
|
*/ |
|
|
|
public installService<T extends Microservice>(this: T, InternalServiceClass: InternalServiceConstructor<T>) { |
|
|
|
let InternalService = new InternalServiceClass(this); |
|
|
|
if (InternalService.NAME in this.services) { |
|
|
|
throw new Error("Install Service Error: Attempted to register multiple services with the same name"); |
|
|
|
if (this.#services.has(InternalServiceClass)) { |
|
|
|
throw new InternalServiceConflictError(InternalServiceClass); |
|
|
|
} |
|
|
|
this.services[InternalService.NAME] = InternalService; |
|
|
|
this.#services.set(InternalServiceClass, new InternalServiceClass(this)); |
|
|
|
} |
|
|
|
|
|
|
|
// Accessors -----------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
/** |
|
|
|
* Get all available services |
|
|
|
* Get an application services instance |
|
|
|
*/ |
|
|
|
public serviceList() { |
|
|
|
return Object.keys(this.services); |
|
|
|
public service<T extends Microservice, U extends InternalServiceConstructor<T>>(this: T, InternalServiceClass: U) { |
|
|
|
if (!this.#services.has(InternalServiceClass)) { |
|
|
|
throw new InternalServiceNotFoundError(InternalServiceClass); |
|
|
|
} |
|
|
|
return <InstanceType<U>>this.#services.get(InternalServiceClass); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Get an application services instance |
|
|
|
* Get the current state of the microservice |
|
|
|
*/ |
|
|
|
public get state() { |
|
|
|
return this.#state; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Set the current state and invoke an onChange event |
|
|
|
*/ |
|
|
|
public service<T extends InternalService<Microservice>>(InternalServiceName: string) { |
|
|
|
assert(InternalServiceName in this.services); |
|
|
|
return <T>this.services[InternalServiceName]; |
|
|
|
protected set state(state: MicroserviceState) { |
|
|
|
this.#state = state; |
|
|
|
this.onStateChange(state); |
|
|
|
} |
|
|
|
} |