feat(git): cache gitmojis locally with ttl
This commit is contained in:
@@ -49,6 +49,67 @@ type GitViewSnapshot = {
|
|||||||
commitMessage: string;
|
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;
|
let gitViewSnapshot: GitViewSnapshot | null = null;
|
||||||
|
|
||||||
const useEffectiveDirectory = () => {
|
const useEffectiveDirectory = () => {
|
||||||
@@ -142,7 +203,7 @@ export const GitView: React.FC = () => {
|
|||||||
const [commitFilesMap, setCommitFilesMap] = React.useState<Map<string, CommitFileEntry[]>>(new Map());
|
const [commitFilesMap, setCommitFilesMap] = React.useState<Map<string, CommitFileEntry[]>>(new Map());
|
||||||
const [loadingCommitHashes, setLoadingCommitHashes] = React.useState<Set<string>>(new Set());
|
const [loadingCommitHashes, setLoadingCommitHashes] = React.useState<Set<string>>(new Set());
|
||||||
const [remoteUrl, setRemoteUrl] = React.useState<string | null>(null);
|
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 [gitmojiSearch, setGitmojiSearch] = React.useState('');
|
||||||
|
|
||||||
const handleCopyCommitHash = React.useCallback((hash: string) => {
|
const handleCopyCommitHash = React.useCallback((hash: string) => {
|
||||||
@@ -243,15 +304,27 @@ export const GitView: React.FC = () => {
|
|||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
|
const cached = readGitmojiCache();
|
||||||
|
if (cached) {
|
||||||
|
setGitmojiEmojis(cached.gitmojis);
|
||||||
|
if (isGitmojiCacheFresh(cached)) {
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadGitmojis = async () => {
|
const loadGitmojis = async () => {
|
||||||
try {
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to load gitmojis: ${response.statusText}`);
|
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) {
|
if (!cancelled) {
|
||||||
setGitmojiEmojis(payload.gitmojis ?? []);
|
setGitmojiEmojis(gitmojis);
|
||||||
|
writeGitmojiCache(gitmojis);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
@@ -260,7 +333,7 @@ export const GitView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadGitmojis();
|
void loadGitmojis();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user