@ -1,2 +1,3 @@ | |||||
export * from "./Microservice"; | export * from "./Microservice"; | ||||
export * from "./InternalService"; | 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"]>; |