Something went wrong. Try again.
Experiment to rebuild Diffuse using web applets.
Something went wrong. Try again.
7.1 kB · 279 lines
TypeScript
at broadcast
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280import type { Applet, AppletEvent } from "@web-applets/sdk";
import QS from "query-string";import { applets } from "@web-applets/sdk";import { type ElementConfigurator, h } from "spellcaster/hyperscript.js";import { effect, isSignal, Signal, signal } from "spellcaster/spellcaster.js";import { xxh32 } from "xxh32";
////////////////////////////////////////////// 🪟 Applet connector////////////////////////////////////////////export async function applet<D>( src: string, opts: { addSlashSuffix?: boolean; applets?: Record<string, string>; container?: HTMLElement | Element; id?: string; setHeight?: boolean; } = {},): Promise<Applet<D>> { src = `${src}${ src.endsWith("/") ? "" : opts.addSlashSuffix === undefined || opts.addSlashSuffix === true ? "/" : "" }`;
if (opts.applets) { src = QS.stringifyUrl({ url: src, query: opts.applets }); }
const existingFrame: HTMLIFrameElement | null = window.document.querySelector(`[src="${src}"]`);
let frame;
if (existingFrame) { frame = existingFrame; } else { frame = document.createElement("iframe"); frame.src = src; if (opts.id) frame.id = opts.id;
if (opts.container) { opts.container.appendChild(frame); } else { window.document.body.appendChild(frame); } }
if (frame.contentWindow === null) { throw new Error("iframe does not have a contentWindow"); }
const applet = await applets.connect<D>(frame.contentWindow).catch((err) => { console.error("Error connecting to " + src, err); throw err; });
if (opts.setHeight) { applet.onresize = () => { frame.height = `${applet.height}px`; frame.classList.add("has-loaded"); }; } else { if (frame.contentDocument?.readyState === "complete") { frame.classList.add("has-loaded"); }
frame.addEventListener("load", () => { frame.classList.add("has-loaded"); }); }
return applet;}
////////////////////////////////////////////// 🪟 Applet registration////////////////////////////////////////////export function register<DataType = any>() { const id = `${location.host}${location.pathname}`; const scope = applets.register<DataType>();
let isMainInstance = true; let waitingForPong = true;
// One instance to rule them all // // Ping other instances to see if there are any. // As long as there aren't any, it is considered the main instance. // // Actions are performed on the main instance, // and data is replicated from main to the other instances. const channel = new BroadcastChannel(id);
channel.addEventListener("message", async (event) => { if (event.data === "PING") { channel.postMessage("PONG"); } else if (event.data?.type === "data") { scope.data = context.codec.decode(event.data.data); } else if (waitingForPong && event.data === "PONG") { waitingForPong = false; isMainInstance = false; } else if (isMainInstance && event.data?.type === "action" && event.data?.actionId) { const result = await scope.actionHandlers[event.data.actionId]?.(...event.data.arguments); channel.postMessage({ type: "actioncomplete", id: event.data.id, result, }); } });
setTimeout(() => (waitingForPong = false), 1000);
channel.postMessage("PING");
scope.ondata = (event) => { if (isMainInstance) { channel.postMessage({ type: "data", data: context.codec.encode(event.data), }); } };
const context = { scope,
get id() { return id; },
get data() { return scope.data; },
set data(data: DataType) { scope.data = data; },
codec: { decode: (data: any) => data as DataType, encode: (data: DataType) => data as any, },
isMainInstance() { return isMainInstance; },
setActionHandler: <H extends Function>(actionId: string, actionHandler: H) => { const handler = (...args: any) => { if (isMainInstance) { return actionHandler(...args); }
const actionMessage = { id: crypto.randomUUID(), type: "action", actionId, arguments: args, };
return new Promise((resolve) => { const actionCallback = (event: MessageEvent) => { if (event.data?.type === "actioncomplete" && event.data?.id === actionMessage.id) { channel.removeEventListener("message", actionCallback); resolve(event.data.result); } };
channel.addEventListener("message", actionCallback); channel.postMessage(actionMessage); }); };
scope.setActionHandler(actionId, handler); }, };
return context;}
////////////////////////////////////////////// 🔮 Reactive state management////////////////////////////////////////////export function reactive<D, T>( applet: Applet<D>, dataFn: (data: D) => T, effectFn: (t: T) => void,) { const [getter, setter] = signal(dataFn(applet.data));
effect(() => { effectFn(getter()); return undefined; });
applet.addEventListener("data", (event: AppletEvent) => { setter(dataFn(event.data)); });}
////////////////////////////////////////////// 🛠️////////////////////////////////////////////export function addScope<O extends object>(astroScope: string, object: O): O { return { ...object, attrs: { ...((object as any).attrs || {}), [`data-astro-cid-${astroScope}`]: "", }, };}
export function appletScopePort() { let port: MessagePort | undefined;
function connection(event: AppletEvent) { if (event.data?.type === "appletconnect") { window.removeEventListener("message", connection); port = (event as any).ports[0]; } }
window.addEventListener("message", connection);
return () => port;}
export function comparable(value: unknown) { return xxh32(JSON.stringify(value));}
export function hs( tag: string, astroScope: string, props?: Record<string, unknown> | Signal<Record<string, unknown>>, configure?: ElementConfigurator,) { const propsWithScope = props && isSignal(props) ? () => addScope(astroScope, props()) : addScope(astroScope, props || {});
return h(tag, propsWithScope, configure);}
export function isPrimitive(test: unknown) { return test !== Object(test);}
export function waitUntilAppletData<A>( applet: Applet<A>, dataFn: (a: A | undefined) => boolean,): Promise<void> { return new Promise((resolve) => { if (dataFn(applet.data) === true) { resolve(); return; }
const callback = (event: AppletEvent) => { if (dataFn(event.data) === true) { applet.removeEventListener("data", callback); resolve(); } };
applet.addEventListener("data", callback); });}
export function waitUntilAppletIsReady(applet: Applet): Promise<void> { return waitUntilAppletData(applet, (data) => !!data?.ready);}