/* * Copyright 2025 coze-dev Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import EventEmitter from 'eventemitter3'; interface BusinessData { code: number; // 0: success, others: error codes, business tidying data?: T; message?: string; } enum MessageType { REQUEST = 'request', RESPONSE = 'response', } interface MessageChannelEvent { syncNo: number; type: MessageType; senderName?: string; toName?: string; eventName?: string; requestData?: unknown; respondData?: BusinessData; } enum ErrorType { TIMEOUT = -1, UNKNOWN = -2, } type DestoryListenerFun = () => void; const DEFAULT_TIMEOUT = 3000; export class PostMessageChannel { private eventEmitter: EventEmitter = new EventEmitter(); private syncEventId: number = Math.ceil(10000 * Math.random()); private senderName = ''; private toName?: string = ''; private targetOrigin = ''; private channelPort: Window; private onMessageFunc?: (event: MessageEvent) => void; public constructor({ channelPort, senderName, toName, targetOrigin = '*', }: { channelPort: Window; senderName: string; toName?: string; targetOrigin?: string; }) { this.channelPort = channelPort; this.senderName = senderName; this.toName = toName; this.targetOrigin = targetOrigin; this.initListner(); } public destory() { this.onMessageFunc && window.removeEventListener('message', this.onMessageFunc); this.eventEmitter.removeAllListeners(); } public async send( eventName: string, data: T1, timeout = DEFAULT_TIMEOUT, ): Promise> { const syncNo = this.syncEventId++; const messageEvent: MessageChannelEvent = { syncNo, type: MessageType.REQUEST, senderName: this.senderName, toName: this.toName, eventName, requestData: data, }; this.channelPort.postMessage(messageEvent, this.targetOrigin); return await this.awaitRespond(syncNo, timeout); } public onRequest( eventName: string, callback: (data: T1) => BusinessData | Promise>, ): DestoryListenerFun { const onHandle = async (event: MessageEvent) => { const messageEvent = event.data as MessageChannelEvent; const result = await callback(messageEvent.requestData as T1); const responseMessageEvent: MessageChannelEvent = { syncNo: messageEvent.syncNo, type: MessageType.RESPONSE, toName: messageEvent.senderName || '', senderName: this.senderName, eventName, respondData: result, }; if (event.source) { // @ts-expect-error -- linter-disable-autofix event.source.postMessage(responseMessageEvent, event.origin); } else { this.channelPort.postMessage(responseMessageEvent, this.targetOrigin); } }; this.eventEmitter.on(`${MessageType.REQUEST}_${eventName}`, onHandle); return () => { this.eventEmitter.off(`${MessageType.REQUEST}_${eventName}`, onHandle); }; } private initListner() { this.onMessageFunc = (event: MessageEvent) => { const messageEvent = event.data as MessageChannelEvent; if ( messageEvent.type === MessageType.RESPONSE && this.senderName === messageEvent.toName ) { this.eventEmitter.emit( `${MessageType.RESPONSE}_${messageEvent.syncNo}`, messageEvent, ); } else if ( messageEvent.type === MessageType.REQUEST && (!messageEvent.toName || this.senderName === messageEvent.toName) ) { this.eventEmitter.emit( `${MessageType.REQUEST}_${messageEvent.eventName}`, event, ); } }; window.addEventListener('message', this.onMessageFunc); } private awaitRespond(syncNo: number, timeout): Promise> { const eventName = `${MessageType.RESPONSE}_${syncNo}`; return new Promise(resolve => { const timeoutId = setTimeout(() => { this.eventEmitter.emit(eventName, { respondData: { code: ErrorType.TIMEOUT, message: 'timeout', }, }); }, timeout); this.eventEmitter.once( eventName, (messageEvent: MessageChannelEvent) => { clearTimeout(timeoutId); resolve( messageEvent.respondData || { code: ErrorType.UNKNOWN, message: 'unknow error', }, ); }, ); }); } }