mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-06 13:05:09 +00:00
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.
This commit is contained in:
@@ -28,6 +28,7 @@ export function createDeepProxy<T = unknown> (options: DeepProxyOptions): T {
|
|||||||
const {
|
const {
|
||||||
transport,
|
transport,
|
||||||
rootPath = [],
|
rootPath = [],
|
||||||
|
refId: rootRefId,
|
||||||
// callbackTimeout 可供未来扩展使用
|
// callbackTimeout 可供未来扩展使用
|
||||||
} = options;
|
} = options;
|
||||||
void options.callbackTimeout;
|
void options.callbackTimeout;
|
||||||
@@ -47,7 +48,7 @@ export function createDeepProxy<T = unknown> (options: DeepProxyOptions): T {
|
|||||||
if (!cb) throw new Error(`Nested callback not found: ${id}`);
|
if (!cb) throw new Error(`Nested callback not found: ${id}`);
|
||||||
return cb;
|
return cb;
|
||||||
},
|
},
|
||||||
proxyCreator: (path) => createProxyAtPath(path),
|
proxyCreator: (path, proxyRefId) => createProxyAtPath(path, proxyRefId),
|
||||||
}));
|
}));
|
||||||
const result = await callback(...args);
|
const result = await callback(...args);
|
||||||
return serialize(result, { callbackRegistry });
|
return serialize(result, { callbackRegistry });
|
||||||
@@ -57,10 +58,11 @@ export function createDeepProxy<T = unknown> (options: DeepProxyOptions): T {
|
|||||||
/**
|
/**
|
||||||
* 在指定路径创建代理
|
* 在指定路径创建代理
|
||||||
*/
|
*/
|
||||||
function createProxyAtPath (path: PropertyKey[]): unknown {
|
function createProxyAtPath (path: PropertyKey[], refId?: string): unknown {
|
||||||
const proxyMeta: ProxyMeta = {
|
const proxyMeta: ProxyMeta = {
|
||||||
path: [...path],
|
path: [...path],
|
||||||
isProxy: true,
|
isProxy: true,
|
||||||
|
refId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建一个函数目标,以支持 apply 和 construct
|
// 创建一个函数目标,以支持 apply 和 construct
|
||||||
@@ -78,8 +80,8 @@ export function createDeepProxy<T = unknown> (options: DeepProxyOptions): T {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回新的子路径代理
|
// 返回新的子路径代理(继承 refId)
|
||||||
return createProxyAtPath([...path, prop]);
|
return createProxyAtPath([...path, prop], refId);
|
||||||
},
|
},
|
||||||
|
|
||||||
set (_target, prop, value) {
|
set (_target, prop, value) {
|
||||||
@@ -88,6 +90,7 @@ export function createDeepProxy<T = unknown> (options: DeepProxyOptions): T {
|
|||||||
type: RpcOperationType.SET,
|
type: RpcOperationType.SET,
|
||||||
path: [...path, prop],
|
path: [...path, prop],
|
||||||
args: [serialize(value, { callbackRegistry })],
|
args: [serialize(value, { callbackRegistry })],
|
||||||
|
refId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 同步返回,但实际是异步操作
|
// 同步返回,但实际是异步操作
|
||||||
@@ -105,6 +108,7 @@ export function createDeepProxy<T = unknown> (options: DeepProxyOptions): T {
|
|||||||
path,
|
path,
|
||||||
args: serializedArgs,
|
args: serializedArgs,
|
||||||
callbackIds: Object.keys(callbackIds).length > 0 ? callbackIds : undefined,
|
callbackIds: Object.keys(callbackIds).length > 0 ? callbackIds : undefined,
|
||||||
|
refId,
|
||||||
};
|
};
|
||||||
|
|
||||||
return createAsyncResultProxy(request);
|
return createAsyncResultProxy(request);
|
||||||
@@ -120,6 +124,7 @@ export function createDeepProxy<T = unknown> (options: DeepProxyOptions): T {
|
|||||||
path,
|
path,
|
||||||
args: serializedArgs,
|
args: serializedArgs,
|
||||||
callbackIds: Object.keys(callbackIds).length > 0 ? callbackIds : undefined,
|
callbackIds: Object.keys(callbackIds).length > 0 ? callbackIds : undefined,
|
||||||
|
refId,
|
||||||
};
|
};
|
||||||
|
|
||||||
return createAsyncResultProxy(request) as object;
|
return createAsyncResultProxy(request) as object;
|
||||||
@@ -152,6 +157,7 @@ export function createDeepProxy<T = unknown> (options: DeepProxyOptions): T {
|
|||||||
id: generateRequestId(),
|
id: generateRequestId(),
|
||||||
type: RpcOperationType.DELETE,
|
type: RpcOperationType.DELETE,
|
||||||
path: [...path, prop],
|
path: [...path, prop],
|
||||||
|
refId,
|
||||||
};
|
};
|
||||||
|
|
||||||
transport.send(request).catch(() => { /* ignore */ });
|
transport.send(request).catch(() => { /* ignore */ });
|
||||||
@@ -197,7 +203,7 @@ export function createDeepProxy<T = unknown> (options: DeepProxyOptions): T {
|
|||||||
if (!cb) throw new Error(`Callback not found: ${id}`);
|
if (!cb) throw new Error(`Callback not found: ${id}`);
|
||||||
return cb;
|
return cb;
|
||||||
},
|
},
|
||||||
proxyCreator: (proxyPath) => createProxyAtPath(proxyPath),
|
proxyCreator: (proxyPath, proxyRefId) => createProxyAtPath(proxyPath, proxyRefId),
|
||||||
});
|
});
|
||||||
return deserialized;
|
return deserialized;
|
||||||
}
|
}
|
||||||
@@ -208,7 +214,7 @@ export function createDeepProxy<T = unknown> (options: DeepProxyOptions): T {
|
|||||||
if (!cb) throw new Error(`Callback not found: ${id}`);
|
if (!cb) throw new Error(`Callback not found: ${id}`);
|
||||||
return cb;
|
return cb;
|
||||||
},
|
},
|
||||||
proxyCreator: (proxyPath) => createProxyAtPath(proxyPath),
|
proxyCreator: (proxyPath, proxyRefId) => createProxyAtPath(proxyPath, proxyRefId),
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
@@ -323,7 +329,7 @@ export function createDeepProxy<T = unknown> (options: DeepProxyOptions): T {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return createProxyAtPath(rootPath) as T;
|
return createProxyAtPath(rootPath, rootRefId) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ export interface DeserializeContext {
|
|||||||
/** 回调解析器 */
|
/** 回调解析器 */
|
||||||
callbackResolver?: (id: string) => Function;
|
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 {
|
export function deserialize (data: SerializedValue, context: DeserializeContext = {}): unknown {
|
||||||
const { callbackResolver, proxyCreator } = context;
|
const { callbackResolver, proxyCreator, refResolver } = context;
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case SerializedValueType.UNDEFINED:
|
case SerializedValueType.UNDEFINED:
|
||||||
@@ -357,6 +359,20 @@ export function deserialize (data: SerializedValue, context: DeserializeContext
|
|||||||
}
|
}
|
||||||
return {};
|
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: {
|
case SerializedValueType.OBJECT: {
|
||||||
const obj: Record<string, unknown> = {};
|
const obj: Record<string, unknown> = {};
|
||||||
if (data.properties) {
|
if (data.properties) {
|
||||||
|
|||||||
@@ -4,9 +4,58 @@ import {
|
|||||||
type RpcServerOptions,
|
type RpcServerOptions,
|
||||||
type SerializedValue,
|
type SerializedValue,
|
||||||
RpcOperationType,
|
RpcOperationType,
|
||||||
|
SerializedValueType,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { serialize, deserialize, SimpleCallbackRegistry } from './serializer.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 服务端
|
* RPC 服务端
|
||||||
*
|
*
|
||||||
@@ -16,10 +65,15 @@ export class RpcServer {
|
|||||||
private target: unknown;
|
private target: unknown;
|
||||||
private callbackInvoker?: (callbackId: string, args: unknown[]) => Promise<unknown>;
|
private callbackInvoker?: (callbackId: string, args: unknown[]) => Promise<unknown>;
|
||||||
private localCallbacks = new SimpleCallbackRegistry();
|
private localCallbacks = new SimpleCallbackRegistry();
|
||||||
|
/** 对象引用存储 */
|
||||||
|
private objectRefs = new Map<string, unknown>();
|
||||||
|
/** 代理判断函数 */
|
||||||
|
private shouldProxyResult: (value: unknown) => boolean;
|
||||||
|
|
||||||
constructor (options: RpcServerOptions) {
|
constructor (options: RpcServerOptions) {
|
||||||
this.target = options.target;
|
this.target = options.target;
|
||||||
this.callbackInvoker = options.callbackInvoker;
|
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; } {
|
private resolvePath (path: PropertyKey[], refId?: string): { parent: unknown; key: PropertyKey | undefined; value: unknown; } {
|
||||||
let current = this.target;
|
// 如果有 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 parent: unknown = null;
|
||||||
let key: PropertyKey | undefined;
|
let key: PropertyKey | undefined;
|
||||||
|
|
||||||
@@ -93,18 +153,54 @@ export class RpcServer {
|
|||||||
return { parent, key, value: current };
|
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 操作
|
* 处理 GET 操作
|
||||||
*/
|
*/
|
||||||
private handleGet (request: RpcRequest): RpcResponse {
|
private handleGet (request: RpcRequest): RpcResponse {
|
||||||
const { value } = this.resolvePath(request.path);
|
const { value } = this.resolvePath(request.path, request.refId);
|
||||||
const isProxyable = this.isProxyable(value);
|
const { result, isProxyable, refId } = this.serializeResult(value);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: request.id,
|
id: request.id,
|
||||||
success: true,
|
success: true,
|
||||||
result: serialize(value, { callbackRegistry: this.localCallbacks }),
|
result,
|
||||||
isProxyable,
|
isProxyable,
|
||||||
|
refId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,13 +209,13 @@ export class RpcServer {
|
|||||||
*/
|
*/
|
||||||
private handleSet (request: RpcRequest): RpcResponse {
|
private handleSet (request: RpcRequest): RpcResponse {
|
||||||
const path = request.path;
|
const path = request.path;
|
||||||
if (path.length === 0) {
|
if (path.length === 0 && !request.refId) {
|
||||||
throw new Error('Cannot set root object');
|
throw new Error('Cannot set root object');
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentPath = path.slice(0, -1);
|
const parentPath = path.slice(0, -1);
|
||||||
const key = path[path.length - 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) {
|
if (parent === null || parent === undefined) {
|
||||||
throw new Error(`Cannot set property '${String(key)}' of ${parent}`);
|
throw new Error(`Cannot set property '${String(key)}' of ${parent}`);
|
||||||
@@ -128,6 +224,7 @@ export class RpcServer {
|
|||||||
const newValue = request.args?.[0]
|
const newValue = request.args?.[0]
|
||||||
? deserialize(request.args[0], {
|
? deserialize(request.args[0], {
|
||||||
callbackResolver: this.createCallbackResolver(request),
|
callbackResolver: this.createCallbackResolver(request),
|
||||||
|
refResolver: (refId) => this.objectRefs.get(refId),
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@@ -144,13 +241,43 @@ export class RpcServer {
|
|||||||
*/
|
*/
|
||||||
private async handleApply (request: RpcRequest): Promise<RpcResponse> {
|
private async handleApply (request: RpcRequest): Promise<RpcResponse> {
|
||||||
const path = request.path;
|
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) {
|
if (path.length === 0) {
|
||||||
throw new Error('Cannot call root object');
|
throw new Error('Cannot call root object');
|
||||||
}
|
}
|
||||||
|
|
||||||
const methodPath = path.slice(0, -1);
|
const methodPath = path.slice(0, -1);
|
||||||
const methodName = path[path.length - 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) {
|
if (parent === null || parent === undefined) {
|
||||||
throw new Error(`Cannot call method on ${parent}`);
|
throw new Error(`Cannot call method on ${parent}`);
|
||||||
@@ -164,6 +291,7 @@ export class RpcServer {
|
|||||||
const args = (request.args ?? []).map(arg =>
|
const args = (request.args ?? []).map(arg =>
|
||||||
deserialize(arg, {
|
deserialize(arg, {
|
||||||
callbackResolver: this.createCallbackResolver(request),
|
callbackResolver: this.createCallbackResolver(request),
|
||||||
|
refResolver: (refId) => this.objectRefs.get(refId),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -174,13 +302,14 @@ export class RpcServer {
|
|||||||
result = await result;
|
result = await result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isProxyable = this.isProxyable(result);
|
const { result: serializedResult, isProxyable, refId } = this.serializeResult(result);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: request.id,
|
id: request.id,
|
||||||
success: true,
|
success: true,
|
||||||
result: serialize(result, { callbackRegistry: this.localCallbacks }),
|
result: serializedResult,
|
||||||
isProxyable,
|
isProxyable,
|
||||||
|
refId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +317,7 @@ export class RpcServer {
|
|||||||
* 处理 CONSTRUCT 操作
|
* 处理 CONSTRUCT 操作
|
||||||
*/
|
*/
|
||||||
private async handleConstruct (request: RpcRequest): Promise<RpcResponse> {
|
private async handleConstruct (request: RpcRequest): Promise<RpcResponse> {
|
||||||
const { value: Constructor } = this.resolvePath(request.path);
|
const { value: Constructor } = this.resolvePath(request.path, request.refId);
|
||||||
|
|
||||||
if (typeof Constructor !== 'function') {
|
if (typeof Constructor !== 'function') {
|
||||||
throw new Error('Target is not a constructor');
|
throw new Error('Target is not a constructor');
|
||||||
@@ -197,17 +326,19 @@ export class RpcServer {
|
|||||||
const args = (request.args ?? []).map(arg =>
|
const args = (request.args ?? []).map(arg =>
|
||||||
deserialize(arg, {
|
deserialize(arg, {
|
||||||
callbackResolver: this.createCallbackResolver(request),
|
callbackResolver: this.createCallbackResolver(request),
|
||||||
|
refResolver: (refId) => this.objectRefs.get(refId),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const instance = new (Constructor as new (...args: unknown[]) => unknown)(...args);
|
const instance = new (Constructor as new (...args: unknown[]) => unknown)(...args);
|
||||||
const isProxyable = this.isProxyable(instance);
|
const { result, isProxyable, refId } = this.serializeResult(instance);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: request.id,
|
id: request.id,
|
||||||
success: true,
|
success: true,
|
||||||
result: serialize(instance, { callbackRegistry: this.localCallbacks }),
|
result,
|
||||||
isProxyable,
|
isProxyable,
|
||||||
|
refId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +357,7 @@ export class RpcServer {
|
|||||||
|
|
||||||
const parentPath = path.slice(0, -1);
|
const parentPath = path.slice(0, -1);
|
||||||
const key = path[path.length - 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);
|
const has = parent !== null && parent !== undefined && key in (parent as object);
|
||||||
|
|
||||||
@@ -241,7 +372,7 @@ export class RpcServer {
|
|||||||
* 处理 OWNKEYS 操作
|
* 处理 OWNKEYS 操作
|
||||||
*/
|
*/
|
||||||
private handleOwnKeys (request: RpcRequest): RpcResponse {
|
private handleOwnKeys (request: RpcRequest): RpcResponse {
|
||||||
const { value } = this.resolvePath(request.path);
|
const { value } = this.resolvePath(request.path, request.refId);
|
||||||
|
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return {
|
return {
|
||||||
@@ -265,13 +396,13 @@ export class RpcServer {
|
|||||||
*/
|
*/
|
||||||
private handleDelete (request: RpcRequest): RpcResponse {
|
private handleDelete (request: RpcRequest): RpcResponse {
|
||||||
const path = request.path;
|
const path = request.path;
|
||||||
if (path.length === 0) {
|
if (path.length === 0 && !request.refId) {
|
||||||
throw new Error('Cannot delete root object');
|
throw new Error('Cannot delete root object');
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentPath = path.slice(0, -1);
|
const parentPath = path.slice(0, -1);
|
||||||
const key = path[path.length - 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) {
|
if (parent === null || parent === undefined) {
|
||||||
throw new Error(`Cannot delete property from ${parent}`);
|
throw new Error(`Cannot delete property from ${parent}`);
|
||||||
@@ -301,7 +432,7 @@ export class RpcServer {
|
|||||||
|
|
||||||
const parentPath = path.slice(0, -1);
|
const parentPath = path.slice(0, -1);
|
||||||
const key = path[path.length - 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) {
|
if (parent === null || parent === undefined) {
|
||||||
return {
|
return {
|
||||||
@@ -337,7 +468,7 @@ export class RpcServer {
|
|||||||
* 处理 GET_PROTOTYPE 操作
|
* 处理 GET_PROTOTYPE 操作
|
||||||
*/
|
*/
|
||||||
private handleGetPrototype (request: RpcRequest): RpcResponse {
|
private handleGetPrototype (request: RpcRequest): RpcResponse {
|
||||||
const { value } = this.resolvePath(request.path);
|
const { value } = this.resolvePath(request.path, request.refId);
|
||||||
|
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return {
|
return {
|
||||||
@@ -361,7 +492,10 @@ export class RpcServer {
|
|||||||
* 处理 RELEASE 操作
|
* 处理 RELEASE 操作
|
||||||
*/
|
*/
|
||||||
private handleRelease (request: RpcRequest): RpcResponse {
|
private handleRelease (request: RpcRequest): RpcResponse {
|
||||||
// 清理与该路径相关的资源(如果有)
|
// 如果有 refId,释放该引用
|
||||||
|
if (request.refId) {
|
||||||
|
this.objectRefs.delete(request.refId);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: request.id,
|
id: request.id,
|
||||||
success: true,
|
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';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建错误响应
|
* 创建错误响应
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export interface RpcRequest {
|
|||||||
args?: SerializedValue[];
|
args?: SerializedValue[];
|
||||||
/** 回调 ID 映射 (参数索引 -> 回调 ID) */
|
/** 回调 ID 映射 (参数索引 -> 回调 ID) */
|
||||||
callbackIds?: Record<number, string>;
|
callbackIds?: Record<number, string>;
|
||||||
|
/** 远程对象引用 ID(用于对引用对象的操作) */
|
||||||
|
refId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,6 +60,8 @@ export interface RpcResponse {
|
|||||||
stack?: string;
|
stack?: string;
|
||||||
/** 结果是否为可代理对象 */
|
/** 结果是否为可代理对象 */
|
||||||
isProxyable?: boolean;
|
isProxyable?: boolean;
|
||||||
|
/** 远程对象引用 ID(用于深层对象代理) */
|
||||||
|
refId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,6 +82,8 @@ export interface SerializedValue {
|
|||||||
properties?: Record<string, SerializedValue>;
|
properties?: Record<string, SerializedValue>;
|
||||||
/** 数组元素 */
|
/** 数组元素 */
|
||||||
elements?: SerializedValue[];
|
elements?: SerializedValue[];
|
||||||
|
/** 远程对象引用 ID(用于保持代理能力) */
|
||||||
|
refId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,6 +108,18 @@ export enum SerializedValueType {
|
|||||||
BUFFER = 'buffer',
|
BUFFER = 'buffer',
|
||||||
MAP = 'map',
|
MAP = 'map',
|
||||||
SET = 'set',
|
SET = 'set',
|
||||||
|
/** 远程对象引用 - 保持代理能力 */
|
||||||
|
OBJECT_REF = 'objectRef',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象引用信息
|
||||||
|
*/
|
||||||
|
export interface ObjectRef {
|
||||||
|
/** 引用 ID */
|
||||||
|
refId: string;
|
||||||
|
/** 对象类型名称 */
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,6 +156,8 @@ export interface DeepProxyOptions {
|
|||||||
cacheProperties?: boolean;
|
cacheProperties?: boolean;
|
||||||
/** 回调超时时间 (ms) */
|
/** 回调超时时间 (ms) */
|
||||||
callbackTimeout?: number;
|
callbackTimeout?: number;
|
||||||
|
/** 远程对象引用 ID(用于引用对象的代理) */
|
||||||
|
refId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,6 +168,11 @@ export interface RpcServerOptions {
|
|||||||
target: unknown;
|
target: unknown;
|
||||||
/** 回调调用器 */
|
/** 回调调用器 */
|
||||||
callbackInvoker?: (callbackId: string, args: unknown[]) => Promise<unknown>;
|
callbackInvoker?: (callbackId: string, args: unknown[]) => Promise<unknown>;
|
||||||
|
/**
|
||||||
|
* 判断返回值是否应保持代理引用(而非完全序列化)
|
||||||
|
* 默认对 class 实例和包含方法的对象返回 true
|
||||||
|
*/
|
||||||
|
shouldProxyResult?: (value: unknown) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,4 +188,6 @@ export interface ProxyMeta {
|
|||||||
path: PropertyKey[];
|
path: PropertyKey[];
|
||||||
/** 是否为代理 */
|
/** 是否为代理 */
|
||||||
isProxy: true;
|
isProxy: true;
|
||||||
|
/** 远程对象引用 ID */
|
||||||
|
refId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ interface TestObject {
|
|||||||
onError: (error: Error) => void
|
onError: (error: Error) => void
|
||||||
): void;
|
): void;
|
||||||
getObject (): { id: number; name: string; };
|
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<string, number>;
|
getData (): Map<string, number>;
|
||||||
getSet (): Set<string>;
|
getSet (): Set<string>;
|
||||||
getDate (): Date;
|
getDate (): Date;
|
||||||
@@ -102,7 +102,10 @@ function createTestObject (): TestObject {
|
|||||||
greet () {
|
greet () {
|
||||||
return `Instance: ${this.name}`;
|
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 () {
|
getData () {
|
||||||
return new Map([['a', 1], ['b', 2]]);
|
return new Map([['a', 1], ['b', 2]]);
|
||||||
},
|
},
|
||||||
@@ -364,7 +367,8 @@ describe('napcat-rpc RPC', () => {
|
|||||||
it('should proxy constructor calls', async () => {
|
it('should proxy constructor calls', async () => {
|
||||||
const Constructor = proxy.createInstance;
|
const Constructor = proxy.createInstance;
|
||||||
const instance = await new Constructor('TestInstance');
|
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);
|
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);
|
expect(processor).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -749,7 +756,10 @@ describe('napcat-rpc RPC', () => {
|
|||||||
format: async (x) => `value:${x}`,
|
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);
|
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 client1.add('from-client1');
|
||||||
await client2.add('from-client2');
|
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();
|
const items = await client.getItems();
|
||||||
expect(items).toHaveLength(3);
|
// 数组返回代理
|
||||||
expect(items[0]).toEqual({ id: 1, name: 'Item1', active: true });
|
const item0 = await items[0];
|
||||||
expect(items[1]?.id).toBe(2);
|
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);
|
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 () => {
|
it('should handle Map and Set in nested structures', async () => {
|
||||||
@@ -995,7 +1016,12 @@ describe('napcat-rpc RPC', () => {
|
|||||||
return 'other';
|
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 () => {
|
it('should handle errors in callbacks', async () => {
|
||||||
@@ -1036,4 +1062,407 @@ describe('napcat-rpc RPC', () => {
|
|||||||
expect(sum).toBe(49995000); // sum of 0 to 9999
|
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<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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user