From 033e370c55f3be3eaff7e09c5b4cacb173a3be98 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sat, 17 Jan 2026 11:17:12 +0200 Subject: [PATCH] feat(git): cache gitmojis locally with ttl --- packages/ui/src/components/views/GitView.tsx | 83 ++++++++++++++++++-- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/components/views/GitView.tsx b/packages/ui/src/components/views/GitView.tsx index 775dec39..ebaac58d 100644 --- a/packages/ui/src/components/views/GitView.tsx +++ b/packages/ui/src/components/views/GitView.tsx @@ -49,6 +49,67 @@ type GitViewSnapshot = { commitMessage: string; }; +type GitmojiEntry = { + emoji: string; + code: string; + description: string; +}; + +type GitmojiCachePayload = { + gitmojis: GitmojiEntry[]; + fetchedAt: number; + version: string; +}; + +const GITMOJI_CACHE_KEY = 'gitmojiCache'; +const GITMOJI_CACHE_TTL_MS = 1000 * 60 * 60 * 24 * 7; +const GITMOJI_CACHE_VERSION = '1'; +const GITMOJI_SOURCE_URL = + 'https://raw.githubusercontent.com/carloscuesta/gitmoji/master/packages/gitmojis/src/gitmojis.json'; + +const isGitmojiEntry = (value: unknown): value is GitmojiEntry => { + if (!value || typeof value !== 'object') return false; + const candidate = value as Record; + return ( + typeof candidate.emoji === 'string' && + typeof candidate.code === 'string' && + typeof candidate.description === 'string' + ); +}; + +const readGitmojiCache = (): GitmojiCachePayload | null => { + if (typeof window === 'undefined') return null; + try { + const raw = localStorage.getItem(GITMOJI_CACHE_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw) as Partial; + if (!parsed || parsed.version !== GITMOJI_CACHE_VERSION || typeof parsed.fetchedAt !== 'number') { + return null; + } + if (!Array.isArray(parsed.gitmojis)) return null; + const gitmojis = parsed.gitmojis.filter(isGitmojiEntry); + return { gitmojis, fetchedAt: parsed.fetchedAt, version: parsed.version }; + } catch { + return null; + } +}; + +const writeGitmojiCache = (gitmojis: GitmojiEntry[]) => { + if (typeof window === 'undefined') return; + try { + const payload: GitmojiCachePayload = { + gitmojis, + fetchedAt: Date.now(), + version: GITMOJI_CACHE_VERSION, + }; + localStorage.setItem(GITMOJI_CACHE_KEY, JSON.stringify(payload)); + } catch { + } +}; + +const isGitmojiCacheFresh = (payload: GitmojiCachePayload) => + Date.now() - payload.fetchedAt < GITMOJI_CACHE_TTL_MS; + let gitViewSnapshot: GitViewSnapshot | null = null; const useEffectiveDirectory = () => { @@ -142,7 +203,7 @@ export const GitView: React.FC = () => { const [commitFilesMap, setCommitFilesMap] = React.useState>(new Map()); const [loadingCommitHashes, setLoadingCommitHashes] = React.useState>(new Set()); const [remoteUrl, setRemoteUrl] = React.useState(null); - const [gitmojiEmojis, setGitmojiEmojis] = React.useState>([]); + const [gitmojiEmojis, setGitmojiEmojis] = React.useState([]); const [gitmojiSearch, setGitmojiSearch] = React.useState(''); const handleCopyCommitHash = React.useCallback((hash: string) => { @@ -243,15 +304,27 @@ export const GitView: React.FC = () => { let cancelled = false; + const cached = readGitmojiCache(); + if (cached) { + setGitmojiEmojis(cached.gitmojis); + if (isGitmojiCacheFresh(cached)) { + return () => { + cancelled = true; + }; + } + } + const loadGitmojis = async () => { try { - const response = await fetch('https://raw.githubusercontent.com/carloscuesta/gitmoji/master/packages/gitmojis/src/gitmojis.json'); + const response = await fetch(GITMOJI_SOURCE_URL); if (!response.ok) { throw new Error(`Failed to load gitmojis: ${response.statusText}`); } - const payload = (await response.json()) as { gitmojis?: Array<{ emoji: string; code: string; description: string }> }; + const payload = (await response.json()) as { gitmojis?: GitmojiEntry[] }; + const gitmojis = Array.isArray(payload.gitmojis) ? payload.gitmojis.filter(isGitmojiEntry) : []; if (!cancelled) { - setGitmojiEmojis(payload.gitmojis ?? []); + setGitmojiEmojis(gitmojis); + writeGitmojiCache(gitmojis); } } catch (error) { if (!cancelled) { @@ -260,7 +333,7 @@ export const GitView: React.FC = () => { } }; - loadGitmojis(); + void loadGitmojis(); return () => { cancelled = true;