Merge branch 'feature/rpc-plugin' into dev

* feature/rpc-plugin:
  feat: add session management and authentication tests for RPC plugin
  feat: add error reason to RPCError in callRequest method
  feat: enhance onCallRequest method with request handling and hook execution
  feat: add session and request handling to CallIncomingBeforeCtx and CallIncomingCtx interfaces
  feat: enhance callRequest method with options validation and hook execution
  feat: comment out abstract onInit and onDestroy methods in AbstractRPCPlugin class
  feat: update CallOutgoingBeforeCtx and CallOutgoingCtx interfaces to use unknown type for options and result
  feat: add unit tests for RPCPlugin hook execution and error handling
  feat: remove unused HookChainInterruptedError class from RPCPlugin
  feat: add RPCPlugin interface and abstract class with hook context definitions
  feat: add plugin management methods to RPCHandler
  feat: add createDeferrablePromise utility function
This commit is contained in:
2025-11-28 14:12:53 +08:00
7 changed files with 437 additions and 27 deletions

View File

@@ -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<typeof provider>();
await expect(api.auth()).rejects.toMatchObject({ reason: 'not login' });
await expect(api.login()).resolves.toBe(loginRes);
await expect(api.auth()).resolves.toBe(authRes);
});
})

View File

@@ -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<typeof provider>();
await expect(api.add(1, 2)).resolves.toBe(3);
expect(count).toBe(CountShouldBe);
});
})

View File

@@ -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)
})
})