| 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 |