From 1d267e47d7b1d234c11812394d3f1a2b88d04e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ai-chan-0411=20=28=E8=97=8D=29?= Date: Sun, 12 Apr 2026 06:24:50 +0900 Subject: [PATCH] feat: implement client-side caching for GitHub API responses (closes #33) Add in-memory cache with 1-day TTL to reduce GitHub API calls. Cache keyed by username (case-insensitive) to avoid duplicate requests. Extracted reusable InMemoryCache class in lib/cache.ts with configurable TTL constants. --- lib/cache.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++++ lib/github.ts | 14 +++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 lib/cache.ts diff --git a/lib/cache.ts b/lib/cache.ts new file mode 100644 index 0000000..960cb12 --- /dev/null +++ b/lib/cache.ts @@ -0,0 +1,46 @@ +interface CacheEntry { + data: T; + expiresAt: number; +} + +class InMemoryCache { + private store = new Map>(); + + set(key: string, data: T, ttlMs: number): void { + this.store.set(key, { + data, + expiresAt: Date.now() + ttlMs, + }); + } + + get(key: string): T | null { + const entry = this.store.get(key) as CacheEntry | undefined; + if (!entry) return null; + if (Date.now() > entry.expiresAt) { + this.store.delete(key); + return null; + } + return entry.data; + } + + has(key: string): boolean { + return this.get(key) !== null; + } + + delete(key: string): void { + this.store.delete(key); + } + + clear(): void { + this.store.clear(); + } +} + +// Singleton cache instance shared across API requests +export const cache = new InMemoryCache(); + +export const CACHE_TTL = { + ONE_DAY: 24 * 60 * 60 * 1000, + ONE_HOUR: 60 * 60 * 1000, + FIVE_MINUTES: 5 * 60 * 1000, +}; diff --git a/lib/github.ts b/lib/github.ts index 4023aac..cf99e1c 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -1,5 +1,6 @@ import { ContributionTotals, GitHubUserData, PullRequestNode, RepoNode } from "@/types/github"; import { graphql } from "@octokit/graphql"; +import { cache, CACHE_TTL } from "./cache"; if (!process.env.GITHUB_TOKEN) { throw new Error("Missing GITHUB_TOKEN"); @@ -60,15 +61,26 @@ const QUERY = /* GraphQL */ ` export async function fetchGitHubUserData( username: string ): Promise { + const cacheKey = `github:user:${username.toLowerCase()}`; + + const cached = cache.get(cacheKey); + if (cached) { + return cached; + } + const { user } = await client<{ user: any }>(QUERY, { login: username }); if (!user) { throw new Error("User not found"); } - return { + const data: GitHubUserData = { repos: user.repositories.nodes as RepoNode[], pullRequests: user.pullRequests.nodes as PullRequestNode[], contributions: user.contributionsCollection as ContributionTotals, }; + + cache.set(cacheKey, data, CACHE_TTL.ONE_DAY); + + return data; }