Experiment to rebuild Diffuse using web applets.
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.connect<D>(frame.contentWindow, {
54 context: opts.context,
55 });
56
57 if (opts.setHeight) {
58 applet.onresize = () => {
59 frame.height = `${applet.height}px`;
60 frame.classList.add("has-loaded");
61 };
62 } else {
63 if (frame.contentDocument?.readyState === "complete") {
64 frame.classList.add("has-loaded");
65 }
66
67 frame.addEventListener("load", () => {
68 frame.classList.add("has-loaded");
69 });
70 }
71
72 return applet;
73}
74
75////////////////////////////////////////////
76// 🔮 Reactive state management
77////////////////////////////////////////////
78export function reactive<D, T>(
79 applet: Applet<D>,
80 dataFn: (data: D) => T,
81 effectFn: (t: T) => void,
82) {
83 const [getter, setter] = signal(dataFn(applet.data));
84
85 effect(() => {
86 effectFn(getter());
87 return undefined;
88 });
89
90 applet.addEventListener("data", (event: AppletEvent) => {
91 setter(dataFn(event.data));
92 });
93}
94
95////////////////////////////////////////////
96// 🛠️
97////////////////////////////////////////////
98export function addScope<O extends object>(astroScope: string, object: O): O {
99 return {
100 ...object,
101 attrs: {
102 ...((object as any).attrs || {}),
103 [`data-astro-cid-${astroScope}`]: "",
104 },
105 };
106}
107
108export function comparable(value: unknown) {
109 return xxh32(JSON.stringify(value));
110}
111
112export function hs(
113 tag: string,
114 astroScope: string,
115 props?: Record<string, unknown> | Signal<Record<string, unknown>>,
116 configure?: ElementConfigurator,
117) {
118 const propsWithScope =
119 props && isSignal(props)
120 ? () => addScope(astroScope, props())
121 : addScope(astroScope, props || {});
122
123 return h(tag, propsWithScope, configure);
124}
125
126export function isPrimitive(test: unknown) {
127 return test !== Object(test);
128}
129
130export function waitUntilAppletIsReady(applet: Applet): Promise<void> {
131 return new Promise((resolve) => {
132 if (applet.data?.ready === true) {
133 resolve();
134 return;
135 }
136
137 const callback = (event: AppletEvent) => {
138 if (event.data?.ready === true) {
139 applet.removeEventListener("data", callback);
140 resolve();
141 }
142 };
143
144 applet.addEventListener("data", callback);
145 });
146}