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