///
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;