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<T>;
|
|
|
|
/**
|
|
* Declare EventEmitter types
|
|
*/
|
|
interface Events {
|
|
"boot": () => void,
|
|
"start": () => void,
|
|
"ready": () => void,
|
|
"shutdown": () => void,
|
|
"finished": () => void
|
|
}
|
|
|
|
/**
|
|
* Torrent IPC events
|
|
*/
|
|
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 extends EventEmitter
|
|
{
|
|
/**
|
|
* The exec promise used to wait for quit event
|
|
*/
|
|
#execPromise?: Promise<number>;
|
|
|
|
/**
|
|
* A handler function to quit the microservice application
|
|
*/
|
|
#quitHandler?: (value: number | PromiseLike<number>) => void;
|
|
|
|
/**
|
|
* The current state of the microservice
|
|
*/
|
|
#state: MicroserviceState = MicroserviceState.Idle;
|
|
|
|
/**
|
|
* A map of the installed services
|
|
*/
|
|
#services = new Map<ThisType<this>, InternalService<Microservice>>();
|
|
|
|
/**
|
|
* 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<T extends Microservice, K extends keyof InternalService<T>>(
|
|
this: T, method: K, ...args: Parameters<InternalService<T>[K]>)
|
|
{
|
|
let services = <InternalService<T>[]> Array.from(this.services().values());
|
|
try {
|
|
await Promise.all(services.map(service => (<any>service[method]).apply(service, args)));
|
|
} catch(e) {
|
|
console.error(e);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Microservice Interface ----------------------------------------------------------------------
|
|
|
|
/**
|
|
* Run the application
|
|
*/
|
|
public async exec(): Promise<number> {
|
|
// 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<number>(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<T extends Microservice>(this: T, Classes: InternalServiceConstructor[]) {
|
|
Classes.forEach(Class => this.installService(Class));
|
|
}
|
|
|
|
/**
|
|
* Install an InternalService into the application
|
|
*/
|
|
public installService<T extends Microservice>(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<T extends Microservice, U extends InternalServiceConstructor<T>>(this: T, Class: U) {
|
|
if (!this.#services.has(Class)) {
|
|
throw new InternalServiceNotFoundError(Class);
|
|
}
|
|
return <InstanceType<U>>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;
|
|
}
|
|
};
|