/// import { ElectronMessagePortPolyfill } from '../interfaces/polyfill.interface'; import { generateRandomString, isObject } from './helper'; /* eslint-disable @typescript-eslint/no-explicit-any */ const EVENT_TIMEOUT_MS = 10000; // 10 seconds; export interface BetterMessagePortOptions { additionalMessagePayload?: Record; } export interface BetterMessagePortResult { name: string; type: 'result'; error?: boolean; result?: unknown; errorMessage?: string; } export interface BetterMessagePortSend { name: string; type: 'send'; args: unknown[]; isSync?: boolean; messageId?: string; } export type BetterMessagePayload = | BetterMessagePortSend | BetterMessagePortResult; type FuncType = (...args: any) => any; type MessagePortType = MessagePort | ElectronMessagePortPolyfill; type ExtractParams = T[K] extends FuncType ? Parameters : T[K] extends Array ? T[K] : [T[K]]; type ExtractReturnType = T[K] extends FuncType ? ReturnType : void; export interface NormalizeMessagePort { postMessage: MessagePortType['postMessage']; onMessage(callback: (event: { data: T }) => void): void; offMessage(callback: (event: { data: T }) => void): void; } type PostMessageFunc = (data: unknown) => void; export interface BetterMessagePortAsyncOptions { eventTimeoutMs?: number; } export class BetterMessagePortAsync { private messages: Map< PropertyKey, { resolve(value: any): void; reject(reason?: any): void } > = new Map(); private listeners: Map any> = new Map(); constructor( private postMessage: PostMessageFunc, private options?: BetterMessagePortAsyncOptions, ) {} async messageHandler(data: BetterMessagePayload) { if ( !isObject(data) || (data.type !== 'result' && data.type !== 'send') || data.isSync ) return; if (data.type === 'result') { const promise = this.messages.get(data.name); if (promise) { if (data.error) { promise.reject(new Error(data.errorMessage)); } else { promise.resolve(data.result); } this.messages.delete(data.name); } return; } const messageListener = this.listeners.get(data.name); const payload: BetterMessagePortResult = { type: 'result', name: data.messageId || data.name, }; if (!messageListener) { payload.error = true; payload.errorMessage = `"${data.name}" doesn't have handler`; this.postMessage(payload); return; } try { payload.result = await messageListener(...data.args); } catch (error) { payload.error = true; payload.errorMessage = (error as Error).message; } this.postMessage(payload); } on< K extends keyof MessagePortEvents, R = ExtractReturnType, >( name: K, callback: ( ...args: ExtractParams ) => Promise>, ) { this.listeners.set(name, callback); return () => this.listeners.delete(name); } off(name: K) { this.listeners.delete(name); } sendMessage( name: K | { name: K; messageId: string }, ...args: ExtractParams ): Promise> { return new Promise((resolve, reject) => { const isObjectEvent = isObject(name); const messageId = isObjectEvent ? name.messageId : `promise::${generateRandomString(5)}`; const eventName = isObjectEvent ? name.name : name; const timeoutMs = this.options?.eventTimeoutMs ?? EVENT_TIMEOUT_MS; const timeout = setTimeout(() => { reject(new Error('TIMEOUT')); this.messages.delete(messageId); }, timeoutMs); this.messages.set(messageId, { resolve(value) { clearTimeout(timeout); resolve(value); }, reject(reason) { clearTimeout(timeout); reject(reason); }, }); this.postMessage({ args, messageId, type: 'send', name: eventName, } as BetterMessagePortSend); }); } destroy() { this.messages.forEach((message) => { message.reject(new Error('CLOSED')); }); } } export class BetterMessagePortSync { private listeners: Record any)[]> = {}; constructor(private postMessage: PostMessageFunc) {} messageHandler(data: BetterMessagePayload) { if (!isObject(data) || data.type === 'result' || !data.isSync) return; const listeners = this.listeners[data.name]; if (!listeners) return; listeners.forEach((listener) => listener(...data.args)); } on( name: K, callback: (...args: ExtractParams) => void, ) { if (!this.listeners[name]) this.listeners[name] = []; this.listeners[name].push(callback); return () => this.off(name, callback); } off( name: K, callback: (...args: ExtractParams) => void, ) { if (!this.listeners[name]) return; const index = this.listeners[name].indexOf(callback); if (index === -1) return; this.listeners[name].splice(index, 1); } sendMessage( name: K, ...args: ExtractParams ): void { this.postMessage({ name, args, type: 'send', isSync: true, } as BetterMessagePortSend); } destroy() { this.listeners = {}; } } class BetterMessagePort { sync: BetterMessagePortSync; async: BetterMessagePortAsync; private nomalizedMessagePort: NormalizeMessagePort; constructor( private messagePort: MessagePortType, options?: BetterMessagePortAsyncOptions, ) { this.nomalizedMessagePort = BetterMessagePort.normalizeMessagePort(messagePort); this.sync = new BetterMessagePortSync( this.nomalizedMessagePort.postMessage, ); this.async = new BetterMessagePortAsync( this.nomalizedMessagePort.postMessage, options, ); this.onMessage = this.onMessage.bind(this); this.nomalizedMessagePort.onMessage(this.onMessage); messagePort.start(); } private onMessage({ data }: { data: BetterMessagePayload }) { if (data?.type === 'send' && data.isSync) { this.sync.messageHandler(data); } else { this.async.messageHandler(data); } } destroy() { this.nomalizedMessagePort.offMessage(this.onMessage); this.messagePort.close(); this.sync.destroy(); this.async.destroy(); } static normalizeMessagePort( messagePort: MessagePortType, ): NormalizeMessagePort { const isMain = 'addEventListener' in messagePort; return { postMessage: messagePort.postMessage.bind(messagePort), onMessage(callback: (event: { data: T }) => void) { if (isMain) messagePort.addEventListener('message', callback); else messagePort.addListener('message', callback); }, offMessage(callback: (event: { data: T }) => void) { if (isMain) messagePort.removeEventListener('message', callback); else messagePort.removeListener('message', callback); }, }; } static createStandalone( type: 'async', port: MessagePortType, ): BetterMessagePortAsync; static createStandalone( type: 'sync', port: MessagePortType, ): BetterMessagePortSync; static createStandalone( type: 'sync' | 'async', port: MessagePortType, ): BetterMessagePortSync | BetterMessagePortAsync { const messagePort = BetterMessagePort.normalizeMessagePort(port); let betterMessagePort: | BetterMessagePortAsync | BetterMessagePortSync; if (type === 'async') { betterMessagePort = new BetterMessagePortAsync(messagePort.postMessage); } else { betterMessagePort = new BetterMessagePortSync(messagePort.postMessage); } messagePort.onMessage(({ data }) => { betterMessagePort.messageHandler(data); }); port.start(); return betterMessagePort; } } export default BetterMessagePort;