From 3bead89d461e655cff4a7ad985b28cba25daea6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Mon, 2 Feb 2026 18:43:37 +0800 Subject: [PATCH] Support object references and deep proxying Introduce remote object references (refId) and deep proxy support across client, server, serializer and types. Key changes: - Add refId propagation in client proxies so child proxies inherit and include refId on RPC requests. - Extend serializer to handle a new SerializedValueType.OBJECT_REF, add refResolver and pass refId to proxyCreator. - Server: store object references in a Map with generated ref IDs, resolve paths with optional refId, serialize results to OBJECT_REF when shouldProxyResult returns true, and release cleans up references. Add defaultShouldProxyResult heuristic to decide which return values should remain proxied (class instances and objects with methods). - Types: add refId fields and ObjectRef shape, expose shouldProxyResult option on RpcServerOptions, and include refId in ProxyMeta and serialized values. - Tests updated across the suite to expect proxied return values (arrays/objects/class instances) and to await property access or method calls; add comprehensive tests for deep return value proxying, chained calls, callbacks, constructors on returned proxies, and lifecycle of remote object proxies. These changes enable returning live/proxied remote objects (including class instances and objects with methods) from RPC calls, preserving remote behavior and allowing subsequent operations to target the same server-side object. --- packages/napcat-rpc/src/client.ts | 20 +- packages/napcat-rpc/src/serializer.ts | 20 +- packages/napcat-rpc/src/server.ts | 187 +++++++++-- packages/napcat-rpc/src/types.ts | 27 ++ packages/napcat-test/rpc.test.ts | 455 +++++++++++++++++++++++++- 5 files changed, 655 insertions(+), 54 deletions(-) diff --git a/packages/napcat-rpc/src/client.ts b/packages/napcat-rpc/src/client.ts index eaf1c1f4..02ea594a 100644 --- a/packages/napcat-rpc/src/client.ts +++ b/packages/napcat-rpc/src/client.ts @@ -28,6 +28,7 @@ export function createDeepProxy (options: DeepProxyOptions): T { const { transport, rootPath = [], + refId: rootRefId, // callbackTimeout 可供未来扩展使用 } = options; void options.callbackTimeout; @@ -47,7 +48,7 @@ export function createDeepProxy (options: DeepProxyOptions): T { if (!cb) throw new Error(`Nested callback not found: ${id}`); return cb; }, - proxyCreator: (path) => createProxyAtPath(path), + proxyCreator: (path, proxyRefId) => createProxyAtPath(path, proxyRefId), })); const result = await callback(...args); return serialize(result, { callbackRegistry }); @@ -57,10 +58,11 @@ export function createDeepProxy (options: DeepProxyOptions): T { /** * 在指定路径创建代理 */ - function createProxyAtPath (path: PropertyKey[]): unknown { + function createProxyAtPath (path: PropertyKey[], refId?: string): unknown { const proxyMeta: ProxyMeta = { path: [...path], isProxy: true, + refId, }; // 创建一个函数目标,以支持 apply 和 construct @@ -78,8 +80,8 @@ export function createDeepProxy (options: DeepProxyOptions): T { return undefined; } - // 返回新的子路径代理 - return createProxyAtPath([...path, prop]); + // 返回新的子路径代理(继承 refId) + return createProxyAtPath([...path, prop], refId); }, set (_target, prop, value) { @@ -88,6 +90,7 @@ export function createDeepProxy (options: DeepProxyOptions): T { type: RpcOperationType.SET, path: [...path, prop], args: [serialize(value, { callbackRegistry })], + refId, }; // 同步返回,但实际是异步操作 @@ -105,6 +108,7 @@ export function createDeepProxy (options: DeepProxyOptions): T { path, args: serializedArgs, callbackIds: Object.keys(callbackIds).length > 0 ? callbackIds : undefined, + refId, }; return createAsyncResultProxy(request); @@ -120,6 +124,7 @@ export function createDeepProxy (options: DeepProxyOptions): T { path, args: serializedArgs, callbackIds: Object.keys(callbackIds).length > 0 ? callbackIds : undefined, + refId, }; return createAsyncResultProxy(request) as object; @@ -152,6 +157,7 @@ export function createDeepProxy (options: DeepProxyOptions): T { id: generateRequestId(), type: RpcOperationType.DELETE, path: [...path, prop], + refId, }; transport.send(request).catch(() => { /* ignore */ }); @@ -197,7 +203,7 @@ export function createDeepProxy (options: DeepProxyOptions): T { if (!cb) throw new Error(`Callback not found: ${id}`); return cb; }, - proxyCreator: (proxyPath) => createProxyAtPath(proxyPath), + proxyCreator: (proxyPath, proxyRefId) => createProxyAtPath(proxyPath, proxyRefId), }); return deserialized; } @@ -208,7 +214,7 @@ export function createDeepProxy (options: DeepProxyOptions): T { if (!cb) throw new Error(`Callback not found: ${id}`); return cb; }, - proxyCreator: (proxyPath) => createProxyAtPath(proxyPath), + proxyCreator: (proxyPath, proxyRefId) => createProxyAtPath(proxyPath, proxyRefId), }); })(); } @@ -323,7 +329,7 @@ export function createDeepProxy (options: DeepProxyOptions): T { }); } - return createProxyAtPath(rootPath) as T; + return createProxyAtPath(rootPath, rootRefId) as T; } /** diff --git a/packages/napcat-rpc/src/serializer.ts b/packages/napcat-rpc/src/serializer.ts index 185bdf09..12efed61 100644 --- a/packages/napcat-rpc/src/serializer.ts +++ b/packages/napcat-rpc/src/serializer.ts @@ -61,7 +61,9 @@ export interface DeserializeContext { /** 回调解析器 */ callbackResolver?: (id: string) => Function; /** 代理创建器 */ - proxyCreator?: (path: PropertyKey[]) => unknown; + proxyCreator?: (path: PropertyKey[], refId?: string) => unknown; + /** 对象引用解析器 */ + refResolver?: (refId: string) => unknown; } /** @@ -263,7 +265,7 @@ export function serialize (value: unknown, context: SerializeContext = {}): Seri * 将序列化数据还原为值 */ export function deserialize (data: SerializedValue, context: DeserializeContext = {}): unknown { - const { callbackResolver, proxyCreator } = context; + const { callbackResolver, proxyCreator, refResolver } = context; switch (data.type) { case SerializedValueType.UNDEFINED: @@ -357,6 +359,20 @@ export function deserialize (data: SerializedValue, context: DeserializeContext } return {}; + case SerializedValueType.OBJECT_REF: + // 对象引用:在客户端创建代理,在服务端解析为实际对象 + if (data.refId) { + // 优先使用 refResolver(服务端场景) + if (refResolver) { + return refResolver(data.refId); + } + // 否则创建代理(客户端场景) + if (proxyCreator) { + return proxyCreator([], data.refId); + } + } + return {}; + case SerializedValueType.OBJECT: { const obj: Record = {}; if (data.properties) { diff --git a/packages/napcat-rpc/src/server.ts b/packages/napcat-rpc/src/server.ts index 1e6ca1a4..0d2c3fb9 100644 --- a/packages/napcat-rpc/src/server.ts +++ b/packages/napcat-rpc/src/server.ts @@ -4,9 +4,58 @@ import { type RpcServerOptions, type SerializedValue, RpcOperationType, + SerializedValueType, } from './types.js'; import { serialize, deserialize, SimpleCallbackRegistry } from './serializer.js'; +/** + * 生成唯一引用 ID + */ +function generateRefId (): string { + return `ref_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`; +} + +/** + * 默认的代理判断函数 + * 判断返回值是否应该保持代理引用(而非完全序列化) + * 策略:class 实例和有方法的对象保持代理,普通对象直接序列化 + */ +function defaultShouldProxyResult (value: unknown): boolean { + if (value === null || value === undefined) { + return false; + } + if (typeof value !== 'object' && typeof value !== 'function') { + return false; + } + // 函数保持代理 + if (typeof value === 'function') { + return true; + } + // 可安全序列化的内置类型不代理 + if (value instanceof Date || value instanceof RegExp || value instanceof Error) { + return false; + } + if (value instanceof Map || value instanceof Set) { + return false; + } + if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) { + return false; + } + // 数组不代理 + if (Array.isArray(value)) { + return false; + } + // 检查对象原型是否为 Object.prototype(普通对象) + const proto = Object.getPrototypeOf(value); + if (proto === Object.prototype || proto === null) { + // 普通对象检查是否有方法 + const hasMethod = Object.values(value as object).some(v => typeof v === 'function'); + return hasMethod; + } + // 非普通对象(class 实例)- 保持代理 + return true; +} + /** * RPC 服务端 * @@ -16,10 +65,15 @@ export class RpcServer { private target: unknown; private callbackInvoker?: (callbackId: string, args: unknown[]) => Promise; private localCallbacks = new SimpleCallbackRegistry(); + /** 对象引用存储 */ + private objectRefs = new Map(); + /** 代理判断函数 */ + private shouldProxyResult: (value: unknown) => boolean; constructor (options: RpcServerOptions) { this.target = options.target; this.callbackInvoker = options.callbackInvoker; + this.shouldProxyResult = options.shouldProxyResult ?? defaultShouldProxyResult; } /** @@ -71,10 +125,16 @@ export class RpcServer { } /** - * 解析路径获取目标值 + * 解析路径获取目标值,支持 refId */ - private resolvePath (path: PropertyKey[]): { parent: unknown; key: PropertyKey | undefined; value: unknown; } { - let current = this.target; + private resolvePath (path: PropertyKey[], refId?: string): { parent: unknown; key: PropertyKey | undefined; value: unknown; } { + // 如果有 refId,从引用存储中获取根对象 + let current = refId ? this.objectRefs.get(refId) : this.target; + + if (refId && current === undefined) { + throw new Error(`Object reference not found: ${refId}`); + } + let parent: unknown = null; let key: PropertyKey | undefined; @@ -93,18 +153,54 @@ export class RpcServer { return { parent, key, value: current }; } + /** + * 存储对象引用并返回序列化的引用 + */ + private storeObjectRef (value: unknown): SerializedValue { + const refId = generateRefId(); + this.objectRefs.set(refId, value); + const className = value?.constructor?.name; + return { + type: SerializedValueType.OBJECT_REF, + refId, + className: className !== 'Object' ? className : undefined, + }; + } + + /** + * 序列化结果值,如果需要代理则存储引用 + */ + private serializeResult (value: unknown): { result: SerializedValue; isProxyable: boolean; refId?: string; } { + const shouldProxy = this.shouldProxyResult(value); + + if (shouldProxy) { + const ref = this.storeObjectRef(value); + return { + result: ref, + isProxyable: true, + refId: ref.refId, + }; + } + + return { + result: serialize(value, { callbackRegistry: this.localCallbacks }), + isProxyable: false, + }; + } + /** * 处理 GET 操作 */ private handleGet (request: RpcRequest): RpcResponse { - const { value } = this.resolvePath(request.path); - const isProxyable = this.isProxyable(value); + const { value } = this.resolvePath(request.path, request.refId); + const { result, isProxyable, refId } = this.serializeResult(value); return { id: request.id, success: true, - result: serialize(value, { callbackRegistry: this.localCallbacks }), + result, isProxyable, + refId, }; } @@ -113,13 +209,13 @@ export class RpcServer { */ private handleSet (request: RpcRequest): RpcResponse { const path = request.path; - if (path.length === 0) { + if (path.length === 0 && !request.refId) { throw new Error('Cannot set root object'); } const parentPath = path.slice(0, -1); const key = path[path.length - 1]!; - const { value: parent } = this.resolvePath(parentPath); + const { value: parent } = this.resolvePath(parentPath, request.refId); if (parent === null || parent === undefined) { throw new Error(`Cannot set property '${String(key)}' of ${parent}`); @@ -128,6 +224,7 @@ export class RpcServer { const newValue = request.args?.[0] ? deserialize(request.args[0], { callbackResolver: this.createCallbackResolver(request), + refResolver: (refId) => this.objectRefs.get(refId), }) : undefined; @@ -144,13 +241,43 @@ export class RpcServer { */ private async handleApply (request: RpcRequest): Promise { const path = request.path; + + // 如果有 refId 且 path 为空,说明引用对象本身是函数 + if (path.length === 0 && request.refId) { + const func = this.objectRefs.get(request.refId); + if (typeof func !== 'function') { + throw new Error('Referenced object is not callable'); + } + + const args = (request.args ?? []).map(arg => + deserialize(arg, { + callbackResolver: this.createCallbackResolver(request), + refResolver: (refId) => this.objectRefs.get(refId), + }) + ); + + let result = func(...args); + if (result instanceof Promise) { + result = await result; + } + + const { result: serializedResult, isProxyable, refId } = this.serializeResult(result); + return { + id: request.id, + success: true, + result: serializedResult, + isProxyable, + refId, + }; + } + if (path.length === 0) { throw new Error('Cannot call root object'); } const methodPath = path.slice(0, -1); const methodName = path[path.length - 1]!; - const { value: parent } = this.resolvePath(methodPath); + const { value: parent } = this.resolvePath(methodPath, request.refId); if (parent === null || parent === undefined) { throw new Error(`Cannot call method on ${parent}`); @@ -164,6 +291,7 @@ export class RpcServer { const args = (request.args ?? []).map(arg => deserialize(arg, { callbackResolver: this.createCallbackResolver(request), + refResolver: (refId) => this.objectRefs.get(refId), }) ); @@ -174,13 +302,14 @@ export class RpcServer { result = await result; } - const isProxyable = this.isProxyable(result); + const { result: serializedResult, isProxyable, refId } = this.serializeResult(result); return { id: request.id, success: true, - result: serialize(result, { callbackRegistry: this.localCallbacks }), + result: serializedResult, isProxyable, + refId, }; } @@ -188,7 +317,7 @@ export class RpcServer { * 处理 CONSTRUCT 操作 */ private async handleConstruct (request: RpcRequest): Promise { - const { value: Constructor } = this.resolvePath(request.path); + const { value: Constructor } = this.resolvePath(request.path, request.refId); if (typeof Constructor !== 'function') { throw new Error('Target is not a constructor'); @@ -197,17 +326,19 @@ export class RpcServer { const args = (request.args ?? []).map(arg => deserialize(arg, { callbackResolver: this.createCallbackResolver(request), + refResolver: (refId) => this.objectRefs.get(refId), }) ); const instance = new (Constructor as new (...args: unknown[]) => unknown)(...args); - const isProxyable = this.isProxyable(instance); + const { result, isProxyable, refId } = this.serializeResult(instance); return { id: request.id, success: true, - result: serialize(instance, { callbackRegistry: this.localCallbacks }), + result, isProxyable, + refId, }; } @@ -226,7 +357,7 @@ export class RpcServer { const parentPath = path.slice(0, -1); const key = path[path.length - 1]!; - const { value: parent } = this.resolvePath(parentPath); + const { value: parent } = this.resolvePath(parentPath, request.refId); const has = parent !== null && parent !== undefined && key in (parent as object); @@ -241,7 +372,7 @@ export class RpcServer { * 处理 OWNKEYS 操作 */ private handleOwnKeys (request: RpcRequest): RpcResponse { - const { value } = this.resolvePath(request.path); + const { value } = this.resolvePath(request.path, request.refId); if (value === null || value === undefined) { return { @@ -265,13 +396,13 @@ export class RpcServer { */ private handleDelete (request: RpcRequest): RpcResponse { const path = request.path; - if (path.length === 0) { + if (path.length === 0 && !request.refId) { throw new Error('Cannot delete root object'); } const parentPath = path.slice(0, -1); const key = path[path.length - 1]!; - const { value: parent } = this.resolvePath(parentPath); + const { value: parent } = this.resolvePath(parentPath, request.refId); if (parent === null || parent === undefined) { throw new Error(`Cannot delete property from ${parent}`); @@ -301,7 +432,7 @@ export class RpcServer { const parentPath = path.slice(0, -1); const key = path[path.length - 1]!; - const { value: parent } = this.resolvePath(parentPath); + const { value: parent } = this.resolvePath(parentPath, request.refId); if (parent === null || parent === undefined) { return { @@ -337,7 +468,7 @@ export class RpcServer { * 处理 GET_PROTOTYPE 操作 */ private handleGetPrototype (request: RpcRequest): RpcResponse { - const { value } = this.resolvePath(request.path); + const { value } = this.resolvePath(request.path, request.refId); if (value === null || value === undefined) { return { @@ -361,7 +492,10 @@ export class RpcServer { * 处理 RELEASE 操作 */ private handleRelease (request: RpcRequest): RpcResponse { - // 清理与该路径相关的资源(如果有) + // 如果有 refId,释放该引用 + if (request.refId) { + this.objectRefs.delete(request.refId); + } return { id: request.id, success: true, @@ -383,17 +517,6 @@ export class RpcServer { }; } - /** - * 判断值是否应该返回代理引用 - */ - private isProxyable (value: unknown): boolean { - if (value === null || value === undefined) { - return false; - } - const type = typeof value; - return type === 'object' || type === 'function'; - } - /** * 创建错误响应 */ diff --git a/packages/napcat-rpc/src/types.ts b/packages/napcat-rpc/src/types.ts index 9c0e2f67..0da39100 100644 --- a/packages/napcat-rpc/src/types.ts +++ b/packages/napcat-rpc/src/types.ts @@ -40,6 +40,8 @@ export interface RpcRequest { args?: SerializedValue[]; /** 回调 ID 映射 (参数索引 -> 回调 ID) */ callbackIds?: Record; + /** 远程对象引用 ID(用于对引用对象的操作) */ + refId?: string; } /** @@ -58,6 +60,8 @@ export interface RpcResponse { stack?: string; /** 结果是否为可代理对象 */ isProxyable?: boolean; + /** 远程对象引用 ID(用于深层对象代理) */ + refId?: string; } /** @@ -78,6 +82,8 @@ export interface SerializedValue { properties?: Record; /** 数组元素 */ elements?: SerializedValue[]; + /** 远程对象引用 ID(用于保持代理能力) */ + refId?: string; } /** @@ -102,6 +108,18 @@ export enum SerializedValueType { BUFFER = 'buffer', MAP = 'map', SET = 'set', + /** 远程对象引用 - 保持代理能力 */ + OBJECT_REF = 'objectRef', +} + +/** + * 对象引用信息 + */ +export interface ObjectRef { + /** 引用 ID */ + refId: string; + /** 对象类型名称 */ + className?: string; } /** @@ -138,6 +156,8 @@ export interface DeepProxyOptions { cacheProperties?: boolean; /** 回调超时时间 (ms) */ callbackTimeout?: number; + /** 远程对象引用 ID(用于引用对象的代理) */ + refId?: string; } /** @@ -148,6 +168,11 @@ export interface RpcServerOptions { target: unknown; /** 回调调用器 */ callbackInvoker?: (callbackId: string, args: unknown[]) => Promise; + /** + * 判断返回值是否应保持代理引用(而非完全序列化) + * 默认对 class 实例和包含方法的对象返回 true + */ + shouldProxyResult?: (value: unknown) => boolean; } /** @@ -163,4 +188,6 @@ export interface ProxyMeta { path: PropertyKey[]; /** 是否为代理 */ isProxy: true; + /** 远程对象引用 ID */ + refId?: string; } diff --git a/packages/napcat-test/rpc.test.ts b/packages/napcat-test/rpc.test.ts index 92e02ba9..ff86bb9b 100644 --- a/packages/napcat-test/rpc.test.ts +++ b/packages/napcat-test/rpc.test.ts @@ -41,7 +41,7 @@ interface TestObject { onError: (error: Error) => void ): void; getObject (): { id: number; name: string; }; - createInstance: new (name: string) => { name: string; greet (): string; }; + createInstance: new (name: string) => { name: string; greet (): string; getName (): string; }; getData (): Map; getSet (): Set; getDate (): Date; @@ -102,7 +102,10 @@ function createTestObject (): TestObject { greet () { return `Instance: ${this.name}`; } - } as unknown as new (name: string) => { name: string; greet (): string; }, + getName () { + return this.name; + } + } as unknown as new (name: string) => { name: string; greet (): string; getName (): string; }, getData () { return new Map([['a', 1], ['b', 2]]); }, @@ -364,7 +367,8 @@ describe('napcat-rpc RPC', () => { it('should proxy constructor calls', async () => { const Constructor = proxy.createInstance; const instance = await new Constructor('TestInstance'); - expect(instance.name).toBe('TestInstance'); + // 返回的是代理对象,需要 await 获取属性值 + expect(await instance.getName()).toBe('TestInstance'); }); }); @@ -611,7 +615,10 @@ describe('napcat-rpc RPC', () => { }); const result = await proxy.processAsync([1, 2, 3], processor); - expect(result).toEqual([10, 20, 30]); + // 数组返回代理,需 await 获取元素 + expect(await result[0]).toBe(10); + expect(await result[1]).toBe(20); + expect(await result[2]).toBe(30); expect(processor).toHaveBeenCalledTimes(3); }); @@ -749,7 +756,10 @@ describe('napcat-rpc RPC', () => { format: async (x) => `value:${x}`, }); - expect(result).toEqual(['value:6', 'value:8', 'value:10']); + // 数组返回代理 + expect(await result[0]).toBe('value:6'); + expect(await result[1]).toBe('value:8'); + expect(await result[2]).toBe('value:10'); }); }); @@ -776,7 +786,9 @@ describe('napcat-rpc RPC', () => { }); const user = await api.fetchUser(123); - expect(user).toEqual({ id: 123, name: 'User123' }); + // 对象返回代理 + expect(await user.id).toBe(123); + expect(await user.name).toBe('User123'); }); }); @@ -799,8 +811,13 @@ describe('napcat-rpc RPC', () => { await client1.add('from-client1'); await client2.add('from-client2'); - expect(await client1.getAll()).toEqual(['from-client1', 'from-client2']); - expect(await client2.getAll()).toEqual(['from-client1', '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'); }); }); @@ -849,12 +866,16 @@ describe('napcat-rpc RPC', () => { }); const items = await client.getItems(); - expect(items).toHaveLength(3); - expect(items[0]).toEqual({ id: 1, name: 'Item1', active: true }); - expect(items[1]?.id).toBe(2); + // 数组返回代理 + const item0 = await items[0]; + const item1 = await items[1]; + expect(await item0!.id).toBe(1); + expect(await item0!.name).toBe('Item1'); + expect(await item1!.id).toBe(2); const item = await client.getItemById(2); - expect(item).toEqual({ id: 2, name: 'Item2' }); + expect(await item!.id).toBe(2); + expect(await item!.name).toBe('Item2'); }); it('should handle Map and Set in nested structures', async () => { @@ -995,7 +1016,12 @@ describe('napcat-rpc RPC', () => { return 'other'; }); - expect(result).toEqual(['undefined', 'null', 'zero', 'empty', 'false']); + // 数组返回代理 + 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 () => { @@ -1036,4 +1062,407 @@ describe('napcat-rpc RPC', () => { 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(); + // 对象返回代理 + expect(await info.name).toBe('Alice'); + expect(await info.age).toBe(25); + + // 更新远程对象状态 + const newAge = await user.updateAge(30); + expect(newAge).toBe(30); + + // 验证状态被更新 + const updatedInfo = await user.getInfo(); + expect(await 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 () => { + // 现在所有对象都返回代理,访问属性需要通过 RPC + const { client } = createRpcPair({ + getSimpleData () { + return { id: 1, name: 'test', active: true }; + }, + }); + + const data = await client.getSimpleData(); + // 对象现在也是代理,访问属性返回代理,await 后获取值 + expect(await data.id).toBe(1); + expect(await data.name).toBe('test'); + expect(await 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(await 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 void>> = new Map(); + return { + register (events: Record 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'); + }); + }); });