diff --git a/packages/ipc/jest.config.ts b/packages/ipc/jest.config.ts new file mode 100644 index 0000000..7c94e14 --- /dev/null +++ b/packages/ipc/jest.config.ts @@ -0,0 +1,11 @@ +import base from "../../jest.config"; + +export default { + ...base, + name: "@autoplex/microservice", + displayName: "Package: Micoservice", + moduleNameMapper: { + '^@src/(.*)$': '/src/$1', + '^@test@/(.*)$': '/test/$1' + } +} diff --git a/packages/ipc/package.json b/packages/ipc/package.json index 3f62cd2..9b63789 100644 --- a/packages/ipc/package.json +++ b/packages/ipc/package.json @@ -1,18 +1,21 @@ { "name": "@autoplex/ipc", - "version": "0.0.0", + "version": "0.1.0", "main": "dist/lib/index.js", "types": "dist/typings/index.d.ts", "license": "MIT", "scripts": { - "build": "yarn run clean && tsc", - "clean": "rimraf ./dist" + "build": "yarn run clean && ttsc", + "clean": "rimraf ./coverage ./dist", + "coverage": "yarn test --coverage", + "test": "jest --silent", + "test:verbose": "jest" }, "devDependencies": { "@types/node-ipc": "^9.1.3" }, "dependencies": { - "@autoplex/microservice": "^0.0.0", + "@autoplex/microservice": "^0.1.0", "node-ipc": "^9.1.4" } } diff --git a/packages/ipc/src/AbstractClient.ts b/packages/ipc/src/AbstractClient.ts new file mode 100644 index 0000000..8a47d5f --- /dev/null +++ b/packages/ipc/src/AbstractClient.ts @@ -0,0 +1,8 @@ +import { AbstractMethodMap } from "./AbstractMethodMap"; +import { IMethodMap } from "./schema"; + + +export abstract class AbstractClient extends AbstractMethodMap +{ + +} diff --git a/packages/ipc/src/AbstractConnection.ts b/packages/ipc/src/AbstractConnection.ts new file mode 100644 index 0000000..175c16f --- /dev/null +++ b/packages/ipc/src/AbstractConnection.ts @@ -0,0 +1,43 @@ +import EventEmitter from "events"; +import { IMethodMap, IPacket } from "./schema"; + +export abstract class AbstractConnection extends EventEmitter +{ + /** + * Write the message + */ + protected abstract write(packet: IPacket): void; + + // --------------------------------------------------------------------------------------------- + + /** + * Encode the packet to send + */ + protected serializePacket(packet: any) { + return packet; + } + + /** + * Deserialize the received packet + */ + protected deserializePacket(packet: string) { + return packet; + } + + /** + * Send a message + */ + public send(method: K, ...args: Parameters) { + this.write({ + method: method.toString(), + args: args.length > 0 ? args : undefined + }) + } + + /** + * Send a request and return a promise of the result + */ + public request(method: K, ...args: Parameters) { + return new Promise<>; + } +} diff --git a/packages/ipc/src/AbstractIpcService.ts b/packages/ipc/src/AbstractIpcService.ts deleted file mode 100644 index b8a32cc..0000000 --- a/packages/ipc/src/AbstractIpcService.ts +++ /dev/null @@ -1,67 +0,0 @@ -import RawIPC = require("node-ipc"); -import { InternalService, Microservice } from "@autoplex/microservice"; -import { IPC, IpcMessageHandler } from "./schema"; - -/** - * An abstract IPC service containing common properties/methods among the server and client - */ -export default abstract class AbstractIpcService extends InternalService -{ - /** - * The IPC instance - */ - private __ipc: IPC|null = null; - - // Implementation Requirements ----------------------------------------------------------------- - - /** - * The path to the socket file - */ - protected abstract readonly SOCKET_PATH: 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; - - /** - * Shutdown the IPC service before it is destroyed - */ - protected abstract shutdownIpc(ipc: IPC|null): Promise; - - // Service Management -------------------------------------------------------------------------- - - /** - * Boot the IPC service - */ - public override 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 override async shutdown() { - await this.shutdownIpc(this.__ipc); - this.__ipc = null; - } - - // Accessors ----------------------------------------------------------------------------------- - - /** - * Get the raw IPC instance - */ - protected get rawIpcInstance(): IPC|null { - return this.__ipc; - } -} diff --git a/packages/ipc/src/AbstractMethodMap.ts b/packages/ipc/src/AbstractMethodMap.ts new file mode 100644 index 0000000..04853a4 --- /dev/null +++ b/packages/ipc/src/AbstractMethodMap.ts @@ -0,0 +1,16 @@ +import { IMethodMap } from "./schema"; + +export abstract class AbstractMethodMap +{ + /** + * The method mapping + */ + protected abstract methodMap: T; + + /** + * Invoke a mapped method + */ + protected invoke(name: K, args: Parameters) { + return this.methodMap[name].apply(this, args); + } +} diff --git a/packages/ipc/src/AbstractServer.ts b/packages/ipc/src/AbstractServer.ts new file mode 100644 index 0000000..54cb122 --- /dev/null +++ b/packages/ipc/src/AbstractServer.ts @@ -0,0 +1,11 @@ +import { AbstractConnection } from "./AbstractConnection"; +import { AbstractMethodMap } from "./AbstractMethodMap"; +import { IMethodMap } from "./schema"; + +type PrependConnection = { + [K in keyof T]: (connection: string, ...args: Parameters) => ReturnType +}; + +export abstract class AbstractServer> extends AbstractMethodMap> +{ +} diff --git a/packages/ipc/src/IpcClientService.ts b/packages/ipc/src/IpcClientService.ts deleted file mode 100644 index 7a3975d..0000000 --- a/packages/ipc/src/IpcClientService.ts +++ /dev/null @@ -1,194 +0,0 @@ -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, IpcError, IpcResponseError, 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(); - if (response.error !== undefined) { - reject(new IpcResponseError(response.error)); - return; - } - resolve(response.data); - }; - // Abort the request - let abort = (error: IpcError) => { - 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; - } -} diff --git a/packages/ipc/src/IpcError.ts b/packages/ipc/src/IpcError.ts deleted file mode 100644 index 8437ef3..0000000 --- a/packages/ipc/src/IpcError.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * 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); - } -} - -/** - * IPC response error type - */ -export class IpcResponseError extends IpcError { - constructor(...args: any[]) { - super(...args); - Object.setPrototypeOf(this, IpcResponseError.prototype); - } -} diff --git a/packages/ipc/src/IpcServerService.ts b/packages/ipc/src/IpcServerService.ts deleted file mode 100644 index 711a2c5..0000000 --- a/packages/ipc/src/IpcServerService.ts +++ /dev/null @@ -1,93 +0,0 @@ -import assert from "assert"; -import { mkdir } from "fs/promises"; -import { Socket } from "net"; -import { dirname } from "path"; -import { Microservice } from "@autoplex/microservice"; -import { IIpcRequest, IPC } from "./schema"; -import AbstractIpcService from "./AbstractIpcService"; - -type IpcServer = IPC["server"]; - -export abstract class IpcServerService extends AbstractIpcService -{ - /** - * 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() { - // no-op - } - - // Service Management -------------------------------------------------------------------------- - - /** - * Boot the IPC service - */ - public bootIpc(ipc: IPC) { - return new Promise(async (resolve) => { - // Create the socket directory if it doesn't exist - await mkdir(dirname(this.SOCKET_PATH), { recursive: true }); - // Serve the IPC server - ipc.serve(this.SOCKET_PATH, () => { - 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) { - 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 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; - } - for (let socket of (this.__server).sockets) { - this.__server.emit(socket, method, data); - } - } -} diff --git a/packages/ipc/src/Request.ts b/packages/ipc/src/Request.ts new file mode 100644 index 0000000..465440b --- /dev/null +++ b/packages/ipc/src/Request.ts @@ -0,0 +1,27 @@ +export class Request implements PromiseLike +{ + public constructor(requestId: number, method: string, args: any[], timeout?: number) { + + } + + /** + * Abort the current request + */ + public abort() { + + } + + /** + * Successful completion of the request + */ + public then() { + + } + + /** + * Caught error in the request + */ + public catch() { + + } +} diff --git a/packages/ipc/src/index.ts b/packages/ipc/src/index.ts index e01bf9a..e678560 100644 --- a/packages/ipc/src/index.ts +++ b/packages/ipc/src/index.ts @@ -1,4 +1 @@ -export * from "./schema"; -export * from "./IpcError"; -export * from "./IpcClientService"; -export * from "./IpcServerService"; +export * from "./AbstractConnection"; diff --git a/packages/ipc/src/schema.ts b/packages/ipc/src/schema.ts index f261c6b..e558c57 100644 --- a/packages/ipc/src/schema.ts +++ b/packages/ipc/src/schema.ts @@ -1,27 +1,19 @@ -import type RawIPC = require("node-ipc"); +import RawIPC from "node-ipc"; /** - * The IPC request structure + * A generic function/method mapping */ -export interface IIpcRequest { - id : number|null, - data?: any +export interface IMethodMap { + [name: string]: (...args: any[]) => any } -/** - * The IPC response structure - */ -export interface IIpcResponse { - data : T, - error?: string +export interface IPacket { + requestId?: number, + method: string, + args?: any[] } /** - * The IPC message handler type - */ -export type IpcMessageHandler = (...args: any[]) => Promise - - /** * HOLY @#$@% WHOEVER MADE THE TYPES FOR `node-ipc` SHOULDB BE HANGED */ export type IPC = InstanceType<(typeof RawIPC)["IPC"]>; diff --git a/packages/ipc/test/unit/AbstractClient.spec.ts b/packages/ipc/test/unit/AbstractClient.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/ipc/test/unit/AbstractConnection.spec.ts b/packages/ipc/test/unit/AbstractConnection.spec.ts new file mode 100644 index 0000000..927adf9 --- /dev/null +++ b/packages/ipc/test/unit/AbstractConnection.spec.ts @@ -0,0 +1,19 @@ +import { AbstractConnection } from "../../src"; + +type MethodMap = { + sendTest(value: number): void +}; + +class MockConnection extends AbstractConnection { + public write = jest.fn(); +} + +describe("Abstract IPC Connections", () => { + it("Should write packet", () => { + let connection = new MockConnection(); + connection.send("sendTest", 10); + // expect(connection.write.mock.calls[0][0]).toMatchObject({ + + // }); + }); +}); diff --git a/packages/ipc/test/unit/AbstractMethodMap.ts b/packages/ipc/test/unit/AbstractMethodMap.ts new file mode 100644 index 0000000..74fdf47 --- /dev/null +++ b/packages/ipc/test/unit/AbstractMethodMap.ts @@ -0,0 +1,23 @@ +import { AbstractMethodMap } from "../../src/AbstractMethodMap"; + +class TestMap extends AbstractMethodMap<{ doSomething(a: number, b: number): number }> { + public mockFn = jest.fn(); + public methodMap = { + doSomething: this.doSomething + }; + public doSomething(a: number, b: number) { + this.mockFn(); // Ensure the correct 'this' context + return a + b; + } + public publicInvoke() { + this.invoke("doSomething", [5, 10]); + } +} + +describe("Abstract Method Map", () => { + it("Invoke mapped method", () => { + let map = new TestMap(); + map.publicInvoke(); + expect(map.mockFn).toHaveBeenCalled() + }); +}); diff --git a/packages/ipc/test/unit/AbstractServer.spec.ts b/packages/ipc/test/unit/AbstractServer.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/ipc/test/unit/Request.spec.ts b/packages/ipc/test/unit/Request.spec.ts new file mode 100644 index 0000000..3470e29 --- /dev/null +++ b/packages/ipc/test/unit/Request.spec.ts @@ -0,0 +1,5 @@ +describe("Request", () => { + it("", () => { + + }); +});