Skip to content

Instantly share code, notes, and snippets.

@clinuxrulz
Last active March 4, 2024 09:15
Show Gist options
  • Save clinuxrulz/77f341832c6025bf10f0b183ee85e072 to your computer and use it in GitHub Desktop.
Save clinuxrulz/77f341832c6025bf10f0b183ee85e072 to your computer and use it in GitHub Desktop.
Browser Plugin Utility (bidirectional asynchronous function calls)
export class Plugin {
private src: String;
private callbacks: any;
private iframe?: HTMLIFrameElement;
private messageHandler: (e: MessageEvent) => void;
private recvMsgHandler: (msg: string) => void;
private nextCallId: number = 0;
private resolveMap: any;
onLoad: () => void = () => {};
private allocCallId(): string {
return "" + (this.nextCallId++);
}
constructor(src: String, callbacks: any) {
this.src = src;
this.callbacks = callbacks;
this.messageHandler = (e: MessageEvent) => {
if (e.source !== this.iframe.contentWindow) {
return;
}
this.recvMsgHandler(e.data);
};
this.recvMsgHandler = (msg: string) => {
var data = JSON.parse(msg);
if (data.type == "call") {
callbacks[data.fnName](data.params).then(result => {
this.iframe.contentWindow.postMessage(
JSON.stringify({
type: "response",
callId: data.callId,
result
}),
"*"
);
});
} else if (data.type == "response") {
this.resolveMap[data.callId](data.result);
delete this.resolveMap[data.callId];
}
};
this.resolveMap = {};
}
install(): void {
let bootstrap =
"var exports = {};" +
"var callWithResult = (() => {" +
"var resolveMap = {};" +
"var allocCallId = (() => {" +
"var callId = 0;" +
"return () => { return \"\" + (callId++); };" +
"})();" +
"function callWithResult2(fnName, params) {" +
"return new Promise(resolve => {" +
"var callId = allocCallId();" +
"resolveMap[callId] = resolve;" +
"parent.postMessage(" +
"JSON.stringify({" +
"type: \"call\"," +
"callId: callId," +
"fnName: fnName," +
"params: params" +
"})," +
"\"*\"" +
");" +
"});" +
"}" +
"window.addEventListener(\"message\", e => {" +
"var data = JSON.parse(e.data);" +
"if (data.type == \"response\") {" +
"var key = data.callId;" +
"resolveMap[key](data.result);" +
"delete resolveMap[key];" +
"} else if (data.type == \"call\") {" +
"exports[data.fnName](data.params).then(result => {" +
"parent.postMessage(" +
"JSON.stringify({" +
"type: \"response\"," +
"callId: data.callId," +
"result: result" +
"})," +
"\"*\"" +
");" +
"});" +
"}" +
"});" +
"return callWithResult2;" +
"})();";
for (var fnName in this.callbacks) {
if (this.callbacks.hasOwnProperty(fnName)) {
bootstrap += "function " + fnName + "(params) { return callWithResult(\"" + fnName + "\", params); };";
}
}
this.iframe = document.createElement("iframe");
this.iframe.addEventListener("load", e => this.onLoad());
this.iframe.style.width = "0";
this.iframe.style.height = "0";
this.iframe.srcdoc = "<html><head></head><body><script>" + bootstrap + this.src + "</script></body></html>";
document.body.append(this.iframe);
window.addEventListener("message", this.messageHandler);
}
uninstall(): void {
document.body.removeChild(this.iframe);
window.removeEventListener("message", this.messageHandler);
}
call(fnName: String, params: any): Promise<any> {
var callId = this.allocCallId();
return new Promise(resolve => {
this.resolveMap[callId] = resolve;
this.iframe.contentWindow.postMessage(
JSON.stringify({
type: "call",
callId,
fnName,
params,
}),
"*"
);
});
}
}
...
var testPlugin = new Plugin(
"testFn(42).then(result => console.log(\"test plugin called testFn with result:\", result));" +
"exports.testFn2 = params => new Promise(resolve => resolve(params));",
{
testFn: (params) => {
return new Promise(resolve => resolve(params));
}
}
);
testPlugin.onLoad = () => {
testPlugin.call("testFn2", 123).then(result => console.log("parent window called testFn2 from plugin with result:", result));
};
onMount(() => {
testPlugin.install();
});
onCleanup(() => {
testPlugin.uninstall();
});
...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment