iframe widget pooling (experimental)
parent
1b242dea55
commit
f3d72ca6f4
|
@ -57,7 +57,6 @@ export async function renderMentions() {
|
||||||
"ps",
|
"ps",
|
||||||
1,
|
1,
|
||||||
` <style>${css}</style>
|
` <style>${css}</style>
|
||||||
<link rel="stylesheet" href="/.client/main.css" />
|
|
||||||
<div id="sb-main"><div id="sb-editor"><div class="cm-editor">
|
<div id="sb-main"><div id="sb-editor"><div class="cm-editor">
|
||||||
<div id="button-bar">
|
<div id="button-bar">
|
||||||
<button id="reload-button" title="Reload"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg></button>
|
<button id="reload-button" title="Reload"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg></button>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
/* Reset SB styles */
|
/* Reset SB styles */
|
||||||
html,
|
|
||||||
body {
|
body {
|
||||||
height: initial !important;
|
font-family: var(--editor-font);
|
||||||
overflow: initial !important;
|
color: var(--root-color);
|
||||||
background-color: var(--root-background-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#sb-main {
|
#sb-main {
|
||||||
|
@ -26,11 +25,6 @@ body {
|
||||||
height: initial !important;
|
height: initial !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: var(--editor-font);
|
|
||||||
color: var(--root-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|
|
@ -33,8 +33,6 @@ export async function wrapHTML(html: string): Promise<string> {
|
||||||
const css = await asset.readAsset("assets/markdown_widget.css");
|
const css = await asset.readAsset("assets/markdown_widget.css");
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<!-- Load SB's own CSS here too -->
|
|
||||||
<link rel="stylesheet" href="/.client/main.css" />
|
|
||||||
<!-- In addition to some custom CSS -->
|
<!-- In addition to some custom CSS -->
|
||||||
<style>${css}</style>
|
<style>${css}</style>
|
||||||
<!-- Wrap the whole thing in something SB-like to get access to styles -->
|
<!-- Wrap the whole thing in something SB-like to get access to styles -->
|
||||||
|
|
|
@ -99,6 +99,16 @@ function loadJsByUrl(url) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<!-- Load SB's own CSS here too -->
|
||||||
|
<link rel="stylesheet" href="/.client/main.css" />
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: initial !important;
|
||||||
|
overflow: initial !important;
|
||||||
|
background-color: var(--root-background-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
|
@ -2,100 +2,185 @@ import { WidgetContent } from "$sb/app_event.ts";
|
||||||
import { Client } from "../client.ts";
|
import { Client } from "../client.ts";
|
||||||
import { panelHtml } from "./panel_html.ts";
|
import { panelHtml } from "./panel_html.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
) {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "setHeight":
|
||||||
|
iframe.style.height = data.height + "px";
|
||||||
|
if (widgetHeightCacheKey) {
|
||||||
|
client.space.setCachedWidgetHeight(
|
||||||
|
widgetHeightCacheKey,
|
||||||
|
data.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (onMessage) {
|
||||||
|
onMessage(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})().catch((e) => {
|
||||||
|
console.error("Message listener error", e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subscribe to message event on global object (to receive messages from iframe)
|
||||||
|
globalThis.addEventListener("message", messageListener);
|
||||||
|
// Only run this code once
|
||||||
|
iframe.onload = null;
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
if (resolvedContent.width) {
|
||||||
|
iframe.style.width = resolvedContent.width + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
export function createWidgetSandboxIFrame(
|
export function createWidgetSandboxIFrame(
|
||||||
client: Client,
|
client: Client,
|
||||||
widgetHeightCacheKey: string | null,
|
widgetHeightCacheKey: string | null,
|
||||||
content: WidgetContent | Promise<WidgetContent>,
|
content: WidgetContent | Promise<WidgetContent>,
|
||||||
onMessage?: (message: any) => void,
|
onMessage?: (message: any) => void,
|
||||||
) {
|
) {
|
||||||
const iframe = document.createElement("iframe");
|
// console.log("Claiming iframe");
|
||||||
iframe.src = "about:blank";
|
const preloadedIFrame = claimIFrame();
|
||||||
|
mountIFrame(
|
||||||
const messageListener = (evt: any) => {
|
preloadedIFrame,
|
||||||
(async () => {
|
client,
|
||||||
if (evt.source !== iframe.contentWindow) {
|
widgetHeightCacheKey,
|
||||||
return;
|
content,
|
||||||
}
|
onMessage,
|
||||||
const data = evt.data;
|
);
|
||||||
if (!data) {
|
return preloadedIFrame.iframe;
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "setHeight":
|
|
||||||
iframe.style.height = data.height + "px";
|
|
||||||
if (widgetHeightCacheKey) {
|
|
||||||
client.space.setCachedWidgetHeight(
|
|
||||||
widgetHeightCacheKey,
|
|
||||||
data.height,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (onMessage) {
|
|
||||||
onMessage(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})().catch((e) => {
|
|
||||||
console.error("Message listener error", e);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
iframe.onload = () => {
|
|
||||||
iframe.contentDocument!.write(panelHtml);
|
|
||||||
|
|
||||||
// Subscribe to message event on global object (to receive messages from iframe)
|
|
||||||
globalThis.addEventListener("message", messageListener);
|
|
||||||
// Only run this code once
|
|
||||||
iframe.onload = null;
|
|
||||||
Promise.resolve(content).then((content) => {
|
|
||||||
if (!iframe.contentWindow) {
|
|
||||||
console.warn("Iframe went away or content was not loaded");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (content.html) {
|
|
||||||
iframe.contentWindow!.postMessage({
|
|
||||||
type: "html",
|
|
||||||
html: content.html,
|
|
||||||
script: content.script,
|
|
||||||
theme: document.getElementsByTagName("html")[0].dataset.theme,
|
|
||||||
});
|
|
||||||
} else if (content.url) {
|
|
||||||
iframe.contentWindow!.location.href = content.url;
|
|
||||||
if (content.height) {
|
|
||||||
iframe.style.height = content.height + "px";
|
|
||||||
}
|
|
||||||
if (content.width) {
|
|
||||||
iframe.style.width = content.width + "px";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).catch(console.error);
|
|
||||||
};
|
|
||||||
|
|
||||||
return iframe;
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue