Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 125 additions & 43 deletions src/hooks/useGitHubData.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, useRef, useEffect } from 'react';
import { classifyCommit } from '../utils/commitClassifier';

export const useGitHubData = (getOctokit: () => any) => {
const [issues, setIssues] = useState([]);
const [prs, setPrs] = useState([]);
const [issues, setIssues] = useState<any[]>([]);
const [prs, setPrs] = useState<any[]>([]);
const [commits, setCommits] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [totalIssues, setTotalIssues] = useState(0);
const [totalPrs, setTotalPrs] = useState(0);
const [totalCommits, setTotalCommits] = useState(0);
const [rateLimited, setRateLimited] = useState(false);

const fetchPaginated = async (octokit: any, username: string, type: string, page = 1, per_page = 10) => {
const fetchPaginated = async (octokit: any, username: string, type: string, page = 1, per_page = 10, signal?: AbortSignal) => {
const q = `author:${username} is:${type}`;
const response = await octokit.request('GET /search/issues', {
q,
sort: 'created',
order: 'desc',
per_page,
page,
request: signal ? { signal } : undefined,
});

return {
Expand All @@ -25,59 +29,137 @@ export const useGitHubData = (getOctokit: () => any) => {
};
};

const fetchCommitsPaginated = async (octokit: any, username: string, page = 1, per_page = 10, signal?: AbortSignal) => {
const q = `author:${username}`;
const response = await octokit.request('GET /search/commits', {
q,
sort: 'author-date',
order: 'desc',
per_page,
page,
headers: {
accept: 'application/vnd.github.cloak-preview+json',
},
request: signal ? { signal } : undefined,
});

const items = response.data.items.map((item: any) => ({
...item,
created_at: item.commit.author?.date || item.commit.committer?.date,
classifiedInfo: classifyCommit(item.commit.message),
}));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return {
items,
total: response.data.total_count,
};
};

// refs for debounce timer and abort controller
const debounceRef = useRef<number | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);

const fetchData = useCallback(
async (username: string, page = 1, perPage = 10) => {

(username: string, page = 1, perPage = 10) => {
const DEBOUNCE_MS = 350;

const octokit = getOctokit();

if (!octokit || !username) return;

setLoading(true);
setError('');

try {
const [issueRes, prRes] = await Promise.all([
fetchPaginated(octokit, username, 'issue', page, perPage),
fetchPaginated(octokit, username, 'pr', page, perPage),
]);

setIssues(issueRes.items);
setPrs(prRes.items);
setTotalIssues(issueRes.total);
setTotalPrs(prRes.total);
setRateLimited(false);
} catch (err: any) {
const errorMessage = err.message?.toLowerCase() || "";
if (err.status === 403) {
setError('GitHub API rate limit exceeded. Please provide a PAT to continue.');
setRateLimited(true);
} else if (errorMessage.includes("do not exist")){
setError('User not found. Please check the spelling of the GitHub username.');
} else if (err.status === 401 || errorMessage.includes("permission")){
setError('Private repository detected. Please input PAT.');
}else if(err.status===404){
setError('Resource not found.');
}
else if (errorMessage.includes("validation failed")) {
setError('Invalid GitHub username or insufficient permissions.');
if (!octokit) return;

// debounce: clear existing timer
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}

debounceRef.current = window.setTimeout(async () => {
// cancel previous in-flight requests
if (abortControllerRef.current) {
try {
abortControllerRef.current.abort();
} catch (e) {
// ignore
}
}
else {
setError(
'Unable to fetch GitHub data. Please verify the username, token, or network connection.'
);

const controller = new AbortController();
abortControllerRef.current = controller;

setLoading(true);
Comment on lines +70 to +88
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Abort the previous request before starting the next debounce window.

The old request keeps running until the new timer fires, so it can still resolve during the 350ms gap and overwrite newer username/page results. Its finally block can also flip loading back to false while the latest request is still in flight.

Suggested fix
-      // debounce: clear existing timer
+      // cancel any in-flight request immediately so stale responses can't win
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = null;
+      }
+
+      // debounce: clear existing timer
       if (debounceRef.current) {
         clearTimeout(debounceRef.current);
       }
 
       debounceRef.current = window.setTimeout(async () => {
-        // cancel previous in-flight requests
-        if (abortControllerRef.current) {
-          try {
-            abortControllerRef.current.abort();
-          } catch (e) {
-            // ignore
-          }
-        }
-
         const controller = new AbortController();
         abortControllerRef.current = controller;
 
         setLoading(true);
         setError('');
@@
         } finally {
-          setLoading(false);
-          if (abortControllerRef.current === controller) abortControllerRef.current = null;
+          if (abortControllerRef.current === controller) {
+            abortControllerRef.current = null;
+            setLoading(false);
+          }
         }
       }, DEBOUNCE_MS);

Also applies to: 130-132

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useGitHubData.ts` around lines 70 - 88, The previous fetch is only
aborted when the new timer fires, allowing it to complete during the debounce
gap and overwrite newer results; fix by aborting any existing abortController as
soon as you start a new debounce window (i.e., when you set/replace
debounceRef.current) and ensure the in-flight request only updates state if its
controller matches the current abortControllerRef. Specifically, call
abortControllerRef.current.abort() (with try/catch) before creating a new timer
in useGitHubData, then when you create the local controller inside the timer,
use that controller to guard result/finally updates (check
abortControllerRef.current === controller before calling setLoading(false) or
setting results) so stale requests cannot flip loading or overwrite newer data.

setError('');

try {
const [issueRes, prRes, commitRes] = await Promise.all([
fetchPaginated(octokit, username, 'issue', page, perPage, controller.signal),
fetchPaginated(octokit, username, 'pr', page, perPage, controller.signal),
fetchCommitsPaginated(octokit, username, page, perPage, controller.signal).catch((err) => {
if (err.name === 'AbortError') return { items: [], total: 0 };
console.error('Commit fetch failed:', err);
return { items: [], total: 0 };
}),
]);

setIssues(issueRes.items);
setPrs(prRes.items);
setCommits(commitRes.items);
setTotalIssues(issueRes.total);
setTotalPrs(prRes.total);
setTotalCommits(commitRes.total);
setRateLimited(false);
} catch (err: any) {
if (err.name === 'AbortError') {
return;
}
const errorMessage = err.message?.toLowerCase() || "";
if (err.status === 403) {
setError('GitHub API rate limit exceeded. Please provide a PAT to continue.');
setRateLimited(true);
} else if (errorMessage.includes("do not exist")){
setError('User not found. Please check the spelling of the GitHub username.');
} else if (err.status === 401 || errorMessage.includes("permission")){
setError('Private repository detected. Please input PAT.');
} else if(err.status===404){
setError('Resource not found.');
} else if (errorMessage.includes("validation failed")) {
setError('Invalid GitHub username or insufficient permissions.');
} else {
setError(
'Unable to fetch GitHub data. Please verify the username, token, or network connection.'
);
}
} finally {
setLoading(false);
if (abortControllerRef.current === controller) abortControllerRef.current = null;
}
} finally {
setLoading(false);
}
}, DEBOUNCE_MS);
},
[getOctokit]
);

// cleanup on unmount: clear debounce timer and abort any in-flight requests
useEffect(() => {
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
if (abortControllerRef.current) {
try {
abortControllerRef.current.abort();
} catch (e) {
// ignore
}
abortControllerRef.current = null;
}
};
}, []);

return {
issues,
prs,
commits,
totalIssues,
totalPrs,
totalCommits,
loading,
error,
fetchData,
Expand Down
Loading
Loading