Skip to content

feat: add SWR local storage cache layer for offline dashboard resiliency (#2801) - #3348

Open
yachikadev wants to merge 9 commits into
Priyanshu-byte-coder:mainfrom
yachikadev:feat/swr-dashboard-cache
Open

feat: add SWR local storage cache layer for offline dashboard resiliency (#2801)#3348
yachikadev wants to merge 9 commits into
Priyanshu-byte-coder:mainfrom
yachikadev:feat/swr-dashboard-cache

Conversation

@yachikadev

Copy link
Copy Markdown
Contributor

Summary

Implements a Stale-While-Revalidate (SWR) local storage cache layer for the dashboard's Streak Tracker and Goal Tracker widgets, so users see their last-known data instantly on load instead of a blank/loading state — even on spotty networks, during GitHub API rate-limiting, or fully offline.

Closes #2801

Changes

  • src/lib/localCache.ts (new) — lightweight saveSnapshot / getSnapshot / clearSnapshot helpers backed by localStorage, with try/catch guards so a full, disabled, or missing localStorage never throws at runtime.
  • src/components/StreakTracker.tsx — on mount, hydrates instantly from the last cached streak/contribution snapshot before the network call resolves. On successful fetch, persists the new snapshot. On fetch failure, falls back to the cached snapshot instead of showing an error.
  • src/components/GoalTracker.tsx — same pattern applied to the goals list: instant hydration from cache, snapshot saved on successful load, cached goals shown if the live fetch fails.

How it works

  1. Instant hydration — a useEffect on mount reads the cached snapshot from localStorage and populates state immediately, skipping the loading spinner if data is available.
  2. Background sync — the existing fetch logic runs unchanged in parallel; on success it silently overwrites both the UI state and the cached snapshot.
  3. Graceful fallback — if the live request fails (offline, rate-limited, etc.), the cached snapshot is shown instead of an error message, whenever one exists.

Notes

  • Scoped to the two widgets driven by /api/metrics and /api/goals (streaks + goals), matching the issue's acceptance criteria. Other widgets are untouched and can adopt the same localCache.ts utility in future PRs if desired.

Adds saveSnapshot/getSnapshot/clearSnapshot helpers with
try-catch guards so localStorage failures (disabled, quota
full, SSR context) never throw at runtime.
Adds saveSnapshot/getSnapshot/clearSnapshot helpers with
try-catch guards so localStorage failures (disabled, quota
full, SSR context) never throw at runtime.
- Load last cached streak/contribution data from localStorage
  on mount before the network request resolves
- Persist fresh data to localStorage on successful fetch
- Fall back to cached snapshot instead of showing an error
  when the live fetch fails (offline / rate-limited)

Part of Priyanshu-byte-coder#2801
@github-actions github-actions Bot added gssoc26 GSSoC 2026 contribution type:feature GSSoC type bonus: new feature labels Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

GSSoC Label Checklist 🏷️

@Priyanshu-byte-coder — please apply the appropriate labels before merging:

Difficulty (pick one):

  • level:beginner — 20 pts
  • level:intermediate — 35 pts
  • level:advanced — 55 pts
  • level:critical — 80 pts

Quality (optional):

  • quality:clean — ×1.2 multiplier
  • quality:exceptional — ×1.5 multiplier

Validation (required to score):

  • gssoc:approved — counts for points
  • gssoc:invalid / gssoc:spam / gssoc:ai-slop — does not score

Type labels (type:*) are auto-detected from files and title. Review and adjust if needed.
Points formula: (difficulty × quality_multiplier) + type_bonus

@github-actions github-actions Bot added type:bug GSSoC type bonus: bug fix type:design GSSoC type bonus: UI/design (+10 pts) type:performance GSSoC type bonus: performance (+15 pts) labels Aug 2, 2026
- Load last cached goals list from localStorage on mount
- Persist fresh goals to localStorage on successful load
- Fall back to cached goals instead of an error message
  when the live fetch fails

Closes Priyanshu-byte-coder#2801
@yachikadev
yachikadev force-pushed the feat/swr-dashboard-cache branch from 7675cab to 5e31350 Compare August 3, 2026 16:43
@Priyanshu-byte-coder

Copy link
Copy Markdown
Owner

Good PR — the stale-while-revalidate approach is right: instant hydration from localCache, fresh snapshot persisted after each fetch, and falling back to the cached snapshot instead of showing an error when the network fails. CI is green and src/lib/localCache.ts is a clean, reusable helper. (Note #2883 attempted the same thing and is now closed, so this is the one I want.)

One thing to fix before I merge — two inline comments are in Hindi:

// SWR fallback: agar live fetch fail ho, cached snapshot dikhta rahe   (GoalTracker.tsx)
// SWR fallback: agar live fetch fail ho, cached snapshot dikhado       (StreakTracker.tsx)

Code comments in this repo are English-only so every contributor can read them. Please reword those two (e.g. // SWR fallback: if the live fetch fails, keep showing the cached snapshot) and I'll merge it.

@yachikadev

Copy link
Copy Markdown
Contributor Author

Hi @Priyanshu-byte-coder , just following up on this PR — it's been open for a while now. Could you review it when you have a moment? Happy to make any changes you'd suggest, or if it's good to go, would appreciate the merge. Thanks!

@PriyanshuValura

Copy link
Copy Markdown

Nice shape overall — localCache.ts is tidy (SSR guard, try/catch, typed, prefixed keys), and the SWR sequence is right: hydrate from cache, fetch, persist. Keying the streak snapshot per account (streak-${selectedAccount}) is a good catch that's easy to miss.

Three things stop me merging it as-is. Two are correctness, not polish.

1. The snapshot never expires. CachedSnapshot stores timestamp, but nothing ever reads it. So a snapshot written three months ago hydrates on mount and calls setLoading(false) — the user sees a stale streak with no spinner and no indication it's old. It gets worse in the failure path: on a fetch error you setError(null) and keep showing the cached copy, so months-old numbers render as if they were live. A dashboard quietly showing wrong figures is worse than one showing an error.

You already have the field to fix it:

export function getSnapshot<T>(key: string, maxAgeMs = 24 * 60 * 60 * 1000) {
  // ...
  if (Date.now() - parsed.timestamp > maxAgeMs) return null;
  return parsed;
}

And on the error path, surfacing something like "showing data from 2 hours ago" would be better than silently swallowing the error.

2. Nothing clears the cache on sign-out. clearSnapshot is exported but never called anywhere in the diff. localStorage is per-origin, not per-user, so if two people use DevTrack in the same browser — a shared laptop, or someone switching GitHub accounts — the second user's first paint hydrates the first user's goals and streak before the network responds. SignOutButton calls signOut({ callbackUrl: "/" }) and touches nothing else.

Either clear the devtrack_cache_ keys on sign-out, or fold the user id into the key the way you already did for account. Keying by user is probably the sturdier of the two.

3. Needs a rebase. The Continuous Integration Security & Audit Suite failure is stale — that job was red repo-wide when you opened this and is green on main now. A rebase should clear it.

Minor, non-blocking: saveSnapshot has no typeof window guard while getSnapshot does. The try/catch covers it in practice, but the asymmetry looks unintentional. Also worth adding a small test for localCache — expiry and the malformed-JSON path are exactly the cases that break quietly later.

Fix 1 and 2 and I'll merge this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gssoc26 GSSoC 2026 contribution type:bug GSSoC type bonus: bug fix type:design GSSoC type bonus: UI/design (+10 pts) type:feature GSSoC type bonus: new feature type:performance GSSoC type bonus: performance (+15 pts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] Add Stale-While-Revalidate (SWR) Local Storage Cache Layer for Offline Dashboard Resiliency

3 participants