Skip to content

fix: load watchable repos in the background and cache them - #24

Merged
facundofarias merged 1 commit into
mainfrom
fix/background-repo-loading
Jul 1, 2026
Merged

facundofarias merged 1 commit into
mainfrom
fix/background-repo-loading

Conversation

@facundofarias

@facundofarias facundofarias commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Reported in #23: on Firefox (Zen) the repo picker showed a spinner "forever." The reporter later clarified it does load — it's just slow, and it doesn't continue if you close the popup.

Root cause: Repos.tsx rebuilt the entire available-repo list (your personal repos plus every org's repos, for each connected platform) from scratch on every open, and did it inside the popup. So:

  • it was slow every time (no caching), and
  • closing the popup aborted the in-flight fetch, so the next open started over.

Fix (background + cache-first)

  • Service worker owns the fetch. New FETCH_AVAILABLE_REPOS message triggers refreshAvailableRepos(), which is deduplicated via an in-flight promise so a reopened popup awaits the same run instead of starting a second. Because it runs in the SW, the fetch survives the popup closing — exactly the behaviour the reporter expected.
  • Cached in chrome.storage. New AvailableReposCache (getCachedAvailableRepos / saveCachedAvailableRepos). The popup renders cache-first (instant on repeat opens) and shows an "Updating…" indicator while refreshing.
  • No more endless spinner. If a refresh loads zero repos across all accounts, the cache records an error and the page shows it instead of spinning. Partial failures still show the repos that did load.

Verification

  • npm run typecheck ✅
  • npm run lint ✅
  • npm run build ✅ (Chrome)

Same code path runs on all platforms, so this is verifiable on Chrome as well as the Firefox build. I couldn't reproduce Zen locally, but the change is deterministic (moves work off the popup lifecycle + adds caching) and directly addresses the reported behaviour.

Fixes #23

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Reworked the repos view to load from cached data first, so results appear faster and continue updating in the background.
    • Added an “Updating…” status indicator while fresh data is being fetched.
  • Bug Fixes

    • Improved handling when repo refreshes fail completely, showing an error state instead of an endless loading spinner.
    • Kept the repos list more stable by preserving saved watch/pin states while new data arrives.

The Repos page rebuilt the full available-repo list (personal repos plus
every org's repos, across each connected platform) from scratch on every
open, inside the popup. That made it slow every time — and because the work
ran in the popup, closing it aborted the fetch and the next open started
over. On slower connections (e.g. Firefox) this looked like an endless
spinner.

Move the fetch into the service worker (FETCH_AVAILABLE_REPOS, dedup'd via
an in-flight promise) and cache the result in chrome.storage. The popup now
renders cache-first, shows an "Updating…" indicator, and re-reads the cache
once the refresh resolves. Because the fetch runs in the SW it keeps going
after the popup closes, and subsequent opens are instant. A refresh that
loads zero repos across all accounts caches an error the page surfaces
instead of spinning forever.

Fixes #23

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a background service-worker-driven cache for available watchable repos across GitHub/GitLab/Bitbucket, with deduped in-flight refreshes persisted to chrome.storage.local. Popup renders cache-first with an "Updating…" indicator and error panel, triggered via a new FETCH_AVAILABLE_REPOS message. Documentation updated accordingly.

Changes

Available Repos Caching and Refresh Flow

Layer / File(s) Summary
Cache storage contract
src/shared/storage.ts, src/shared/types.ts
Adds AVAILABLE_REPOS_CACHE_KEY, AvailableRepo/AvailableReposCache types, getCachedAvailableRepos/saveCachedAvailableRepos helpers, clearAll() cleanup, and a new FETCH_AVAILABLE_REPOS message variant.
Background service worker refresh flow
src/background/service-worker.ts
Adds deduped in-flight refresh logic that fetches repos per platform/account, captures per-account errors, persists the cache, and handles FETCH_AVAILABLE_REPOS messages.
Popup cache-first rendering and UI states
src/popup/pages/Repos.tsx
Builds repo list from cached data merged with watch state, loads cache-first then triggers background refresh, shows "Updating…" status and an error panel on empty state.
Architecture documentation updates
CLAUDE.md
Documents the background available-repo fetch, cache-first popup behavior, and design decisions including zero-repos error handling (references issue #23).

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Repos as Repos.tsx
  participant Worker as service-worker.ts
  participant Storage as storage.ts (chrome.storage.local)

  Repos->>Storage: getCachedAvailableRepos()
  Storage-->>Repos: cached repos (or null)
  Repos->>Worker: sendMessage(FETCH_AVAILABLE_REPOS)
  Worker->>Worker: refreshAvailableRepos() (dedup in-flight)
  Worker->>Worker: fetch repos per platform/account
  Worker->>Storage: saveCachedAvailableRepos({repos, updatedAt, error})
  Worker-->>Repos: {done: true}
  Repos->>Storage: getCachedAvailableRepos()
  Storage-->>Repos: updated repos
  Repos->>Repos: buildRepoList() + render (Updating.../error)
Loading

Suggested reviewers: thdurante, MartaKar

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: moving repo loading to the background and caching it.
Description check ✅ Passed It includes the problem, fix, and verification, so the key required information is present.
Linked Issues check ✅ Passed It addresses #23 by moving repo fetches to the service worker, caching results, and showing an error instead of an endless spinner.
Out of Scope Changes check ✅ Passed The docs updates mirror the code changes and no unrelated behavior changes stand out.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/background-repo-loading

Comment @coderabbitai help to get the list of available commands.

@facundofarias
facundofarias merged commit 4726e52 into main Jul 1, 2026
2 of 3 checks passed
@facundofarias
facundofarias deleted the fix/background-repo-loading branch July 1, 2026 09:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/popup/pages/Repos.tsx (1)

199-202: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Error panel lacks an announcing role for screen readers.

The loading state uses role="status" (Line 196) so assistive tech announces it, but this new error panel has no role/aria-live, so screen-reader users may not be notified when the fetch fails.

♿ Suggested fix
-        ) : error && repos.length === 0 ? (
-          <div className="px-4 py-8 text-center text-xs text-red-500 dark:text-red-400">
+        ) : error && repos.length === 0 ? (
+          <div role="alert" className="px-4 py-8 text-center text-xs text-red-500 dark:text-red-400">
             {error}
           </div>

As per path instructions, "preserve accessibility requirements such as ARIA labels/roles, aria-live... " — this new status panel should follow the same pattern as the adjacent loading state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/popup/pages/Repos.tsx` around lines 199 - 202, The error panel in Repos
should be announced to screen readers just like the adjacent loading state.
Update the conditional error render in Repos.tsx to include an appropriate
accessibility announcement pattern, such as a status/alert role and/or aria-live
on the error container, so assistive tech is notified when fetch fails.

Source: Path instructions

src/background/service-worker.ts (1)

95-111: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Accounts are fetched sequentially — consider parallelizing.

Each account's repo fetch is awaited inside the for loop, so total refresh time is the sum of all accounts' latencies rather than the max. With multiple connected accounts this noticeably delays the "Updating…" resolution.

♻️ Suggested parallelization
-  for (const account of accounts) {
-    try {
-      if (account.platform === 'github') {
-        const ghRepos = await github.getUserRepos(account.token);
-        for (const r of ghRepos) repos.push({ platform: 'github', fullName: r.full_name });
-      } else if (account.platform === 'gitlab') {
-        const glRepos = await gitlab.getUserProjects(account.token);
-        for (const r of glRepos) repos.push({ platform: 'gitlab', fullName: r.path_with_namespace });
-      } else if (account.platform === 'bitbucket') {
-        const bbRepos = await bitbucket.getUserRepositories(account.token);
-        for (const r of bbRepos) repos.push({ platform: 'bitbucket', fullName: r.full_name });
-      }
-    } catch (err) {
-      console.error(`[PR Radar] Failed to fetch repos for ${account.platform}:`, err);
-      failed.push(account.platform);
-    }
-  }
+  await Promise.all(accounts.map(async (account) => {
+    try {
+      if (account.platform === 'github') {
+        const ghRepos = await github.getUserRepos(account.token);
+        for (const r of ghRepos) repos.push({ platform: 'github', fullName: r.full_name });
+      } else if (account.platform === 'gitlab') {
+        const glRepos = await gitlab.getUserProjects(account.token);
+        for (const r of glRepos) repos.push({ platform: 'gitlab', fullName: r.path_with_namespace });
+      } else if (account.platform === 'bitbucket') {
+        const bbRepos = await bitbucket.getUserRepositories(account.token);
+        for (const r of bbRepos) repos.push({ platform: 'bitbucket', fullName: r.full_name });
+      }
+    } catch (err) {
+      console.error(`[PR Radar] Failed to fetch repos for ${account.platform}:`, err);
+      failed.push(account.platform);
+    }
+  }));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/background/service-worker.ts` around lines 95 - 111, The repo refresh in
service-worker.ts is processing each account sequentially inside the for-loop,
which makes total latency add up across accounts. Refactor the account fetch
logic in the repo-loading flow to run the per-account GitHub/GitLab/Bitbucket
requests in parallel (for example by building a set of promises and awaiting
them together), while preserving the existing platform-specific mapping, error
handling, and failed-account tracking in the try/catch around each account’s
fetch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/background/service-worker.ts`:
- Around line 122-139: The FETCH_AVAILABLE_REPOS path in
refreshAvailableRepos/doRefreshAvailableRepos can hang indefinitely because the
underlying API calls still rely on plain fetch() without any abort or timeout.
Add a timeout-based AbortController around the available-repos refresh flow
(either inside the shared GitHub/GitLab/Bitbucket request helpers or at the
service-worker message handling boundary) so stalled requests are cancelled and
the popup can recover instead of մն staying in refreshing. Keep the change
localized to the refreshAvailableRepos, doRefreshAvailableRepos, and relevant
API helper methods.

In `@src/shared/storage.ts`:
- Around line 105-136: Move the domain types `AvailableRepo` and
`AvailableReposCache` out of `storage.ts` and into `types.ts`, matching how
`WatchedRepo` is defined and consumed elsewhere. Update
`getCachedAvailableRepos` and `saveCachedAvailableRepos` in `storage.ts` to
import the types from `@/shared/types`, and change any imports such as in
`Repos.tsx` to reference `AvailableRepo` from `@/shared/types` instead of
`@/shared/storage`. Keep `AVAILABLE_REPOS_CACHE_KEY` and the storage helpers in
`storage.ts`; only the type definitions should move.

---

Nitpick comments:
In `@src/background/service-worker.ts`:
- Around line 95-111: The repo refresh in service-worker.ts is processing each
account sequentially inside the for-loop, which makes total latency add up
across accounts. Refactor the account fetch logic in the repo-loading flow to
run the per-account GitHub/GitLab/Bitbucket requests in parallel (for example by
building a set of promises and awaiting them together), while preserving the
existing platform-specific mapping, error handling, and failed-account tracking
in the try/catch around each account’s fetch.

In `@src/popup/pages/Repos.tsx`:
- Around line 199-202: The error panel in Repos should be announced to screen
readers just like the adjacent loading state. Update the conditional error
render in Repos.tsx to include an appropriate accessibility announcement
pattern, such as a status/alert role and/or aria-live on the error container, so
assistive tech is notified when fetch fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b382a491-2e54-4413-a6d1-6d9f05c0f85d

📥 Commits

Reviewing files that changed from the base of the PR and between 9675b80 and 6fe0f68.

📒 Files selected for processing (5)
  • CLAUDE.md
  • src/background/service-worker.ts
  • src/popup/pages/Repos.tsx
  • src/shared/storage.ts
  • src/shared/types.ts

Comment on lines +122 to +139
// Deduplicate concurrent refreshes: a popup reopened mid-fetch awaits the same
// in-flight run rather than kicking off a second one.
function refreshAvailableRepos(): Promise<void> {
if (!availableReposRefresh) {
availableReposRefresh = doRefreshAvailableRepos().finally(() => {
availableReposRefresh = null;
});
}
return availableReposRefresh;
}

chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) => {
if (message.type === 'POLL_NOW') {
pollPRs().then(() => sendResponse({ done: true }));
return true; // keep channel open for async sendResponse
} else if (message.type === 'FETCH_AVAILABLE_REPOS') {
refreshAvailableRepos().then(() => sendResponse({ done: true }));
return true; // keep channel open for async sendResponse

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -A8 'export (async )?function getUserRepos|export (async )?function getUserProjects|export (async )?function getUserRepositories' src/shared/api
rg -n 'AbortController|signal:|timeout' src/shared/api

Repository: deployhq/pr-radar

Length of output: 2286


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- service worker outline ---'
ast-grep outline src/background/service-worker.ts --view expanded || true

echo '--- popup Repos outline ---'
ast-grep outline src/popup/Repos.tsx --view expanded || true

echo '--- shared api outlines ---'
ast-grep outline src/shared/api/github.ts --view expanded || true
ast-grep outline src/shared/api/gitlab.ts --view expanded || true
ast-grep outline src/shared/api/bitbucket.ts --view expanded || true

echo '--- relevant ranges: service worker ---'
sed -n '1,220p' src/background/service-worker.ts | cat -n

echo '--- relevant ranges: Repos popup ---'
sed -n '1,260p' src/popup/Repos.tsx | cat -n

echo '--- relevant ranges: github api ---'
sed -n '1,280p' src/shared/api/github.ts | cat -n

echo '--- relevant ranges: gitlab api ---'
sed -n '1,220p' src/shared/api/gitlab.ts | cat -n

echo '--- relevant ranges: bitbucket api ---'
sed -n '1,220p' src/shared/api/bitbucket.ts | cat -n

Repository: deployhq/pr-radar

Length of output: 21325


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- locate popup repo component ---'
fd -HI 'Repos.tsx|Repos.ts|repos.tsx|repos.ts' src

echo '--- locate FETCH_AVAILABLE_REPOS sendMessage ---'
rg -n -A4 -B4 "FETCH_AVAILABLE_REPOS|refreshAvailableRepos|sendMessage\\(" src

echo '--- github fetch wrappers ---'
sed -n '1,120p' src/shared/api/github.ts | cat -n

echo '--- gitlab fetch wrappers ---'
sed -n '1,80p' src/shared/api/gitlab.ts | cat -n

echo '--- bitbucket fetch wrappers ---'
sed -n '1,60p' src/shared/api/bitbucket.ts | cat -n

Repository: deployhq/pr-radar

Length of output: 20501


Add a timeout to the available-repos refresh

doRefreshAvailableRepos() already handles per-account API failures, but src/shared/api/github.ts, src/shared/api/gitlab.ts, and src/shared/api/bitbucket.ts still use plain fetch() with no timeout/abort path. One stalled request can leave FETCH_AVAILABLE_REPOS pending and the popup stuck in refreshing; wrap those calls (or the popup sendMessage) with an AbortController/timeout so the refresh fails closed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/background/service-worker.ts` around lines 122 - 139, The
FETCH_AVAILABLE_REPOS path in refreshAvailableRepos/doRefreshAvailableRepos can
hang indefinitely because the underlying API calls still rely on plain fetch()
without any abort or timeout. Add a timeout-based AbortController around the
available-repos refresh flow (either inside the shared GitHub/GitLab/Bitbucket
request helpers or at the service-worker message handling boundary) so stalled
requests are cancelled and the popup can recover instead of մն staying in
refreshing. Keep the change localized to the refreshAvailableRepos,
doRefreshAvailableRepos, and relevant API helper methods.

Comment thread src/shared/storage.ts
Comment on lines +105 to +136
// === Available repos cache ===
// The full list of repos the user *could* watch. Fetching it is slow (personal
// repos + every org's repos, across each connected platform), so the background
// service worker fetches it and caches the result here. The Repos page renders
// from this cache instantly and lets the refresh run in the background — which
// means it survives the popup being closed. See issue #23.

export const AVAILABLE_REPOS_CACHE_KEY = 'pr_radar_available_repos';

export interface AvailableRepo {
platform: Platform;
fullName: string;
}

export interface AvailableReposCache {
repos: AvailableRepo[];
updatedAt: number;
// Set only when nothing could be loaded (every connected account errored), so
// the popup can show an error instead of an endless spinner. A partial failure
// still caches the repos that did load and leaves this undefined.
error?: string;
}

export async function getCachedAvailableRepos(): Promise<AvailableReposCache | null> {
const result = await chrome.storage.local.get(AVAILABLE_REPOS_CACHE_KEY);
return result[AVAILABLE_REPOS_CACHE_KEY] ?? null;
}

export async function saveCachedAvailableRepos(cache: AvailableReposCache): Promise<void> {
await chrome.storage.local.set({ [AVAILABLE_REPOS_CACHE_KEY]: cache });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Move AvailableRepo/AvailableReposCache domain types into types.ts.

These are domain types (analogous to WatchedRepo, which already lives in types.ts), but they're defined here in storage.ts instead. Repos.tsx even imports AvailableRepo from @/shared/storage rather than @/shared/types, which is inconsistent with how other domain types are sourced.

♻️ Suggested move
--- a/src/shared/types.ts
+++ b/src/shared/types.ts
+export interface AvailableRepo {
+  platform: Platform;
+  fullName: string;
+}
+
+export interface AvailableReposCache {
+  repos: AvailableRepo[];
+  updatedAt: number;
+  error?: string;
+}
--- a/src/shared/storage.ts
+++ b/src/shared/storage.ts
-export interface AvailableRepo {
-  platform: Platform;
-  fullName: string;
-}
-
-export interface AvailableReposCache {
-  repos: AvailableRepo[];
-  updatedAt: number;
-  // Set only when nothing could be loaded ...
-  error?: string;
-}
+import type { AvailableRepo, AvailableReposCache } from './types';

As per path instructions, "In TypeScript source files, use the shared types.ts and constants.ts definitions for domain types, status colors, platform labels, and sound options."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/storage.ts` around lines 105 - 136, Move the domain types
`AvailableRepo` and `AvailableReposCache` out of `storage.ts` and into
`types.ts`, matching how `WatchedRepo` is defined and consumed elsewhere. Update
`getCachedAvailableRepos` and `saveCachedAvailableRepos` in `storage.ts` to
import the types from `@/shared/types`, and change any imports such as in
`Repos.tsx` to reference `AvailableRepo` from `@/shared/types` instead of
`@/shared/storage`. Keep `AVAILABLE_REPOS_CACHE_KEY` and the storage helpers in
`storage.ts`; only the type definitions should move.

Source: Path instructions

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Does not load repos on Firefox (Zen)

1 participant