rwadurian/backend/mpc-system/services/service-party-app/electron/modules/grpc-client.ts

737 lines
21 KiB
TypeScript

import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import * as path from 'path';
import { EventEmitter } from 'events';
import { app } from 'electron';
// 定义 proto 包结构类型
interface ProtoPackage {
mpc?: {
router?: {
v1?: {
MessageRouter?: grpc.ServiceClientConstructor;
};
};
};
}
// 延迟加载的 Proto 定义
let packageDefinition: protoLoader.PackageDefinition | null = null;
// Proto 文件路径 - 在打包后需要从 app.asar.unpacked 或 resources 目录加载
function getProtoPath(): string {
// 开发环境
if (!app.isPackaged) {
return path.join(__dirname, '../../proto/message_router.proto');
}
// 生产环境 - proto 文件需要解包
return path.join(process.resourcesPath, 'proto/message_router.proto');
}
// 延迟加载 Proto 定义
function loadProtoDefinition(): protoLoader.PackageDefinition {
if (!packageDefinition) {
const protoPath = getProtoPath();
console.log('Loading proto from:', protoPath);
packageDefinition = protoLoader.loadSync(protoPath, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
}
return packageDefinition;
}
// Note: field names must match proto definitions with keepCase: true
// Proto uses snake_case: session_id, session_type, threshold_n, threshold_t
interface SessionInfo {
session_id: string;
session_type: string;
threshold_n: number;
threshold_t: number;
message_hash?: Buffer;
keygen_session_id?: string;
status?: string;
}
interface PartyInfo {
party_id: string;
party_index: number;
}
interface JoinSessionResponse {
success: boolean;
session_info?: SessionInfo;
party_index: number;
other_parties: PartyInfo[];
}
interface MPCMessage {
message_id: string;
session_id: string;
from_party: string;
is_broadcast: boolean;
round_number: number;
payload: Buffer;
created_at: string;
}
interface SessionEvent {
event_id: string;
event_type: string;
session_id: string;
threshold_n: number;
threshold_t: number;
selected_parties: string[];
join_tokens: Record<string, string>;
message_hash?: Buffer;
}
// Raw proto response (snake_case)
interface RegisteredPartyProto {
party_id: string;
role: string;
online: boolean;
registered_at: string;
last_seen_at: string;
}
interface GetRegisteredPartiesResponse {
parties: RegisteredPartyProto[];
}
// Converted response (camelCase) - used by callers
interface RegisteredParty {
partyId: string;
role: string;
online: boolean;
registeredAt: string;
lastSeenAt: string;
}
// 重连配置
interface ReconnectConfig {
maxRetries: number;
initialDelayMs: number;
maxDelayMs: number;
backoffMultiplier: number;
}
const DEFAULT_RECONNECT_CONFIG: ReconnectConfig = {
maxRetries: 10,
initialDelayMs: 1000,
maxDelayMs: 30000,
backoffMultiplier: 2,
};
/**
* gRPC 客户端 - 连接到 Message Router
*
* 连接地址格式:
* - 开发环境: localhost:50051 (不加密)
* - 生产环境: mpc-grpc.szaiai.com:443 (TLS 加密)
*
* 特性:
* - 自动重连机制(指数退避)
* - 事件流断开后自动重新订阅
* - 心跳失败后自动重连
*/
export class GrpcClient extends EventEmitter {
private client: grpc.Client | null = null;
private connected = false;
private partyId: string | null = null;
private partyRole: string | null = null;
private heartbeatInterval: NodeJS.Timeout | null = null;
private messageStream: grpc.ClientReadableStream<MPCMessage> | null = null;
private eventStream: grpc.ClientReadableStream<SessionEvent> | null = null;
// 重连相关
private reconnectConfig: ReconnectConfig;
private currentAddress: string | null = null;
private currentUseTLS: boolean | undefined;
private isReconnecting = false;
private reconnectAttempts = 0;
private reconnectTimeout: NodeJS.Timeout | null = null;
private shouldReconnect = true;
// 消息流状态(用于重连后恢复)
private activeMessageSubscription: { sessionId: string; partyId: string } | null = null;
private eventStreamSubscribed = false;
// 心跳失败计数
private heartbeatFailCount = 0;
private readonly MAX_HEARTBEAT_FAILS = 3;
constructor(reconnectConfig?: Partial<ReconnectConfig>) {
super();
this.reconnectConfig = { ...DEFAULT_RECONNECT_CONFIG, ...reconnectConfig };
}
/**
* 连接到 Message Router
* @param address 完整地址,格式: host:port (例如 mpc-grpc.szaiai.com:443 或 localhost:50051)
* @param useTLS 是否使用 TLS 加密 (默认: 自动检测,端口 443 使用 TLS)
*/
async connect(address: string, useTLS?: boolean): Promise<void> {
// 保存连接参数用于重连
this.currentAddress = address;
this.currentUseTLS = useTLS;
this.shouldReconnect = true;
return this.doConnect(address, useTLS);
}
private async doConnect(address: string, useTLS?: boolean): Promise<void> {
return new Promise((resolve, reject) => {
const definition = loadProtoDefinition();
const proto = grpc.loadPackageDefinition(definition) as ProtoPackage;
const MessageRouter = proto.mpc?.router?.v1?.MessageRouter;
if (!MessageRouter) {
reject(new Error('Failed to load MessageRouter service definition'));
return;
}
// 解析地址,如果没有端口则默认使用 443
let targetAddress = address;
if (!address.includes(':')) {
targetAddress = `${address}:443`;
}
// 自动检测是否使用 TLS: 端口 443 或显式指定
const port = parseInt(targetAddress.split(':')[1], 10);
const shouldUseTLS = useTLS !== undefined ? useTLS : (port === 443);
// 创建凭证
const credentials = shouldUseTLS
? grpc.credentials.createSsl() // TLS 加密 (生产环境)
: grpc.credentials.createInsecure(); // 不加密 (开发环境)
console.log(`[gRPC] Connecting to Message Router: ${targetAddress} (TLS: ${shouldUseTLS})`);
this.client = new MessageRouter(
targetAddress,
credentials
) as grpc.Client;
// 等待连接就绪
const deadline = new Date();
deadline.setSeconds(deadline.getSeconds() + 10);
(this.client as grpc.Client & { waitForReady: (deadline: Date, callback: (err?: Error) => void) => void })
.waitForReady(deadline, (err?: Error) => {
if (err) {
reject(err);
} else {
this.connected = true;
this.reconnectAttempts = 0; // 重置重连计数
this.heartbeatFailCount = 0;
console.log('[gRPC] Connected successfully');
this.emit('connected');
resolve();
}
});
});
}
/**
* 断开连接(不会自动重连)
*/
disconnect(): void {
this.shouldReconnect = false;
this.cleanupConnection();
}
/**
* 清理连接资源
*/
private cleanupConnection(): void {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
if (this.messageStream) {
try {
this.messageStream.cancel();
} catch (e) {
// 忽略取消错误
}
this.messageStream = null;
}
if (this.eventStream) {
try {
this.eventStream.cancel();
} catch (e) {
// 忽略取消错误
}
this.eventStream = null;
}
if (this.client) {
try {
(this.client as grpc.Client & { close: () => void }).close();
} catch (e) {
// 忽略关闭错误
}
this.client = null;
}
this.connected = false;
}
/**
* 触发重连
*/
private async triggerReconnect(reason: string): Promise<void> {
if (!this.shouldReconnect || this.isReconnecting || !this.currentAddress) {
return;
}
console.log(`[gRPC] Triggering reconnect: ${reason}`);
this.isReconnecting = true;
this.connected = false;
this.emit('disconnected', reason);
// 清理现有连接
this.cleanupConnection();
// 计算延迟时间(指数退避)
const delay = Math.min(
this.reconnectConfig.initialDelayMs * Math.pow(this.reconnectConfig.backoffMultiplier, this.reconnectAttempts),
this.reconnectConfig.maxDelayMs
);
console.log(`[gRPC] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/${this.reconnectConfig.maxRetries})`);
this.reconnectTimeout = setTimeout(async () => {
this.reconnectAttempts++;
if (this.reconnectAttempts > this.reconnectConfig.maxRetries) {
console.error('[gRPC] Max reconnect attempts reached');
this.isReconnecting = false;
this.emit('reconnectFailed', 'Max retries exceeded');
return;
}
try {
await this.doConnect(this.currentAddress!, this.currentUseTLS);
// 重新注册
if (this.partyId && this.partyRole) {
console.log(`[gRPC] Re-registering as party: ${this.partyId}`);
await this.registerParty(this.partyId, this.partyRole);
}
// 重新订阅事件流
if (this.eventStreamSubscribed && this.partyId) {
console.log('[gRPC] Re-subscribing to session events');
this.subscribeSessionEvents(this.partyId);
}
// 重新订阅消息流
if (this.activeMessageSubscription) {
console.log(`[gRPC] Re-subscribing to messages for session: ${this.activeMessageSubscription.sessionId}`);
this.subscribeMessages(this.activeMessageSubscription.sessionId, this.activeMessageSubscription.partyId);
}
this.isReconnecting = false;
this.emit('reconnected');
} catch (err) {
console.error(`[gRPC] Reconnect attempt ${this.reconnectAttempts} failed:`, (err as Error).message);
this.isReconnecting = false;
// 继续尝试重连
this.triggerReconnect('Previous reconnect attempt failed');
}
}, delay);
}
/**
* 检查是否已连接
*/
isConnected(): boolean {
return this.connected;
}
/**
* 获取当前 Party ID
*/
getPartyId(): string | null {
return this.partyId;
}
/**
* 注册为参与方
*/
async registerParty(partyId: string, role: string): Promise<void> {
if (!this.client) {
throw new Error('Not connected');
}
return new Promise((resolve, reject) => {
(this.client as grpc.Client & { registerParty: (req: unknown, callback: (err: Error | null, res: { success: boolean }) => void) => void })
.registerParty(
{
party_id: partyId,
party_role: role,
version: '1.0.0',
},
(err: Error | null, response: { success: boolean }) => {
if (err) {
reject(err);
} else if (!response.success) {
reject(new Error('Registration failed'));
} else {
this.partyId = partyId;
this.partyRole = role;
this.startHeartbeat();
resolve();
}
}
);
});
}
/**
* 开始心跳(带重连逻辑)
*/
private startHeartbeat(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
this.heartbeatFailCount = 0;
this.heartbeatInterval = setInterval(() => {
if (this.client && this.partyId) {
(this.client as grpc.Client & { heartbeat: (req: unknown, callback: (err: Error | null) => void) => void })
.heartbeat(
{ party_id: this.partyId },
(err: Error | null) => {
if (err) {
this.heartbeatFailCount++;
console.error(`[gRPC] Heartbeat failed (${this.heartbeatFailCount}/${this.MAX_HEARTBEAT_FAILS}):`, err.message);
this.emit('connectionError', err);
// 连续失败多次后触发重连
if (this.heartbeatFailCount >= this.MAX_HEARTBEAT_FAILS) {
this.triggerReconnect('Heartbeat failed');
}
} else {
// 心跳成功,重置失败计数
this.heartbeatFailCount = 0;
}
}
);
}
}, 30000); // 每 30 秒一次
}
/**
* 加入会话
*/
async joinSession(sessionId: string, partyId: string, joinToken: string): Promise<JoinSessionResponse> {
if (!this.client) {
throw new Error('Not connected');
}
return new Promise((resolve, reject) => {
(this.client as grpc.Client & { joinSession: (req: unknown, callback: (err: Error | null, res: JoinSessionResponse) => void) => void })
.joinSession(
{
session_id: sessionId,
party_id: partyId,
join_token: joinToken,
},
(err: Error | null, response: JoinSessionResponse) => {
if (err) {
reject(err);
} else {
resolve(response);
}
}
);
});
}
/**
* 订阅会话事件(带自动重连)
*/
subscribeSessionEvents(partyId: string): void {
if (!this.client) {
throw new Error('Not connected');
}
// 标记已订阅(用于重连后恢复)
this.eventStreamSubscribed = true;
// 取消现有流 - 先移除事件监听器再取消,防止旧流的 end 事件触发重连
if (this.eventStream) {
const oldStream = this.eventStream;
oldStream.removeAllListeners();
try {
oldStream.cancel();
} catch (e) {
// 忽略
}
}
this.eventStream = (this.client as grpc.Client & { subscribeSessionEvents: (req: unknown) => grpc.ClientReadableStream<SessionEvent> })
.subscribeSessionEvents({ party_id: partyId });
// 保存当前流的引用,用于在事件处理器中检查是否是当前活跃的流
const currentStream = this.eventStream;
this.eventStream.on('data', (event: SessionEvent) => {
this.emit('sessionEvent', event);
});
this.eventStream.on('error', (err: Error) => {
console.error('[gRPC] Session event stream error:', err.message);
this.emit('streamError', err);
// 只有当前活跃的流才触发重连,防止旧流的事件干扰
if (currentStream !== this.eventStream) {
console.log('[gRPC] Ignoring error from old event stream');
return;
}
// 非主动取消的错误触发重连
if (!err.message.includes('CANCELLED') && this.shouldReconnect) {
this.triggerReconnect('Event stream error');
}
});
this.eventStream.on('end', () => {
console.log('[gRPC] Session event stream ended');
this.emit('streamEnd');
// 只有当前活跃的流才触发重连,防止旧流的事件干扰
if (currentStream !== this.eventStream) {
console.log('[gRPC] Ignoring end from old event stream');
return;
}
// 流结束也触发重连
if (this.shouldReconnect && this.eventStreamSubscribed) {
this.triggerReconnect('Event stream ended');
}
});
}
/**
* 取消订阅会话事件
*/
unsubscribeSessionEvents(): void {
this.eventStreamSubscribed = false;
if (this.eventStream) {
try {
this.eventStream.cancel();
} catch (e) {
// 忽略
}
this.eventStream = null;
}
}
/**
* 订阅 MPC 消息(带自动重连)
*/
subscribeMessages(sessionId: string, partyId: string): void {
if (!this.client) {
throw new Error('Not connected');
}
if (!this.connected) {
throw new Error('gRPC client not connected');
}
// 保存订阅状态(用于重连后恢复)
this.activeMessageSubscription = { sessionId, partyId };
// 取消现有流 - 先移除事件监听器再取消,防止旧流的 end 事件触发重连
if (this.messageStream) {
const oldStream = this.messageStream;
oldStream.removeAllListeners();
try {
oldStream.cancel();
} catch (e) {
console.log('[gRPC] Ignored error while canceling old message stream:', (e as Error).message);
}
this.messageStream = null;
}
try {
this.messageStream = (this.client as grpc.Client & { subscribeMessages: (req: unknown) => grpc.ClientReadableStream<MPCMessage> })
.subscribeMessages({
session_id: sessionId,
party_id: partyId,
});
} catch (e) {
console.error('[gRPC] Failed to create message stream:', (e as Error).message);
this.activeMessageSubscription = null;
throw e;
}
// 保存当前流的引用,用于在事件处理器中检查是否是当前活跃的流
const currentStream = this.messageStream;
this.messageStream.on('data', (message: MPCMessage) => {
this.emit('mpcMessage', message);
});
this.messageStream.on('error', (err: Error) => {
console.error('[gRPC] Message stream error:', err.message);
this.emit('messageStreamError', err);
// 只有当前活跃的流才触发重连,防止旧流的事件干扰
if (currentStream !== this.messageStream) {
console.log('[gRPC] Ignoring error from old message stream');
return;
}
// 非主动取消的错误触发重连
if (!err.message.includes('CANCELLED') && this.shouldReconnect && this.activeMessageSubscription) {
this.triggerReconnect('Message stream error');
}
});
this.messageStream.on('end', () => {
console.log('[gRPC] Message stream ended');
this.emit('messageStreamEnd');
// 只有当前活跃的流才触发重连,防止旧流的事件干扰
if (currentStream !== this.messageStream) {
console.log('[gRPC] Ignoring end from old message stream');
return;
}
// 流结束也触发重连
if (this.shouldReconnect && this.activeMessageSubscription) {
this.triggerReconnect('Message stream ended');
}
});
}
/**
* 取消订阅 MPC 消息
*/
unsubscribeMessages(): void {
this.activeMessageSubscription = null;
if (this.messageStream) {
try {
this.messageStream.cancel();
} catch (e) {
// 忽略
}
this.messageStream = null;
}
}
/**
* 发送 MPC 消息
*/
async routeMessage(
sessionId: string,
fromParty: string,
toParties: string[],
roundNumber: number,
payload: Buffer
): Promise<string> {
if (!this.client) {
throw new Error('Not connected');
}
return new Promise((resolve, reject) => {
(this.client as grpc.Client & { routeMessage: (req: unknown, callback: (err: Error | null, res: { message_id: string }) => void) => void })
.routeMessage(
{
session_id: sessionId,
from_party: fromParty,
to_parties: toParties,
round_number: roundNumber,
payload: payload,
},
(err: Error | null, response: { message_id: string }) => {
if (err) {
reject(err);
} else {
resolve(response.message_id);
}
}
);
});
}
/**
* 报告完成
*/
async reportCompletion(sessionId: string, partyId: string, publicKey: Buffer): Promise<boolean> {
if (!this.client) {
throw new Error('Not connected');
}
return new Promise((resolve, reject) => {
(this.client as grpc.Client & { reportCompletion: (req: unknown, callback: (err: Error | null, res: { all_completed: boolean }) => void) => void })
.reportCompletion(
{
session_id: sessionId,
party_id: partyId,
public_key: publicKey,
},
(err: Error | null, response: { all_completed: boolean }) => {
if (err) {
reject(err);
} else {
resolve(response.all_completed);
}
}
);
});
}
/**
* 获取已注册的参与方列表
* @param roleFilter 可选,按角色过滤 (persistent/delegate/temporary)
* @param onlyOnline 可选,只返回在线的参与方
*/
async getRegisteredParties(roleFilter?: string, onlyOnline?: boolean): Promise<RegisteredParty[]> {
if (!this.client) {
throw new Error('Not connected');
}
return new Promise((resolve, reject) => {
(this.client as grpc.Client & { getRegisteredParties: (req: unknown, callback: (err: Error | null, res: GetRegisteredPartiesResponse) => void) => void })
.getRegisteredParties(
{
role_filter: roleFilter || '',
only_online: onlyOnline || false,
},
(err: Error | null, response: GetRegisteredPartiesResponse) => {
if (err) {
reject(err);
} else {
// 转换字段名从 snake_case 到 camelCase
const parties = (response.parties || []).map((p: { party_id?: string; partyId?: string; role?: string; party_role?: string; online?: boolean; registered_at?: string; registeredAt?: string; last_seen_at?: string; lastSeenAt?: string }) => ({
partyId: p.party_id || p.partyId || '',
role: p.role || p.party_role || '',
online: p.online || false,
registeredAt: p.registered_at || p.registeredAt || '',
lastSeenAt: p.last_seen_at || p.lastSeenAt || '',
}));
resolve(parties);
}
}
);
});
}
}