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, sample, 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;
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, { context: opts.context });
54
55 if (opts.setHeight) {
56 applet.onresize = () => {
57 frame.height = `${applet.height}px`;
58 frame.classList.add("has-loaded");
59 };
60 } else {
61 if (frame.contentDocument?.readyState === "complete") {
62 frame.classList.add("has-loaded");
63 }
64
65 frame.addEventListener("load", () => {
66 frame.classList.add("has-loaded");
67 });
68 }
69
70 return applet;
71}
72
73////////////////////////////////////////////
74// 🔮 Reactive state management
75////////////////////////////////////////////
76export function reactive<D, T>(
77 applet: Applet<D>,
78 dataFn: (data: D) => T,
79 effectFn: (t: T) => void,
80) {
81 const [getter, setter] = signal(dataFn(applet.data));
82
83 effect(() => effectFn(getter()));
84
85 applet.addEventListener("data", (event: AppletEvent) => {
86 setter(dataFn(event.data));
87 });
88}
89
90////////////////////////////////////////////
91// 🛠️
92////////////////////////////////////////////
93export function addScope<O extends Object>(astroScope: string, object: O): O {
94 return {
95 ...object,
96 attrs: { ...((object as any).attrs || {}), [`data-astro-cid-${astroScope}`]: "" },
97 };
98}
99
100export function comparable(value: unknown) {
101 return xxh32(JSON.stringify(value));
102}
103
104export function hs(
105 tag: string,
106 astroScope: string,
107 props?: Record<string, unknown> | Signal<Record<string, unknown>>,
108 configure?: ElementConfigurator,
109) {
110 const propsWithScope =
111 props && isSignal(props)
112 ? () => addScope(astroScope, props())
113 : addScope(astroScope, props || {});
114
115 return h(tag, propsWithScope, configure);
116}
117
118export function isPrimitive(test: unknown) {
119 return test !== Object(test);
120}