From b0bcd64b415901f240e14ae1e326a1bbaa8d198c Mon Sep 17 00:00:00 2001 From: tone Date: Thu, 27 Nov 2025 20:35:14 +0800 Subject: [PATCH 01/12] feat: add createDeferrablePromise utility function --- src/utils/utils.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index b164b99..2fbda51 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -81,4 +81,20 @@ export function markAsPublicMethod | u markAs(obj); return obj; +} + +export function createDeferrablePromise() { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: unknown) => void; + + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + return { + promise, + resolve, + reject + }; } \ No newline at end of file From 99c673a8dc3066a432eb6c2cc38e41972206fb45 Mon Sep 17 00:00:00 2001 From: tone Date: Thu, 27 Nov 2025 20:43:07 +0800 Subject: [PATCH 02/12] feat: add plugin management methods to RPCHandler --- src/core/RPCHandler.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/core/RPCHandler.ts b/src/core/RPCHandler.ts index 1dba807..03959a0 100644 --- a/src/core/RPCHandler.ts +++ b/src/core/RPCHandler.ts @@ -3,6 +3,7 @@ import { RPCClient } from "./RPCClient"; import { RPCServer } from "./RPCServer"; import { RPCProvider } from "./RPCProvider"; import { RPCSession } from "./RPCSession"; +import { RPCPlugin } from "./RPCPlugin"; const DefaultListenOptions = { port: 5201, @@ -34,6 +35,7 @@ export class RPCHandler extends EventEmitter { private provider?: RPCProvider; private accessKey?: string; private config: RPCConfig; + private plugins: RPCPlugin[] = []; constructor( args?: { @@ -88,6 +90,29 @@ export class RPCHandler extends EventEmitter { } } + loadPlugin(plugin: RPCPlugin): boolean { + const plugins = this.plugins; + if (plugins.includes(plugin)) { + return false; + } + plugins.push(plugin); + return true; + } + + unloadPlugin(plugin: RPCPlugin): boolean { + const plugins = this.plugins; + const idx = plugins.indexOf(plugin); + if (idx === -1) { + return false; + } + plugins.splice(idx, 1); + return true; + } + + getPlugins(): RPCPlugin[] { + return [...this.plugins]; + } + async connect(options: { url?: string; accessKey?: string; From 24a14a8e1c1aa8e590c7ddbb3969e5d22dbaab60 Mon Sep 17 00:00:00 2001 From: tone Date: Thu, 27 Nov 2025 21:25:27 +0800 Subject: [PATCH 03/12] feat: add RPCPlugin interface and abstract class with hook context definitions --- src/core/RPCPlugin.ts | 76 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/core/RPCPlugin.ts diff --git a/src/core/RPCPlugin.ts b/src/core/RPCPlugin.ts new file mode 100644 index 0000000..f621a05 --- /dev/null +++ b/src/core/RPCPlugin.ts @@ -0,0 +1,76 @@ +import { RPCSession } from "./RPCSession"; + +// interface BaseHookRuntimeCtx { +// nexts: RPCPlugin[]; +// setNextPlugins: (plugins: RPCPlugin[]) => void +// } + +export interface BaseHookCtx { + +} + +export interface CallOutgoingBeforeCtx extends BaseHookCtx { + session: RPCSession; + options: { + fnPath: string; + args: any[]; + }; +} + +export interface CallOutgoingCtx extends CallOutgoingBeforeCtx { + result: any; + setResult: (data: any) => void; +} + +export interface CallIncomingBeforeCtx extends BaseHookCtx { + +} + +export interface CallIncomingCtx extends CallIncomingBeforeCtx { + +} + +export type NormalMethodReturn = Promise | void; +export type HookFn = (ctx: Ctx) => NormalMethodReturn; + +export interface RPCPluginHooksCtx { + onCallOutgoingBefore: CallOutgoingBeforeCtx; + onCallOutgoing: CallOutgoingCtx; + onCallIncomingBefore: CallIncomingBeforeCtx; + onCallIncoming: CallIncomingCtx; +} + +export type RPCPluginHooks = { + [K in keyof RPCPluginHooksCtx]?: HookFn; +}; + + +export interface RPCPlugin extends RPCPluginHooks { + onInit?(): void; + onDestroy?(): void; +} + +export abstract class AbstractRPCPlugin implements RPCPlugin { + abstract onInit?(): void; + abstract onDestroy?(): void; + abstract onCallOutgoingBefore?(ctx: CallOutgoingBeforeCtx): NormalMethodReturn; + abstract onCallOutgoing?(ctx: CallOutgoingCtx): NormalMethodReturn; + abstract onCallIncomingBefore?(ctx: CallIncomingBeforeCtx): NormalMethodReturn; + abstract onCallIncoming?(ctx: CallIncomingCtx): NormalMethodReturn; +} + +type HookName = keyof RPCPluginHooksCtx; + +export class HookChainInterruptedError extends Error { }; + +type HookRunner = (ctx: Ctx) => Promise; +export function createHookRunner( + plugins: RPCPlugin[], + hookName: K, +): HookRunner { + return async (ctx: RPCPluginHooksCtx[K]) => { + for (const plugin of plugins) { + await (plugin[hookName] as HookFn | undefined)?.(ctx); + } + }; +} \ No newline at end of file From bc1445d3a5f1128424abb22e03f4f8e2cbdf1117 Mon Sep 17 00:00:00 2001 From: tone Date: Thu, 27 Nov 2025 21:31:49 +0800 Subject: [PATCH 04/12] feat: remove unused HookChainInterruptedError class from RPCPlugin --- src/core/RPCPlugin.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/RPCPlugin.ts b/src/core/RPCPlugin.ts index f621a05..391bba1 100644 --- a/src/core/RPCPlugin.ts +++ b/src/core/RPCPlugin.ts @@ -61,8 +61,6 @@ export abstract class AbstractRPCPlugin implements RPCPlugin { type HookName = keyof RPCPluginHooksCtx; -export class HookChainInterruptedError extends Error { }; - type HookRunner = (ctx: Ctx) => Promise; export function createHookRunner( plugins: RPCPlugin[], From 809a3759d9c29a3200f3a7e265c37948260c4344 Mon Sep 17 00:00:00 2001 From: tone Date: Thu, 27 Nov 2025 21:36:41 +0800 Subject: [PATCH 05/12] feat: add unit tests for RPCPlugin hook execution and error handling --- __tests__/unit/core/RPCPlugin.test.ts | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 __tests__/unit/core/RPCPlugin.test.ts diff --git a/__tests__/unit/core/RPCPlugin.test.ts b/__tests__/unit/core/RPCPlugin.test.ts new file mode 100644 index 0000000..5fb797d --- /dev/null +++ b/__tests__/unit/core/RPCPlugin.test.ts @@ -0,0 +1,43 @@ +import { createHookRunner, RPCPlugin } from "@/core/RPCPlugin" +import type { CallIncomingCtx } from "@/core/RPCPlugin" + +describe('RPCPlugin.test', () => { + const plugin3 = { + onCallIncoming(ctx: CallIncomingCtx) { throw new Error() } + } as RPCPlugin; + const plugin4 = { + async onCallIncoming(ctx: CallIncomingCtx) { throw new Error() } + } as RPCPlugin; + + test('should be resolved', async () => { + const plugin1 = { + onCallIncoming: jest.fn(), + } as RPCPlugin; + const plugin2 = { + async onCallIncoming(ctx: CallIncomingCtx) { } + } as RPCPlugin; + + const plugins = [plugin1, plugin2]; + const hookRunner = createHookRunner(plugins, 'onCallIncoming'); + await hookRunner({} as any); + expect(plugin1.onCallIncoming).toHaveBeenCalled() + }) + test('should be resolved2', async () => { + const plugins = [] as RPCPlugin[]; + const hookRunner = createHookRunner(plugins, 'onCallIncoming'); + await hookRunner({} as any); + expect.assertions(0); + }) + + test('should be rejected1', async () => { + const plugins = [plugin3]; + const hookRunner = createHookRunner(plugins, 'onCallIncoming'); + await expect(hookRunner({} as any)).rejects.toThrow(Error) + }) + + test('should be rejected2', async () => { + const plugins = [plugin4]; + const hookRunner = createHookRunner(plugins, 'onCallIncoming'); + await expect(hookRunner({} as any)).rejects.toThrow(Error) + }) +}) \ No newline at end of file From 500e7c8fa6fb6d2fb64b5bb30d11b804bc26ba12 Mon Sep 17 00:00:00 2001 From: tone Date: Thu, 27 Nov 2025 22:30:21 +0800 Subject: [PATCH 06/12] feat: update CallOutgoingBeforeCtx and CallOutgoingCtx interfaces to use unknown type for options and result --- src/core/RPCPlugin.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/core/RPCPlugin.ts b/src/core/RPCPlugin.ts index 391bba1..f056050 100644 --- a/src/core/RPCPlugin.ts +++ b/src/core/RPCPlugin.ts @@ -11,15 +11,13 @@ export interface BaseHookCtx { export interface CallOutgoingBeforeCtx extends BaseHookCtx { session: RPCSession; - options: { - fnPath: string; - args: any[]; - }; + options: unknown; + setOptions: (opt: unknown) => void; } export interface CallOutgoingCtx extends CallOutgoingBeforeCtx { - result: any; - setResult: (data: any) => void; + result: unknown; + setResult: (res: unknown) => void; } export interface CallIncomingBeforeCtx extends BaseHookCtx { From 720a79ca7a2c8adf74e1b8774a35f2e762beb097 Mon Sep 17 00:00:00 2001 From: tone Date: Thu, 27 Nov 2025 22:30:29 +0800 Subject: [PATCH 07/12] feat: comment out abstract onInit and onDestroy methods in AbstractRPCPlugin class --- src/core/RPCPlugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/RPCPlugin.ts b/src/core/RPCPlugin.ts index f056050..31e0b85 100644 --- a/src/core/RPCPlugin.ts +++ b/src/core/RPCPlugin.ts @@ -49,8 +49,8 @@ export interface RPCPlugin extends RPCPluginHooks { } export abstract class AbstractRPCPlugin implements RPCPlugin { - abstract onInit?(): void; - abstract onDestroy?(): void; + // abstract onInit?(): void; + // abstract onDestroy?(): void; abstract onCallOutgoingBefore?(ctx: CallOutgoingBeforeCtx): NormalMethodReturn; abstract onCallOutgoing?(ctx: CallOutgoingCtx): NormalMethodReturn; abstract onCallIncomingBefore?(ctx: CallIncomingBeforeCtx): NormalMethodReturn; From ebb9ed21ad30bd35e730228d0e242ec4069f0479 Mon Sep 17 00:00:00 2001 From: tone Date: Thu, 27 Nov 2025 22:31:06 +0800 Subject: [PATCH 08/12] feat: enhance callRequest method with options validation and hook execution --- src/core/RPCSession.ts | 127 ++++++++++++++++++++++++++++++++++------- 1 file changed, 107 insertions(+), 20 deletions(-) diff --git a/src/core/RPCSession.ts b/src/core/RPCSession.ts index d76e799..5dced55 100644 --- a/src/core/RPCSession.ts +++ b/src/core/RPCSession.ts @@ -1,4 +1,4 @@ -import { isPublicMethod, ToDeepPromise } from "@/utils/utils"; +import { isArray, isObject, isPublicMethod, ToDeepPromise } from "@/utils/utils"; import { RPCConnection } from "./RPCConnection"; import { RPCHandler } from "./RPCHandler"; import { RPCProvider } from "./RPCProvider"; @@ -8,6 +8,7 @@ import { RPCError, RPCErrorCode } from "./RPCError"; import { makeCallPacket, makeCallResponsePacket, parseCallPacket, parseCallResponsePacket } from "./RPCCommon"; import { RPCPacket } from "./RPCPacket"; import { EventEmitter } from "@/utils/EventEmitter"; +import { createHookRunner } from "./RPCPlugin"; function getProviderFunction(provider: RPCProvider, fnPath: string): [(...args: any[]) => Promise, object] | null { @@ -111,6 +112,45 @@ export class RPCSession { }); } + function setOptions(opt: unknown) { + if (!isObject(opt)) { + return; + } + + if ('fnPath' in opt && 'args' in opt && 'timeout' in opt) { + const { fnPath, args, timeout } = opt; + if (typeof fnPath !== 'string') { + return; + } + if (!isArray(args)) { + return; + } + if (typeof timeout !== 'number') { + return; + } + options = { + ...options, + fnPath, + args, + timeout, + } + } + } + + const hookRunner = createHookRunner(this.rpcHandler.getPlugins(), 'onCallOutgoingBefore'); + await hookRunner({ + session: this, + options: { ...options }, + setOptions, + }); + + /** due to `await hookRunner` */ + if (this.connection.closed) { + throw new RPCError({ + errorCode: RPCErrorCode.CONNECTION_DISCONNECTED, + }); + } + const { fnPath, args } = options; const packet = makeCallPacket({ fnPath, @@ -135,28 +175,75 @@ export class RPCSession { })(); const handleCallResponsePacket = (packet: RPCPacket) => { - const result = parseCallResponsePacket(packet); - if (result === null) { - return reject(new RPCError({ + let result = parseCallResponsePacket(packet); + function setResult(res: unknown) { + if (!isObject(res)) { + return; + } + + const { success, error } = res; + + if (typeof success === 'object' && typeof error === 'object') { + if (success && !error) { + if ('data' in success) { + result = { + success: { data: success.data }, + error: null, + } + } + } else if (!success && error) { + const { errorCode, reason } = error; + if (typeof errorCode === 'number' && typeof reason === 'string') { + result = { + success: null, + error: { + errorCode, + reason, + }, + } + } + } + } + } + + const hookRunner = createHookRunner(this.rpcHandler.getPlugins(), 'onCallOutgoing'); + hookRunner({ + session: this, + options: { ...options }, + setOptions, + result, + setResult, + }).then(() => { + if (result === null) { + return reject(new RPCError({ + errorCode: RPCErrorCode.UNKNOWN_ERROR, + }));; + } + + const { success, error } = result; + if (success) { + return resolve(success.data); + } + + if (error) { + return reject(new RPCError({ + errorCode: error.errorCode, + reason: error.reason + })); + } + + reject(new RPCError({ errorCode: RPCErrorCode.UNKNOWN_ERROR, }));; - } + }).catch((e) => { + if (e instanceof RPCError) { + return reject(e); + } - const { success, error } = result; - if (success) { - return resolve(success.data); - } - - if (error) { - return reject(new RPCError({ - errorCode: error.errorCode, - reason: error.reason - })); - } - - return reject(new RPCError({ - errorCode: RPCErrorCode.UNKNOWN_ERROR, - }));; + reject(new RPCError({ + errorCode: RPCErrorCode.UNKNOWN_ERROR, + })) + }) } this.callResponseEmitter.once(packet.id, handleCallResponsePacket); From 1b5281e0c1ab3f3b81982e12bef04ad69cca12e0 Mon Sep 17 00:00:00 2001 From: tone Date: Fri, 28 Nov 2025 11:08:03 +0800 Subject: [PATCH 09/12] feat: add session and request handling to CallIncomingBeforeCtx and CallIncomingCtx interfaces --- src/core/RPCPlugin.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/RPCPlugin.ts b/src/core/RPCPlugin.ts index 31e0b85..96207d9 100644 --- a/src/core/RPCPlugin.ts +++ b/src/core/RPCPlugin.ts @@ -21,11 +21,14 @@ export interface CallOutgoingCtx extends CallOutgoingBeforeCtx { } export interface CallIncomingBeforeCtx extends BaseHookCtx { - + session: RPCSession; + request: unknown; + setRequest: (req: unknown) => void; } export interface CallIncomingCtx extends CallIncomingBeforeCtx { - + response: unknown; + setResponse: (res: unknown) => void; } export type NormalMethodReturn = Promise | void; From 9a8733a77d77895a8eb05c720d82e5e026d55bba Mon Sep 17 00:00:00 2001 From: tone Date: Fri, 28 Nov 2025 11:08:19 +0800 Subject: [PATCH 10/12] feat: enhance onCallRequest method with request handling and hook execution --- src/core/RPCSession.ts | 86 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 7 deletions(-) diff --git a/src/core/RPCSession.ts b/src/core/RPCSession.ts index 5dced55..a099c80 100644 --- a/src/core/RPCSession.ts +++ b/src/core/RPCSession.ts @@ -257,9 +257,81 @@ export class RPCSession { } private async onCallRequest(packet: RPCPacket): Promise { - const request = parseCallPacket(packet); - if (request === null) { + let request = parseCallPacket(packet); + const instance = this; + + function setRequest(req: unknown) { + if (!isObject(req)) { + return; + } + const { fnPath, args } = req; + if (typeof fnPath === 'string' && isArray(args)) { + request = { + ...request, + fnPath, + args, + }; + } + } + + const hookRunnerContext = { + session: this, + request, + setRequest, + } + + + + async function makeResponse(o: Parameters[0]) { + const hookRunner = createHookRunner(instance.rpcHandler.getPlugins(), 'onCallIncoming'); + try { + await hookRunner({ + ...hookRunnerContext, + response: o, + setResponse: (res: unknown) => { + /** TODO: Implement stricter validation of the response object in the future */ + o = res as Parameters[0]; + }, + }) + } catch (error) { + return makeCallResponsePacket({ + ...o, + ...(error instanceof RPCError ? { + errorCode: error.errorCode, + reason: error.reason, + } : { + errorCode: RPCErrorCode.SERVER_ERROR, + reason: `${error}` + }) + }) + } + return makeCallResponsePacket({ + ...o, + }) + } + + + + const hookRunner = createHookRunner(this.rpcHandler.getPlugins(), 'onCallIncomingBefore'); + try { + await hookRunner(hookRunnerContext); + } catch (error) { + return makeResponse({ + status: 'error', + requestPacket: packet, + ...(error instanceof RPCError ? { + errorCode: error.errorCode, + reason: error.reason + } : { + errorCode: RPCErrorCode.SERVER_ERROR, + reason: `${error}`, + }) + }) + } + + if (request === null) { + return makeResponse({ status: 'error', requestPacket: packet, errorCode: RPCErrorCode.CALL_PROTOCOL_ERROR, @@ -269,7 +341,7 @@ export class RPCSession { // call the function const provider = this.rpcHandler.getProvider(); if (!provider) { - return makeCallResponsePacket({ + return makeResponse({ status: 'error', requestPacket: packet, errorCode: RPCErrorCode.PROVIDER_NOT_AVAILABLE, @@ -279,7 +351,7 @@ export class RPCSession { const { fnPath, args } = request; const fnRes = getProviderFunction(provider, fnPath); if (!fnRes) { - return makeCallResponsePacket({ + return makeResponse({ status: 'error', requestPacket: packet, errorCode: RPCErrorCode.METHOD_NOT_FOUND, @@ -290,7 +362,7 @@ export class RPCSession { const { enableMethodProtection } = this.rpcHandler.getConfig(); if (enableMethodProtection) { if (!isPublicMethod(fn)) { - return makeCallResponsePacket({ + return makeResponse({ status: 'error', requestPacket: packet, errorCode: RPCErrorCode.METHOD_PROTECTED, @@ -300,13 +372,13 @@ export class RPCSession { try { const result = await fn.bind(fnThis)(...args); - return makeCallResponsePacket({ + return makeResponse({ status: 'success', requestPacket: packet, data: result, }) } catch (error) { - return makeCallResponsePacket({ + return makeResponse({ status: 'error', requestPacket: packet, errorCode: RPCErrorCode.SERVER_ERROR, From 40d0e793583a6921aedf62f5f582939417e7df06 Mon Sep 17 00:00:00 2001 From: tone Date: Fri, 28 Nov 2025 13:35:03 +0800 Subject: [PATCH 11/12] feat: add error reason to RPCError in callRequest method --- src/core/RPCSession.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/RPCSession.ts b/src/core/RPCSession.ts index a099c80..8c66044 100644 --- a/src/core/RPCSession.ts +++ b/src/core/RPCSession.ts @@ -242,6 +242,7 @@ export class RPCSession { reject(new RPCError({ errorCode: RPCErrorCode.UNKNOWN_ERROR, + reason: e instanceof Error ? e.message : `${e}` })) }) } From 12afcb7a8277866304f9199c67e5d498cfab27f5 Mon Sep 17 00:00:00 2001 From: tone Date: Fri, 28 Nov 2025 14:12:37 +0800 Subject: [PATCH 12/12] feat: add session management and authentication tests for RPC plugin --- .../rpc-plugin/rpc-plugin.ctx.session.test.ts | 49 +++++++++++++++++++ .../e2e/rpc-plugin/rpc-plugin.hook.test.ts | 42 ++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 __tests__/e2e/rpc-plugin/rpc-plugin.ctx.session.test.ts create mode 100644 __tests__/e2e/rpc-plugin/rpc-plugin.hook.test.ts diff --git a/__tests__/e2e/rpc-plugin/rpc-plugin.ctx.session.test.ts b/__tests__/e2e/rpc-plugin/rpc-plugin.ctx.session.test.ts new file mode 100644 index 0000000..5ea4736 --- /dev/null +++ b/__tests__/e2e/rpc-plugin/rpc-plugin.ctx.session.test.ts @@ -0,0 +1,49 @@ +import { RPCError } from "@/core/RPCError"; +import { CallIncomingBeforeCtx, NormalMethodReturn } from "@/core/RPCPlugin"; +import { AbstractRPCPlugin, RPCHandler } from "@/index"; +import { getRandomAvailablePort, isObject } from "@/utils/utils"; + +describe('rpc-plugin.ctx.session.test', () => { + const userInfo = 'userinfo'; + const users = new WeakMap(); + class RPCTestPlugin implements AbstractRPCPlugin { + onCallIncomingBefore(ctx: CallIncomingBeforeCtx): NormalMethodReturn { + if (users.has(ctx.session)) { + return; + } + + if (isObject(ctx.request)) { + if (ctx.request.fnPath === 'login') { + users.set(ctx.session, userInfo); + return; + } + } + + throw new RPCError({ + reason: 'not login' + }); + } + } + + test('session', async () => { + const server = new RPCHandler(); + const loginRes = 'login'; + const authRes = 'auth'; + const provider = { + login() { return loginRes }, + auth() { return authRes } + } + server.setProvider(provider); + server.loadPlugin(new RPCTestPlugin()); + const client = new RPCHandler(); + + const port = await getRandomAvailablePort(); + await server.listen({ port }); + + const session = await client.connect({ url: `http://localhost:${port}` }); + const api = session.getAPI(); + await expect(api.auth()).rejects.toMatchObject({ reason: 'not login' }); + await expect(api.login()).resolves.toBe(loginRes); + await expect(api.auth()).resolves.toBe(authRes); + }); +}) \ No newline at end of file diff --git a/__tests__/e2e/rpc-plugin/rpc-plugin.hook.test.ts b/__tests__/e2e/rpc-plugin/rpc-plugin.hook.test.ts new file mode 100644 index 0000000..b54ee49 --- /dev/null +++ b/__tests__/e2e/rpc-plugin/rpc-plugin.hook.test.ts @@ -0,0 +1,42 @@ +import { CallIncomingBeforeCtx, CallIncomingCtx, CallOutgoingBeforeCtx, CallOutgoingCtx, NormalMethodReturn } from "@/core/RPCPlugin"; +import { AbstractRPCPlugin, RPCHandler } from "@/index"; +import { getRandomAvailablePort } from "@/utils/utils"; + +describe('rpc-plugin.hook.test', () => { + let count = 0; + class RPCTestPlugin implements AbstractRPCPlugin { + onCallIncomingBefore(ctx: CallIncomingBeforeCtx): NormalMethodReturn { + count += 1; + } + onCallIncoming(ctx: CallIncomingCtx): NormalMethodReturn { + count += 2; + } + onCallOutgoingBefore(ctx: CallOutgoingBeforeCtx): NormalMethodReturn { + count += 4; + } + onCallOutgoing(ctx: CallOutgoingCtx): NormalMethodReturn { + count += 8; + } + } + const CountShouldBe = 15; + + test('count', async () => { + const server = new RPCHandler(); + const provider = { + add(a: number, b: number) { return a + b } + } + server.setProvider(provider); + server.loadPlugin(new RPCTestPlugin()); + const client = new RPCHandler(); + client.loadPlugin(new RPCTestPlugin()); + + const port = await getRandomAvailablePort(); + await server.listen({ port }); + + + const session = await client.connect({ url: `http://localhost:${port}` }); + const api = session.getAPI(); + await expect(api.add(1, 2)).resolves.toBe(3); + expect(count).toBe(CountShouldBe); + }); +}) \ No newline at end of file