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

184 lines
5.2 KiB
TypeScript
Raw Normal View History

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import {
buildSidebarDataSyncKey,
getSidebarDataFreshness,
} from "@/components/sidebar/sidebar-sync";
2025-11-23 10:55:04 +08:00
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";
}
2025-11-23 10:55:04 +08:00
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;
}
2025-11-23 10:55:04 +08:00
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 {
2025-11-23 10:55:04 +08:00
const workspaceId = initialData.activeWorkspaceId;
const convexSidebar = useConvexSidebarData(workspaceId);
const shouldUseHttpFallback =
!convexSidebar.hasLiveSubscription && convexSidebar.canUseHttpFallback;
const [manualSnapshotState, setManualSnapshotState] = useState<{
workspaceId: string;
data: SidebarInitialData | null;
}>({
workspaceId,
data: null,
});
2025-11-23 10:55:04 +08:00
const httpQuery = useQuery({
2025-11-23 10:55:04 +08:00
queryKey: ["sidebar", workspaceId],
queryFn: () => requestSidebarData(workspaceId),
initialData,
staleTime: 1000 * 60,
enabled: shouldUseHttpFallback,
2025-11-23 10:55:04 +08:00
});
const manualSnapshot =
manualSnapshotState.workspaceId === workspaceId
? manualSnapshotState.data
: null;
const baseLiveData = convexSidebar.data ?? httpQuery.data ?? initialData;
const baseLiveDataSyncKey = useMemo(
() => buildSidebarDataSyncKey(baseLiveData),
[baseLiveData],
);
const baseLiveDataFreshness = useMemo(
() => getSidebarDataFreshness(baseLiveData),
[baseLiveData],
);
const manualSnapshotSyncKey = useMemo(
() => (manualSnapshot ? buildSidebarDataSyncKey(manualSnapshot) : null),
[manualSnapshot],
);
const manualSnapshotFreshness = useMemo(
() => (manualSnapshot ? getSidebarDataFreshness(manualSnapshot) : Number.NEGATIVE_INFINITY),
[manualSnapshot],
);
const liveData = useMemo(() => {
if (!manualSnapshot) {
return baseLiveData;
}
if (manualSnapshotSyncKey === baseLiveDataSyncKey) {
return baseLiveData;
}
return manualSnapshotFreshness >= baseLiveDataFreshness
? manualSnapshot
: baseLiveData;
}, [
baseLiveData,
baseLiveDataFreshness,
baseLiveDataSyncKey,
manualSnapshot,
manualSnapshotFreshness,
manualSnapshotSyncKey,
]);
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 (workspaceId) {
try {
const refreshedSnapshot = await requestSidebarData(workspaceId);
setManualSnapshotState({
workspaceId,
data: refreshedSnapshot,
});
return refreshedSnapshot;
} catch {
}
}
if (convexSidebar.hasLiveSubscription) {
await convexRefetchRef.current();
return liveDataRef.current;
}
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,
]);
2025-11-23 10:55:04 +08:00
}