Files
fusion/packages/dashboard/app/hooks/useMemoryBackendStatus.ts
gsxdsm 4e9eb8b8a1 feat(FN-1771): add dashboard memory settings UX with backend selector and capability-aware editing
- Add /api/memory/backend endpoint returning current backend, capabilities, and available backends
- Create useMemoryBackendStatus hook with polling and frontend-friendly interface
- Add MemorySettingsSection to SettingsModal with backend selector dropdown
- Implement capability-aware UI: readonly backends disable save, fallback on unknown types
- Add comprehensive tests for API endpoints and hook behavior
- Update architecture docs with memory backend architecture section
2026-04-13 22:32:31 -07:00

149 lines
3.9 KiB
TypeScript

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<MemoryBackendStatus | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const abortRef = useRef<AbortController | null>(null);
const pollRef = useRef<ReturnType<typeof setInterval> | 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,
};
}