Experiment to rebuild Diffuse using web applets.
at s3 3.7 kB view raw
1import type { Applet, AppletEvent } from "@web-applets/sdk"; 2 3import { applets } from "@web-applets/sdk"; 4import { type ElementConfigurator, h } from "spellcaster/hyperscript.js"; 5import { effect, isSignal, Signal, signal } from "spellcaster/spellcaster.js"; 6import { xxh32 } from "xxh32"; 7 8//////////////////////////////////////////// 9// 🪟 Applet initialiser 10//////////////////////////////////////////// 11export async function applet<D>( 12 src: string, 13 opts: { 14 addSlashSuffix?: boolean; 15 context?: Window; 16 container?: HTMLElement | Element; 17 id?: string; 18 setHeight?: boolean; 19 } = {}, 20): Promise<Applet<D>> { 21 src = `${src}${ 22 src.endsWith("/") 23 ? "" 24 : opts.addSlashSuffix === undefined || opts.addSlashSuffix === true 25 ? "/" 26 : "" 27 }`; 28 29 const existingFrame: HTMLIFrameElement | null = (opts.context || window).document.querySelector( 30 `[src="${src}"]`, 31 ); 32 33 let frame; 34 35 if (existingFrame) { 36 frame = existingFrame; 37 } else { 38 frame = document.createElement("iframe"); 39 frame.src = src; 40 if (opts.id) frame.id = opts.id; 41 42 if (opts.container) { 43 opts.container.appendChild(frame); 44 } else { 45 (opts.context || window).document.body.appendChild(frame); 46 } 47 } 48 49 if (frame.contentWindow === null) { 50 throw new Error("iframe does not have a contentWindow"); 51 } 52 53 const applet = await applets 54 .connect<D>(frame.contentWindow, { 55 context: opts.context, 56 }) 57 .catch((err) => { 58 console.error("Error connecting to " + src, err); 59 throw err; 60 }); 61 62 if (opts.setHeight) { 63 applet.onresize = () => { 64 frame.height = `${applet.height}px`; 65 frame.classList.add("has-loaded"); 66 }; 67 } else { 68 if (frame.contentDocument?.readyState === "complete") { 69 frame.classList.add("has-loaded"); 70 } 71 72 frame.addEventListener("load", () => { 73 frame.classList.add("has-loaded"); 74 }); 75 } 76 77 return applet; 78} 79 80//////////////////////////////////////////// 81// 🔮 Reactive state management 82//////////////////////////////////////////// 83export function reactive<D, T>( 84 applet: Applet<D>, 85 dataFn: (data: D) => T, 86 effectFn: (t: T) => void, 87) { 88 const [getter, setter] = signal(dataFn(applet.data)); 89 90 effect(() => { 91 effectFn(getter()); 92 return undefined; 93 }); 94 95 applet.addEventListener("data", (event: AppletEvent) => { 96 setter(dataFn(event.data)); 97 }); 98} 99 100//////////////////////////////////////////// 101// 🛠️ 102//////////////////////////////////////////// 103export function addScope<O extends object>(astroScope: string, object: O): O { 104 return { 105 ...object, 106 attrs: { 107 ...((object as any).attrs || {}), 108 [`data-astro-cid-${astroScope}`]: "", 109 }, 110 }; 111} 112 113export function comparable(value: unknown) { 114 return xxh32(JSON.stringify(value)); 115} 116 117export function hs( 118 tag: string, 119 astroScope: string, 120 props?: Record<string, unknown> | Signal<Record<string, unknown>>, 121 configure?: ElementConfigurator, 122) { 123 const propsWithScope = 124 props && isSignal(props) 125 ? () => addScope(astroScope, props()) 126 : addScope(astroScope, props || {}); 127 128 return h(tag, propsWithScope, configure); 129} 130 131export function isPrimitive(test: unknown) { 132 return test !== Object(test); 133} 134 135export function waitUntilAppletIsReady(applet: Applet): Promise<void> { 136 return new Promise((resolve) => { 137 if (applet.data?.ready === true) { 138 resolve(); 139 return; 140 } 141 142 const callback = (event: AppletEvent) => { 143 if (event.data?.ready === true) { 144 applet.removeEventListener("data", callback); 145 resolve(); 146 } 147 }; 148 149 applet.addEventListener("data", callback); 150 }); 151}