// @ts-ignore import script from './iframe-script.raw.js'; enum IFrameMessageType { error = 'error', execute = 'execute', executeResult = 'execute.res', function = 'function', functionRes = 'function.res', } const CALL_TIMEOUT = 10000; const MSG_IDENTIFIER = 'vev-iframe'; type IFrameMessage = { messageId: number; responseId?: number; type: IFrameMessageType; value: any; }; const packMsg = (value: any) => [MSG_IDENTIFIER, value]; function createIframe(resolve: (iframe: HTMLIFrameElement) => void) { const frame = document.createElement('iframe'); document.body.appendChild(frame); frame.setAttribute('sandbox', 'allow-scripts allow-forms allow-presentation'); frame.setAttribute('style', 'position:fixed;opacity:0.001;top:-1000px;left:-1000px'); frame.srcdoc = ''; frame.onload = () => resolve(frame); } export class IFrameHandler { private iframePromise: Promise; private lastMessageId = 0; private lastFunctionId = 0; private listener: { [messageId: number]: (type: IFrameMessageType, value: any) => void; } = {}; private functionMap: { [functionId: number]: (...args: any[]) => any } = {}; constructor() { this.iframePromise = new Promise(createIframe).then(this.initFrame.bind(this)); } async getIframe(): Promise { return this.iframePromise; } private initFrame(iframe: HTMLIFrameElement) { window.addEventListener('message', (e) => { if ( e.origin === 'null' && e.source === iframe.contentWindow && Array.isArray(e.data) && e.data[0] === 'vev-iframe' ) { const [, data] = e.data; this.handleMessage(data); } }); return iframe; } private handleMessage({ type, value, responseId, messageId }: IFrameMessage) { if (responseId && responseId in this.listener) this.listener[responseId](type, value); switch (type) { case IFrameMessageType.function: return this.handleFunctionCall(messageId, value); } } private async handleFunctionCall( messageId: number, { functionId, args = [], }: { functionId: number; args: any[]; }, ) { const func = this.functionMap[functionId]; if (func) { const value = await func(...args); this.send(IFrameMessageType.functionRes, value, messageId); } } async execute(functionScript: string, ...args: any[]): Promise { const messageId = await this.send(IFrameMessageType.execute, { script: functionScript, args, }); return this.awaitResponse(messageId); } private awaitResponse(id: number) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { delete this.listener[id]; reject(); }, CALL_TIMEOUT); this.listener[id] = (type, value) => { clearTimeout(timeout); if (type === IFrameMessageType.error) reject(value); resolve(value); }; }); } private async send(type: IFrameMessageType, value: any, responseId?: number): Promise { const iframe = await this.getIframe(); const messageId = ++this.lastMessageId; const message = { type, messageId, responseId, value: this.serializeValue(value), } as IFrameMessage; iframe.contentWindow?.postMessage(packMsg(message), '*'); return messageId; } private serializeValue = (arg: any): any => { if (Array.isArray(arg)) return arg.map(this.serializeValue); switch (typeof arg) { case 'function': return this.registerFunction(arg); case 'object': { const res: Record = {}; for (const key in arg) { res[key] = this.serializeValue(arg[key]); } return res; } } return arg; }; private registerFunction(func: (...args: any) => any) { const id = ++this.lastFunctionId; this.functionMap[id] = func; return { type: 'vev-function', id }; } }