import { createContext, useContext, useSyncExternalStore, useCallback } from "react"; import type { Stats } from "../../shared/types"; type Listener = () => void; export interface StatsStore { subscribe: (listener: Listener) => () => void; getSnapshot: () => Map; getNodeSnapshot: (uid: string) => Stats | undefined; update: (statsMap: Map) => void; /** Internal version per node — used by useNodeStats to detect changes */ _nodeVersions: Map; _globalVersion: number; } export function createStatsStore(): StatsStore { let current = new Map(); const listeners = new Set(); const nodeVersions = new Map(); let globalVersion = 0; function notify() { for (const l of listeners) l(); } return { subscribe(listener: Listener) { listeners.add(listener); return () => listeners.delete(listener); }, getSnapshot() { return current; }, getNodeSnapshot(uid: string) { return current.get(uid); }, update(statsMap: Map) { let changed = false; for (const [key, value] of statsMap) { const existing = current.get(key); if (!existing || existing.cpu !== value.cpu || existing.mem_mb !== value.mem_mb) { current.set(key, value); nodeVersions.set(key, (nodeVersions.get(key) || 0) + 1); changed = true; } } if (changed) { globalVersion++; notify(); } }, _nodeVersions: nodeVersions, _globalVersion: globalVersion, }; } export const StatsStoreContext = createContext(null); /** * Subscribe to stats for a single node — only re-renders when THAT node's stats change. */ export function useNodeStats(uid: string): Stats | undefined { const store = useContext(StatsStoreContext); if (!store) throw new Error("useNodeStats must be used within StatsStoreContext.Provider"); const subscribe = useCallback( (cb: () => void) => store.subscribe(cb), [store] ); // Snapshot returns a value that changes identity only when this node's stats change const getSnapshot = useCallback(() => { const version = store._nodeVersions.get(uid) || 0; const stats = store.getNodeSnapshot(uid); // Return a stable reference: version acts as the cache key for useSyncExternalStore return { version, stats }; }, [store, uid]); // useSyncExternalStore compares by Object.is, so we need a ref-stable approach // We use a wrapper that caches the result object when version hasn't changed const cached = useSyncExternalStoreWithNodeCache(subscribe, store, uid); return cached; } // Internal helper: caches snapshot per uid so useSyncExternalStore sees stable refs const nodeSnapshotCaches = new WeakMap< StatsStore, Map >(); function useSyncExternalStoreWithNodeCache( subscribe: (cb: () => void) => () => void, store: StatsStore, uid: string ): Stats | undefined { const getSnapshot = useCallback(() => { if (!nodeSnapshotCaches.has(store)) { nodeSnapshotCaches.set(store, new Map()); } const cache = nodeSnapshotCaches.get(store)!; const currentVersion = store._nodeVersions.get(uid) || 0; const cached = cache.get(uid); if (cached && cached.version === currentVersion) { return cached; } const entry = { version: currentVersion, stats: store.getNodeSnapshot(uid) }; cache.set(uid, entry); return entry; }, [store, uid]); const snapshot = useSyncExternalStore(subscribe, getSnapshot); return snapshot.stats; } /** * Subscribe to the full stats map — re-renders on ANY stats change. * Use sparingly (e.g., for total resource display in header). */ export function useAllStats(): Map { const store = useContext(StatsStoreContext); if (!store) throw new Error("useAllStats must be used within StatsStoreContext.Provider"); const subscribe = useCallback( (cb: () => void) => store.subscribe(cb), [store] ); const getSnapshot = useCallback(() => store.getSnapshot(), [store]); return useSyncExternalStore(subscribe, getSnapshot); }