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 extends AbstractIpcService { /** * 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((resolve, _) => { this.rawIpcInstance!.connectTo(this.NAME, this.SOCKET_PATH, () => { this.__isConnected = false; this.__requestId = 0; this.__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) { 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((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, { 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, { id: null, data }); } // Accessors ----------------------------------------------------------------------------------- /** * Get the connection status of the IPC connection */ public get isConnected() { return this.__isConnected; } }