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
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}