feat: complete tree shell cutover and regression coverage

This commit is contained in:
lix-2026
2026-04-18 09:38:16 +08:00
parent d3876e56eb
commit 111a87d4fd
53 changed files with 5440 additions and 516 deletions
@@ -40,21 +40,64 @@ const baseSidebarData: SidebarInitialData = {
nodeId: "root",
parentNodeId: null,
nodeType: "page",
projectionKind: "sidebar_tree",
title: "Root",
depth: 0,
position: 0,
childCount: 1,
expandable: true,
expandedByDefault: true,
capabilities: [
"expand",
"open",
"drag",
"drop",
"select",
"create-child",
"rename",
"archive",
"restore",
"context-menu",
"reorder",
],
resourceMeta: {
resourceKind: "document",
documentId: "root",
workspaceId: "ws_1",
iconHint: "page",
},
iconHint: "page",
},
{
nodeId: "child",
parentNodeId: "root",
nodeType: "page",
projectionKind: "sidebar_tree",
title: "Child",
depth: 1,
position: 1,
childCount: 0,
expandable: false,
expandedByDefault: true,
capabilities: [
"open",
"drag",
"drop",
"select",
"create-child",
"rename",
"archive",
"restore",
"context-menu",
"reorder",
],
resourceMeta: {
resourceKind: "document",
documentId: "child",
workspaceId: "ws_1",
iconHint: "page",
},
iconHint: "page",
},
],
edges: [
@@ -9,6 +9,7 @@ import {
parseTreeStreamMessage,
} from "@/lib/tree-stream/protocol";
import { applyTreeStreamDelta, type TreeStreamDeltaEvent } from "@/lib/tree-stream/tree-delta";
import { ensureMnoteWebAuthCookie } from "@/lib/mnote-web-auth";
export interface SidebarTreeStreamState {
data: SidebarInitialData | null;
@@ -60,9 +61,8 @@ export function useSidebarTreeStream(initialData: SidebarInitialData): SidebarTr
return undefined;
}
const url = buildWorkspaceTreeStreamUrl(baseUrl, workspaceId, state.cursor);
const eventSource = new EventSource(url, { withCredentials: true });
eventSourceRef.current = eventSource;
let cancelled = false;
let eventSource: EventSource | null = null;
const handleMessage = (event: MessageEvent<string>) => {
const envelope = parseTreeStreamMessage({
@@ -121,21 +121,45 @@ export function useSidebarTreeStream(initialData: SidebarInitialData): SidebarTr
status: previous.data ? "live" : "fallback",
error: previous.error ?? new Error("tree stream 连接失败"),
}));
eventSource.close();
eventSourceRef.current = null;
eventSource?.close();
if (eventSourceRef.current === eventSource) {
eventSourceRef.current = null;
}
};
eventSource.addEventListener("snapshot", handleMessage as EventListener);
eventSource.addEventListener("delta", handleMessage as EventListener);
eventSource.addEventListener("resync", handleMessage as EventListener);
eventSource.onmessage = handleMessage;
eventSource.onerror = handleError;
void (async () => {
try {
await ensureMnoteWebAuthCookie();
if (cancelled) {
return;
}
const url = buildWorkspaceTreeStreamUrl(baseUrl, workspaceId, state.cursor);
eventSource = new EventSource(url, { withCredentials: true });
eventSourceRef.current = eventSource;
eventSource.addEventListener("snapshot", handleMessage as EventListener);
eventSource.addEventListener("delta", handleMessage as EventListener);
eventSource.addEventListener("resync", handleMessage as EventListener);
eventSource.onmessage = handleMessage;
eventSource.onerror = handleError;
} catch {
if (cancelled) {
return;
}
setState((previous) => ({
...previous,
status: previous.data ? "live" : "fallback",
error: previous.error ?? new Error("tree stream 鉴权失败"),
}));
}
})();
return () => {
eventSource.removeEventListener("snapshot", handleMessage as EventListener);
eventSource.removeEventListener("delta", handleMessage as EventListener);
eventSource.removeEventListener("resync", handleMessage as EventListener);
eventSource.close();
cancelled = true;
eventSource?.removeEventListener("snapshot", handleMessage as EventListener);
eventSource?.removeEventListener("delta", handleMessage as EventListener);
eventSource?.removeEventListener("resync", handleMessage as EventListener);
eventSource?.close();
if (eventSourceRef.current === eventSource) {
eventSourceRef.current = null;
}