You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

240 lines
6.1 KiB

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;
}
};