diff --git a/src/shared/storage/CacheStore.ts b/src/shared/storage/CacheStore.ts new file mode 100644 index 00000000..e722e528 --- /dev/null +++ b/src/shared/storage/CacheStore.ts @@ -0,0 +1,18 @@ +import { CachedData } from '@shared/types/CachedData'; +import { createLocalStore, debugStore } from 'chrome-extension-toolkit'; + +import { generateRandomId } from '../util/random'; + +interface ICacheStore { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 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..43ca8e57 --- /dev/null +++ b/src/shared/types/CachedData.ts @@ -0,0 +1,4 @@ +export type CachedData = { + data: T; + dataFetched: number; +}; \ No newline at end of file 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..dcff7a68 100644 --- a/src/views/lib/getGitHubStats.ts +++ b/src/views/lib/getGitHubStats.ts @@ -1,4 +1,7 @@ import { Octokit } from '@octokit/rest'; +import { CachedData } from '@shared/types/CachedData'; +import { CacheStore } from '@shared/storage/CacheStore'; +import { serialize } from 'chrome-extension-toolkit'; // Types type TeamMember = { @@ -20,11 +23,6 @@ type ContributorStats = { author: { login: string }; }; -type CachedData = { - data: T; - dataFetched: Date; -}; - type FetchResult = { data: T; dataFetched: Date; @@ -68,24 +66,30 @@ 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 = {}; } - private getCachedData(key: string): CachedData | null { - const cachedItem = this.cache.get(key); - if (cachedItem && Date.now() - cachedItem.dataFetched.getTime() < CACHE_TTL) { + private async getCachedData(key: string): Promise | null> { + if(Object.keys(this.cache).length === 0) { + this.cache = await CacheStore.get('github') as Record>; + } + const cachedItem = this.cache[key]; + if (cachedItem && Date.now() - new Date(cachedItem.dataFetched).getTime() < 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) { + this.cache = await CacheStore.get('github') as Record>; + } + this.cache[key] = { data, dataFetched: (new Date()).getTime() }; + await CacheStore.set('github', this.cache); } private async fetchWithRetry(fetchFn: () => Promise, retries: number = 3, delay: number = 5000): Promise { @@ -103,12 +107,12 @@ export class GitHubStatsService { 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, }; @@ -128,21 +132,50 @@ 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 response = await fetch(`https://api.github.com/users/${contributor}`); + const json = await response.json(); + if(json.name) { + name = json.name; + } + } catch (e) { + console.error(e); + } + } + + await this.setCachedData(cacheKey, name); + 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 +191,7 @@ export class GitHubStatsService { lastUpdated: new Date(), isCached: false, }; - this.setCachedData(cacheKey, fetchResult.data); + await this.setCachedData(cacheKey, fetchResult.data); return fetchResult; } @@ -174,6 +207,7 @@ export class GitHubStatsService { adminGitHubStats: Record; userGitHubStats: Record; contributors: string[]; + names: Record; dataFetched: Date; lastUpdated: Date; isCached: boolean; @@ -222,10 +256,13 @@ export class GitHubStatsService { }) ); + const names = await this.fetchContributorNames(contributors); + return { adminGitHubStats, userGitHubStats, contributors, + names, dataFetched: oldestDataFetch, lastUpdated: new Date(), isCached: allCached,