22
33import type { ContributionCalendar } from '../types' ;
44import { calculateStreak } from './calculate' ;
5+ import { TTLCache } from './cache' ;
56
67interface GitHubRepo {
78 stargazers_count : number ;
@@ -22,12 +23,55 @@ type GitHubContributionResponse = {
2223 errors ?: Array < { message : string } > ;
2324} ;
2425
26+ type FetchOptions = {
27+ bypassCache ?: boolean ;
28+ } ;
29+
30+ export const GITHUB_CACHE_TTL_MS = 5 * 60 * 1000 ;
31+
32+ interface GitHubUserProfile {
33+ login : string ;
34+ name : string | null ;
35+ avatar_url : string ;
36+ public_repos : number ;
37+ followers : number ;
38+ following : number ;
39+ created_at : string ;
40+ bio : string | null ;
41+ location : string | null ;
42+ plan ?: { name ?: string } | null ;
43+ }
44+
45+ const contributionsCache = new TTLCache < ContributionCalendar > ( ) ;
46+ const profileCache = new TTLCache < GitHubUserProfile > ( ) ;
47+ const reposCache = new TTLCache < GitHubRepo [ ] > ( ) ;
48+
49+ function cacheKey ( kind : 'contributions' | 'profile' | 'repos' , username : string ) : string {
50+ return `${ kind } :${ username . toLowerCase ( ) } ` ;
51+ }
52+
53+ export function clearGitHubApiCacheForTests ( ) : void {
54+ contributionsCache . clear ( ) ;
55+ profileCache . clear ( ) ;
56+ reposCache . clear ( ) ;
57+ }
58+
2559const getHeaders = ( ) => ( {
2660 Authorization : `bearer ${ process . env . GITHUB_PAT || process . env . GITHUB_TOKEN } ` ,
2761 'Content-Type' : 'application/json' ,
2862} ) ;
2963
30- export async function fetchGitHubContributions ( username : string ) : Promise < ContributionCalendar > {
64+ export async function fetchGitHubContributions (
65+ username : string ,
66+ options : FetchOptions = { }
67+ ) : Promise < ContributionCalendar > {
68+ const key = cacheKey ( 'contributions' , username ) ;
69+
70+ if ( ! options . bypassCache ) {
71+ const cached = contributionsCache . get ( key ) ;
72+ if ( cached ) return cached ;
73+ }
74+
3175 const query = `
3276 query($login: String!) {
3377 user(login: $login) {
@@ -51,7 +95,7 @@ export async function fetchGitHubContributions(username: string): Promise<Contri
5195 method : 'POST' ,
5296 headers : getHeaders ( ) ,
5397 body : JSON . stringify ( { query, variables : { login : username } } ) ,
54- cache : 'no-store' , // Cache handled at the API route
98+ cache : 'no-store' , // Cache handled by our in-memory layer + API route headers
5599 } ) ;
56100
57101 if ( ! res . ok ) {
@@ -69,10 +113,26 @@ export async function fetchGitHubContributions(username: string): Promise<Contri
69113 throw new Error ( `GitHub user "${ username } " not found` ) ;
70114 }
71115
72- return data . data . user . contributionsCollection . contributionCalendar ;
116+ const calendar = data . data . user . contributionsCollection . contributionCalendar ;
117+
118+ if ( ! options . bypassCache ) {
119+ contributionsCache . set ( key , calendar , GITHUB_CACHE_TTL_MS ) ;
120+ }
121+
122+ return calendar ;
73123}
74124
75- export async function fetchUserProfile ( username : string ) {
125+ export async function fetchUserProfile (
126+ username : string ,
127+ options : FetchOptions = { }
128+ ) : Promise < GitHubUserProfile > {
129+ const key = cacheKey ( 'profile' , username ) ;
130+
131+ if ( ! options . bypassCache ) {
132+ const cached = profileCache . get ( key ) ;
133+ if ( cached ) return cached ;
134+ }
135+
76136 const res = await fetch ( `${ GITHUB_REST_URL } /users/${ username } ` , {
77137 headers : getHeaders ( ) ,
78138 cache : 'no-store' ,
@@ -83,10 +143,26 @@ export async function fetchUserProfile(username: string) {
83143 throw new Error ( `GitHub REST API error: ${ res . status } ` ) ;
84144 }
85145
86- return res . json ( ) ;
146+ const profile = ( await res . json ( ) ) as GitHubUserProfile ;
147+
148+ if ( ! options . bypassCache ) {
149+ profileCache . set ( key , profile , GITHUB_CACHE_TTL_MS ) ;
150+ }
151+
152+ return profile ;
87153}
88154
89- export async function fetchUserRepos ( username : string ) {
155+ export async function fetchUserRepos (
156+ username : string ,
157+ options : FetchOptions = { }
158+ ) : Promise < GitHubRepo [ ] > {
159+ const key = cacheKey ( 'repos' , username ) ;
160+
161+ if ( ! options . bypassCache ) {
162+ const cached = reposCache . get ( key ) ;
163+ if ( cached ) return cached ;
164+ }
165+
90166 const res = await fetch ( `${ GITHUB_REST_URL } /users/${ username } /repos?per_page=100&sort=pushed` , {
91167 headers : getHeaders ( ) ,
92168 cache : 'no-store' ,
@@ -96,15 +172,21 @@ export async function fetchUserRepos(username: string) {
96172 throw new Error ( `GitHub REST API error: ${ res . status } ` ) ;
97173 }
98174
99- return res . json ( ) ;
175+ const repos = ( await res . json ( ) ) as GitHubRepo [ ] ;
176+
177+ if ( ! options . bypassCache ) {
178+ reposCache . set ( key , repos , GITHUB_CACHE_TTL_MS ) ;
179+ }
180+
181+ return repos ;
100182}
101183
102- export async function getFullDashboardData ( username : string ) {
184+ export async function getFullDashboardData ( username : string , options : FetchOptions = { } ) {
103185 try {
104186 const [ profileData , reposData , calendarData ] = await Promise . all ( [
105- fetchUserProfile ( username ) ,
106- fetchUserRepos ( username ) ,
107- fetchGitHubContributions ( username ) ,
187+ fetchUserProfile ( username , options ) ,
188+ fetchUserRepos ( username , options ) ,
189+ fetchGitHubContributions ( username , options ) ,
108190 ] ) ;
109191
110192 // Pre-compute streak + stars early so developerScore can use them
0 commit comments