state-event-target
Version:
Primary state with event-driven replicas.
79 lines (69 loc) • 1.96 kB
text/typescript
import { WindowBroadcast } from "./Broadcast";
type SinkEventTarget = EventTarget & {
set: (...args: any[]) => void;
clear: () => void;
onMissing: (cb: EventListener) => void;
};
type SinkEventHandlers<T extends SinkEventTarget> = {
[eventType: string]: (target: T, detail: any) => void;
};
export function sink<T extends SinkEventTarget>(
fromBroadcastSource: WindowBroadcast<any>,
toSinkTarget: T,
namespace: string,
eventHandlers: SinkEventHandlers<T> = {
"state:update": (target, detail) => {
if ("param" in detail || "key" in detail) {
target.set(detail.param ?? detail.key, detail.value);
} else {
target.set(detail.value);
}
},
"state:reset": (target) => target.clear(),
},
): T {
function handleMessage(event: MessageEvent<any>) {
const { type, namespace: msgNamespace, detail } = event.data;
if (msgNamespace !== namespace) {
return;
}
if (eventHandlers[type]) {
eventHandlers[type](toSinkTarget, detail);
} else {
toSinkTarget.dispatchEvent(new CustomEvent(type, { detail }));
}
}
fromBroadcastSource.onMessage(handleMessage);
toSinkTarget.onMissing((e: Event) => {
const detail = (e as CustomEvent).detail;
if (detail) {
fromBroadcastSource.postMessage({
type: "state:request",
namespace,
detail:
"param" in detail
? { param: detail.param }
: "key" in detail
? { key: detail.key }
: {},
});
}
});
return new Proxy(toSinkTarget, {
get(obj, prop) {
const value = obj[prop as keyof T];
if (prop === "destroy") {
return () => {
fromBroadcastSource.detach();
if (typeof value === "function") {
value.call(obj);
}
};
}
if (typeof value === "function") {
return value.bind(obj);
}
return value;
},
}) as T;
}