1
0
silverbullet/web/components/widget_sandbox_iframe.ts

187 lines
5.1 KiB
TypeScript
Raw Normal View History

import { WidgetContent } from "$sb/app_event.ts";
import { Client } from "../client.ts";
import { panelHtml } from "./panel_html.ts";
2023-11-12 10:50:49 +00:00
/**
* Implements sandbox widgets using iframe with a pooling mechanism to speed up loading
*/
type PreloadedIFrame = {
// The wrapped iframe element
iframe: HTMLIFrameElement;
// Has the iframe been used yet?
used: boolean;
// Is it ready (that is: has the initial load happened)
ready: Promise<void>;
};
const iframePool = new Set<PreloadedIFrame>();
const desiredPoolSize = 5;
const gcInterval = 5000;
function populateIFramePool() {
while (iframePool.size < desiredPoolSize) {
iframePool.add(prepareSandboxIFrame());
}
}
populateIFramePool();
setInterval(() => {
// Iterate over all iframes
for (const preloadedIframe of iframePool) {
if (
// Is this iframe in use, but has it since been removed from the DOM?
preloadedIframe.used && !document.body.contains(preloadedIframe.iframe)
) {
// Ditch it
console.log("Garbage collecting iframe", preloadedIframe);
iframePool.delete(preloadedIframe);
}
}
populateIFramePool();
}, gcInterval);
export function prepareSandboxIFrame(): PreloadedIFrame {
console.log("Preloading iframe");
const iframe = document.createElement("iframe");
iframe.src = "about:blank";
const ready = new Promise<void>((resolve) => {
iframe.onload = () => {
iframe.contentDocument!.write(panelHtml);
resolve();
};
});
return {
iframe,
used: false,
ready,
};
}
function claimIFrame(): PreloadedIFrame {
for (const preloadedIframe of iframePool) {
if (!preloadedIframe.used) {
console.log("Took iframe from pool");
preloadedIframe.used = true;
return preloadedIframe;
}
}
// Nothing available in the pool, let's spin up a new one and add it to the pool
const newPreloadedIFrame = prepareSandboxIFrame();
newPreloadedIFrame.used = true;
iframePool.add(newPreloadedIFrame);
return newPreloadedIFrame;
}
export function mountIFrame(
preloadedIFrame: PreloadedIFrame,
client: Client,
widgetHeightCacheKey: string | null,
content: WidgetContent | Promise<WidgetContent>,
onMessage?: (message: any) => void,
) {
2023-11-12 10:50:49 +00:00
const iframe = preloadedIFrame.iframe;
preloadedIFrame.ready.then(async () => {
const messageListener = (evt: any) => {
(async () => {
if (evt.source !== iframe.contentWindow) {
return;
}
const data = evt.data;
if (!data) {
return;
}
switch (data.type) {
case "syscall": {
const { id, name, args } = data;
try {
const result = await client.system.localSyscall(name, args);
if (!iframe.contentWindow) {
// iFrame already went away
return;
}
iframe.contentWindow!.postMessage({
type: "syscall-response",
id,
result,
});
} catch (e: any) {
if (!iframe.contentWindow) {
// iFrame already went away
return;
}
iframe.contentWindow!.postMessage({
type: "syscall-response",
id,
error: e.message,
});
}
2023-11-12 10:50:49 +00:00
break;
}
2023-11-12 10:50:49 +00:00
case "setHeight":
iframe.style.height = data.height + "px";
if (widgetHeightCacheKey) {
client.space.setCachedWidgetHeight(
widgetHeightCacheKey,
data.height,
);
}
break;
default:
if (onMessage) {
onMessage(data);
}
}
2023-11-12 10:50:49 +00:00
})().catch((e) => {
console.error("Message listener error", e);
});
};
2023-10-09 19:02:57 +00:00
// Subscribe to message event on global object (to receive messages from iframe)
globalThis.addEventListener("message", messageListener);
// Only run this code once
iframe.onload = null;
2023-11-12 10:50:49 +00:00
const resolvedContent = await Promise.resolve(content);
if (!iframe.contentWindow) {
console.warn("Iframe went away or content was not loaded");
return;
}
if (resolvedContent.html) {
iframe.contentWindow!.postMessage({
type: "html",
html: resolvedContent.html,
script: resolvedContent.script,
theme: document.getElementsByTagName("html")[0].dataset.theme,
});
} else if (resolvedContent.url) {
iframe.contentWindow!.location.href = resolvedContent.url;
if (resolvedContent.height) {
iframe.style.height = resolvedContent.height + "px";
2023-10-10 07:38:06 +00:00
}
2023-11-12 10:50:49 +00:00
if (resolvedContent.width) {
iframe.style.width = resolvedContent.width + "px";
}
2023-11-12 10:50:49 +00:00
}
}).catch(console.error);
}
2023-11-12 10:50:49 +00:00
export function createWidgetSandboxIFrame(
client: Client,
widgetHeightCacheKey: string | null,
content: WidgetContent | Promise<WidgetContent>,
onMessage?: (message: any) => void,
) {
// console.log("Claiming iframe");
const preloadedIFrame = claimIFrame();
mountIFrame(
preloadedIFrame,
client,
widgetHeightCacheKey,
content,
onMessage,
);
return preloadedIFrame.iframe;
}