mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 06:31:13 +00:00
Add napcat-rpc package with deep RPC
Introduce a new napcat-rpc package implementing a deep-proxy RPC system. Adds client (createDeepProxy, proxy helpers), server (RpcServer, createRpcServer), serializer (serialize/deserialize, callback registry), and transport layers (LocalTransport, MessageTransport, message server handler), plus an easy API (createRpcPair, mockRemote, createServer, createClient). Includes TypeScript types, tsconfig and package.json. Wire-up: add package alias in napcat-schema vite config and add napcat-rpc dependency to napcat-test along with comprehensive rpc tests.
This commit is contained in:
parent
52b6627ebd
commit
a4527fd8ca
25
packages/napcat-rpc/package.json
Normal file
25
packages/napcat-rpc/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
352
packages/napcat-rpc/src/client.ts
Normal file
352
packages/napcat-rpc/src/client.ts
Normal file
@ -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<T = unknown> (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<PropertyKey, unknown>;
|
||||
|
||||
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<unknown> | null = null;
|
||||
|
||||
const getResult = async (): Promise<unknown> => {
|
||||
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<PropertyKey, unknown>;
|
||||
|
||||
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<unknown>, path: PropertyKey[]): unknown {
|
||||
const target = function () { } as unknown as Record<PropertyKey, unknown>;
|
||||
|
||||
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<PropertyKey, unknown>)[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<PropertyKey, unknown>)[key];
|
||||
}
|
||||
|
||||
const method = (value as Record<PropertyKey, unknown>)[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<symbol, ProxyMeta | undefined>)[PROXY_META];
|
||||
if (meta && meta.isProxy === true) {
|
||||
return meta;
|
||||
}
|
||||
} catch {
|
||||
// 忽略访问错误
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为 RPC 代理
|
||||
*/
|
||||
export function isRpcProxy (value: unknown): boolean {
|
||||
return getProxyMeta(value) !== undefined;
|
||||
}
|
||||
130
packages/napcat-rpc/src/easy.ts
Normal file
130
packages/napcat-rpc/src/easy.ts
Normal file
@ -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<T> {
|
||||
/** 客户端代理 - 在这里操作就像直接操作服务端的变量 */
|
||||
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<T extends object> (target: T): RpcPair<T> {
|
||||
const transport = new LocalTransport(target);
|
||||
const client = createDeepProxy<T>({ 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<T extends object> (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<T extends object> (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<typeof myApi>(server.getTransport());
|
||||
*
|
||||
* await client.someMethod();
|
||||
* ```
|
||||
*/
|
||||
export function createClient<T extends object> (transport: LocalTransport): T {
|
||||
return createDeepProxy<T>({ transport });
|
||||
}
|
||||
|
||||
// 重新导出常用工具
|
||||
export { getProxyMeta, isRpcProxy };
|
||||
export type { ProxyMeta };
|
||||
60
packages/napcat-rpc/src/index.ts
Normal file
60
packages/napcat-rpc/src/index.ts
Normal file
@ -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';
|
||||
386
packages/napcat-rpc/src/serializer.ts
Normal file
386
packages/napcat-rpc/src/serializer.ts
Normal file
@ -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<string, Function>();
|
||||
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<object, SerializedValue>;
|
||||
/** 深度限制 */
|
||||
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<symbol, ProxyMeta | undefined>)[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<string, unknown>)[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<string, unknown> = {};
|
||||
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<number, string> {
|
||||
const result: Record<number, string> = {};
|
||||
args.forEach((arg, index) => {
|
||||
if (arg.type === SerializedValueType.FUNCTION && arg.callbackId) {
|
||||
result[index] = arg.callbackId;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
433
packages/napcat-rpc/src/server.ts
Normal file
433
packages/napcat-rpc/src/server.ts
Normal file
@ -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<unknown>;
|
||||
private localCallbacks = new SimpleCallbackRegistry();
|
||||
|
||||
constructor (options: RpcServerOptions) {
|
||||
this.target = options.target;
|
||||
this.callbackInvoker = options.callbackInvoker;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 RPC 请求
|
||||
*/
|
||||
async handleRequest (request: RpcRequest): Promise<RpcResponse> {
|
||||
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<PropertyKey, unknown>)[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<PropertyKey, unknown>)[key] = newValue;
|
||||
|
||||
return {
|
||||
id: request.id,
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 APPLY 操作
|
||||
*/
|
||||
private async handleApply (request: RpcRequest): Promise<RpcResponse> {
|
||||
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<PropertyKey, unknown>)[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<RpcResponse> {
|
||||
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<PropertyKey, unknown>)[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<SerializedValue> {
|
||||
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);
|
||||
}
|
||||
204
packages/napcat-rpc/src/transport.ts
Normal file
204
packages/napcat-rpc/src/transport.ts
Normal file
@ -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<SerializedValue>;
|
||||
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<RpcResponse> {
|
||||
// 模拟网络延迟(可选)
|
||||
// await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
return this.server.handleRequest(request);
|
||||
}
|
||||
|
||||
onCallback (handler: (callbackId: string, args: SerializedValue[]) => Promise<SerializedValue>): void {
|
||||
this.callbackHandler = handler;
|
||||
}
|
||||
|
||||
close (): void {
|
||||
this.clientCallbacks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息传输层接口
|
||||
*/
|
||||
export interface MessageTransportOptions {
|
||||
/** 发送消息 */
|
||||
sendMessage: (message: string) => void | Promise<void>;
|
||||
/** 接收消息时的回调 */
|
||||
onMessage: (handler: (message: string) => void) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于消息的传输层
|
||||
*
|
||||
* 可用于跨进程/网络通信
|
||||
*/
|
||||
export class MessageTransport implements RpcTransport {
|
||||
private pendingRequests = new Map<string, {
|
||||
resolve: (response: RpcResponse) => void;
|
||||
reject: (error: Error) => void;
|
||||
}>();
|
||||
private callbackHandler?: (callbackId: string, args: SerializedValue[]) => Promise<SerializedValue>;
|
||||
private sendMessage: (message: string) => void | Promise<void>;
|
||||
|
||||
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<RpcResponse> {
|
||||
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<SerializedValue>): 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<void>;
|
||||
onMessage: (handler: (message: string) => void) => void;
|
||||
}): void {
|
||||
const pendingCallbacks = new Map<string, {
|
||||
resolve: (result: SerializedValue) => 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<unknown>((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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
166
packages/napcat-rpc/src/types.ts
Normal file
166
packages/napcat-rpc/src/types.ts
Normal file
@ -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<number, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string, SerializedValue>;
|
||||
/** 数组元素 */
|
||||
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<RpcResponse>;
|
||||
/** 注册回调处理器 */
|
||||
onCallback?(handler: (callbackId: string, args: SerializedValue[]) => Promise<SerializedValue>): void;
|
||||
/** 关闭连接 */
|
||||
close?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 服务端处理器接口
|
||||
*/
|
||||
export interface RpcServerHandler {
|
||||
/** 处理请求 */
|
||||
handleRequest (request: RpcRequest): Promise<RpcResponse>;
|
||||
/** 调用客户端回调 */
|
||||
invokeCallback?(callbackId: string, args: unknown[]): Promise<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深层代理选项
|
||||
*/
|
||||
export interface DeepProxyOptions {
|
||||
/** 传输层 */
|
||||
transport: RpcTransport;
|
||||
/** 根路径 */
|
||||
rootPath?: PropertyKey[];
|
||||
/** 是否缓存属性 */
|
||||
cacheProperties?: boolean;
|
||||
/** 回调超时时间 (ms) */
|
||||
callbackTimeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 服务端选项
|
||||
*/
|
||||
export interface RpcServerOptions {
|
||||
/** 目标对象 */
|
||||
target: unknown;
|
||||
/** 回调调用器 */
|
||||
callbackInvoker?: (callbackId: string, args: unknown[]) => Promise<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理元数据符号
|
||||
*/
|
||||
export const PROXY_META = Symbol('PROXY_META');
|
||||
|
||||
/**
|
||||
* 代理元数据
|
||||
*/
|
||||
export interface ProxyMeta {
|
||||
/** 访问路径 */
|
||||
path: PropertyKey[];
|
||||
/** 是否为代理 */
|
||||
isProxy: true;
|
||||
}
|
||||
21
packages/napcat-rpc/tsconfig.json
Normal file
21
packages/napcat-rpc/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"../*/"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@ -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'),
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"napcat-core": "workspace:*",
|
||||
"napcat-rpc": "workspace:*",
|
||||
"napcat-image-size": "workspace:*"
|
||||
}
|
||||
}
|
||||
1039
packages/napcat-test/rpc.test.ts
Normal file
1039
packages/napcat-test/rpc.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -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'),
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user