diff --git a/packages/napcat-rpc/package.json b/packages/napcat-rpc/package.json new file mode 100644 index 00000000..3523a5c2 --- /dev/null +++ b/packages/napcat-rpc/package.json @@ -0,0 +1,25 @@ +{ + "name": "napcat-rpc", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "src/index.ts", + "scripts": { + "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + }, + "exports": { + ".": { + "import": "./src/index.ts" + }, + "./src/*": { + "import": "./src/*" + } + }, + "dependencies": {}, + "devDependencies": { + "@types/node": "^22.0.1" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file diff --git a/packages/napcat-rpc/src/client.ts b/packages/napcat-rpc/src/client.ts new file mode 100644 index 00000000..eaf1c1f4 --- /dev/null +++ b/packages/napcat-rpc/src/client.ts @@ -0,0 +1,352 @@ +import { + type DeepProxyOptions, + type ProxyMeta, + RpcOperationType, + PROXY_META, + type RpcRequest, +} from './types.js'; +import { + serialize, + deserialize, + SimpleCallbackRegistry, + extractCallbackIds, +} from './serializer.js'; + +/** + * 生成唯一请求 ID + */ +function generateRequestId (): string { + return `req_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`; +} + +/** + * 创建深层 RPC 代理 + * + * 将所有属性访问、方法调用等操作转换为 RPC 请求 + */ +export function createDeepProxy (options: DeepProxyOptions): T { + const { + transport, + rootPath = [], + // callbackTimeout 可供未来扩展使用 + } = options; + void options.callbackTimeout; + + const callbackRegistry = new SimpleCallbackRegistry(); + + // 注册回调处理器 + if (transport.onCallback) { + transport.onCallback(async (callbackId, serializedArgs) => { + const callback = callbackRegistry.get(callbackId); + if (!callback) { + throw new Error(`Callback not found: ${callbackId}`); + } + const args = serializedArgs.map(arg => deserialize(arg, { + callbackResolver: (id) => { + const cb = callbackRegistry.get(id); + if (!cb) throw new Error(`Nested callback not found: ${id}`); + return cb; + }, + proxyCreator: (path) => createProxyAtPath(path), + })); + const result = await callback(...args); + return serialize(result, { callbackRegistry }); + }); + } + + /** + * 在指定路径创建代理 + */ + function createProxyAtPath (path: PropertyKey[]): unknown { + const proxyMeta: ProxyMeta = { + path: [...path], + isProxy: true, + }; + + // 创建一个函数目标,以支持 apply 和 construct + const target = function () { } as unknown as Record; + + return new Proxy(target, { + get (_target, prop) { + // 返回代理元数据 + if (prop === PROXY_META) { + return proxyMeta; + } + + // then 方法特殊处理,使代理可以被 await + if (prop === 'then') { + return undefined; + } + + // 返回新的子路径代理 + return createProxyAtPath([...path, prop]); + }, + + set (_target, prop, value) { + const request: RpcRequest = { + id: generateRequestId(), + type: RpcOperationType.SET, + path: [...path, prop], + args: [serialize(value, { callbackRegistry })], + }; + + // 同步返回,但实际是异步操作 + transport.send(request).catch(() => { /* ignore */ }); + return true; + }, + + apply (_target, _thisArg, args) { + const serializedArgs = args.map(arg => serialize(arg, { callbackRegistry })); + const callbackIds = extractCallbackIds(serializedArgs); + + const request: RpcRequest = { + id: generateRequestId(), + type: RpcOperationType.APPLY, + path, + args: serializedArgs, + callbackIds: Object.keys(callbackIds).length > 0 ? callbackIds : undefined, + }; + + return createAsyncResultProxy(request); + }, + + construct (_target, args): object { + const serializedArgs = args.map(arg => serialize(arg, { callbackRegistry })); + const callbackIds = extractCallbackIds(serializedArgs); + + const request: RpcRequest = { + id: generateRequestId(), + type: RpcOperationType.CONSTRUCT, + path, + args: serializedArgs, + callbackIds: Object.keys(callbackIds).length > 0 ? callbackIds : undefined, + }; + + return createAsyncResultProxy(request) as object; + }, + + has (_target, prop) { + // 检查是否为代理元数据符号 + if (prop === PROXY_META) { + return true; + } + // 同步返回 true,实际检查通过异步完成 + return true; + }, + + ownKeys () { + // 返回空数组,实际键需要通过异步获取 + return []; + }, + + getOwnPropertyDescriptor (_target, _prop) { + return { + configurable: true, + enumerable: true, + writable: true, + }; + }, + + deleteProperty (_target, prop) { + const request: RpcRequest = { + id: generateRequestId(), + type: RpcOperationType.DELETE, + path: [...path, prop], + }; + + transport.send(request).catch(() => { /* ignore */ }); + return true; + }, + + getPrototypeOf () { + return Object.prototype; + }, + }); + } + + /** + * 创建异步结果代理 + * 返回一个 Promise-like 对象,可以被 await, + * 同时也可以继续链式访问属性 + */ + function createAsyncResultProxy (request: RpcRequest): unknown { + let resultPromise: Promise | null = null; + + const getResult = async (): Promise => { + if (!resultPromise) { + resultPromise = (async () => { + const response = await transport.send(request); + + if (!response.success) { + const error = new Error(response.error ?? 'RPC call failed'); + if (response.stack) { + error.stack = response.stack; + } + throw error; + } + + if (response.result === undefined) { + return undefined; + } + + // 如果结果是可代理对象,返回代理 + if (response.isProxyable && response.result) { + const deserialized = deserialize(response.result, { + callbackResolver: (id) => { + const cb = callbackRegistry.get(id); + if (!cb) throw new Error(`Callback not found: ${id}`); + return cb; + }, + proxyCreator: (proxyPath) => createProxyAtPath(proxyPath), + }); + return deserialized; + } + + return deserialize(response.result, { + callbackResolver: (id) => { + const cb = callbackRegistry.get(id); + if (!cb) throw new Error(`Callback not found: ${id}`); + return cb; + }, + proxyCreator: (proxyPath) => createProxyAtPath(proxyPath), + }); + })(); + } + return resultPromise; + }; + + // 创建一个可链式访问的代理 + const target = function () { } as unknown as Record; + + return new Proxy(target, { + get (_target, prop) { + if (prop === 'then') { + return (resolve: (value: unknown) => void, reject: (error: unknown) => void) => { + getResult().then(resolve, reject); + }; + } + + if (prop === 'catch') { + return (reject: (error: unknown) => void) => { + getResult().catch(reject); + }; + } + + if (prop === 'finally') { + return (callback: () => void) => { + getResult().finally(callback); + }; + } + + if (prop === PROXY_META) { + return undefined; + } + + // 链式访问:等待结果后访问其属性 + return createChainedProxy(getResult(), [prop]); + }, + + apply (_target, _thisArg, args) { + // 等待结果后调用 + return getResult().then(result => { + if (typeof result === 'function') { + return result(...args); + } + throw new Error('Result is not callable'); + }); + }, + }); + } + + /** + * 创建链式代理 + * 用于处理 await result.prop.method() 这样的链式调用 + */ + function createChainedProxy (parentPromise: Promise, path: PropertyKey[]): unknown { + const target = function () { } as unknown as Record; + + return new Proxy(target, { + get (_target, prop) { + if (prop === 'then') { + return (resolve: (value: unknown) => void, reject: (error: unknown) => void) => { + parentPromise + .then(parent => { + let value: unknown = parent; + for (const key of path) { + if (value === null || value === undefined) { + return undefined; + } + value = (value as Record)[key]; + } + resolve(value); + }) + .catch(reject); + }; + } + + if (prop === 'catch') { + return (reject: (error: unknown) => void) => { + parentPromise.catch(reject); + }; + } + + if (prop === 'finally') { + return (callback: () => void) => { + parentPromise.finally(callback); + }; + } + + return createChainedProxy(parentPromise, [...path, prop]); + }, + + apply (_target, _thisArg, args) { + return parentPromise.then(parent => { + let value: unknown = parent; + const pathToMethod = path.slice(0, -1); + const methodName = path[path.length - 1]; + + for (const key of pathToMethod) { + if (value === null || value === undefined) { + throw new Error(`Cannot access property '${String(key)}' of ${value}`); + } + value = (value as Record)[key]; + } + + const method = (value as Record)[methodName!]; + if (typeof method !== 'function') { + throw new Error(`${String(methodName)} is not a function`); + } + + return method.call(value, ...args); + }); + }, + }); + } + + return createProxyAtPath(rootPath) as T; +} + +/** + * 获取代理的元数据 + */ +export function getProxyMeta (proxy: unknown): ProxyMeta | undefined { + if (proxy != null && (typeof proxy === 'object' || typeof proxy === 'function')) { + try { + // 直接访问 Symbol 属性,代理的 get 陷阱会返回元数据 + const meta = (proxy as Record)[PROXY_META]; + if (meta && meta.isProxy === true) { + return meta; + } + } catch { + // 忽略访问错误 + } + } + return undefined; +} + +/** + * 检查是否为 RPC 代理 + */ +export function isRpcProxy (value: unknown): boolean { + return getProxyMeta(value) !== undefined; +} diff --git a/packages/napcat-rpc/src/easy.ts b/packages/napcat-rpc/src/easy.ts new file mode 100644 index 00000000..06b75a70 --- /dev/null +++ b/packages/napcat-rpc/src/easy.ts @@ -0,0 +1,130 @@ +/** + * 简化版 RPC API + * + * 提供一键创建完全隔离的 client/server 对 + * 在 client 端操作就像直接操作 server 端的变量一样 + */ + +import { LocalTransport } from './transport.js'; +import { createDeepProxy, getProxyMeta, isRpcProxy } from './client.js'; +import { RpcServer } from './server.js'; +import type { ProxyMeta } from './types.js'; + +/** + * RPC 配对结果 + */ +export interface RpcPair { + /** 客户端代理 - 在这里操作就像直接操作服务端的变量 */ + client: T; + /** 服务端原始对象 */ + server: T; + /** 关闭连接 */ + close (): void; +} + +/** + * 创建 RPC 配对 + * + * 快速创建完全隔离的 client/server 对,client 端的所有操作都会通过 RPC 传递到 server 端执行 + * + * @example + * ```ts + * const { client, server } = createRpcPair({ + * name: 'test', + * greet: (msg: string) => `Hello, ${msg}!`, + * register: (handlers: { onSuccess: Function, onError: Function }) => { + * handlers.onSuccess('done'); + * } + * }); + * + * // 在 client 端操作,就像直接操作 server 端的变量 + * await client.greet('world'); // 返回 'Hello, world!' + * + * // 支持包含多个回调的对象 + * await client.register({ + * onSuccess: (result) => console.log(result), + * onError: (err) => console.error(err) + * }); + * ``` + */ +export function createRpcPair (target: T): RpcPair { + const transport = new LocalTransport(target); + const client = createDeepProxy({ transport }); + + return { + client, + server: target, + close: () => transport.close(), + }; +} + +/** + * 模拟远程变量 + * + * 将一个本地变量包装成"看起来像远程变量"的代理,所有操作都通过 RPC 隔离 + * + * @example + * ```ts + * const remoteApi = mockRemote({ + * counter: 0, + * increment() { return ++this.counter; }, + * async fetchData(id: number) { return { id, data: 'test' }; } + * }); + * + * // 所有操作都是异步的,通过 RPC 隔离 + * await remoteApi.increment(); // 1 + * await remoteApi.fetchData(123); // { id: 123, data: 'test' } + * ``` + */ +export function mockRemote (target: T): T { + return createRpcPair(target).client; +} + +/** + * 创建 RPC 服务端 + * + * @example + * ```ts + * const server = createServer({ + * users: new Map(), + * addUser(id: string, name: string) { + * this.users.set(id, { name }); + * return true; + * } + * }); + * + * // 获取传输层供客户端连接 + * const transport = server.getTransport(); + * ``` + */ +export function createServer (target: T): { + target: T; + handler: RpcServer; + getTransport (): LocalTransport; +} { + const handler = new RpcServer({ target }); + return { + target, + handler, + getTransport: () => new LocalTransport(target), + }; +} + +/** + * 创建指向服务端的客户端 + * + * @example + * ```ts + * const server = createServer(myApi); + * const client = createClient(server.getTransport()); + * + * await client.someMethod(); + * ``` + */ +export function createClient (transport: LocalTransport): T { + return createDeepProxy({ transport }); +} + +// 重新导出常用工具 +export { getProxyMeta, isRpcProxy }; +export type { ProxyMeta }; diff --git a/packages/napcat-rpc/src/index.ts b/packages/napcat-rpc/src/index.ts new file mode 100644 index 00000000..1ca0aad2 --- /dev/null +++ b/packages/napcat-rpc/src/index.ts @@ -0,0 +1,60 @@ +/** + * napcat-rpc + * + * 深层 RPC 代理库 - 将对象的所有层级操作转换为 RPC 调用 + */ + +// 简化 API(推荐使用) +export { + createRpcPair, + mockRemote, + createServer, + createClient, +} from './easy.js'; + +// 类型导出 +export { + RpcOperationType, + SerializedValueType, + PROXY_META, + type RpcRequest, + type RpcResponse, + type SerializedValue, + type RpcTransport, + type RpcServerHandler, + type RpcServerOptions, + type DeepProxyOptions, + type ProxyMeta, +} from './types.js'; + +// 序列化工具 +export { + serialize, + deserialize, + extractCallbackIds, + SimpleCallbackRegistry, + type CallbackRegistry, + type SerializeContext, + type DeserializeContext, +} from './serializer.js'; + +// 客户端代理 +export { + createDeepProxy, + getProxyMeta, + isRpcProxy, +} from './client.js'; + +// 服务端 +export { + RpcServer, + createRpcServer, +} from './server.js'; + +// 传输层 +export { + LocalTransport, + MessageTransport, + createMessageServerHandler, + type MessageTransportOptions, +} from './transport.js'; diff --git a/packages/napcat-rpc/src/serializer.ts b/packages/napcat-rpc/src/serializer.ts new file mode 100644 index 00000000..185bdf09 --- /dev/null +++ b/packages/napcat-rpc/src/serializer.ts @@ -0,0 +1,386 @@ +import { + SerializedValue, + SerializedValueType, + PROXY_META, + type ProxyMeta, +} from './types.js'; + +/** + * 回调注册器接口 + */ +export interface CallbackRegistry { + register (fn: Function): string; + get (id: string): Function | undefined; + remove (id: string): void; +} + +/** + * 简单的回调注册器实现 + */ +export class SimpleCallbackRegistry implements CallbackRegistry { + private callbacks = new Map(); + private counter = 0; + + register (fn: Function): string { + const id = `cb_${++this.counter}_${Date.now()}`; + this.callbacks.set(id, fn); + return id; + } + + get (id: string): Function | undefined { + return this.callbacks.get(id); + } + + remove (id: string): void { + this.callbacks.delete(id); + } + + clear (): void { + this.callbacks.clear(); + } +} + +/** + * 序列化上下文 + */ +export interface SerializeContext { + /** 回调注册器 */ + callbackRegistry?: CallbackRegistry; + /** 已序列化对象映射(用于循环引用检测) */ + seen?: WeakMap; + /** 深度限制 */ + maxDepth?: number; + /** 当前深度 */ + currentDepth?: number; +} + +/** + * 反序列化上下文 + */ +export interface DeserializeContext { + /** 回调解析器 */ + callbackResolver?: (id: string) => Function; + /** 代理创建器 */ + proxyCreator?: (path: PropertyKey[]) => unknown; +} + +/** + * 将值序列化为可传输格式 + */ +export function serialize (value: unknown, context: SerializeContext = {}): SerializedValue { + const { + callbackRegistry, + seen = new WeakMap(), + maxDepth = 50, + currentDepth = 0, + } = context; + + // 深度检查 + if (currentDepth > maxDepth) { + return { type: SerializedValueType.STRING, value: '[Max depth exceeded]' }; + } + + // 基本类型处理 + if (value === undefined) { + return { type: SerializedValueType.UNDEFINED }; + } + + if (value === null) { + return { type: SerializedValueType.NULL }; + } + + const valueType = typeof value; + + if (valueType === 'boolean') { + return { type: SerializedValueType.BOOLEAN, value }; + } + + if (valueType === 'number') { + const numValue = value as number; + if (Number.isNaN(numValue)) { + return { type: SerializedValueType.NUMBER, value: 'NaN' }; + } + if (!Number.isFinite(numValue)) { + return { type: SerializedValueType.NUMBER, value: numValue > 0 ? 'Infinity' : '-Infinity' }; + } + return { type: SerializedValueType.NUMBER, value }; + } + + if (valueType === 'bigint') { + return { type: SerializedValueType.BIGINT, value: (value as bigint).toString() }; + } + + if (valueType === 'string') { + return { type: SerializedValueType.STRING, value }; + } + + if (valueType === 'symbol') { + return { + type: SerializedValueType.SYMBOL, + value: (value as symbol).description ?? '', + }; + } + + if (valueType === 'function') { + const fn = value as Function; + if (callbackRegistry) { + const callbackId = callbackRegistry.register(fn); + return { + type: SerializedValueType.FUNCTION, + callbackId, + className: fn.name || 'anonymous', + }; + } + return { + type: SerializedValueType.FUNCTION, + className: fn.name || 'anonymous', + }; + } + + // 对象类型处理 + const obj = value as object; + + // 检查是否为代理对象 + if (PROXY_META in obj) { + const meta = (obj as Record)[PROXY_META]; + if (meta) { + return { + type: SerializedValueType.PROXY_REF, + proxyPath: meta.path, + }; + } + } + + // 循环引用检测 + if (seen.has(obj)) { + return seen.get(obj)!; + } + + // Date + if (obj instanceof Date) { + return { type: SerializedValueType.DATE, value: obj.toISOString() }; + } + + // RegExp + if (obj instanceof RegExp) { + return { + type: SerializedValueType.REGEXP, + value: { source: obj.source, flags: obj.flags }, + }; + } + + // Error + if (obj instanceof Error) { + return { + type: SerializedValueType.ERROR, + value: obj.message, + className: obj.constructor.name, + properties: { + stack: serialize(obj.stack, { ...context, seen, currentDepth: currentDepth + 1 }), + }, + }; + } + + // Buffer / Uint8Array + if (obj instanceof Uint8Array) { + return { + type: SerializedValueType.BUFFER, + value: Array.from(obj as Uint8Array), + }; + } + + // Node.js Buffer + if (typeof globalThis !== 'undefined' && 'Buffer' in globalThis) { + const BufferClass = (globalThis as unknown as { Buffer: { isBuffer (obj: unknown): boolean; }; }).Buffer; + if (BufferClass.isBuffer(obj)) { + return { + type: SerializedValueType.BUFFER, + value: Array.from(obj as unknown as Uint8Array), + }; + } + } + + // Map + if (obj instanceof Map) { + const entries: SerializedValue[] = []; + const nextContext = { ...context, seen, currentDepth: currentDepth + 1 }; + for (const [k, v] of obj) { + entries.push(serialize([k, v], nextContext)); + } + return { + type: SerializedValueType.MAP, + elements: entries, + }; + } + + // Set + if (obj instanceof Set) { + const elements: SerializedValue[] = []; + const nextContext = { ...context, seen, currentDepth: currentDepth + 1 }; + for (const v of obj) { + elements.push(serialize(v, nextContext)); + } + return { + type: SerializedValueType.SET, + elements, + }; + } + + // Promise + if (obj instanceof Promise) { + return { type: SerializedValueType.PROMISE }; + } + + // Array + if (Array.isArray(obj)) { + const result: SerializedValue = { + type: SerializedValueType.ARRAY, + elements: [], + }; + seen.set(obj, result); + const nextContext = { ...context, seen, currentDepth: currentDepth + 1 }; + result.elements = obj.map(item => serialize(item, nextContext)); + return result; + } + + // 普通对象 + const result: SerializedValue = { + type: SerializedValueType.OBJECT, + className: obj.constructor?.name ?? 'Object', + properties: {}, + }; + seen.set(obj, result); + + const nextContext = { ...context, seen, currentDepth: currentDepth + 1 }; + for (const key of Object.keys(obj)) { + result.properties![key] = serialize((obj as Record)[key], nextContext); + } + + return result; +} + +/** + * 将序列化数据还原为值 + */ +export function deserialize (data: SerializedValue, context: DeserializeContext = {}): unknown { + const { callbackResolver, proxyCreator } = context; + + switch (data.type) { + case SerializedValueType.UNDEFINED: + return undefined; + + case SerializedValueType.NULL: + return null; + + case SerializedValueType.BOOLEAN: + return data.value; + + case SerializedValueType.NUMBER: + if (data.value === 'NaN') return NaN; + if (data.value === 'Infinity') return Infinity; + if (data.value === '-Infinity') return -Infinity; + return data.value; + + case SerializedValueType.BIGINT: + return BigInt(data.value as string); + + case SerializedValueType.STRING: + return data.value; + + case SerializedValueType.SYMBOL: + return Symbol(data.value as string); + + case SerializedValueType.FUNCTION: + if (data.callbackId && callbackResolver) { + return callbackResolver(data.callbackId); + } + // 返回一个占位函数 + return function placeholder () { + throw new Error('Remote function cannot be called without callback resolver'); + }; + + case SerializedValueType.DATE: + return new Date(data.value as string); + + case SerializedValueType.REGEXP: { + const { source, flags } = data.value as { source: string; flags: string; }; + return new RegExp(source, flags); + } + + case SerializedValueType.ERROR: { + const error = new Error(data.value as string); + if (data.properties?.['stack']) { + error.stack = deserialize(data.properties['stack'], context) as string; + } + return error; + } + + case SerializedValueType.BUFFER: { + const arr = data.value as number[]; + if (typeof globalThis !== 'undefined' && 'Buffer' in globalThis) { + const BufferClass = (globalThis as unknown as { Buffer: { from (arr: number[]): Uint8Array; }; }).Buffer; + return BufferClass.from(arr); + } + return new Uint8Array(arr); + } + + case SerializedValueType.MAP: { + const map = new Map(); + if (data.elements) { + for (const element of data.elements) { + const [k, v] = deserialize(element, context) as [unknown, unknown]; + map.set(k, v); + } + } + return map; + } + + case SerializedValueType.SET: { + const set = new Set(); + if (data.elements) { + for (const element of data.elements) { + set.add(deserialize(element, context)); + } + } + return set; + } + + case SerializedValueType.PROMISE: + return Promise.resolve(undefined); + + case SerializedValueType.ARRAY: + return (data.elements ?? []).map(elem => deserialize(elem, context)); + + case SerializedValueType.PROXY_REF: + if (data.proxyPath && proxyCreator) { + return proxyCreator(data.proxyPath); + } + return {}; + + case SerializedValueType.OBJECT: { + const obj: Record = {}; + if (data.properties) { + for (const [key, val] of Object.entries(data.properties)) { + obj[key] = deserialize(val, context); + } + } + return obj; + } + + default: + return undefined; + } +} + +/** + * 提取序列化参数中的回调ID映射 + */ +export function extractCallbackIds (args: SerializedValue[]): Record { + const result: Record = {}; + args.forEach((arg, index) => { + if (arg.type === SerializedValueType.FUNCTION && arg.callbackId) { + result[index] = arg.callbackId; + } + }); + return result; +} diff --git a/packages/napcat-rpc/src/server.ts b/packages/napcat-rpc/src/server.ts new file mode 100644 index 00000000..1e6ca1a4 --- /dev/null +++ b/packages/napcat-rpc/src/server.ts @@ -0,0 +1,433 @@ +import { + type RpcRequest, + type RpcResponse, + type RpcServerOptions, + type SerializedValue, + RpcOperationType, +} from './types.js'; +import { serialize, deserialize, SimpleCallbackRegistry } from './serializer.js'; + +/** + * RPC 服务端 + * + * 处理来自客户端的 RPC 请求,在目标对象上执行操作 + */ +export class RpcServer { + private target: unknown; + private callbackInvoker?: (callbackId: string, args: unknown[]) => Promise; + private localCallbacks = new SimpleCallbackRegistry(); + + constructor (options: RpcServerOptions) { + this.target = options.target; + this.callbackInvoker = options.callbackInvoker; + } + + /** + * 处理 RPC 请求 + */ + async handleRequest (request: RpcRequest): Promise { + try { + switch (request.type) { + case RpcOperationType.GET: + return this.handleGet(request); + + case RpcOperationType.SET: + return this.handleSet(request); + + case RpcOperationType.APPLY: + return await this.handleApply(request); + + case RpcOperationType.CONSTRUCT: + return await this.handleConstruct(request); + + case RpcOperationType.HAS: + return this.handleHas(request); + + case RpcOperationType.OWNKEYS: + return this.handleOwnKeys(request); + + case RpcOperationType.DELETE: + return this.handleDelete(request); + + case RpcOperationType.GET_DESCRIPTOR: + return this.handleGetDescriptor(request); + + case RpcOperationType.GET_PROTOTYPE: + return this.handleGetPrototype(request); + + case RpcOperationType.RELEASE: + return this.handleRelease(request); + + default: + return { + id: request.id, + success: false, + error: `Unknown operation type: ${request.type}`, + }; + } + } catch (error) { + return this.createErrorResponse(request.id, error); + } + } + + /** + * 解析路径获取目标值 + */ + private resolvePath (path: PropertyKey[]): { parent: unknown; key: PropertyKey | undefined; value: unknown; } { + let current = this.target; + let parent: unknown = null; + let key: PropertyKey | undefined; + + for (let i = 0; i < path.length; i++) { + parent = current; + key = path[i]; + if (key === undefined) { + throw new Error('Path contains undefined key'); + } + if (current === null || current === undefined) { + throw new Error(`Cannot access property '${String(key)}' of ${current}`); + } + current = (current as Record)[key]; + } + + return { parent, key, value: current }; + } + + /** + * 处理 GET 操作 + */ + private handleGet (request: RpcRequest): RpcResponse { + const { value } = this.resolvePath(request.path); + const isProxyable = this.isProxyable(value); + + return { + id: request.id, + success: true, + result: serialize(value, { callbackRegistry: this.localCallbacks }), + isProxyable, + }; + } + + /** + * 处理 SET 操作 + */ + private handleSet (request: RpcRequest): RpcResponse { + const path = request.path; + if (path.length === 0) { + 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); + + if (parent === null || parent === undefined) { + throw new Error(`Cannot set property '${String(key)}' of ${parent}`); + } + + const newValue = request.args?.[0] + ? deserialize(request.args[0], { + callbackResolver: this.createCallbackResolver(request), + }) + : undefined; + + (parent as Record)[key] = newValue; + + return { + id: request.id, + success: true, + }; + } + + /** + * 处理 APPLY 操作 + */ + private async handleApply (request: RpcRequest): Promise { + const path = request.path; + 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); + + if (parent === null || parent === undefined) { + throw new Error(`Cannot call method on ${parent}`); + } + + const method = (parent as Record)[methodName]; + if (typeof method !== 'function') { + throw new Error(`${String(methodName)} is not a function`); + } + + const args = (request.args ?? []).map(arg => + deserialize(arg, { + callbackResolver: this.createCallbackResolver(request), + }) + ); + + let result = method.call(parent, ...args); + + // 处理 Promise + if (result instanceof Promise) { + result = await result; + } + + const isProxyable = this.isProxyable(result); + + return { + id: request.id, + success: true, + result: serialize(result, { callbackRegistry: this.localCallbacks }), + isProxyable, + }; + } + + /** + * 处理 CONSTRUCT 操作 + */ + private async handleConstruct (request: RpcRequest): Promise { + const { value: Constructor } = this.resolvePath(request.path); + + if (typeof Constructor !== 'function') { + throw new Error('Target is not a constructor'); + } + + const args = (request.args ?? []).map(arg => + deserialize(arg, { + callbackResolver: this.createCallbackResolver(request), + }) + ); + + const instance = new (Constructor as new (...args: unknown[]) => unknown)(...args); + const isProxyable = this.isProxyable(instance); + + return { + id: request.id, + success: true, + result: serialize(instance, { callbackRegistry: this.localCallbacks }), + isProxyable, + }; + } + + /** + * 处理 HAS 操作 + */ + private handleHas (request: RpcRequest): RpcResponse { + const path = request.path; + if (path.length === 0) { + return { + id: request.id, + success: true, + result: serialize(true), + }; + } + + const parentPath = path.slice(0, -1); + const key = path[path.length - 1]!; + const { value: parent } = this.resolvePath(parentPath); + + const has = parent !== null && parent !== undefined && key in (parent as object); + + return { + id: request.id, + success: true, + result: serialize(has), + }; + } + + /** + * 处理 OWNKEYS 操作 + */ + private handleOwnKeys (request: RpcRequest): RpcResponse { + const { value } = this.resolvePath(request.path); + + if (value === null || value === undefined) { + return { + id: request.id, + success: true, + result: serialize([]), + }; + } + + const keys = Reflect.ownKeys(value as object); + + return { + id: request.id, + success: true, + result: serialize(keys.map(k => (typeof k === 'symbol' ? k.description ?? '' : String(k)))), + }; + } + + /** + * 处理 DELETE 操作 + */ + private handleDelete (request: RpcRequest): RpcResponse { + const path = request.path; + if (path.length === 0) { + 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); + + if (parent === null || parent === undefined) { + throw new Error(`Cannot delete property from ${parent}`); + } + + const deleted = delete (parent as Record)[key]; + + return { + id: request.id, + success: true, + result: serialize(deleted), + }; + } + + /** + * 处理 GET_DESCRIPTOR 操作 + */ + private handleGetDescriptor (request: RpcRequest): RpcResponse { + const path = request.path; + if (path.length === 0) { + return { + id: request.id, + success: true, + result: serialize(undefined), + }; + } + + const parentPath = path.slice(0, -1); + const key = path[path.length - 1]!; + const { value: parent } = this.resolvePath(parentPath); + + if (parent === null || parent === undefined) { + return { + id: request.id, + success: true, + result: serialize(undefined), + }; + } + + const descriptor = Object.getOwnPropertyDescriptor(parent as object, key); + + if (!descriptor) { + return { + id: request.id, + success: true, + result: serialize(undefined), + }; + } + + // 序列化描述符(排除 value 和 get/set 函数) + return { + id: request.id, + success: true, + result: serialize({ + configurable: descriptor.configurable, + enumerable: descriptor.enumerable, + writable: descriptor.writable, + }), + }; + } + + /** + * 处理 GET_PROTOTYPE 操作 + */ + private handleGetPrototype (request: RpcRequest): RpcResponse { + const { value } = this.resolvePath(request.path); + + if (value === null || value === undefined) { + return { + id: request.id, + success: true, + result: serialize(null), + }; + } + + const proto = Object.getPrototypeOf(value); + const name = proto?.constructor?.name ?? 'Object'; + + return { + id: request.id, + success: true, + result: serialize({ name }), + }; + } + + /** + * 处理 RELEASE 操作 + */ + private handleRelease (request: RpcRequest): RpcResponse { + // 清理与该路径相关的资源(如果有) + return { + id: request.id, + success: true, + }; + } + + /** + * 创建回调解析器 + */ + private createCallbackResolver (_request: RpcRequest): (id: string) => Function { + return (callbackId: string) => { + // 创建一个代理函数,调用时会通过 callbackInvoker 发送回客户端 + return async (...args: unknown[]) => { + if (!this.callbackInvoker) { + throw new Error('Callback invoker not configured'); + } + return this.callbackInvoker(callbackId, args); + }; + }; + } + + /** + * 判断值是否应该返回代理引用 + */ + private isProxyable (value: unknown): boolean { + if (value === null || value === undefined) { + return false; + } + const type = typeof value; + return type === 'object' || type === 'function'; + } + + /** + * 创建错误响应 + */ + private createErrorResponse (requestId: string, error: unknown): RpcResponse { + if (error instanceof Error) { + return { + id: requestId, + success: false, + error: error.message, + stack: error.stack, + }; + } + return { + id: requestId, + success: false, + error: String(error), + }; + } + + /** + * 调用客户端回调 + */ + async invokeCallback (callbackId: string, args: unknown[]): Promise { + if (!this.callbackInvoker) { + throw new Error('Callback invoker not configured'); + } + const result = await this.callbackInvoker(callbackId, args); + return serialize(result, { callbackRegistry: this.localCallbacks }); + } +} + +/** + * 创建 RPC 服务端 + */ +export function createRpcServer (options: RpcServerOptions): RpcServer { + return new RpcServer(options); +} diff --git a/packages/napcat-rpc/src/transport.ts b/packages/napcat-rpc/src/transport.ts new file mode 100644 index 00000000..ef7ef5ed --- /dev/null +++ b/packages/napcat-rpc/src/transport.ts @@ -0,0 +1,204 @@ +import { + type RpcTransport, + type RpcRequest, + type RpcResponse, + type SerializedValue, +} from './types.js'; +import { RpcServer } from './server.js'; +import { serialize, deserialize, SimpleCallbackRegistry } from './serializer.js'; + +/** + * 本地传输层 + * + * 用于在同一进程内进行 RPC 调用,主要用于测试 + */ +export class LocalTransport implements RpcTransport { + private server: RpcServer; + private callbackHandler?: (callbackId: string, args: SerializedValue[]) => Promise; + private clientCallbacks = new SimpleCallbackRegistry(); + + constructor (target: unknown) { + this.server = new RpcServer({ + target, + callbackInvoker: async (callbackId, args) => { + if (!this.callbackHandler) { + throw new Error('Callback handler not registered'); + } + const serializedArgs = args.map(arg => serialize(arg, { callbackRegistry: this.clientCallbacks })); + const result = await this.callbackHandler(callbackId, serializedArgs); + return deserialize(result); + }, + }); + } + + async send (request: RpcRequest): Promise { + // 模拟网络延迟(可选) + // await new Promise(resolve => setTimeout(resolve, 0)); + + return this.server.handleRequest(request); + } + + onCallback (handler: (callbackId: string, args: SerializedValue[]) => Promise): void { + this.callbackHandler = handler; + } + + close (): void { + this.clientCallbacks.clear(); + } +} + +/** + * 消息传输层接口 + */ +export interface MessageTransportOptions { + /** 发送消息 */ + sendMessage: (message: string) => void | Promise; + /** 接收消息时的回调 */ + onMessage: (handler: (message: string) => void) => void; +} + +/** + * 基于消息的传输层 + * + * 可用于跨进程/网络通信 + */ +export class MessageTransport implements RpcTransport { + private pendingRequests = new Map void; + reject: (error: Error) => void; + }>(); + private callbackHandler?: (callbackId: string, args: SerializedValue[]) => Promise; + private sendMessage: (message: string) => void | Promise; + + constructor (options: MessageTransportOptions) { + this.sendMessage = options.sendMessage; + + options.onMessage(async (message) => { + const data = JSON.parse(message) as { + type: 'response' | 'callback' | 'callback_response'; + id: string; + response?: RpcResponse; + callbackId?: string; + args?: SerializedValue[]; + result?: SerializedValue; + error?: string; + }; + + if (data.type === 'response') { + const pending = this.pendingRequests.get(data.id); + if (pending && data.response) { + this.pendingRequests.delete(data.id); + pending.resolve(data.response); + } + } else if (data.type === 'callback') { + // 处理来自服务端的回调调用 + if (this.callbackHandler && data.callbackId && data.args) { + try { + const result = await this.callbackHandler(data.callbackId, data.args); + await this.sendMessage(JSON.stringify({ + type: 'callback_response', + id: data.id, + result, + })); + } catch (error) { + await this.sendMessage(JSON.stringify({ + type: 'callback_response', + id: data.id, + error: error instanceof Error ? error.message : String(error), + })); + } + } + } + }); + } + + async send (request: RpcRequest): Promise { + return new Promise((resolve, reject) => { + this.pendingRequests.set(request.id, { resolve, reject }); + + const message = JSON.stringify({ + type: 'request', + request, + }); + + Promise.resolve(this.sendMessage(message)).catch(reject); + }); + } + + onCallback (handler: (callbackId: string, args: SerializedValue[]) => Promise): void { + this.callbackHandler = handler; + } + + close (): void { + for (const [, pending] of this.pendingRequests) { + pending.reject(new Error('Transport closed')); + } + this.pendingRequests.clear(); + } +} + +/** + * 创建消息传输层的服务端处理器 + */ +export function createMessageServerHandler (target: unknown, options: { + sendMessage: (message: string) => void | Promise; + onMessage: (handler: (message: string) => void) => void; +}): void { + const pendingCallbacks = new Map void; + reject: (error: Error) => void; + }>(); + + let callbackIdCounter = 0; + + const server = new RpcServer({ + target, + callbackInvoker: async (callbackId, args) => { + const id = `cb_call_${++callbackIdCounter}`; + const serializedArgs = args.map(arg => serialize(arg)); + + return new Promise((resolve, reject) => { + pendingCallbacks.set(id, { + resolve: (result) => resolve(deserialize(result)), + reject, + }); + + options.sendMessage(JSON.stringify({ + type: 'callback', + id, + callbackId, + args: serializedArgs, + })); + }); + }, + }); + + options.onMessage(async (message) => { + const data = JSON.parse(message) as { + type: 'request' | 'callback_response'; + id: string; + request?: RpcRequest; + result?: SerializedValue; + error?: string; + }; + + if (data.type === 'request' && data.request) { + const response = await server.handleRequest(data.request); + await options.sendMessage(JSON.stringify({ + type: 'response', + id: data.request.id, + response, + })); + } else if (data.type === 'callback_response') { + const pending = pendingCallbacks.get(data.id); + if (pending) { + pendingCallbacks.delete(data.id); + if (data.error) { + pending.reject(new Error(data.error)); + } else if (data.result) { + pending.resolve(data.result); + } + } + } + }); +} diff --git a/packages/napcat-rpc/src/types.ts b/packages/napcat-rpc/src/types.ts new file mode 100644 index 00000000..9c0e2f67 --- /dev/null +++ b/packages/napcat-rpc/src/types.ts @@ -0,0 +1,166 @@ +/** + * RPC 操作类型 + */ +export enum RpcOperationType { + /** 获取属性 */ + GET = 'get', + /** 设置属性 */ + SET = 'set', + /** 调用方法 */ + APPLY = 'apply', + /** 构造函数调用 */ + CONSTRUCT = 'construct', + /** 检查属性是否存在 */ + HAS = 'has', + /** 获取所有键 */ + OWNKEYS = 'ownKeys', + /** 删除属性 */ + DELETE = 'deleteProperty', + /** 获取属性描述符 */ + GET_DESCRIPTOR = 'getOwnPropertyDescriptor', + /** 获取原型 */ + GET_PROTOTYPE = 'getPrototypeOf', + /** 回调调用 */ + CALLBACK = 'callback', + /** 释放资源 */ + RELEASE = 'release', +} + +/** + * RPC 请求消息 + */ +export interface RpcRequest { + /** 请求 ID */ + id: string; + /** 操作类型 */ + type: RpcOperationType; + /** 访问路径 (从根对象开始) */ + path: PropertyKey[]; + /** 参数 (用于 set, apply, construct) */ + args?: SerializedValue[]; + /** 回调 ID 映射 (参数索引 -> 回调 ID) */ + callbackIds?: Record; +} + +/** + * RPC 响应消息 + */ +export interface RpcResponse { + /** 请求 ID */ + id: string; + /** 是否成功 */ + success: boolean; + /** 返回值 */ + result?: SerializedValue; + /** 错误信息 */ + error?: string; + /** 错误堆栈 */ + stack?: string; + /** 结果是否为可代理对象 */ + isProxyable?: boolean; +} + +/** + * 序列化后的值 + */ +export interface SerializedValue { + /** 值类型 */ + type: SerializedValueType; + /** 原始值(用于基本类型) */ + value?: unknown; + /** 对象类型名称 */ + className?: string; + /** 回调 ID(用于函数) */ + callbackId?: string; + /** 代理路径(用于可代理对象) */ + proxyPath?: PropertyKey[]; + /** 数组元素或对象属性 */ + properties?: Record; + /** 数组元素 */ + elements?: SerializedValue[]; +} + +/** + * 序列化值类型 + */ +export enum SerializedValueType { + UNDEFINED = 'undefined', + NULL = 'null', + BOOLEAN = 'boolean', + NUMBER = 'number', + BIGINT = 'bigint', + STRING = 'string', + SYMBOL = 'symbol', + FUNCTION = 'function', + OBJECT = 'object', + ARRAY = 'array', + DATE = 'date', + REGEXP = 'regexp', + ERROR = 'error', + PROMISE = 'promise', + PROXY_REF = 'proxyRef', + BUFFER = 'buffer', + MAP = 'map', + SET = 'set', +} + +/** + * RPC 传输层接口 + */ +export interface RpcTransport { + /** 发送请求并等待响应 */ + send (request: RpcRequest): Promise; + /** 注册回调处理器 */ + onCallback?(handler: (callbackId: string, args: SerializedValue[]) => Promise): void; + /** 关闭连接 */ + close?(): void; +} + +/** + * RPC 服务端处理器接口 + */ +export interface RpcServerHandler { + /** 处理请求 */ + handleRequest (request: RpcRequest): Promise; + /** 调用客户端回调 */ + invokeCallback?(callbackId: string, args: unknown[]): Promise; +} + +/** + * 深层代理选项 + */ +export interface DeepProxyOptions { + /** 传输层 */ + transport: RpcTransport; + /** 根路径 */ + rootPath?: PropertyKey[]; + /** 是否缓存属性 */ + cacheProperties?: boolean; + /** 回调超时时间 (ms) */ + callbackTimeout?: number; +} + +/** + * RPC 服务端选项 + */ +export interface RpcServerOptions { + /** 目标对象 */ + target: unknown; + /** 回调调用器 */ + callbackInvoker?: (callbackId: string, args: unknown[]) => Promise; +} + +/** + * 代理元数据符号 + */ +export const PROXY_META = Symbol('PROXY_META'); + +/** + * 代理元数据 + */ +export interface ProxyMeta { + /** 访问路径 */ + path: PropertyKey[]; + /** 是否为代理 */ + isProxy: true; +} diff --git a/packages/napcat-rpc/tsconfig.json b/packages/napcat-rpc/tsconfig.json new file mode 100644 index 00000000..8c8bbfff --- /dev/null +++ b/packages/napcat-rpc/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "noEmit": true, + "baseUrl": ".", + "paths": { + "@/*": [ + "../*/" + ] + } + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/packages/napcat-schema/vite.config.ts b/packages/napcat-schema/vite.config.ts index 79787b43..bbe173d4 100644 --- a/packages/napcat-schema/vite.config.ts +++ b/packages/napcat-schema/vite.config.ts @@ -15,6 +15,7 @@ export default defineConfig({ resolve: { conditions: ['node', 'default'], alias: { + '@/napcat-rpc': resolve(__dirname, '../napcat-rpc'), '@/napcat-onebot': resolve(__dirname, '../napcat-onebot'), '@/napcat-common': resolve(__dirname, '../napcat-common'), '@/napcat-schema': resolve(__dirname, './src'), diff --git a/packages/napcat-test/package.json b/packages/napcat-test/package.json index 812c57bb..5df18293 100644 --- a/packages/napcat-test/package.json +++ b/packages/napcat-test/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "napcat-core": "workspace:*", + "napcat-rpc": "workspace:*", "napcat-image-size": "workspace:*" } } \ No newline at end of file diff --git a/packages/napcat-test/rpc.test.ts b/packages/napcat-test/rpc.test.ts new file mode 100644 index 00000000..92e02ba9 --- /dev/null +++ b/packages/napcat-test/rpc.test.ts @@ -0,0 +1,1039 @@ +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; + add (a: number, b: number): number; + multiply (a: number, b: number): number; + withCallback (fn: (x: number) => number): number; + asyncWithCallback (fn: (x: number) => Promise): Promise; + multiCallback ( + onSuccess: (result: string) => void, + onError: (error: Error) => void + ): void; + getObject (): { id: number; name: string; }; + createInstance: new (name: string) => { name: string; greet (): string; }; + getData (): Map; + getSet (): Set; + getDate (): Date; + getBuffer (): Uint8Array; + throwError (): never; + asyncThrowError (): Promise; +} + +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) { + 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}`; + } + } as unknown as new (name: string) => { name: string; greet (): 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([['a', 1], ['b', 2]]); + const result = deserialize(serialize(map)) as Map; + 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; + 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({ 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).get('a')).toBe(1); + }); + + it('should handle Set return values', async () => { + const result = await proxy.getSet(); + expect(result).toBeInstanceOf(Set); + expect((result as Set).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'); + expect(instance.name).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({ 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({ 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({ 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({ 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 + ): Promise => { + const result = await processor(value); + return result * 2; + }, + }; + + const transport = new LocalTransport(target); + const proxy = createDeepProxy({ 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 + ): Promise => { + const results: number[] = []; + for (const item of data) { + results.push(await processor(item)); + } + return results; + }, + }; + + const transport = new LocalTransport(target); + const proxy = createDeepProxy({ 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); + expect(result).toEqual([10, 20, 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({ 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({ 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; + onMessage: (msg: string) => Promise; + onDisconnect: (reason: string) => Promise; + }) { + 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; + validate: (x: number) => Promise; + format: (x: number) => Promise; + }) { + 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(result).toEqual(['value:6', 'value:8', '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).toEqual({ id: 123, name: '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(server.getTransport()); + const client2 = createClient(server.getTransport()); + + 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']); + }); + }); + + // ========== 多层调用场景 ========== + 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).toHaveLength(3); + expect(items[0]).toEqual({ id: 1, name: 'Item1', active: true }); + expect(items[1]?.id).toBe(2); + + const item = await client.getItemById(2); + expect(item).toEqual({ id: 2, name: '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) { + // 模拟事件触发 + 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; + step2: (x: number) => Promise; + step3: (x: number) => Promise; + } + ) { + 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; + onProgress: (percent: number) => Promise; + afterComplete: () => Promise; + }) { + 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) { + 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(result).toEqual(['undefined', 'null', 'zero', 'empty', 'false']); + }); + + it('should handle errors in callbacks', async () => { + const { client } = createRpcPair({ + async executeWithErrorHandler ( + action: () => Promise, + onError: (err: Error) => Promise + ) { + 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 + }); + }); +}); diff --git a/packages/napcat-test/vitest.config.ts b/packages/napcat-test/vitest.config.ts index 3451aa60..7dc4f836 100644 --- a/packages/napcat-test/vitest.config.ts +++ b/packages/napcat-test/vitest.config.ts @@ -8,6 +8,7 @@ export default defineConfig({ }, resolve: { alias: { + '@/napcat-rpc': resolve(__dirname, '../napcat-rpc'), '@/napcat-image-size': resolve(__dirname, '../napcat-image-size'), '@/napcat-test': resolve(__dirname, '.'), '@/napcat-common': resolve(__dirname, '../napcat-common'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37a2c8a7..1bcf636f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -302,6 +302,12 @@ importers: specifier: ^22.0.1 version: 22.19.1 + packages/napcat-rpc: + devDependencies: + '@types/node': + specifier: ^22.0.1 + version: 22.19.1 + packages/napcat-schema: dependencies: '@sinclair/typebox': @@ -357,6 +363,9 @@ importers: napcat-image-size: specifier: workspace:* version: link:../napcat-image-size + napcat-rpc: + specifier: workspace:* + version: link:../napcat-rpc devDependencies: vitest: specifier: ^4.0.9