diff --git a/src/shared/storage/CacheStore.ts b/src/shared/storage/CacheStore.ts new file mode 100644 index 00000000..455dd7c5 --- /dev/null +++ b/src/shared/storage/CacheStore.ts @@ -0,0 +1,15 @@ +import type { CachedData } from '@shared/types/CachedData'; +import { createLocalStore, debugStore } from 'chrome-extension-toolkit'; + +interface ICacheStore { + github: Record>; +} + +/** + * A store that is used for storing cached data such as GitHub contributors + */ +export const CacheStore = createLocalStore({ + github: {}, +}); + +debugStore({ cacheStore: CacheStore }); diff --git a/src/shared/types/CachedData.ts b/src/shared/types/CachedData.ts new file mode 100644 index 00000000..03c8fc53 --- /dev/null +++ b/src/shared/types/CachedData.ts @@ -0,0 +1,8 @@ +/** + * Represents cached data with its fetch timestamp + * @template T The type of the cached data + */ +export type CachedData = { + data: T; + dataFetched: number; +}; diff --git a/src/views/components/settings/Settings.tsx b/src/views/components/settings/Settings.tsx index 8ccec15b..6fb63f66 100644 --- a/src/views/components/settings/Settings.tsx +++ b/src/views/components/settings/Settings.tsx @@ -566,7 +566,7 @@ export default function Settings(): JSX.Element { className='text-ut-burntorange font-semibold hover:cursor-pointer' onClick={() => window.open(`https://github.com/${username}`, '_blank')} > - @{username} + {githubStats.names[username]}

Contributor

{showGitHubStats && ( diff --git a/src/views/lib/getGitHubStats.ts b/src/views/lib/getGitHubStats.ts index 2d3bc307..d9d8b3a6 100644 --- a/src/views/lib/getGitHubStats.ts +++ b/src/views/lib/getGitHubStats.ts @@ -1,4 +1,6 @@ import { Octokit } from '@octokit/rest'; +import { CacheStore } from '@shared/storage/CacheStore'; +import type { CachedData } from '@shared/types/CachedData'; // Types type TeamMember = { @@ -20,9 +22,8 @@ type ContributorStats = { author: { login: string }; }; -type CachedData = { - data: T; - dataFetched: Date; +type ContributorUser = { + name: string | undefined; }; type FetchResult = { @@ -36,6 +37,7 @@ type FetchResult = { const CACHE_TTL = 1 * 60 * 60 * 1000; // 1 hour in milliseconds const REPO_OWNER = 'Longhorn-Developers'; const REPO_NAME = 'UT-Registration-Plus'; +const CONTRIBUTORS_API_ROUTE = `/repos/${REPO_OWNER}/${REPO_NAME}/stats/contributors`; export const LONGHORN_DEVELOPERS_ADMINS = [ { name: 'Sriram Hariharan', role: 'Founder', githubUsername: 'sghsri' }, @@ -68,24 +70,41 @@ export type LD_ADMIN_GITHUB_USERNAMES = (typeof LONGHORN_DEVELOPERS_ADMINS)[numb */ export class GitHubStatsService { private octokit: Octokit; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private cache: Map>; + private cache: Record>; constructor(githubToken?: string) { this.octokit = githubToken ? new Octokit({ auth: githubToken }) : new Octokit(); - this.cache = new Map(); + this.cache = {} as Record>; } - private getCachedData(key: string): CachedData | null { - const cachedItem = this.cache.get(key); - if (cachedItem && Date.now() - cachedItem.dataFetched.getTime() < CACHE_TTL) { - return cachedItem; + private async getCachedData(key: string): Promise | null> { + if (Object.keys(this.cache).length === 0) { + const githubCache = await CacheStore.get('github'); + if (githubCache && typeof githubCache === 'object') { + this.cache = githubCache as Record>; + } + } + + const cachedItem = this.cache[key] as CachedData | undefined; + if (cachedItem) { + const timeDifference = Date.now() - cachedItem.dataFetched; + if (timeDifference < CACHE_TTL) { + return cachedItem; + } } return null; } - private setCachedData(key: string, data: T): void { - this.cache.set(key, { data, dataFetched: new Date() }); + private async setCachedData(key: string, data: T): Promise { + if (Object.keys(this.cache).length === 0) { + const githubCache = await CacheStore.get('github'); + if (githubCache && typeof githubCache === 'object') { + this.cache = githubCache as Record>; + } + } + + this.cache[key] = { data, dataFetched: Date.now() }; + await CacheStore.set('github', this.cache); } private async fetchWithRetry(fetchFn: () => Promise, retries: number = 3, delay: number = 5000): Promise { @@ -101,25 +120,32 @@ export class GitHubStatsService { } } + private async fetchGitHub(route: string): Promise { + try { + const url = new URL(route, 'https://github.cachedapi.com'); + const response = await fetch(url); + return await response.json(); + } catch (error: unknown) { + const url = new URL(route, 'https://api.github.com'); + const response = await fetch(url); + return await response.json(); + } + } + private async fetchContributorStats(): Promise> { const cacheKey = `contributor_stats_${REPO_OWNER}_${REPO_NAME}`; - const cachedStats = this.getCachedData(cacheKey); + const cachedStats = await this.getCachedData(cacheKey); if (cachedStats) { return { data: cachedStats.data, - dataFetched: cachedStats.dataFetched, + dataFetched: new Date(cachedStats.dataFetched), lastUpdated: new Date(), isCached: true, }; } - const { data } = await this.fetchWithRetry(() => - this.octokit.repos.getContributorsStats({ - owner: REPO_OWNER, - repo: REPO_NAME, - }) - ); + const data = await this.fetchWithRetry(() => this.fetchGitHub(CONTRIBUTORS_API_ROUTE)); if (Array.isArray(data)) { const fetchResult: FetchResult = { @@ -128,21 +154,51 @@ export class GitHubStatsService { lastUpdated: new Date(), isCached: false, }; - this.setCachedData(cacheKey, fetchResult.data); + await this.setCachedData(cacheKey, fetchResult.data); return fetchResult; } throw new Error('Invalid response format'); } + private async fetchContributorNames(contributors: string[]): Promise> { + const names: Record = {}; + await Promise.all( + contributors.map(async contributor => { + const cacheKey = `contributor_name_${contributor}`; + const cachedName = await this.getCachedData(cacheKey); + let name = `@${contributor}`; + + if (cachedName) { + name = cachedName.data; + } else { + try { + const data = (await this.fetchWithRetry(() => + this.fetchGitHub(`/users/${contributor}`) + )) as ContributorUser; + if (data.name) { + name = data.name; + } + await this.setCachedData(cacheKey, name); + } catch (e) { + console.error(e); + } + } + + names[contributor] = name; + }) + ); + return names; + } + private async fetchMergedPRsCount(username: string): Promise> { const cacheKey = `merged_prs_${username}`; - const cachedCount = this.getCachedData(cacheKey); + const cachedCount = await this.getCachedData(cacheKey); if (cachedCount !== null) { return { data: cachedCount.data, - dataFetched: cachedCount.dataFetched, + dataFetched: new Date(cachedCount.dataFetched), lastUpdated: new Date(), isCached: true, }; @@ -158,7 +214,7 @@ export class GitHubStatsService { lastUpdated: new Date(), isCached: false, }; - this.setCachedData(cacheKey, fetchResult.data); + await this.setCachedData(cacheKey, fetchResult.data); return fetchResult; } @@ -174,6 +230,7 @@ export class GitHubStatsService { adminGitHubStats: Record; userGitHubStats: Record; contributors: string[]; + names: Record; dataFetched: Date; lastUpdated: Date; isCached: boolean; @@ -222,10 +279,13 @@ export class GitHubStatsService { }) ); + const names = await this.fetchContributorNames(contributors); + return { adminGitHubStats, userGitHubStats, contributors, + names, dataFetched: oldestDataFetch, lastUpdated: new Date(), isCached: allCached,