import { useState, useEffect, useCallback, useRef } from "react"; import { fetchMemoryBackendStatus, type MemoryBackendStatus } from "../api"; interface UseMemoryBackendStatusOptions { /** Project ID for multi-project contexts */ projectId?: string; /** Auto-refresh interval in ms (default: 60 seconds) */ pollInterval?: number; /** Whether to auto-refresh (default: false) */ autoRefresh?: boolean; } /** * Hook for fetching memory backend status and capabilities. * * Features: * - Fetches backend status on mount * - Optional auto-refresh polling * - Manual refresh capability * - Loading and error states * - Guards against stale async updates on unmount/re-render * - Respects project context via optional projectId */ export function useMemoryBackendStatus(options: UseMemoryBackendStatusOptions = {}) { const { projectId, pollInterval = 60_000, autoRefresh = false } = options; const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const abortRef = useRef(null); const pollRef = useRef | null>(null); // Track if the component is still mounted to prevent stale updates const mountedRef = useRef(true); const fetchStatus = useCallback(async (isManual = false) => { // Cancel any in-flight request if (abortRef.current) { abortRef.current.abort(); } abortRef.current = new AbortController(); if (isManual) { setLoading(true); setError(null); } try { const data = await fetchMemoryBackendStatus(projectId); // Guard against stale updates when component unmounts or project changes if (!mountedRef.current) { return; } setStatus(data); setError(null); setLastUpdated(new Date()); setLoading(false); } catch (err: unknown) { // Don't update state if the request was aborted if (err instanceof Error && err.name === "AbortError") { return; } // Guard against stale updates if (!mountedRef.current) { return; } const message = err instanceof Error ? err.message : "Failed to fetch memory backend status"; setError(message); setLoading(false); } }, [projectId]); // Initial fetch useEffect(() => { mountedRef.current = true; fetchStatus(false); return () => { mountedRef.current = false; }; }, [fetchStatus]); // Auto-refresh polling useEffect(() => { if (!autoRefresh) return; pollRef.current = setInterval(() => { fetchStatus(false); }, pollInterval); return () => { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } }; }, [autoRefresh, pollInterval, fetchStatus]); // Cleanup on unmount useEffect(() => { return () => { mountedRef.current = false; if (abortRef.current) { abortRef.current.abort(); } if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } }; }, []); const refresh = useCallback(() => { return fetchStatus(true); }, [fetchStatus]); // Derived convenience getters const currentBackend = status?.currentBackend ?? null; const capabilities = status?.capabilities ?? null; const availableBackends = status?.availableBackends ?? []; const isReadable = capabilities?.readable ?? false; const isWritable = capabilities?.writable ?? false; const supportsAtomicWrite = capabilities?.supportsAtomicWrite ?? false; return { // Raw status status, // Convenience getters currentBackend, capabilities, availableBackends, isReadable, isWritable, supportsAtomicWrite, // State loading, error, lastUpdated, // Actions refresh, }; }