@ -1,2 +1,3 @@ | |||
export * from "./Microservice"; | |||
export * from "./InternalService"; | |||
export * from "./ipc"; |
@ -0,0 +1,68 @@ | |||
import RawIPC = require("node-ipc"); | |||
import { InternalService } from "../InternalService"; | |||
import { Microservice } from "../Microservice"; | |||
import { IPC, IpcMessageHandler } from "./schema"; | |||
/** | |||
* An abstract IPC service containing common properties/methods among the server and client | |||
*/ | |||
export default abstract class AbstractIpcService<M extends Microservice = Microservice> extends InternalService<M> | |||
{ | |||
/** | |||
* The IPC instance | |||
*/ | |||
private __ipc: IPC|null = null; | |||
// Implementation Requirements ----------------------------------------------------------------- | |||
/** | |||
* The path to the socket file | |||
*/ | |||
protected abstract get socketPath(): string; | |||
/** | |||
* Add a message handler for the service | |||
*/ | |||
protected abstract addMessageHandler(method: string, handle: IpcMessageHandler): void; | |||
/** | |||
* Boot the IPC service after configuration is complete | |||
*/ | |||
protected abstract bootIpc(ipc: IPC): Promise<void>; | |||
/** | |||
* Shutdown the IPC service before it is destroyed | |||
*/ | |||
protected abstract shutdownIpc(ipc: IPC|null): Promise<void>; | |||
// Service Management -------------------------------------------------------------------------- | |||
/** | |||
* Boot the IPC service | |||
*/ | |||
public async boot() { | |||
// Create the IPC socket | |||
this.__ipc = new RawIPC.IPC(); | |||
this.__ipc.config.id = this.name; | |||
this.__ipc.config.retry = 1500; | |||
this.__ipc.config.silent = true; | |||
await this.bootIpc(this.__ipc); | |||
} | |||
/** | |||
* Shutdown the IPC service | |||
*/ | |||
public async shutdown() { | |||
await this.shutdownIpc(this.__ipc); | |||
this.__ipc = null; | |||
} | |||
// Accessors ----------------------------------------------------------------------------------- | |||
/** | |||
* Get the raw IPC instance | |||
*/ | |||
protected get rawIpcInstance(): IPC|null { | |||
return this.__ipc; | |||
} | |||
} |
@ -0,0 +1,190 @@ | |||
import { Socket } from "net"; | |||
import assert from "assert"; | |||
import { Microservice } from "../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, reject) => { | |||
this.rawIpcInstance!.connectTo(this.name, this.socketPath, () => { | |||
this.__isConnected = true; | |||
this.__requestId = 0; | |||
this.__socket = ipc!.of[this.name]; | |||
this.installEventHandlers(this.__socket!); | |||
this.installMessageHandlers(); | |||
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; | |||
} | |||
} |
@ -0,0 +1,29 @@ | |||
/** | |||
* Generic IPC Error type | |||
*/ | |||
export class IpcError extends Error { | |||
constructor(...args: any[]) { | |||
super(...args); | |||
Object.setPrototypeOf(this, IpcError.prototype); | |||
} | |||
} | |||
/** | |||
* IPC connection error type | |||
*/ | |||
export class IpcConnectionError extends IpcError { | |||
constructor(...args: any[]) { | |||
super(...args); | |||
Object.setPrototypeOf(this, IpcConnectionError.prototype); | |||
} | |||
} | |||
/** | |||
* IPC timeout error type | |||
*/ | |||
export class IpcTimeoutError extends IpcError { | |||
constructor(...args: any[]) { | |||
super(...args); | |||
Object.setPrototypeOf(this, IpcTimeoutError.prototype); | |||
} | |||
} |
@ -0,0 +1,92 @@ | |||
import assert from "assert"; | |||
import { mkdir } from "fs/promises"; | |||
import { Socket } from "net"; | |||
import { dirname } from "path"; | |||
import { Microservice } from "../Microservice"; | |||
import { IIpcRequest, IPC } from "./schema"; | |||
import AbstractIpcService from "./AbstractIpcService"; | |||
type IpcServer = IPC["server"]; | |||
export abstract class IpcServerService<M extends Microservice = Microservice> extends AbstractIpcService<M> | |||
{ | |||
/** | |||
* The IPC server instance | |||
*/ | |||
private __server!: IpcServer|null; | |||
// Service Implementation ---------------------------------------------------------------------- | |||
/** | |||
* Install the event handlers for receiving on the IPC socket | |||
* | |||
* Example: this.addMessageHandler("some_event", this.onSomeEvent) | |||
*/ | |||
protected installMessageHandlers() { | |||
console.log("Installing from parent"); | |||
// no-op | |||
} | |||
// Service Management -------------------------------------------------------------------------- | |||
/** | |||
* Boot the IPC service | |||
*/ | |||
public bootIpc(ipc: IPC) { | |||
return new Promise<void>(async (resolve) => { | |||
// Create the socket directory if it doesn't exist | |||
await mkdir(dirname(this.socketPath), { recursive: true }); | |||
// Serve the IPC server | |||
ipc.serve(this.socketPath, () => { | |||
this.__server = ipc.server; | |||
this.installMessageHandlers(); | |||
resolve(); | |||
}); | |||
ipc.server.start(); | |||
}); | |||
} | |||
/** | |||
* Add a message/request handler for the server | |||
*/ | |||
protected addMessageHandler(method: string, handle: (...args: any[]) => Promise<any>) { | |||
assert(this.__server !== null, "Attempted to add events to null server"); | |||
this.__server.on(method, async (request: IIpcRequest, socket: Socket) => { | |||
let handlerPromise = handle.apply(this, [request.data]); | |||
if (request.id === null) { | |||
handlerPromise.catch(error => this.log("Error:", method, error, request)); | |||
return; | |||
} | |||
const responseMethod = `method_response_${request.id}`; | |||
try { | |||
this.__server!.emit(socket, responseMethod, { data: await handlerPromise }); | |||
} catch(error) { | |||
this.log(this.log("Error:", method, error, request)); | |||
this.__server!.emit(socket, responseMethod, { error }); | |||
} | |||
}); | |||
} | |||
/** | |||
* Shutdown the IPC service | |||
*/ | |||
public async shutdownIpc(ipc: IPC|null) { | |||
this.__server?.stop(); | |||
this.__server = null; | |||
for (let socket of <Socket[]>Object.values(ipc?.of ?? [])) { | |||
socket.destroy(); | |||
} | |||
} | |||
// Methods ------------------------------------------------------------------------------------- | |||
/** | |||
* Broadcast a message to all connected clients | |||
*/ | |||
public broadcast(method: string, data?: any) { | |||
if (this.__server === null) { | |||
return; | |||
} | |||
this.__server.emit(method, data); | |||
} | |||
} |
@ -0,0 +1,4 @@ | |||
export * from "./schema"; | |||
export * from "./IpcError"; | |||
export * from "./IpcClientService"; | |||
export * from "./IpcServerService"; |
@ -0,0 +1,27 @@ | |||
import type RawIPC = require("node-ipc"); | |||
/** | |||
* The IPC request structure | |||
*/ | |||
export interface IIpcRequest { | |||
id: number|null, | |||
data ?: any | |||
} | |||
/** | |||
* The IPC response structure | |||
*/ | |||
export interface IIpcResponse { | |||
data ?: any, | |||
error?: string | Error | |||
} | |||
/** | |||
* The IPC message handler type | |||
*/ | |||
export type IpcMessageHandler = (...args: any[]) => Promise<any> | |||
/** | |||
* HOLY @#$@% WHOEVER MADE THE TYPES FOR `node-ipc` SHOULDB BE HANGED | |||
*/ | |||
export type IPC = InstanceType<(typeof RawIPC)["IPC"]>; |