| 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | import { scopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime/environment"; |
| 3 | import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; |
| 4 | import { |
| 5 | createLinkedPullRequestSummaryAtomFamily, |
| 6 | pullRequestDetailToVcsStatus, |
| 7 | } from "@t3tools/client-runtime/state/pull-requests"; |
| 8 | import { Atom } from "effect/reactivity"; |
| 9 | import { useCallback, useEffect, useMemo } from "react"; |
| 10 | |
| 11 | import { connectionAtomRuntime } from "../connection/runtime"; |
| 12 | import { appAtomRegistry } from "./atom-registry"; |
| 13 | import { serverEnvironment } from "./server"; |
| 14 | import { useEnvironmentQuery } from "./query"; |
| 15 | import { |
| 16 | resolveThreadPrSource, |
| 17 | presentThreadPr, |
| 18 | type ThreadPrPresentation, |
| 19 | } from "./thread-pr-presentation"; |
| 20 | |
| 21 | const pullRequestSummaryAtom = createLinkedPullRequestSummaryAtomFamily(connectionAtomRuntime); |
| 22 | const MAX_THREAD_PR_SNAPSHOTS = 500; |
| 23 | |
| 24 | interface ThreadPrSnapshot { |
| 25 | readonly identity: string; |
| 26 | readonly presentation: ThreadPrPresentation; |
| 27 | } |
| 28 | |
| 29 | // One bounded cache survives row virtualization without retaining one live |
| 30 | // atom for every thread or pull request ever seen. |
| 31 | const threadPrSnapshotsAtom = Atom.make<ReadonlyMap<string, ThreadPrSnapshot>>(new Map()).pipe( |
| 32 | Atom.keepAlive, |
| 33 | Atom.withLabel("mobile:thread-pr-snapshots"), |
| 34 | ); |
| 35 | |
| 36 | export { |
| 37 | presentThreadPr, |
| 38 | type ThreadPr, |
| 39 | type ThreadPrPresentation, |
| 40 | } from "./thread-pr-presentation"; |
| 41 | |
| 42 | /** |
| 43 | * Linked PRs use server snapshots. Branch fallback and legacy references share |
| 44 | * a live summary request across visible rows in the same environment. |
| 45 | */ |
| 46 | export function useThreadPr(thread: EnvironmentThreadShell): ThreadPrPresentation | null { |
| 47 | const supportsLinks = useAtomValue( |
| 48 | serverEnvironment.configValueAtom(thread.environmentId), |
| 49 | (config) => config?.environment.capabilities.threadPullRequests === true, |
| 50 | ); |
| 51 | const { linkedPresentation, pullRequestRef } = useMemo( |
| 52 | () => |
| 53 | resolveThreadPrSource( |
| 54 | { |
| 55 | pullRequests: thread.pullRequests, |
| 56 | linkedPullRequest: thread.linkedPullRequest, |
| 57 | branchPullRequest: thread.branchPullRequest, |
| 58 | }, |
| 59 | { threadPullRequests: supportsLinks }, |
| 60 | ), |
| 61 | [thread.pullRequests, thread.linkedPullRequest, thread.branchPullRequest, supportsLinks], |
| 62 | ); |
| 63 | const threadKey = scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)); |
| 64 | const snapshotIdentity = JSON.stringify(pullRequestRef); |
| 65 | // Select this row's entry so writes for other rows do not re-render it. |
| 66 | const snapshotEntry = useAtomValue( |
| 67 | threadPrSnapshotsAtom, |
| 68 | useCallback( |
| 69 | (current: ReadonlyMap<string, ThreadPrSnapshot>) => current.get(threadKey), |
| 70 | [threadKey], |
| 71 | ), |
| 72 | ); |
| 73 | const snapshot = snapshotEntry?.identity === snapshotIdentity ? snapshotEntry.presentation : null; |
| 74 | const pullRequestSummary = useEnvironmentQuery( |
| 75 | pullRequestRef === null |
| 76 | ? null |
| 77 | : pullRequestSummaryAtom({ |
| 78 | environmentId: thread.environmentId, |
| 79 | input: { |
| 80 | projectId: pullRequestRef.projectId, |
| 81 | repository: pullRequestRef.repository, |
| 82 | number: pullRequestRef.number, |
| 83 | }, |
| 84 | }), |
| 85 | ); |
| 86 | |
| 87 | const live = useMemo<ThreadPrPresentation | null | undefined>(() => { |
| 88 | if (pullRequestRef === null) return null; |
| 89 | const summary = pullRequestSummary.data; |
| 90 | return summary === null |
| 91 | ? undefined |
| 92 | : presentThreadPr(pullRequestDetailToVcsStatus(summary), { |
| 93 | kind: summary.provider, |
| 94 | name: summary.provider, |
| 95 | baseUrl: "", |
| 96 | }); |
| 97 | }, [pullRequestRef, pullRequestSummary.data]); |
| 98 | |
| 99 | useEffect(() => { |
| 100 | if (live === undefined) return; |
| 101 | appAtomRegistry.modify(threadPrSnapshotsAtom, (current) => { |
| 102 | const existing = current.get(threadKey); |
| 103 | if (live === null) { |
| 104 | if (existing === undefined) return [false, current]; |
| 105 | const next = new Map(current); |
| 106 | next.delete(threadKey); |
| 107 | return [true, next]; |
| 108 | } |
| 109 | if (existing?.identity === snapshotIdentity && existing.presentation === live) { |
| 110 | return [false, current]; |
| 111 | } |
| 112 | const next = new Map(current); |
| 113 | next.delete(threadKey); |
| 114 | next.set(threadKey, { identity: snapshotIdentity, presentation: live }); |
| 115 | while (next.size > MAX_THREAD_PR_SNAPSHOTS) { |
| 116 | const oldestKey = next.keys().next().value; |
| 117 | if (oldestKey === undefined) break; |
| 118 | next.delete(oldestKey); |
| 119 | } |
| 120 | return [true, next]; |
| 121 | }); |
| 122 | }, [live, snapshotIdentity, threadKey]); |
| 123 | |
| 124 | return linkedPresentation ?? (live === undefined ? snapshot : live); |
| 125 | } |