nitropage
Version:
A free and open source, extensible visual page builder based on SolidStart.
141 lines (118 loc) • 3.37 kB
text/typescript
import {
Accessor,
createSignal,
onCleanup,
onMount,
startTransition,
} from "solid-js";
import { produce, SetStoreFunction } from "solid-js/store";
import { isServer } from "solid-js/web";
import { makeEventListener } from "./eventListener";
const UNREADY_TYPE = "$unready";
const READY_TYPE = "$ready";
const READY_CONFIRMED_TYPE = "$ready_ok";
export type Message = {
type:
| typeof UNREADY_TYPE
| typeof READY_TYPE
| typeof READY_CONFIRMED_TYPE
| (string & {});
data?: any;
rawData?: any;
};
export type PostMessage = (message: Message) => void;
export const createIframeSync = (options: {
id: string;
iframeRef?: Accessor<HTMLIFrameElement | undefined>;
onMessage?: (message: Message) => void;
}) => {
const { id, iframeRef } = options;
const [connected, setConnected] = createSignal(false);
const remoteWindow = () => iframeRef?.()?.contentWindow ?? parent;
const getQueues = (w: Window): Record<string, Record<number, any>> =>
((w as any)._iframeSyncQueues ??= {});
const getQueue = (w?: Window) => {
if (isServer) return [];
const queues = getQueues(w ?? window);
queues[id] ??= {};
return queues[id];
};
const queue = getQueue();
const remoteQueue = () => getQueue(remoteWindow());
let dataId = 0;
const postMessage: PostMessage = (message) => {
if (message.rawData != null) {
const id = dataId++;
remoteQueue()[id] = message.rawData;
message.rawData = id;
}
remoteWindow().postMessage(message);
};
makeEventListener(
() => window,
"message",
(event: MessageEvent) => {
const message = event.data as Message;
if (message.type === UNREADY_TYPE) {
setConnected(false);
return;
}
if (message.type === READY_TYPE) {
postMessage({ type: READY_CONFIRMED_TYPE });
return;
}
if (message.type === READY_CONFIRMED_TYPE) {
if (!connected()) {
postMessage({ type: READY_CONFIRMED_TYPE });
}
setConnected(true);
return;
}
if (message.rawData != null) {
const id = message.rawData;
message.rawData = queue[id];
delete queue[id];
}
options.onMessage?.(message);
},
);
const onDisconnect = () => {
postMessage({ type: UNREADY_TYPE });
};
onMount(() => {
postMessage({ type: READY_TYPE });
onCleanup(onDisconnect);
});
makeEventListener(() => window, "pagehide", onDisconnect);
return [connected, postMessage] as const;
};
const SET_STATE_TYPE = "$setStore";
export const makeSyncedSetStore = <T>(
id: string,
setState: SetStoreFunction<T>,
postMessage: (message: Message) => void,
) => {
let promise: Promise<any> | undefined;
const setStateWithTransition = async (fn: any) => {
await promise;
promise = startTransition(() => {
try {
setState(produce(fn));
} catch (err) {
console.error(err);
}
});
};
const onMessage = (message: Message) => {
if (message.type !== SET_STATE_TYPE || message.data !== id) {
return;
}
setStateWithTransition(message.rawData);
return true;
};
const setter = (fn: (state: T) => void) => {
setState(produce(fn));
postMessage({ type: SET_STATE_TYPE, data: id, rawData: fn });
};
return [setter, onMessage] as const;
};