Skip to content

Commit 44ed4f1

Browse files
authored
perf(github): Reduce GitHub API cache size by storing only required response fields (JhaSourav07#2076)
## Description ### What does this PR do? This PR optimizes GitHub API response caching by storing only the fields required by the application instead of caching the entire GitHub REST API response. ### Changes Made * Added `sanitizeUserProfile()` to retain only the required profile fields. * Added `sanitizeRepo()` to retain only the repository fields used by the application. * Updated `fetchUserProfile()` to sanitize profile data before caching and returning it. * Updated `fetchUserRepos()` to sanitize repository data before caching and returning it. * Added unit tests to verify that unnecessary fields are stripped before caching. ### Benefits * Reduces cache memory footprint. * Decreases cache storage requirements. * Improves cache efficiency while preserving existing functionality. * Prevents unnecessary GitHub API payload data from being stored. ### Testing * Added sanitization tests for profile responses. * Added sanitization tests for repository responses. * Verified ESLint passes successfully. * Verified all related tests pass locally. * Confirmed all project tests pass after the change. ### Issue Fixed Closes JhaSourav07#1856 --- ## Pillar 🚀 Performance Optimization --- ## Checklist * [x] I have linked the correct issue. * [x] My code follows the project's coding standards. * [x] I have tested my changes locally. * [x] ESLint passes successfully. * [x] Existing functionality remains unaffected. * [x] Added/updated tests where applicable. * [x] This PR is focused on a single issue and does not include unrelated changes.
2 parents a318b59 + d3cae6a commit 44ed4f1

2 files changed

Lines changed: 90 additions & 4 deletions

File tree

lib/github.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,32 @@ describe('fetchUserProfile', () => {
522522
expect(result.avatar_url).toBe(mockProfile.avatar_url);
523523
});
524524

525+
it('sanitizes the profile by removing extra fields before returning', async () => {
526+
const mockProfile = {
527+
login: 'octocat',
528+
name: 'The Octocat',
529+
avatar_url: 'https://avatar.url',
530+
public_repos: 8,
531+
followers: 100,
532+
following: 5,
533+
created_at: '2011-01-25T18:44:36Z',
534+
bio: 'GitHub mascot',
535+
location: 'San Francisco',
536+
extra_field: 'should be removed',
537+
another_extra: 123,
538+
plan: { name: 'pro', space: 1000 },
539+
};
540+
541+
vi.mocked(fetch).mockResolvedValue(mockResponse(mockProfile));
542+
543+
const result = (await fetchUserProfile('octocat')) as unknown as Record<string, unknown>;
544+
545+
expect(result.login).toBe('octocat');
546+
expect(result.extra_field).toBeUndefined();
547+
expect(result.another_extra).toBeUndefined();
548+
expect(result.plan).toEqual({ name: 'pro' });
549+
});
550+
525551
it('encodes the username before using it in the REST profile path', async () => {
526552
vi.mocked(fetch).mockResolvedValue(
527553
mockResponse({
@@ -568,6 +594,29 @@ describe('fetchUserRepos', () => {
568594
expect(result[0].stargazers_count).toBe(1);
569595
});
570596

597+
it('sanitizes repo objects by removing extra fields before returning', async () => {
598+
vi.mocked(fetch).mockResolvedValue(
599+
mockResponse([
600+
{
601+
name: 'some-repo',
602+
stargazers_count: 10,
603+
language: 'TypeScript',
604+
id: 12345,
605+
private: false,
606+
owner: { login: 'octocat' },
607+
},
608+
])
609+
);
610+
611+
const result = (await fetchUserRepos('octocat')) as unknown as Record<string, unknown>[];
612+
613+
expect(result[0].stargazers_count).toBe(10);
614+
expect(result[0].language).toBe('TypeScript');
615+
expect(result[0].id).toBeUndefined();
616+
expect(result[0].private).toBeUndefined();
617+
expect(result[0].owner).toBeUndefined();
618+
});
619+
571620
it('returns a full three-repo payload with the expected star counts and languages', async () => {
572621
const mockedRepos = [
573622
{ stargazers_count: 7, language: 'TypeScript' },

lib/github.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,41 @@ interface GitHubUserProfile {
275275
plan?: { name?: string } | null;
276276
}
277277

278+
/**
279+
* Sanitizes a GitHub user profile to only include required fields.
280+
* This reduces the memory footprint of cached data.
281+
*/
282+
function sanitizeUserProfile(profile: GitHubUserProfile): GitHubUserProfile {
283+
return {
284+
login: profile.login,
285+
name: profile.name,
286+
avatar_url: profile.avatar_url,
287+
public_repos: profile.public_repos,
288+
followers: profile.followers,
289+
following: profile.following,
290+
created_at: profile.created_at,
291+
bio: profile.bio,
292+
location: profile.location,
293+
type: profile.type,
294+
plan: profile.plan ? { name: profile.plan.name } : null,
295+
};
296+
}
297+
298+
/**
299+
* Sanitizes a GitHub repository object to only include required fields.
300+
* This reduces the memory footprint of cached data.
301+
*/
302+
function sanitizeRepo(repo: GitHubRepo): GitHubRepo {
303+
return {
304+
name: repo.name,
305+
stargazers_count: repo.stargazers_count,
306+
language: repo.language,
307+
fork: repo.fork,
308+
forks_count: repo.forks_count,
309+
updated_at: repo.updated_at,
310+
};
311+
}
312+
278313
export function cacheKey(
279314
kind: 'contributions' | 'profile' | 'repos' | 'repos:contributed',
280315
username: string,
@@ -583,8 +618,9 @@ async function fetchProfileUncached(
583618
}
584619

585620
const profile = (await res.json()) as GitHubUserProfile;
586-
if (!options.bypassCache) await profileCache.set(key, profile, GITHUB_CACHE_TTL_MS);
587-
return profile;
621+
const sanitizedProfile = sanitizeUserProfile(profile);
622+
if (!options.bypassCache) await profileCache.set(key, sanitizedProfile, GITHUB_CACHE_TTL_MS);
623+
return sanitizedProfile;
588624
}
589625

590626
export async function fetchUserRepos(
@@ -622,7 +658,7 @@ async function fetchReposUncached(
622658
}
623659

624660
const firstPageRepos = (await firstPageRes.json()) as GitHubRepo[];
625-
const allRepos: GitHubRepo[] = [...firstPageRepos];
661+
const allRepos: GitHubRepo[] = firstPageRepos.map(sanitizeRepo);
626662

627663
const MAX_PAGES = 3;
628664

@@ -649,7 +685,8 @@ async function fetchReposUncached(
649685
throw new Error(`GitHub REST API error: ${response.status}`);
650686
}
651687

652-
return (await response.json()) as GitHubRepo[];
688+
const repos = (await response.json()) as GitHubRepo[];
689+
return repos.map(sanitizeRepo);
653690
})
654691
);
655692

0 commit comments

Comments
 (0)