import assert from "assert"; import { EventEmitter } from "events"; import { InternalServiceConflictError, InternalServiceNotFoundError } from "./errors"; import { InternalService } from "./InternalService"; import { ExitCode, MicroserviceState } from "./schema"; /** * The InternalService constructor type */ type InternalServiceConstructor< T extends Microservice = Microservice > = new (microservice: T) => InternalService; /** * Declare EventEmitter types */ interface Events { "boot": () => void, "start": () => void, "ready": () => void, "shutdown": () => void, "finished": () => void } /** * Torrent IPC events */ export declare interface Microservice { on(event: U, listener: Events[U]): this, emit(event: U, ...args: Parameters): boolean } /** * The main application class */ export class Microservice extends EventEmitter { /** * The exec promise used to wait for quit event */ #execPromise?: Promise; /** * A handler function to quit the microservice application */ #quitHandler?: (value: number | PromiseLike) => void; /** * The current state of the microservice */ #state: MicroserviceState = MicroserviceState.Idle; /** * A map of the installed services */ #services = new Map, InternalService>(); /** * Indicate if exec has been invoked */ #hasStarted: boolean = false; // Microservice State Procedures --------------------------------------------------------------- /** * Invoke the boot phase of the microservice */ protected async boot() { process.on("SIGINT", this.quit.bind(this)); this.setState(MicroserviceState.Booting); this.emit("boot"); return await this.dispatch("boot"); } /** * Invoke the start phase of the microservice */ protected async start() { this.setState(MicroserviceState.Starting); this.emit("start"); return await this.dispatch("start"); } /** * Invoke the run phase of the microservice */ protected async run() { this.setState(MicroserviceState.Running); this.emit("ready"); assert(this.#execPromise !== undefined); let exitCode = await this.#execPromise; return exitCode; } /** * Invoke the shutdown phase of the microservice */ protected async shutdown() { process.off("SIGINT", this.quit.bind(this)); this.setState(MicroserviceState.Quitting); this.emit("shutdown"); return await this.dispatch("shutdown"); } // Internal Service Handling ------------------------------------------------------------------- /** * Dispatch an event across all installed services */ protected async dispatch>( this: T, method: K, ...args: Parameters[K]>) { let services = []> Array.from(this.services().values()); try { await Promise.all(services.map(service => (service[method]).apply(service, args))); } catch(e) { console.error(e); return false; } return true; } // Microservice Interface ---------------------------------------------------------------------- /** * Run the application */ public async exec(): Promise { // Exit if not in an idling state if (this.#hasStarted) { throw Error("Cannot exec an already-started microservice"); } // Indicate that the microservice has started this.#hasStarted = true; // 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(resolve => this.#quitHandler = (exitCode) => { resolve(exitCode); 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 ExitCode.BootError; } // Start the internal services if (!hasQuit && !await this.start()) { console.error("Failed to start the microservice"); return ExitCode.StartError; } // 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; } // Shutdown the microservice if (!await this.shutdown()) { console.error("Failed to shutdown the microservice"); return ExitCode.ShutdownError; } return exitCode; })(); this.setState(MicroserviceState.Finished); return exitCode; } /** * Quit the application */ public quit(code: number = ExitCode.Ok) { if (this.state() == MicroserviceState.Idle) { this.setState(MicroserviceState.Finished); return; } if (this.state() > MicroserviceState.Running || this.#quitHandler === undefined) { return; } this.#quitHandler(code); } /** * Install InternalServices into th eapplication */ public installServices(this: T, Classes: InternalServiceConstructor[]) { Classes.forEach(Class => this.installService(Class)); } /** * Install an InternalService into the application */ public installService(this: T, Class: InternalServiceConstructor) { if (this.#services.has(Class)) { throw new InternalServiceConflictError(Class); } this.#services.set(Class, new Class(this)); } // Accessors ----------------------------------------------------------------------------------- /** * Fetch an instance of an installed service */ public service>(this: T, Class: U) { if (!this.#services.has(Class)) { throw new InternalServiceNotFoundError(Class); } return >this.#services.get(Class); } /** * Get the map of installed services */ public services() { return this.#services; } /** * Get the current state of the microservice */ public state() { return this.#state; } // Mutators ------------------------------------------------------------------------------------ /** * Set the current state of the microservice */ protected setState(state: MicroserviceState) { this.#state = state; } };