返回 DeepSeek-Reasonix
useProjectTreeListRuntime.ts
根目录 / desktop / frontend / src / lib / useProjectTreeListRuntime.ts
1 import { useCallback, useRef, useState } from "react";
2 import { releaseReadSnapshot } from "./readSnapshot";
3 import { invalidateProjectTreeTopicLoads } from "./projectTreeTopic";
4 import {
5 createProjectTreeRequestLimiter,
6 projectTreeRuntimeWindowLimits,
7 resetProjectTreeRuntimeWindowLimits,
8 type ProjectTreeListPageState,
9 } from "./projectTreeWindow";
10
11 export function useProjectTreeListRuntime() {
12 const topicRevisionRef = useRef<Record<string, number>>({});
13 const topicCompletePageRef = useRef<Record<string, { signature: string; revision: number }>>({});
14 const [topicPageState, setTopicPageState] = useState<Record<string, ProjectTreeListPageState>>({});
15 const topicPageStateRef = useRef(topicPageState);
16 const updateTopicPageState = useCallback((key: string, next: ProjectTreeListPageState) => {
17 // Publish synchronously so sibling effects see the same request/cache state
18 // before React commits the corresponding render.
19 const updated = { ...topicPageStateRef.current, [key]: { ...next, refreshing: Boolean(next.loading && next.refreshing) } };
20 topicPageStateRef.current = updated;
21 setTopicPageState(updated);
22 }, []);
23
24 const [topicWindowLimits, setTopicWindowLimits] = useState<Record<string, number>>(projectTreeRuntimeWindowLimits);
25 const topicWindowLimitsRef = useRef(topicWindowLimits);
26 topicWindowLimitsRef.current = topicWindowLimits;
27 const resetTopicWindowLimits = useCallback((projectKey?: string) => {
28 resetProjectTreeRuntimeWindowLimits(projectKey);
29 setTopicWindowLimits((current) => {
30 const prefix = projectKey ? `${projectKey}\u001f` : "";
31 const next = projectKey
32 ? Object.fromEntries(Object.entries(current).filter(([key]) => !key.startsWith(prefix)))
33 : {};
34 if (Object.keys(next).length === Object.keys(current).length) return current;
35 topicWindowLimitsRef.current = next;
36 return next;
37 });
38 }, []);
39
40 const topicLoadSeqRef = useRef<Record<string, number>>({});
41 const topicLoadPendingRef = useRef<Record<string, number>>({});
42 const topicRequestLimiterRef = useRef(createProjectTreeRequestLimiter(4));
43 const topicLoadErrorRef = useRef<Record<string, string>>({});
44 const invalidateProjectTopicLists = useCallback((projectKey: string) => {
45 const prefix = `${projectKey}\u001f`;
46 invalidateProjectTreeTopicLoads(topicLoadSeqRef.current, [projectKey]);
47 for (const records of [topicLoadPendingRef.current, topicRevisionRef.current, topicCompletePageRef.current]) {
48 for (const key of Object.keys(records)) if (key.startsWith(prefix)) delete records[key];
49 }
50 const next = Object.fromEntries(Object.entries(topicPageStateRef.current).map(([key, state]) => {
51 if (!key.startsWith(prefix)) return [key, state];
52 releaseReadSnapshot(state.snapshotId);
53 return [key, { ...state, snapshotId: undefined, nextCursor: undefined, loading: false, refreshing: false, initialized: false, error: undefined }];
54 }));
55 topicPageStateRef.current = next;
56 setTopicPageState(next);
57 }, []);
58
59 return {
60 topicRevisionRef,
61 topicCompletePageRef,
62 topicPageState,
63 setTopicPageState,
64 topicPageStateRef,
65 updateTopicPageState,
66 topicWindowLimits,
67 setTopicWindowLimits,
68 topicWindowLimitsRef,
69 resetTopicWindowLimits,
70 topicLoadSeqRef,
71 topicLoadPendingRef,
72 topicRequestLimiterRef,
73 topicLoadErrorRef,
74 invalidateProjectTopicLists,
75 };
76 }
77
77 lines TYPESCRIPT