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