feat(git): cache gitmojis locally with ttl

This commit is contained in:
Bohdan Triapitsyn
2026-01-17 11:17:12 +02:00
parent b02712bee1
commit 033e370c55
+78 -5
View File
@@ -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<string, unknown>;
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<GitmojiCachePayload>;
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<Map<string, CommitFileEntry[]>>(new Map());
const [loadingCommitHashes, setLoadingCommitHashes] = React.useState<Set<string>>(new Set());
const [remoteUrl, setRemoteUrl] = React.useState<string | null>(null);
const [gitmojiEmojis, setGitmojiEmojis] = React.useState<Array<{ emoji: string; code: string; description: string }>>([]);
const [gitmojiEmojis, setGitmojiEmojis] = React.useState<GitmojiEntry[]>([]);
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;