mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 06:31:13 +00:00
Serialize non-function properties of server-side objects as cachedProps so simple property reads don't require additional RPCs. Added cachedProps to SerializedValue, have RpcServer.storeObjectRef serialize and attach cachedProps (skipping functions), updated serializer to deserialize cachedProps and pass them to proxyCreator, and updated client proxy creation to accept cachedProps and return cached top-level properties directly. Tests updated to expect direct property access for serialized/simple objects and arrays.
1469 lines
43 KiB
TypeScript
1469 lines
43 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import {
|
|
// 简化 API
|
|
createRpcPair,
|
|
mockRemote,
|
|
createServer,
|
|
createClient,
|
|
// 底层 API
|
|
createDeepProxy,
|
|
LocalTransport,
|
|
serialize,
|
|
deserialize,
|
|
SerializedValueType,
|
|
isRpcProxy,
|
|
getProxyMeta,
|
|
RpcServer,
|
|
MessageTransport,
|
|
createMessageServerHandler,
|
|
} from '@/napcat-rpc/src';
|
|
|
|
// 测试用目标对象
|
|
interface TestObject {
|
|
name: string;
|
|
count: number;
|
|
nested: {
|
|
value: string;
|
|
deep: {
|
|
level: number;
|
|
getData (): { info: string; };
|
|
};
|
|
};
|
|
items: string[];
|
|
greet (name: string): string;
|
|
asyncGreet (name: string): Promise<string>;
|
|
add (a: number, b: number): number;
|
|
multiply (a: number, b: number): number;
|
|
withCallback (fn: (x: number) => number): number;
|
|
asyncWithCallback (fn: (x: number) => Promise<number>): Promise<number>;
|
|
multiCallback (
|
|
onSuccess: (result: string) => void,
|
|
onError: (error: Error) => void
|
|
): void;
|
|
getObject (): { id: number; name: string; };
|
|
createInstance: new (name: string) => { name: string; greet (): string; getName (): string; };
|
|
getData (): Map<string, number>;
|
|
getSet (): Set<string>;
|
|
getDate (): Date;
|
|
getBuffer (): Uint8Array;
|
|
throwError (): never;
|
|
asyncThrowError (): Promise<never>;
|
|
}
|
|
|
|
function createTestObject (): TestObject {
|
|
return {
|
|
name: 'test',
|
|
count: 42,
|
|
nested: {
|
|
value: 'nested-value',
|
|
deep: {
|
|
level: 3,
|
|
getData () {
|
|
return { info: 'deep-info' };
|
|
},
|
|
},
|
|
},
|
|
items: ['a', 'b', 'c'],
|
|
greet (name: string) {
|
|
return `Hello, ${name}!`;
|
|
},
|
|
async asyncGreet (name: string) {
|
|
await Promise.resolve();
|
|
return `Async Hello, ${name}!`;
|
|
},
|
|
add (a: number, b: number) {
|
|
return a + b;
|
|
},
|
|
multiply (a: number, b: number) {
|
|
return a * b;
|
|
},
|
|
withCallback (fn: (x: number) => number) {
|
|
return fn(10) * 2;
|
|
},
|
|
async asyncWithCallback (fn: (x: number) => Promise<number>) {
|
|
const result = await fn(5);
|
|
return result + 100;
|
|
},
|
|
multiCallback (
|
|
onSuccess: (result: string) => void,
|
|
onError: (error: Error) => void
|
|
) {
|
|
try {
|
|
onSuccess('Operation completed');
|
|
} catch (e) {
|
|
onError(e as Error);
|
|
}
|
|
},
|
|
getObject () {
|
|
return { id: 1, name: 'object-name' };
|
|
},
|
|
createInstance: class TestClass {
|
|
constructor (public name: string) { }
|
|
greet () {
|
|
return `Instance: ${this.name}`;
|
|
}
|
|
getName () {
|
|
return this.name;
|
|
}
|
|
} as unknown as new (name: string) => { name: string; greet (): string; getName (): string; },
|
|
getData () {
|
|
return new Map([['a', 1], ['b', 2]]);
|
|
},
|
|
getSet () {
|
|
return new Set(['x', 'y', 'z']);
|
|
},
|
|
getDate () {
|
|
return new Date('2024-01-15T10:30:00.000Z');
|
|
},
|
|
getBuffer () {
|
|
return new Uint8Array([1, 2, 3, 4, 5]);
|
|
},
|
|
throwError () {
|
|
throw new Error('Test error');
|
|
},
|
|
async asyncThrowError () {
|
|
await Promise.resolve();
|
|
throw new Error('Async test error');
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('napcat-rpc RPC', () => {
|
|
describe('serialize / deserialize', () => {
|
|
it('should serialize and deserialize primitive types', () => {
|
|
// undefined
|
|
expect(deserialize(serialize(undefined))).toBeUndefined();
|
|
|
|
// null
|
|
expect(deserialize(serialize(null))).toBeNull();
|
|
|
|
// boolean
|
|
expect(deserialize(serialize(true))).toBe(true);
|
|
expect(deserialize(serialize(false))).toBe(false);
|
|
|
|
// number
|
|
expect(deserialize(serialize(42))).toBe(42);
|
|
expect(deserialize(serialize(3.14))).toBe(3.14);
|
|
expect(deserialize(serialize(NaN))).toBeNaN();
|
|
expect(deserialize(serialize(Infinity))).toBe(Infinity);
|
|
expect(deserialize(serialize(-Infinity))).toBe(-Infinity);
|
|
|
|
// bigint
|
|
expect(deserialize(serialize(BigInt('9007199254740993')))).toBe(BigInt('9007199254740993'));
|
|
|
|
// string
|
|
expect(deserialize(serialize('hello'))).toBe('hello');
|
|
expect(deserialize(serialize(''))).toBe('');
|
|
});
|
|
|
|
it('should serialize and deserialize Date', () => {
|
|
const date = new Date('2024-01-15T10:30:00.000Z');
|
|
const result = deserialize(serialize(date)) as Date;
|
|
expect(result).toBeInstanceOf(Date);
|
|
expect(result.toISOString()).toBe(date.toISOString());
|
|
});
|
|
|
|
it('should serialize and deserialize RegExp', () => {
|
|
const regex = /test\d+/gi;
|
|
const result = deserialize(serialize(regex)) as RegExp;
|
|
expect(result).toBeInstanceOf(RegExp);
|
|
expect(result.source).toBe(regex.source);
|
|
expect(result.flags).toBe(regex.flags);
|
|
});
|
|
|
|
it('should serialize and deserialize Error', () => {
|
|
const error = new Error('test error');
|
|
const result = deserialize(serialize(error)) as Error;
|
|
expect(result).toBeInstanceOf(Error);
|
|
expect(result.message).toBe('test error');
|
|
});
|
|
|
|
it('should serialize and deserialize arrays', () => {
|
|
const arr = [1, 'two', { three: 3 }, [4, 5]];
|
|
const result = deserialize(serialize(arr));
|
|
expect(result).toEqual(arr);
|
|
});
|
|
|
|
it('should serialize and deserialize objects', () => {
|
|
const obj = {
|
|
name: 'test',
|
|
count: 42,
|
|
nested: {
|
|
value: 'nested',
|
|
},
|
|
};
|
|
const result = deserialize(serialize(obj));
|
|
expect(result).toEqual(obj);
|
|
});
|
|
|
|
it('should serialize and deserialize Map', () => {
|
|
const map = new Map<string, number>([['a', 1], ['b', 2]]);
|
|
const result = deserialize(serialize(map)) as Map<string, number>;
|
|
expect(result).toBeInstanceOf(Map);
|
|
expect(result.get('a')).toBe(1);
|
|
expect(result.get('b')).toBe(2);
|
|
});
|
|
|
|
it('should serialize and deserialize Set', () => {
|
|
const set = new Set(['x', 'y', 'z']);
|
|
const result = deserialize(serialize(set)) as Set<string>;
|
|
expect(result).toBeInstanceOf(Set);
|
|
expect(result.has('x')).toBe(true);
|
|
expect(result.has('y')).toBe(true);
|
|
expect(result.has('z')).toBe(true);
|
|
});
|
|
|
|
it('should serialize and deserialize Uint8Array', () => {
|
|
const buffer = new Uint8Array([1, 2, 3, 4, 5]);
|
|
const result = deserialize(serialize(buffer)) as Uint8Array;
|
|
expect(result).toBeInstanceOf(Uint8Array);
|
|
expect(Array.from(result)).toEqual([1, 2, 3, 4, 5]);
|
|
});
|
|
|
|
it('should handle circular references gracefully', () => {
|
|
const obj: { self?: unknown; } = {};
|
|
obj.self = obj;
|
|
// 应该不抛出错误
|
|
const serialized = serialize(obj);
|
|
expect(serialized.type).toBe(SerializedValueType.OBJECT);
|
|
});
|
|
});
|
|
|
|
describe('LocalTransport + createDeepProxy', () => {
|
|
let target: TestObject;
|
|
let transport: LocalTransport;
|
|
let proxy: TestObject;
|
|
|
|
beforeEach(() => {
|
|
target = createTestObject();
|
|
transport = new LocalTransport(target);
|
|
proxy = createDeepProxy<TestObject>({ transport });
|
|
});
|
|
|
|
it('should identify RPC proxy', () => {
|
|
expect(isRpcProxy(proxy)).toBe(true);
|
|
expect(isRpcProxy({})).toBe(false);
|
|
expect(isRpcProxy(null)).toBe(false);
|
|
});
|
|
|
|
it('should get proxy metadata', () => {
|
|
const meta = getProxyMeta(proxy);
|
|
expect(meta).toBeDefined();
|
|
expect(meta?.isProxy).toBe(true);
|
|
expect(meta?.path).toEqual([]);
|
|
|
|
const nestedMeta = getProxyMeta(proxy.nested);
|
|
expect(nestedMeta?.path).toEqual(['nested']);
|
|
});
|
|
|
|
describe('property access', () => {
|
|
it('should access nested properties through method call', async () => {
|
|
// 通过方法调用获取嵌套属性
|
|
const result = await proxy.nested.deep.getData();
|
|
expect(result).toEqual({ info: 'deep-info' });
|
|
});
|
|
});
|
|
|
|
describe('method calls', () => {
|
|
it('should call synchronous methods', async () => {
|
|
const result = await proxy.greet('World');
|
|
expect(result).toBe('Hello, World!');
|
|
});
|
|
|
|
it('should call methods with multiple arguments', async () => {
|
|
const sum = await proxy.add(5, 3);
|
|
expect(sum).toBe(8);
|
|
|
|
const product = await proxy.multiply(4, 7);
|
|
expect(product).toBe(28);
|
|
});
|
|
|
|
it('should call async methods', async () => {
|
|
const result = await proxy.asyncGreet('Async');
|
|
expect(result).toBe('Async Hello, Async!');
|
|
});
|
|
|
|
it('should call deeply nested methods', async () => {
|
|
const result = await proxy.nested.deep.getData();
|
|
expect(result).toEqual({ info: 'deep-info' });
|
|
});
|
|
});
|
|
|
|
describe('callback proxying', () => {
|
|
it('should proxy callbacks to remote (async pattern)', async () => {
|
|
// RPC 回调本质上是异步的,因此服务端必须 await 回调结果
|
|
// 测试 asyncWithCallback 场景(服务端已经 await 回调)
|
|
const callback = vi.fn(async (x: number) => x * 3);
|
|
const result = await proxy.asyncWithCallback(callback);
|
|
|
|
expect(callback).toHaveBeenCalledWith(5);
|
|
expect(result).toBe(115); // (5 * 3) + 100
|
|
});
|
|
|
|
it('should proxy async callbacks', async () => {
|
|
const callback = vi.fn(async (x: number) => {
|
|
await Promise.resolve();
|
|
return x * 4;
|
|
});
|
|
const result = await proxy.asyncWithCallback(callback);
|
|
|
|
expect(callback).toHaveBeenCalledWith(5);
|
|
expect(result).toBe(120); // (5 * 4) + 100
|
|
});
|
|
|
|
it('should handle multiple callbacks', async () => {
|
|
const onSuccess = vi.fn();
|
|
const onError = vi.fn();
|
|
|
|
await proxy.multiCallback(onSuccess, onError);
|
|
|
|
expect(onSuccess).toHaveBeenCalledWith('Operation completed');
|
|
expect(onError).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('return value types', () => {
|
|
it('should handle object return values', async () => {
|
|
const result = await proxy.getObject();
|
|
expect(result).toEqual({ id: 1, name: 'object-name' });
|
|
});
|
|
|
|
it('should handle Map return values', async () => {
|
|
const result = await proxy.getData();
|
|
expect(result).toBeInstanceOf(Map);
|
|
expect((result as Map<string, number>).get('a')).toBe(1);
|
|
});
|
|
|
|
it('should handle Set return values', async () => {
|
|
const result = await proxy.getSet();
|
|
expect(result).toBeInstanceOf(Set);
|
|
expect((result as Set<string>).has('x')).toBe(true);
|
|
});
|
|
|
|
it('should handle Date return values', async () => {
|
|
const result = await proxy.getDate();
|
|
expect(result).toBeInstanceOf(Date);
|
|
expect((result as Date).toISOString()).toBe('2024-01-15T10:30:00.000Z');
|
|
});
|
|
|
|
it('should handle Uint8Array return values', async () => {
|
|
const result = await proxy.getBuffer();
|
|
expect(result).toBeInstanceOf(Uint8Array);
|
|
expect(Array.from(result as Uint8Array)).toEqual([1, 2, 3, 4, 5]);
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should propagate sync errors', async () => {
|
|
await expect(proxy.throwError()).rejects.toThrow('Test error');
|
|
});
|
|
|
|
it('should propagate async errors', async () => {
|
|
await expect(proxy.asyncThrowError()).rejects.toThrow('Async test error');
|
|
});
|
|
});
|
|
|
|
describe('constructor proxying', () => {
|
|
it('should proxy constructor calls', async () => {
|
|
const Constructor = proxy.createInstance;
|
|
const instance = await new Constructor('TestInstance');
|
|
// 返回的是代理对象,需要 await 获取属性值
|
|
expect(await instance.getName()).toBe('TestInstance');
|
|
});
|
|
});
|
|
|
|
describe('property modification', () => {
|
|
it('should set properties through proxy', async () => {
|
|
proxy.count = 100;
|
|
// 等待一下让异步操作完成
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
expect(target.count).toBe(100);
|
|
});
|
|
|
|
it('should set nested properties', async () => {
|
|
proxy.nested.value = 'modified';
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
expect(target.nested.value).toBe('modified');
|
|
});
|
|
});
|
|
|
|
describe('delete property', () => {
|
|
it('should delete properties through proxy', async () => {
|
|
(target as { deletable?: string; }).deletable = 'will be deleted';
|
|
delete (proxy as { deletable?: string; }).deletable;
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
expect((target as { deletable?: string; }).deletable).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('RpcServer', () => {
|
|
it('should handle GET requests', async () => {
|
|
const target = { name: 'test', nested: { value: 123 } };
|
|
const server = new RpcServer({ target });
|
|
|
|
const response = await server.handleRequest({
|
|
id: 'req1',
|
|
type: 'get' as never,
|
|
path: ['name'],
|
|
});
|
|
|
|
expect(response.success).toBe(true);
|
|
expect(deserialize(response.result!)).toBe('test');
|
|
});
|
|
|
|
it('should handle SET requests', async () => {
|
|
const target = { name: 'test' };
|
|
const server = new RpcServer({ target });
|
|
|
|
const response = await server.handleRequest({
|
|
id: 'req1',
|
|
type: 'set' as never,
|
|
path: ['name'],
|
|
args: [serialize('modified')],
|
|
});
|
|
|
|
expect(response.success).toBe(true);
|
|
expect(target.name).toBe('modified');
|
|
});
|
|
|
|
it('should handle APPLY requests', async () => {
|
|
const target = {
|
|
add: (a: number, b: number) => a + b,
|
|
};
|
|
const server = new RpcServer({ target });
|
|
|
|
const response = await server.handleRequest({
|
|
id: 'req1',
|
|
type: 'apply' as never,
|
|
path: ['add'],
|
|
args: [serialize(3), serialize(4)],
|
|
});
|
|
|
|
expect(response.success).toBe(true);
|
|
expect(deserialize(response.result!)).toBe(7);
|
|
});
|
|
|
|
it('should handle errors gracefully', async () => {
|
|
const target = {
|
|
fail: () => {
|
|
throw new Error('Intentional failure');
|
|
},
|
|
};
|
|
const server = new RpcServer({ target });
|
|
|
|
const response = await server.handleRequest({
|
|
id: 'req1',
|
|
type: 'apply' as never,
|
|
path: ['fail'],
|
|
args: [],
|
|
});
|
|
|
|
expect(response.success).toBe(false);
|
|
expect(response.error).toBe('Intentional failure');
|
|
});
|
|
});
|
|
|
|
describe('MessageTransport', () => {
|
|
it('should communicate via message channel', async () => {
|
|
const target = {
|
|
echo: (msg: string) => `Echo: ${msg}`,
|
|
add: (a: number, b: number) => a + b,
|
|
};
|
|
|
|
// 模拟消息通道
|
|
type MessageHandler = (message: string) => void;
|
|
let clientHandler: MessageHandler = () => { };
|
|
let serverHandler: MessageHandler = () => { };
|
|
|
|
// 创建服务端处理器
|
|
createMessageServerHandler(target, {
|
|
sendMessage: (msg) => {
|
|
setTimeout(() => clientHandler(msg), 0);
|
|
},
|
|
onMessage: (handler) => {
|
|
serverHandler = handler;
|
|
},
|
|
});
|
|
|
|
// 创建客户端传输层
|
|
const transport = new MessageTransport({
|
|
sendMessage: (msg) => {
|
|
setTimeout(() => serverHandler(msg), 0);
|
|
},
|
|
onMessage: (handler) => {
|
|
clientHandler = handler;
|
|
},
|
|
});
|
|
|
|
const proxy = createDeepProxy<typeof target>({ transport });
|
|
|
|
// 测试调用
|
|
const echoResult = await proxy.echo('Hello');
|
|
expect(echoResult).toBe('Echo: Hello');
|
|
|
|
const addResult = await proxy.add(10, 20);
|
|
expect(addResult).toBe(30);
|
|
|
|
transport.close();
|
|
});
|
|
});
|
|
|
|
describe('complex scenarios', () => {
|
|
it('should handle chained method calls with await', async () => {
|
|
const target = {
|
|
calculator: {
|
|
add: (a: number, b: number) => a + b,
|
|
multiply: (a: number, b: number) => a * b,
|
|
},
|
|
};
|
|
|
|
const transport = new LocalTransport(target);
|
|
const proxy = createDeepProxy<typeof target>({ transport });
|
|
|
|
// RPC 场景中,可以通过嵌套路径直接调用方法
|
|
const sum = await proxy.calculator.add(5, 3);
|
|
expect(sum).toBe(8);
|
|
|
|
const product = await proxy.calculator.multiply(4, 5);
|
|
expect(product).toBe(20);
|
|
});
|
|
|
|
it('should handle arrays with objects', async () => {
|
|
const target = {
|
|
getUsers: () => [
|
|
{ id: 1, name: 'Alice' },
|
|
{ id: 2, name: 'Bob' },
|
|
],
|
|
};
|
|
|
|
const transport = new LocalTransport(target);
|
|
const proxy = createDeepProxy<typeof target>({ transport });
|
|
|
|
const users = await proxy.getUsers();
|
|
expect(users).toHaveLength(2);
|
|
expect(users[0]).toEqual({ id: 1, name: 'Alice' });
|
|
expect(users[1]).toEqual({ id: 2, name: 'Bob' });
|
|
});
|
|
|
|
it('should handle async callbacks that return objects', async () => {
|
|
const target = {
|
|
transformAsync: async (items: number[], fn: (item: number) => Promise<{ doubled: number; }>) => {
|
|
const results: { doubled: number; }[] = [];
|
|
for (const item of items) {
|
|
results.push(await fn(item));
|
|
}
|
|
return results;
|
|
},
|
|
};
|
|
|
|
const transport = new LocalTransport(target);
|
|
const proxy = createDeepProxy<typeof target>({ transport });
|
|
|
|
const callback = vi.fn(async (x: number) => ({ doubled: x * 2 }));
|
|
const result = await proxy.transformAsync([1, 2, 3], callback);
|
|
|
|
expect(callback).toHaveBeenCalledTimes(3);
|
|
expect(result).toEqual([
|
|
{ doubled: 2 },
|
|
{ doubled: 4 },
|
|
{ doubled: 6 },
|
|
]);
|
|
});
|
|
|
|
it('should handle nested callback with Promise', async () => {
|
|
const target = {
|
|
processWithCallback: async (
|
|
value: number,
|
|
processor: (x: number) => Promise<number>
|
|
): Promise<number> => {
|
|
const result = await processor(value);
|
|
return result * 2;
|
|
},
|
|
};
|
|
|
|
const transport = new LocalTransport(target);
|
|
const proxy = createDeepProxy<typeof target>({ transport });
|
|
|
|
const callback = vi.fn(async (x: number) => x + 10);
|
|
const result = await proxy.processWithCallback(5, callback);
|
|
|
|
expect(callback).toHaveBeenCalledWith(5);
|
|
expect(result).toBe(30); // (5 + 10) * 2
|
|
});
|
|
|
|
it('should handle Promise-returning callbacks', async () => {
|
|
const target = {
|
|
processAsync: async (
|
|
data: number[],
|
|
processor: (item: number) => Promise<number>
|
|
): Promise<number[]> => {
|
|
const results: number[] = [];
|
|
for (const item of data) {
|
|
results.push(await processor(item));
|
|
}
|
|
return results;
|
|
},
|
|
};
|
|
|
|
const transport = new LocalTransport(target);
|
|
const proxy = createDeepProxy<typeof target>({ transport });
|
|
|
|
const processor = vi.fn(async (x: number) => {
|
|
await new Promise(r => setTimeout(r, 1));
|
|
return x * 10;
|
|
});
|
|
|
|
const result = await proxy.processAsync([1, 2, 3], processor);
|
|
// 数组返回代理,需 await 获取元素
|
|
expect(await result[0]).toBe(10);
|
|
expect(await result[1]).toBe(20);
|
|
expect(await result[2]).toBe(30);
|
|
expect(processor).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it('should maintain this context in methods', async () => {
|
|
const target = {
|
|
value: 100,
|
|
getValue () {
|
|
return this.value;
|
|
},
|
|
double () {
|
|
return this.value * 2;
|
|
},
|
|
};
|
|
|
|
const transport = new LocalTransport(target);
|
|
const proxy = createDeepProxy<typeof target>({ transport });
|
|
|
|
expect(await proxy.getValue()).toBe(100);
|
|
expect(await proxy.double()).toBe(200);
|
|
});
|
|
|
|
it('should handle Symbol properties', async () => {
|
|
const sym = Symbol('test');
|
|
const target = {
|
|
[sym]: 'symbol-value',
|
|
getSymbolValue () {
|
|
return this[sym];
|
|
},
|
|
};
|
|
|
|
const transport = new LocalTransport(target);
|
|
const proxy = createDeepProxy<typeof target>({ transport });
|
|
|
|
const result = await proxy.getSymbolValue();
|
|
expect(result).toBe('symbol-value');
|
|
});
|
|
});
|
|
|
|
// ========== 简化 API 测试 ==========
|
|
describe('Easy API - createRpcPair', () => {
|
|
it('should create isolated client/server pair', async () => {
|
|
const { client, server } = createRpcPair({
|
|
counter: 0,
|
|
increment () {
|
|
return ++this.counter;
|
|
},
|
|
getCounter () {
|
|
return this.counter;
|
|
},
|
|
});
|
|
|
|
// client 端操作会影响 server 端
|
|
expect(await client.increment()).toBe(1);
|
|
expect(await client.increment()).toBe(2);
|
|
expect(server.counter).toBe(2);
|
|
expect(await client.getCounter()).toBe(2);
|
|
});
|
|
|
|
it('should support nested objects', async () => {
|
|
const { client } = createRpcPair({
|
|
user: {
|
|
profile: {
|
|
name: 'Alice',
|
|
age: 25,
|
|
getInfo () {
|
|
return `${this.name}, ${this.age}`;
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const info = await client.user.profile.getInfo();
|
|
expect(info).toBe('Alice, 25');
|
|
});
|
|
|
|
it('should support .register({ cb1, cb2 }) pattern', async () => {
|
|
const results: string[] = [];
|
|
|
|
const { client } = createRpcPair({
|
|
async register (handlers: {
|
|
onConnect: () => Promise<void>;
|
|
onMessage: (msg: string) => Promise<void>;
|
|
onDisconnect: (reason: string) => Promise<void>;
|
|
}) {
|
|
await handlers.onConnect();
|
|
await handlers.onMessage('Hello');
|
|
await handlers.onMessage('World');
|
|
await handlers.onDisconnect('bye');
|
|
return 'registered';
|
|
},
|
|
});
|
|
|
|
const result = await client.register({
|
|
onConnect: async () => {
|
|
results.push('connected');
|
|
},
|
|
onMessage: async (msg) => {
|
|
results.push(`msg:${msg}`);
|
|
},
|
|
onDisconnect: async (reason) => {
|
|
results.push(`disconnected:${reason}`);
|
|
},
|
|
});
|
|
|
|
expect(result).toBe('registered');
|
|
expect(results).toEqual([
|
|
'connected',
|
|
'msg:Hello',
|
|
'msg:World',
|
|
'disconnected:bye',
|
|
]);
|
|
});
|
|
|
|
it('should support complex callback objects with return values', async () => {
|
|
const { client } = createRpcPair({
|
|
async process (handlers: {
|
|
transform: (x: number) => Promise<number>;
|
|
validate: (x: number) => Promise<boolean>;
|
|
format: (x: number) => Promise<string>;
|
|
}) {
|
|
const values: string[] = [];
|
|
for (const num of [1, 2, 3, 4, 5]) {
|
|
const transformed = await handlers.transform(num);
|
|
if (await handlers.validate(transformed)) {
|
|
values.push(await handlers.format(transformed));
|
|
}
|
|
}
|
|
return values;
|
|
},
|
|
});
|
|
|
|
const result = await client.process({
|
|
transform: async (x) => x * 2,
|
|
validate: async (x) => x > 5,
|
|
format: async (x) => `value:${x}`,
|
|
});
|
|
|
|
// 数组返回代理
|
|
expect(await result[0]).toBe('value:6');
|
|
expect(await result[1]).toBe('value:8');
|
|
expect(await result[2]).toBe('value:10');
|
|
});
|
|
});
|
|
|
|
describe('Easy API - mockRemote', () => {
|
|
it('should create remote-like proxy easily', async () => {
|
|
const api = mockRemote({
|
|
add: (a: number, b: number) => a + b,
|
|
multiply: (a: number, b: number) => a * b,
|
|
});
|
|
|
|
expect(await api.add(2, 3)).toBe(5);
|
|
expect(await api.multiply(4, 5)).toBe(20);
|
|
});
|
|
|
|
it('should handle async methods', async () => {
|
|
const api = mockRemote({
|
|
async fetchUser (id: number) {
|
|
return { id, name: `User${id}` };
|
|
},
|
|
async delay (ms: number) {
|
|
await new Promise(r => setTimeout(r, ms));
|
|
return 'done';
|
|
},
|
|
});
|
|
|
|
const user = await api.fetchUser(123);
|
|
// 对象返回代理,属性已缓存,可直接访问
|
|
expect(user.id).toBe(123);
|
|
expect(user.name).toBe('User123');
|
|
});
|
|
});
|
|
|
|
describe('Easy API - createServer/createClient', () => {
|
|
it('should create server and connect clients', async () => {
|
|
const server = createServer({
|
|
data: [] as string[],
|
|
add (item: string) {
|
|
this.data.push(item);
|
|
return this.data.length;
|
|
},
|
|
getAll () {
|
|
return [...this.data];
|
|
},
|
|
});
|
|
|
|
const client1 = createClient<typeof server.target>(server.getTransport());
|
|
const client2 = createClient<typeof server.target>(server.getTransport());
|
|
|
|
await client1.add('from-client1');
|
|
await client2.add('from-client2');
|
|
|
|
// 数组返回代理
|
|
const list1 = await client1.getAll();
|
|
const list2 = await client2.getAll();
|
|
expect(await list1[0]).toBe('from-client1');
|
|
expect(await list1[1]).toBe('from-client2');
|
|
expect(await list2[0]).toBe('from-client1');
|
|
expect(await list2[1]).toBe('from-client2');
|
|
});
|
|
});
|
|
|
|
// ========== 多层调用场景 ==========
|
|
describe('Multi-level deep proxy operations', () => {
|
|
it('should handle 5+ levels of nesting', async () => {
|
|
const { client } = createRpcPair({
|
|
level1: {
|
|
level2: {
|
|
level3: {
|
|
level4: {
|
|
level5: {
|
|
getValue () {
|
|
return 'deep-value';
|
|
},
|
|
async getAsyncValue () {
|
|
return 'async-deep-value';
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(await client.level1.level2.level3.level4.level5.getValue()).toBe('deep-value');
|
|
expect(await client.level1.level2.level3.level4.level5.getAsyncValue()).toBe('async-deep-value');
|
|
});
|
|
|
|
it('should handle array of objects with data', async () => {
|
|
const { client } = createRpcPair({
|
|
getItems () {
|
|
return [
|
|
{ id: 1, name: 'Item1', active: true },
|
|
{ id: 2, name: 'Item2', active: false },
|
|
{ id: 3, name: 'Item3', active: true },
|
|
];
|
|
},
|
|
getItemById (id: number) {
|
|
const items = [
|
|
{ id: 1, name: 'Item1' },
|
|
{ id: 2, name: 'Item2' },
|
|
];
|
|
return items.find(item => item.id === id);
|
|
},
|
|
});
|
|
|
|
const items = await client.getItems();
|
|
// 数组直接序列化,元素可直接访问
|
|
expect(items[0]!.id).toBe(1);
|
|
expect(items[0]!.name).toBe('Item1');
|
|
expect(items[1]!.id).toBe(2);
|
|
|
|
const item = await client.getItemById(2);
|
|
// 普通对象(无方法)直接序列化
|
|
expect(item!.id).toBe(2);
|
|
expect(item!.name).toBe('Item2');
|
|
});
|
|
|
|
it('should handle Map and Set in nested structures', async () => {
|
|
const { client } = createRpcPair({
|
|
storage: {
|
|
getMap () {
|
|
return new Map([
|
|
['key1', { value: 1 }],
|
|
['key2', { value: 2 }],
|
|
]);
|
|
},
|
|
getSet () {
|
|
return new Set([1, 2, 3, 4, 5]);
|
|
},
|
|
},
|
|
});
|
|
|
|
const map = await client.storage.getMap();
|
|
expect(map).toBeInstanceOf(Map);
|
|
expect(map.get('key1')).toEqual({ value: 1 });
|
|
|
|
const set = await client.storage.getSet();
|
|
expect(set).toBeInstanceOf(Set);
|
|
expect(set.has(3)).toBe(true);
|
|
});
|
|
|
|
it('should handle event emitter pattern', async () => {
|
|
const events: string[] = [];
|
|
|
|
const { client } = createRpcPair({
|
|
eventEmitter: {
|
|
async on (event: string, handler: (data: string) => Promise<void>) {
|
|
// 模拟事件触发
|
|
await handler(`${event}:data1`);
|
|
await handler(`${event}:data2`);
|
|
return () => { /* unsubscribe */ };
|
|
},
|
|
},
|
|
});
|
|
|
|
await client.eventEmitter.on('message', async (data) => {
|
|
events.push(data);
|
|
});
|
|
|
|
expect(events).toEqual(['message:data1', 'message:data2']);
|
|
});
|
|
|
|
it('should handle Promise chain in callbacks', async () => {
|
|
const { client } = createRpcPair({
|
|
async pipeline (
|
|
input: number,
|
|
stages: {
|
|
step1: (x: number) => Promise<number>;
|
|
step2: (x: number) => Promise<number>;
|
|
step3: (x: number) => Promise<string>;
|
|
}
|
|
) {
|
|
const r1 = await stages.step1(input);
|
|
const r2 = await stages.step2(r1);
|
|
const r3 = await stages.step3(r2);
|
|
return r3;
|
|
},
|
|
});
|
|
|
|
const result = await client.pipeline(5, {
|
|
step1: async (x) => x * 2, // 10
|
|
step2: async (x) => x + 3, // 13
|
|
step3: async (x) => `result:${x}`, // 'result:13'
|
|
});
|
|
|
|
expect(result).toBe('result:13');
|
|
});
|
|
|
|
it('should handle mixed sync/async handlers in object', async () => {
|
|
const logs: string[] = [];
|
|
|
|
const { client } = createRpcPair({
|
|
async execute (handlers: {
|
|
beforeStart: () => Promise<void>;
|
|
onProgress: (percent: number) => Promise<void>;
|
|
afterComplete: () => Promise<string>;
|
|
}) {
|
|
await handlers.beforeStart();
|
|
await handlers.onProgress(25);
|
|
await handlers.onProgress(50);
|
|
await handlers.onProgress(75);
|
|
await handlers.onProgress(100);
|
|
return await handlers.afterComplete();
|
|
},
|
|
});
|
|
|
|
const result = await client.execute({
|
|
beforeStart: async () => {
|
|
logs.push('started');
|
|
},
|
|
onProgress: async (percent) => {
|
|
logs.push(`progress:${percent}%`);
|
|
},
|
|
afterComplete: async () => {
|
|
logs.push('completed');
|
|
return 'success';
|
|
},
|
|
});
|
|
|
|
expect(result).toBe('success');
|
|
expect(logs).toEqual([
|
|
'started',
|
|
'progress:25%',
|
|
'progress:50%',
|
|
'progress:75%',
|
|
'progress:100%',
|
|
'completed',
|
|
]);
|
|
});
|
|
});
|
|
|
|
// ========== 边界情况 ==========
|
|
describe('Edge cases', () => {
|
|
it('should handle undefined and null in callbacks', async () => {
|
|
const { client } = createRpcPair({
|
|
async process (handler: (val: unknown) => Promise<unknown>) {
|
|
const results: unknown[] = [];
|
|
results.push(await handler(undefined));
|
|
results.push(await handler(null));
|
|
results.push(await handler(0));
|
|
results.push(await handler(''));
|
|
results.push(await handler(false));
|
|
return results;
|
|
},
|
|
});
|
|
|
|
const result = await client.process(async (val) => {
|
|
if (val === undefined) return 'undefined';
|
|
if (val === null) return 'null';
|
|
if (val === 0) return 'zero';
|
|
if (val === '') return 'empty';
|
|
if (val === false) return 'false';
|
|
return 'other';
|
|
});
|
|
|
|
// 数组返回代理
|
|
expect(await result[0]).toBe('undefined');
|
|
expect(await result[1]).toBe('null');
|
|
expect(await result[2]).toBe('zero');
|
|
expect(await result[3]).toBe('empty');
|
|
expect(await result[4]).toBe('false');
|
|
});
|
|
|
|
it('should handle errors in callbacks', async () => {
|
|
const { client } = createRpcPair({
|
|
async executeWithErrorHandler (
|
|
action: () => Promise<string>,
|
|
onError: (err: Error) => Promise<string>
|
|
) {
|
|
try {
|
|
return await action();
|
|
} catch (e) {
|
|
return await onError(e as Error);
|
|
}
|
|
},
|
|
});
|
|
|
|
const result = await client.executeWithErrorHandler(
|
|
async () => {
|
|
throw new Error('action failed');
|
|
},
|
|
async (err) => {
|
|
return `caught: ${err.message}`;
|
|
}
|
|
);
|
|
|
|
expect(result).toBe('caught: action failed');
|
|
});
|
|
|
|
it('should handle large data transfer', async () => {
|
|
const { client } = createRpcPair({
|
|
processLargeArray (arr: number[]) {
|
|
return arr.reduce((a, b) => a + b, 0);
|
|
},
|
|
});
|
|
|
|
const largeArray = Array.from({ length: 10000 }, (_, i) => i);
|
|
const sum = await client.processLargeArray(largeArray);
|
|
expect(sum).toBe(49995000); // sum of 0 to 9999
|
|
});
|
|
});
|
|
|
|
describe('Deep return value proxying (class instances)', () => {
|
|
it('should keep class instance as proxy and call methods remotely', async () => {
|
|
class User {
|
|
constructor (public name: string, public age: number) { }
|
|
|
|
greet () {
|
|
return `Hi, I am ${this.name}`;
|
|
}
|
|
|
|
getInfo () {
|
|
return { name: this.name, age: this.age };
|
|
}
|
|
|
|
updateAge (newAge: number) {
|
|
this.age = newAge;
|
|
return this.age;
|
|
}
|
|
}
|
|
|
|
const { client } = createRpcPair({
|
|
createUser (name: string, age: number) {
|
|
return new User(name, age);
|
|
},
|
|
});
|
|
|
|
const user = await client.createUser('Alice', 25);
|
|
|
|
// user 应该是代理,调用方法时发送 RPC 请求
|
|
const greeting = await user.greet();
|
|
expect(greeting).toBe('Hi, I am Alice');
|
|
|
|
const info = await user.getInfo();
|
|
// getInfo 返回普通对象,直接序列化,属性可直接访问
|
|
expect(info.name).toBe('Alice');
|
|
expect(info.age).toBe(25);
|
|
|
|
// 更新远程对象状态
|
|
const newAge = await user.updateAge(30);
|
|
expect(newAge).toBe(30);
|
|
|
|
// 验证状态被更新
|
|
const updatedInfo = await user.getInfo();
|
|
expect(updatedInfo.age).toBe(30);
|
|
});
|
|
|
|
it('should support chained method calls on returned class instance', async () => {
|
|
class Counter {
|
|
value = 0;
|
|
|
|
increment () {
|
|
this.value++;
|
|
return this;
|
|
}
|
|
|
|
decrement () {
|
|
this.value--;
|
|
return this;
|
|
}
|
|
|
|
getValue () {
|
|
return this.value;
|
|
}
|
|
}
|
|
|
|
const { client } = createRpcPair({
|
|
createCounter () {
|
|
return new Counter();
|
|
},
|
|
});
|
|
|
|
const counter = await client.createCounter();
|
|
|
|
// 链式调用
|
|
await counter.increment();
|
|
await counter.increment();
|
|
await counter.increment();
|
|
await counter.decrement();
|
|
|
|
const value = await counter.getValue();
|
|
expect(value).toBe(2);
|
|
});
|
|
|
|
it('should handle nested class instances', async () => {
|
|
class Address {
|
|
constructor (public city: string, public country: string) { }
|
|
|
|
getFullAddress () {
|
|
return `${this.city}, ${this.country}`;
|
|
}
|
|
}
|
|
|
|
class Person {
|
|
constructor (public name: string, public address: Address) { }
|
|
|
|
getAddress () {
|
|
return this.address;
|
|
}
|
|
|
|
getFormattedInfo () {
|
|
return `${this.name} lives in ${this.address.city}`;
|
|
}
|
|
}
|
|
|
|
const { client } = createRpcPair({
|
|
createPerson (name: string, city: string, country: string) {
|
|
return new Person(name, new Address(city, country));
|
|
},
|
|
});
|
|
|
|
const person = await client.createPerson('Bob', 'Tokyo', 'Japan');
|
|
|
|
const info = await person.getFormattedInfo();
|
|
expect(info).toBe('Bob lives in Tokyo');
|
|
|
|
// 获取嵌套对象(如果 Address 也是类实例,也应该是代理)
|
|
const address = await person.getAddress();
|
|
const fullAddress = await address.getFullAddress();
|
|
expect(fullAddress).toBe('Tokyo, Japan');
|
|
});
|
|
|
|
it('should handle objects with methods (not class instances)', async () => {
|
|
// 这种对象有方法,应该也返回代理
|
|
const { client } = createRpcPair({
|
|
createApi () {
|
|
let data = 'initial';
|
|
return {
|
|
getData () {
|
|
return data;
|
|
},
|
|
setData (newData: string) {
|
|
data = newData;
|
|
},
|
|
processData (transformer: (d: string) => string) {
|
|
data = transformer(data);
|
|
return data;
|
|
},
|
|
};
|
|
},
|
|
});
|
|
|
|
const api = await client.createApi();
|
|
|
|
const initial = await api.getData();
|
|
expect(initial).toBe('initial');
|
|
|
|
await api.setData('updated');
|
|
const updated = await api.getData();
|
|
expect(updated).toBe('updated');
|
|
|
|
// 测试回调
|
|
const result = await api.processData((d: string) => d.toUpperCase());
|
|
expect(result).toBe('UPDATED');
|
|
});
|
|
|
|
it('should proxy simple objects and allow property access', async () => {
|
|
// 简单对象(无方法)直接序列化,属性可直接访问
|
|
const { client } = createRpcPair({
|
|
getSimpleData () {
|
|
return { id: 1, name: 'test', active: true };
|
|
},
|
|
});
|
|
|
|
const data = await client.getSimpleData();
|
|
// 普通对象直接序列化,属性可直接访问
|
|
expect(data.id).toBe(1);
|
|
expect(data.name).toBe('test');
|
|
expect(data.active).toBe(true);
|
|
});
|
|
|
|
it('should handle async methods in returned class', async () => {
|
|
class AsyncService {
|
|
private value = 0;
|
|
|
|
async fetchAndAdd (amount: number) {
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
this.value += amount;
|
|
return this.value;
|
|
}
|
|
|
|
async getValue () {
|
|
await new Promise(resolve => setTimeout(resolve, 5));
|
|
return this.value;
|
|
}
|
|
}
|
|
|
|
const { client } = createRpcPair({
|
|
createService () {
|
|
return new AsyncService();
|
|
},
|
|
});
|
|
|
|
const service = await client.createService();
|
|
|
|
const first = await service.fetchAndAdd(10);
|
|
expect(first).toBe(10);
|
|
|
|
const second = await service.fetchAndAdd(5);
|
|
expect(second).toBe(15);
|
|
|
|
const current = await service.getValue();
|
|
expect(current).toBe(15);
|
|
});
|
|
|
|
it('should handle constructor on returned proxy', async () => {
|
|
class Factory {
|
|
create (name: string): { name: string; } {
|
|
return { name };
|
|
}
|
|
|
|
Widget = class Widget {
|
|
constructor (public id: number) { }
|
|
getId () {
|
|
return this.id;
|
|
}
|
|
};
|
|
}
|
|
|
|
const { client } = createRpcPair({
|
|
getFactory () {
|
|
return new Factory();
|
|
},
|
|
});
|
|
|
|
const factory = await client.getFactory();
|
|
|
|
// 调用返回代理上的方法,返回带方法的对象被代理
|
|
const obj = await factory.create('widget1');
|
|
// 属性已缓存,可直接访问
|
|
expect(obj.name).toBe('widget1');
|
|
|
|
// 在返回代理上调用构造函数
|
|
const Widget = await factory.Widget;
|
|
const widget = await new Widget(123);
|
|
const id = await widget.getId();
|
|
expect(id).toBe(123);
|
|
});
|
|
|
|
it('should support getService().method({fn: callback}) pattern', async () => {
|
|
// 核心场景:链式调用 + 回调
|
|
const { client } = createRpcPair({
|
|
getService () {
|
|
return {
|
|
execute (options: { onProgress: (p: number) => void; onComplete: (r: string) => void; }) {
|
|
options.onProgress(25);
|
|
options.onProgress(50);
|
|
options.onProgress(100);
|
|
options.onComplete('done');
|
|
return 'success';
|
|
},
|
|
process (data: { transformer: (x: number) => number; }) {
|
|
return data.transformer(10);
|
|
},
|
|
};
|
|
},
|
|
});
|
|
|
|
const service = await client.getService();
|
|
|
|
const progressValues: number[] = [];
|
|
let completedWith = '';
|
|
|
|
const result = await service.execute({
|
|
onProgress: (p: number) => {
|
|
progressValues.push(p);
|
|
},
|
|
onComplete: (r: string) => {
|
|
completedWith = r;
|
|
},
|
|
});
|
|
|
|
expect(result).toBe('success');
|
|
expect(progressValues).toEqual([25, 50, 100]);
|
|
expect(completedWith).toBe('done');
|
|
|
|
// 测试返回值的回调
|
|
const transformed = await service.process({
|
|
transformer: (x: number) => x * 3,
|
|
});
|
|
expect(transformed).toBe(30);
|
|
});
|
|
|
|
it('should support deep chain call with callbacks at any level', async () => {
|
|
const { client } = createRpcPair({
|
|
getApi () {
|
|
return {
|
|
getModule () {
|
|
return {
|
|
getHandler () {
|
|
return {
|
|
handle (input: number, callbacks: { onSuccess: (r: number) => void; onError: (e: string) => void; }) {
|
|
if (input > 0) {
|
|
callbacks.onSuccess(input * 2);
|
|
} else {
|
|
callbacks.onError('negative input');
|
|
}
|
|
return input > 0;
|
|
},
|
|
};
|
|
},
|
|
};
|
|
},
|
|
};
|
|
},
|
|
});
|
|
|
|
const api = await client.getApi();
|
|
const module = await api.getModule();
|
|
const handler = await module.getHandler();
|
|
|
|
let successResult = 0;
|
|
const isSuccess = await handler.handle(5, {
|
|
onSuccess: (r: number) => { successResult = r; },
|
|
onError: () => { },
|
|
});
|
|
|
|
expect(isSuccess).toBe(true);
|
|
expect(successResult).toBe(10);
|
|
|
|
let errorMsg = '';
|
|
const isFailed = await handler.handle(-1, {
|
|
onSuccess: () => { },
|
|
onError: (e: string) => { errorMsg = e; },
|
|
});
|
|
|
|
expect(isFailed).toBe(false);
|
|
expect(errorMsg).toBe('negative input');
|
|
});
|
|
|
|
it('should support register pattern with multiple callbacks', async () => {
|
|
const { client } = createRpcPair({
|
|
createEventEmitter () {
|
|
const listeners: Map<string, Array<(...args: unknown[]) => void>> = new Map();
|
|
return {
|
|
register (events: Record<string, (...args: unknown[]) => void>) {
|
|
for (const [name, handler] of Object.entries(events)) {
|
|
if (!listeners.has(name)) listeners.set(name, []);
|
|
listeners.get(name)!.push(handler);
|
|
}
|
|
},
|
|
emit (name: string, ...args: unknown[]) {
|
|
const handlers = listeners.get(name) ?? [];
|
|
handlers.forEach(h => h(...args));
|
|
},
|
|
};
|
|
},
|
|
});
|
|
|
|
const emitter = await client.createEventEmitter();
|
|
|
|
const events: Array<{ type: string; data: unknown; }> = [];
|
|
|
|
await emitter.register({
|
|
data: (value: unknown) => events.push({ type: 'data', data: value }),
|
|
error: (err: unknown) => events.push({ type: 'error', data: err }),
|
|
complete: () => events.push({ type: 'complete', data: null }),
|
|
});
|
|
|
|
await emitter.emit('data', 'hello');
|
|
await emitter.emit('data', 'world');
|
|
await emitter.emit('error', 'oops');
|
|
await emitter.emit('complete');
|
|
|
|
expect(events).toEqual([
|
|
{ type: 'data', data: 'hello' },
|
|
{ type: 'data', data: 'world' },
|
|
{ type: 'error', data: 'oops' },
|
|
{ type: 'complete', data: null },
|
|
]);
|
|
});
|
|
|
|
it('should handle array return values with proxy', async () => {
|
|
// 通过 getService 返回的类实例来测试数组代理
|
|
class ItemService {
|
|
getItems () {
|
|
return [
|
|
{ id: 1, name: 'item1' },
|
|
{ id: 2, name: 'item2' },
|
|
];
|
|
}
|
|
getItem (index: number) {
|
|
return { id: index, getValue: () => `item${index}` };
|
|
}
|
|
}
|
|
const { client } = createRpcPair({
|
|
getItemService () {
|
|
return new ItemService();
|
|
},
|
|
});
|
|
|
|
const service = await client.getItemService();
|
|
|
|
// 通过代理的 service 调用方法获取数组
|
|
const items = await service.getItems();
|
|
expect(items).toEqual([
|
|
{ id: 1, name: 'item1' },
|
|
{ id: 2, name: 'item2' },
|
|
]);
|
|
|
|
// 获取单个 item 作为代理(因为它有方法)
|
|
const item = await service.getItem(3);
|
|
expect(await item.getValue()).toBe('item3');
|
|
});
|
|
});
|
|
});
|