Files
mnote/wolai-frontend/src/hooks/use-sidebar-data.ts
T

155 lines
4.6 KiB
TypeScript

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<unknown>;
source: "convex-live" | "http-fallback" | "initial";
}
const STABLE_SIDEBAR_CACHE_LIMIT = 12;
const stableSidebarDataCache = new Map<string, SidebarInitialData>();
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<SidebarInitialData> {
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<SidebarDataResult>(() => {
return {
data: stableLiveData,
isLoading,
error,
refetch,
source,
};
}, [
error,
isLoading,
refetch,
stableLiveData,
source,
]);
}