|
6 | 6 | useIntegrationStore, |
7 | 7 | } from "@features/integrations/stores/integrationStore"; |
8 | 8 | import { useQueries } from "@tanstack/react-query"; |
9 | | -import { useCallback, useEffect, useMemo } from "react"; |
| 9 | +import { useCallback, useEffect, useMemo, useState } from "react"; |
| 10 | +import { useAuthenticatedInfiniteQuery } from "./useAuthenticatedInfiniteQuery"; |
10 | 11 | import { useAuthenticatedQuery } from "./useAuthenticatedQuery"; |
11 | 12 |
|
12 | 13 | const integrationKeys = { |
@@ -67,18 +68,92 @@ function useAllGithubRepositories(githubIntegrations: Integration[]) { |
67 | 68 | }); |
68 | 69 | } |
69 | 70 |
|
| 71 | +// Keep the first page small so it returns in a single upstream GitHub round |
| 72 | +// trip (GitHub's max per_page is 100), then fetch the remainder in larger |
| 73 | +// chunks to keep the total number of client/PostHog round trips low. |
| 74 | +const BRANCHES_FIRST_PAGE_SIZE = 100; |
| 75 | +const BRANCHES_PAGE_SIZE = 1000; |
| 76 | + |
| 77 | +interface GithubBranchesPage { |
| 78 | + branches: string[]; |
| 79 | + defaultBranch: string | null; |
| 80 | + hasMore: boolean; |
| 81 | +} |
| 82 | + |
70 | 83 | export function useGithubBranches( |
71 | 84 | integrationId?: number, |
72 | 85 | repo?: string | null, |
73 | 86 | ) { |
74 | | - return useAuthenticatedQuery( |
| 87 | + // While paused we stop chaining `fetchNextPage` calls. The flag is scoped |
| 88 | + // to the current query target and resets whenever it changes, so switching |
| 89 | + // repos or integrations starts a fresh fetch. |
| 90 | + const [paused, setPaused] = useState(false); |
| 91 | + // biome-ignore lint/correctness/useExhaustiveDependencies: intentional reset on key change |
| 92 | + useEffect(() => { |
| 93 | + setPaused(false); |
| 94 | + }, [integrationId, repo]); |
| 95 | + |
| 96 | + const query = useAuthenticatedInfiniteQuery<GithubBranchesPage, number>( |
75 | 97 | integrationKeys.branches(integrationId, repo), |
76 | | - async (client) => { |
77 | | - if (!integrationId || !repo) return { branches: [], defaultBranch: null }; |
78 | | - return await client.getGithubBranches(integrationId, repo); |
| 98 | + async (client, offset) => { |
| 99 | + if (!integrationId || !repo) { |
| 100 | + return { branches: [], defaultBranch: null, hasMore: false }; |
| 101 | + } |
| 102 | + const pageSize = |
| 103 | + offset === 0 ? BRANCHES_FIRST_PAGE_SIZE : BRANCHES_PAGE_SIZE; |
| 104 | + return await client.getGithubBranchesPage( |
| 105 | + integrationId, |
| 106 | + repo, |
| 107 | + offset, |
| 108 | + pageSize, |
| 109 | + ); |
| 110 | + }, |
| 111 | + { |
| 112 | + initialPageParam: 0, |
| 113 | + getNextPageParam: (lastPage, allPages) => { |
| 114 | + if (!lastPage.hasMore) return undefined; |
| 115 | + return allPages.reduce((n, p) => n + p.branches.length, 0); |
| 116 | + }, |
79 | 117 | }, |
80 | | - { staleTime: 0, refetchOnMount: "always" }, |
81 | 118 | ); |
| 119 | + |
| 120 | + // Auto-fetch remaining pages in the background whenever we are not paused. |
| 121 | + // Any in-flight page is allowed to finish and land in the cache; the pause |
| 122 | + // just prevents us from kicking off the next one. Resuming picks up from |
| 123 | + // wherever `getNextPageParam` computes the next offset to be. |
| 124 | + useEffect(() => { |
| 125 | + if (paused) return; |
| 126 | + if (query.hasNextPage && !query.isFetchingNextPage) { |
| 127 | + query.fetchNextPage(); |
| 128 | + } |
| 129 | + }, [ |
| 130 | + paused, |
| 131 | + query.hasNextPage, |
| 132 | + query.isFetchingNextPage, |
| 133 | + query.fetchNextPage, |
| 134 | + ]); |
| 135 | + |
| 136 | + const data = useMemo(() => { |
| 137 | + if (!query.data?.pages.length) { |
| 138 | + return { branches: [] as string[], defaultBranch: null }; |
| 139 | + } |
| 140 | + return { |
| 141 | + branches: query.data.pages.flatMap((p) => p.branches), |
| 142 | + defaultBranch: query.data.pages[0]?.defaultBranch ?? null, |
| 143 | + }; |
| 144 | + }, [query.data?.pages]); |
| 145 | + |
| 146 | + const pauseLoadingMore = useCallback(() => setPaused(true), []); |
| 147 | + const resumeLoadingMore = useCallback(() => setPaused(false), []); |
| 148 | + |
| 149 | + return { |
| 150 | + data, |
| 151 | + isPending: query.isPending, |
| 152 | + isFetchingMore: |
| 153 | + !paused && (query.isFetchingNextPage || (query.hasNextPage ?? false)), |
| 154 | + pauseLoadingMore, |
| 155 | + resumeLoadingMore, |
| 156 | + }; |
82 | 157 | } |
83 | 158 |
|
84 | 159 | export function useRepositoryIntegration() { |
|
0 commit comments