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; 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 | null = null; private eventStream: grpc.ClientReadableStream | 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) { 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 { // 保存连接参数用于重连 this.currentAddress = address; this.currentUseTLS = useTLS; this.shouldReconnect = true; return this.doConnect(address, useTLS); } private async doConnect(address: string, useTLS?: boolean): Promise { 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 { 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 { 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 { 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 }) .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 }) .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 { 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 { 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 { 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); } } ); }); } }