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, }; }