import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { buildSidebarDataSyncKey, getSidebarDataFreshness, } from "@/components/sidebar/sidebar-sync"; import type { SidebarInitialData } from "@/components/sidebar/types"; import { useConvexSidebarData } from "@/hooks/use-convex-sidebar-data"; export interface SidebarDataResult { data: SidebarInitialData; isLoading: boolean; error: Error | null; refetch: () => Promise; source: "convex-live" | "http-fallback" | "initial"; } const STABLE_SIDEBAR_CACHE_LIMIT = 12; const stableSidebarDataCache = new Map(); function getStableSidebarData(syncKey: string, data: SidebarInitialData): SidebarInitialData { const cached = stableSidebarDataCache.get(syncKey); if (cached) { return cached; } stableSidebarDataCache.set(syncKey, data); if (stableSidebarDataCache.size > STABLE_SIDEBAR_CACHE_LIMIT) { const oldestKey = stableSidebarDataCache.keys().next().value; if (typeof oldestKey === "string") { stableSidebarDataCache.delete(oldestKey); } } return data; } async function requestSidebarData(workspaceId: string): Promise { const response = await fetch(`/api/sidebar?workspaceId=${workspaceId}`, { method: "GET", credentials: "include", }); if (!response.ok) { const payload = await response.json().catch(() => ({})); const message = payload?.error ?? "获取侧边栏数据失败"; throw new Error(message); } return response.json(); } export function useSidebarData(initialData: SidebarInitialData): SidebarDataResult { const workspaceId = initialData.activeWorkspaceId; const convexSidebar = useConvexSidebarData(workspaceId); const [manualSnapshot, setManualSnapshot] = useState<{ workspaceId: string; data: SidebarInitialData; } | null>(null); const shouldUseHttpFallback = !convexSidebar.hasLiveSubscription && convexSidebar.canUseHttpFallback; const httpQuery = useQuery({ queryKey: ["sidebar", workspaceId], queryFn: () => requestSidebarData(workspaceId), initialData, staleTime: 1000 * 60, enabled: shouldUseHttpFallback, }); const baseLiveData = useMemo(() => { const currentManualSnapshot = manualSnapshot?.workspaceId === workspaceId ? manualSnapshot.data : null; const candidates = [ initialData, httpQuery.data, convexSidebar.data, currentManualSnapshot, ].filter((item): item is SidebarInitialData => Boolean(item)); return candidates.reduce((selected, candidate) => { const selectedFreshness = getSidebarDataFreshness(selected); const candidateFreshness = getSidebarDataFreshness(candidate); return candidateFreshness >= selectedFreshness ? candidate : selected; }, initialData); }, [ convexSidebar.data, httpQuery.data, initialData, manualSnapshot, workspaceId, ]); const liveData = baseLiveData; const isLoading = convexSidebar.hasLiveSubscription ? convexSidebar.isLoading : shouldUseHttpFallback ? httpQuery.isLoading : false; const source: SidebarDataResult["source"] = convexSidebar.data ? "convex-live" : shouldUseHttpFallback && httpQuery.data ? "http-fallback" : "initial"; const error = convexSidebar.error ?? httpQuery.error ?? null; const liveDataRef = useRef(liveData); const convexRefetchRef = useRef(convexSidebar.refetch); const httpRefetchRef = useRef(httpQuery.refetch); const liveDataSyncKey = useMemo(() => buildSidebarDataSyncKey(liveData), [liveData]); useEffect(() => { liveDataRef.current = liveData; }, [liveData]); useEffect(() => { convexRefetchRef.current = convexSidebar.refetch; }, [convexSidebar]); useEffect(() => { httpRefetchRef.current = httpQuery.refetch; }, [httpQuery]); const refetch = useCallback(async () => { if (convexSidebar.hasLiveSubscription) { await convexRefetchRef.current(); const snapshot = await requestSidebarData(workspaceId); setManualSnapshot({ workspaceId, data: snapshot }); return snapshot; } if (shouldUseHttpFallback) { return httpRefetchRef.current(); } return liveDataRef.current; }, [ convexSidebar.hasLiveSubscription, shouldUseHttpFallback, workspaceId, ]); const stableLiveData = getStableSidebarData(liveDataSyncKey, liveData); return useMemo(() => { return { data: stableLiveData, isLoading, error, refetch, source, }; }, [ error, isLoading, refetch, stableLiveData, source, ]); }