diff --git a/pkgs/clan-app/ui/index.d.ts b/pkgs/clan-app/ui/index.d.ts index ae9aa8315..02786227c 100644 --- a/pkgs/clan-app/ui/index.d.ts +++ b/pkgs/clan-app/ui/index.d.ts @@ -1,4 +1,4 @@ -import { ProcessMessage } from "./src/hooks/api"; +import { ProcessMessage } from "./src/hooks/notify"; export {}; diff --git a/pkgs/clan-app/ui/src/hooks/api.ts b/pkgs/clan-app/ui/src/hooks/api.ts index 6a5c15b7f..7a1e7b0b5 100644 --- a/pkgs/clan-app/ui/src/hooks/api.ts +++ b/pkgs/clan-app/ui/src/hooks/api.ts @@ -99,14 +99,3 @@ export const callApi = ( }, }; }; - -export interface ProcessMessage { - topic: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: any; - origin: string | null; -} - -window.notifyBus = (data) => { - console.debug("Channel function called with data:", data); -}; diff --git a/pkgs/clan-app/ui/src/hooks/notify.ts b/pkgs/clan-app/ui/src/hooks/notify.ts new file mode 100644 index 000000000..d3af30841 --- /dev/null +++ b/pkgs/clan-app/ui/src/hooks/notify.ts @@ -0,0 +1,89 @@ +import { createSignal, onCleanup } from "solid-js"; +import { OperationNames } from "./api"; + +export interface ProcessMessage< + TData = unknown, + TTopic extends string = string, +> { + topic: TTopic; + data: TData; + origin: string | null; +} + +interface Subscriber { + filter: (msg: T) => boolean; + callback: (msg: T) => void; +} + +const subscribers: Subscriber[] = []; + +// Declare the global function on the window type +declare global { + interface Window { + notifyBus: (msg: ProcessMessage) => void; + } +} + +window.notifyBus = (msg: ProcessMessage) => { + console.debug("notifyBus called with:", msg); + for (const sub of subscribers) { + try { + if (sub.filter(msg)) { + sub.callback(msg); + } + } catch (e) { + console.error("Subscriber threw an error:", e); + } + } +}; + +/** + * Subscribe to any message + * + * Returns a function to unsubsribe itself + * + * consider using useNotify for reactive usage on solidjs + */ +export function _subscribeNotify( + filter: (msg: T) => boolean, + callback: (msg: T) => void, +) { + // Cast to shared subscriber type for storage + const sub: Subscriber = { + filter: filter as (msg: ProcessMessage) => boolean, + callback: callback as (msg: ProcessMessage) => void, + }; + subscribers.push(sub); + return () => { + const idx = subscribers.indexOf(sub); + if (idx >= 0) subscribers.splice(idx, 1); + }; +} + +/** + * Returns a reactive signal that tracks a message by the given filter predicate + * The signal has the value of the last message where filter was true + * null in case no message was recieved yet + */ +export function useNotify( + filter: (msg: T) => boolean = () => true as boolean, +) { + const [message, setMessage] = createSignal(null); + + const unsubscribe = _subscribeNotify(filter, (msg) => setMessage(() => msg)); + + onCleanup(unsubscribe); + + return message; +} + +/** + * Tracks any message that was sent from this api origin + * + */ +export function useNotifyOrigin< + T extends ProcessMessage = ProcessMessage, + K extends OperationNames = OperationNames, +>(origin: K) { + return useNotify((m) => m.origin === origin); +}