export type NodeSendEventFn = (opts: { nodeId: string; event: string; payloadJSON?: string | null; }) => void; export type NodeListConnectedFn = () => Array<{ nodeId: string }>; export type NodeSubscriptionManager = { subscribe: (nodeId: string, sessionKey: string) => void; unsubscribe: (nodeId: string, sessionKey: string) => void; unsubscribeAll: (nodeId: string) => void; sendToSession: ( sessionKey: string, event: string, payload: unknown, sendEvent?: NodeSendEventFn | null, ) => void; sendToAllSubscribed: ( event: string, payload: unknown, sendEvent?: NodeSendEventFn | null, ) => void; sendToAllConnected: ( event: string, payload: unknown, listConnected?: NodeListConnectedFn | null, sendEvent?: NodeSendEventFn | null, ) => void; clear: () => void; }; export function createNodeSubscriptionManager(): NodeSubscriptionManager { const nodeSubscriptions = new Map>(); const sessionSubscribers = new Map>(); const toPayloadJSON = (payload: unknown) => (payload ? JSON.stringify(payload) : null); const subscribe = (nodeId: string, sessionKey: string) => { const normalizedNodeId = nodeId.trim(); const normalizedSessionKey = sessionKey.trim(); if (!normalizedNodeId || !normalizedSessionKey) return; let nodeSet = nodeSubscriptions.get(normalizedNodeId); if (!nodeSet) { nodeSet = new Set(); nodeSubscriptions.set(normalizedNodeId, nodeSet); } if (nodeSet.has(normalizedSessionKey)) return; nodeSet.add(normalizedSessionKey); let sessionSet = sessionSubscribers.get(normalizedSessionKey); if (!sessionSet) { sessionSet = new Set(); sessionSubscribers.set(normalizedSessionKey, sessionSet); } sessionSet.add(normalizedNodeId); }; const unsubscribe = (nodeId: string, sessionKey: string) => { const normalizedNodeId = nodeId.trim(); const normalizedSessionKey = sessionKey.trim(); if (!normalizedNodeId || !normalizedSessionKey) return; const nodeSet = nodeSubscriptions.get(normalizedNodeId); nodeSet?.delete(normalizedSessionKey); if (nodeSet?.size === 0) nodeSubscriptions.delete(normalizedNodeId); const sessionSet = sessionSubscribers.get(normalizedSessionKey); sessionSet?.delete(normalizedNodeId); if (sessionSet?.size === 0) sessionSubscribers.delete(normalizedSessionKey); }; const unsubscribeAll = (nodeId: string) => { const normalizedNodeId = nodeId.trim(); const nodeSet = nodeSubscriptions.get(normalizedNodeId); if (!nodeSet) return; for (const sessionKey of nodeSet) { const sessionSet = sessionSubscribers.get(sessionKey); sessionSet?.delete(normalizedNodeId); if (sessionSet?.size === 0) sessionSubscribers.delete(sessionKey); } nodeSubscriptions.delete(normalizedNodeId); }; const sendToSession = ( sessionKey: string, event: string, payload: unknown, sendEvent?: NodeSendEventFn | null, ) => { const normalizedSessionKey = sessionKey.trim(); if (!normalizedSessionKey || !sendEvent) return; const subs = sessionSubscribers.get(normalizedSessionKey); if (!subs || subs.size === 0) return; const payloadJSON = toPayloadJSON(payload); for (const nodeId of subs) { sendEvent({ nodeId, event, payloadJSON }); } }; const sendToAllSubscribed = ( event: string, payload: unknown, sendEvent?: NodeSendEventFn | null, ) => { if (!sendEvent) return; const payloadJSON = toPayloadJSON(payload); for (const nodeId of nodeSubscriptions.keys()) { sendEvent({ nodeId, event, payloadJSON }); } }; const sendToAllConnected = ( event: string, payload: unknown, listConnected?: NodeListConnectedFn | null, sendEvent?: NodeSendEventFn | null, ) => { if (!sendEvent || !listConnected) return; const payloadJSON = toPayloadJSON(payload); for (const node of listConnected()) { sendEvent({ nodeId: node.nodeId, event, payloadJSON }); } }; const clear = () => { nodeSubscriptions.clear(); sessionSubscribers.clear(); }; return { subscribe, unsubscribe, unsubscribeAll, sendToSession, sendToAllSubscribed, sendToAllConnected, clear, }; }