import { Socket } from "net";
|
|
import assert from "assert";
|
|
import { Microservice } from "@autoplex/microservice";
|
|
import AbstractIpcService from "./AbstractIpcService";
|
|
import { IIpcResponse, IIpcRequest, IPC } from "./schema";
|
|
import { IpcConnectionError, IpcTimeoutError } from "./IpcError";
|
|
|
|
export abstract class IpcClientService<M extends Microservice = Microservice> extends AbstractIpcService<M>
|
|
{
|
|
/**
|
|
* Indicate if there is an active connection to the IPC
|
|
*/
|
|
private __isConnected: boolean = false;
|
|
|
|
/**
|
|
* The most recent request ID
|
|
*/
|
|
private __requestId!: number;
|
|
|
|
/**
|
|
* The active IPC socket
|
|
*/
|
|
private __socket: Socket|null = null;
|
|
|
|
// Service Implementation ----------------------------------------------------------------------
|
|
|
|
/**
|
|
* Install the event handlers for receiving on the IPC socket
|
|
*
|
|
* Example: this.addMessageHandler("some_event", this.onSomeEvent)
|
|
*/
|
|
protected installMessageHandlers() {
|
|
// no-op
|
|
}
|
|
|
|
// Service Management --------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Boot the IPC client service
|
|
*/
|
|
public bootIpc(ipc: IPC) {
|
|
// Connect to the server
|
|
return new Promise<void>((resolve, _) => {
|
|
this.rawIpcInstance!.connectTo(this.NAME, this.SOCKET_PATH, () => {
|
|
this.__isConnected = false;
|
|
this.__requestId = 0;
|
|
this.__socket = <Socket>ipc!.of[this.NAME];
|
|
this.installEventHandlers(this.__socket!);
|
|
this.installMessageHandlers();
|
|
this.__socket!.once("connect", resolve);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Install the event handlers for the IPC socket
|
|
*/
|
|
protected installEventHandlers(socket: Socket) {
|
|
socket.on("connect", () => this.onConnect());
|
|
socket.on("error", (error: any) => this.onError(error));
|
|
socket.on("disconnect", () => this.onDisconnect());
|
|
socket.on("destroy", () => this.onDestroy());
|
|
}
|
|
|
|
/**
|
|
* Add a handler for an event broadcasted by the server
|
|
*/
|
|
protected addMessageHandler(method: string, handle: (...args: any[]) => Promise<any>) {
|
|
assert(this.__socket !== null, "Attempted to add events to null socket");
|
|
this.__socket.on(method, async (data: any) => handle.apply(this, [data]));
|
|
}
|
|
|
|
/**
|
|
* Shutdown the IPC service
|
|
*/
|
|
public async shutdownIpc(ipc: IPC|null) {
|
|
ipc?.disconnect(this.NAME);
|
|
this.__socket?.removeAllListeners();
|
|
this.__socket?.destroy();
|
|
this.__socket = null;
|
|
}
|
|
|
|
// Socket Event Handlers -----------------------------------------------------------------------
|
|
|
|
/**
|
|
* Invoked when the client established a connection to an IPC server
|
|
*/
|
|
protected onConnect() {
|
|
this.log("IPC: Connection established");
|
|
this.__isConnected = true;
|
|
|
|
}
|
|
|
|
/**
|
|
* Invoked when an IPC error occurs
|
|
*/
|
|
protected onError(error: string | Error) {
|
|
if (this.__isConnected) {
|
|
this.log("IPC: Error occurred:", error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoked when disconnected from an IPC server
|
|
*/
|
|
protected onDisconnect() {
|
|
if (this.__isConnected) {
|
|
this.log("IPC: Disconnected");
|
|
}
|
|
this.__isConnected = false;
|
|
}
|
|
|
|
/**
|
|
* Invoked when the IPC socket has been destroyed
|
|
*/
|
|
protected onDestroy() {
|
|
this.log("IPC: Destroyed");
|
|
this.__isConnected = false;
|
|
}
|
|
|
|
// Methods -------------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Perform a general request and wait for a response
|
|
*/
|
|
protected async request(method: string, data?: any, timeout: number|null = null) {
|
|
return new Promise<IIpcResponse>((resolve, reject) => {
|
|
// If the client is not connected to a server, reject immediately
|
|
if (!this.__isConnected || this.__socket === null) {
|
|
reject(new IpcConnectionError("Not connected"));
|
|
return;
|
|
}
|
|
// Clean up event listeners
|
|
let cleanUp = () => {
|
|
if (responseTimeout !== null) {
|
|
clearTimeout(responseTimeout);
|
|
}
|
|
if (this.__socket === null) {
|
|
return;
|
|
}
|
|
this.__socket.off(responseMethod, respond);
|
|
this.__socket.off("disconnect", respond);
|
|
this.__socket.off("destroy", respond);
|
|
};
|
|
// Handle the response
|
|
let respond = (response: IIpcResponse) => {
|
|
cleanUp();
|
|
resolve(response);
|
|
};
|
|
// Abort the request
|
|
let abort = (error: Error) => {
|
|
cleanUp();
|
|
reject(error);
|
|
};
|
|
// Fetch a request ID and declare a timeout
|
|
const requestId = this.__requestId++;
|
|
const responseMethod = `method_response_${requestId}`;
|
|
// Include timeout mechanism in the off chance something breaks
|
|
let responseTimeout: NodeJS.Timeout|null = null;
|
|
if (timeout !== null) {
|
|
responseTimeout = setTimeout(() => abort(
|
|
new IpcTimeoutError("Timeout")
|
|
), timeout);
|
|
}
|
|
this.__socket.once("disconnect", () => abort(new IpcConnectionError("Disconnected")));
|
|
this.__socket.once("destroy", () => abort(new IpcConnectionError("Destroyed")));
|
|
this.__socket.once(responseMethod, respond);
|
|
this.__socket.emit(method, <IIpcRequest>{ id: requestId, data });
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Send a message over IPC without waiting for a response
|
|
*/
|
|
protected send(method: string, data?: any) {
|
|
if (!this.__isConnected || this.__socket === null) {
|
|
throw new IpcConnectionError("Not connected");
|
|
}
|
|
this.__socket.emit(method, <IIpcRequest>{ id: null, data });
|
|
}
|
|
|
|
// Accessors -----------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Get the connection status of the IPC connection
|
|
*/
|
|
public get isConnected() {
|
|
return this.__isConnected;
|
|
}
|
|
}
|