feat(editor): save leptos island and page aggregate alignment progress
- switch main document flow toward leptos tiptap island host and generated runtime assets - align page aggregate loading, page head single-source updates, and AI tool result recovery - add tests and smoke scripts for title sync, AI route recovery, and editor host cutover
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { useMemo } from "react";
|
||||
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";
|
||||
|
||||
@@ -11,6 +15,24 @@ export interface SidebarDataResult {
|
||||
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",
|
||||
@@ -31,6 +53,13 @@ export function useSidebarData(initialData: SidebarInitialData): SidebarDataResu
|
||||
const convexSidebar = useConvexSidebarData(workspaceId);
|
||||
const shouldUseHttpFallback =
|
||||
!convexSidebar.hasLiveSubscription && convexSidebar.canUseHttpFallback;
|
||||
const [manualSnapshotState, setManualSnapshotState] = useState<{
|
||||
workspaceId: string;
|
||||
data: SidebarInitialData | null;
|
||||
}>({
|
||||
workspaceId,
|
||||
data: null,
|
||||
});
|
||||
|
||||
const httpQuery = useQuery({
|
||||
queryKey: ["sidebar", workspaceId],
|
||||
@@ -40,41 +69,115 @@ export function useSidebarData(initialData: SidebarInitialData): SidebarDataResu
|
||||
enabled: shouldUseHttpFallback,
|
||||
});
|
||||
|
||||
return useMemo<SidebarDataResult>(() => {
|
||||
const liveData = convexSidebar.data ?? httpQuery.data ?? initialData;
|
||||
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 refetch = async () => {
|
||||
if (convexSidebar.hasLiveSubscription) {
|
||||
await convexSidebar.refetch();
|
||||
return liveData;
|
||||
}
|
||||
if (shouldUseHttpFallback) {
|
||||
return httpQuery.refetch();
|
||||
}
|
||||
return liveData;
|
||||
};
|
||||
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: liveData,
|
||||
data: stableLiveData,
|
||||
isLoading,
|
||||
error: convexSidebar.error ?? httpQuery.error ?? null,
|
||||
error,
|
||||
refetch,
|
||||
source,
|
||||
};
|
||||
}, [
|
||||
convexSidebar,
|
||||
httpQuery,
|
||||
initialData,
|
||||
shouldUseHttpFallback,
|
||||
error,
|
||||
isLoading,
|
||||
refetch,
|
||||
stableLiveData,
|
||||
source,
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user