2026-04-22 05:57:06 +08:00
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
2026-04-17 23:36:24 +08:00
|
|
|
import { useQuery } from "@tanstack/react-query";
|
2026-04-22 05:57:06 +08:00
|
|
|
import {
|
|
|
|
|
buildSidebarDataSyncKey,
|
|
|
|
|
getSidebarDataFreshness,
|
|
|
|
|
} from "@/components/sidebar/sidebar-sync";
|
2025-11-23 10:55:04 +08:00
|
|
|
import type { SidebarInitialData } from "@/components/sidebar/types";
|
2026-04-17 23:36:24 +08:00
|
|
|
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
|
|
|
|
2026-04-22 05:57:06 +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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:36:24 +08:00
|
|
|
export function useSidebarData(initialData: SidebarInitialData): SidebarDataResult {
|
2025-11-23 10:55:04 +08:00
|
|
|
const workspaceId = initialData.activeWorkspaceId;
|
2026-04-17 23:36:24 +08:00
|
|
|
const convexSidebar = useConvexSidebarData(workspaceId);
|
|
|
|
|
const shouldUseHttpFallback =
|
|
|
|
|
!convexSidebar.hasLiveSubscription && convexSidebar.canUseHttpFallback;
|
2026-04-22 05:57:06 +08:00
|
|
|
const [manualSnapshotState, setManualSnapshotState] = useState<{
|
|
|
|
|
workspaceId: string;
|
|
|
|
|
data: SidebarInitialData | null;
|
|
|
|
|
}>({
|
|
|
|
|
workspaceId,
|
|
|
|
|
data: null,
|
|
|
|
|
});
|
2025-11-23 10:55:04 +08:00
|
|
|
|
2026-04-17 23:36:24 +08:00
|
|
|
const httpQuery = useQuery({
|
2025-11-23 10:55:04 +08:00
|
|
|
queryKey: ["sidebar", workspaceId],
|
|
|
|
|
queryFn: () => requestSidebarData(workspaceId),
|
|
|
|
|
initialData,
|
|
|
|
|
staleTime: 1000 * 60,
|
2026-04-17 23:36:24 +08:00
|
|
|
enabled: shouldUseHttpFallback,
|
2025-11-23 10:55:04 +08:00
|
|
|
});
|
2026-04-17 23:36:24 +08:00
|
|
|
|
2026-04-22 05:57:06 +08:00
|
|
|
const manualSnapshot =
|
|
|
|
|
manualSnapshotState.workspaceId === workspaceId
|
|
|
|
|
? manualSnapshotState.data
|
|
|
|
|
: null;
|
2026-04-17 23:36:24 +08:00
|
|
|
|
2026-04-22 05:57:06 +08:00
|
|
|
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>(() => {
|
2026-04-17 23:36:24 +08:00
|
|
|
return {
|
2026-04-22 05:57:06 +08:00
|
|
|
data: stableLiveData,
|
2026-04-17 23:36:24 +08:00
|
|
|
isLoading,
|
2026-04-22 05:57:06 +08:00
|
|
|
error,
|
2026-04-17 23:36:24 +08:00
|
|
|
refetch,
|
|
|
|
|
source,
|
|
|
|
|
};
|
|
|
|
|
}, [
|
2026-04-22 05:57:06 +08:00
|
|
|
error,
|
|
|
|
|
isLoading,
|
|
|
|
|
refetch,
|
|
|
|
|
stableLiveData,
|
|
|
|
|
source,
|
2026-04-17 23:36:24 +08:00
|
|
|
]);
|
2025-11-23 10:55:04 +08:00
|
|
|
}
|