fix: coalesce fetchReposForAllHosts so stale all-host loads can't resurrect removed projects (#7020)#7288
Conversation
…urrect removed projects (stablyai#7020) repos:changed fires once per persisted mutation, so deleting a project group with contained projects emits an N+1 burst. In the runtime-active sidebar that burst refreshes via fetchReposForAllHosts, which had no stale-fetch guard: stablyai#7024 scoped its monotonic generation guard to fetchRepos only (a shared counter let an unrelated fetchRepos abort an in-flight all-host load and drop every host's repos). So overlapping all-host loads could resolve out of order and an earlier one that read pre-removal state could resolve last, resurrecting removed projects as ghost sidebar rows until restart. Single-flight fetchReposForAllHosts: run one load at a time and, when events arrive mid-load, run exactly one trailing reload afterwards. The trailing reload reads at or after the final mutation, so the sidebar settles on the final persisted state; the burst's O(N^2) per-host fetches also collapse to ~1 trailing reload. This is method-local (no shared counter, no cross-call interference), closing the all-host gap stablyai#7024 left open. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
📝 WalkthroughWalkthroughThis PR adds single-flight coordination to ChangesRelated issues: Related PRs: None identified. Suggested labels: bug, tested Suggested reviewers: None identified. Sequence Diagram(s)sequenceDiagram
participant fetchReposForAllHosts
participant loadReposForAllHosts
fetchReposForAllHosts->>fetchReposForAllHosts: set reposAllHostsLoadCyclePromise
fetchReposForAllHosts->>loadReposForAllHosts: load local repos, then runtime repos
fetchReposForAllHosts->>fetchReposForAllHosts: set reposAllHostsReloadRequested on overlap
fetchReposForAllHosts->>loadReposForAllHosts: rerun once if reload was requested
fetchReposForAllHosts->>fetchReposForAllHosts: clear reposAllHostsLoadCyclePromise
Poem A burst of calls, one path to keep, 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 39ea396c-ed3a-4dc5-921b-7c8bb633ab0f
📒 Files selected for processing (2)
src/renderer/src/store/slices/repos-all-hosts-stale-fetch.test.tssrc/renderer/src/store/slices/repos.ts
…ed cycle Overlapping calls resolved as soon as the trailing reload was requested, so an awaited caller (cold-start metrics, dependent fetches that read repos right after awaiting) could continue before the refresh its event triggered landed. Hold the in-flight cycle promise in the slice and return it to overlapping callers so every caller waits for the same completed all-host refresh. Addresses CodeRabbit review on stablyai#7288. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Summary
Follow-up to #7024, closing the all-host gap it deliberately left open.
Deleting a Project Group with "Remove contained projects" could leave the removed projects as stale, unusable sidebar rows until restart (#7020).
repos:changedis broadcast once per persisted mutation, so a group delete that also removes N contained projects emits an N+1 burst.#7024 fixed the default path by scoping a monotonic generation guard to
fetchReposonly — deliberately, because a shared counter let an unrelatedfetchReposbump the generation and abort an in-flight all-host load, dropping every host's repos. That left the runtime-active sidebar path, which refreshes viafetchReposForAllHosts, unguarded: an N+1 burst starts N+1 overlapping all-host loads, and an earlier one that read pre-removal state can resolve last and resurrect the removed projects.Fix
Single-flight
fetchReposForAllHosts:Scope: only the repos all-host path (the reported symptom is project ghost rows). The equivalent
fetchProjectGroupsForAllHosts/fetchFolderWorkspacesForAllHostsout-of-order refresh is known but deferred.Screenshots
No visual change.
Testing
pnpm lint(oxlint + react-doctor) on changed filestsgotypecheck (config/tsconfig.tc.web.json)repos-all-hosts-stale-fetch.test.ts— burst coalesces into one trailing reload that reads the final state; isolated event runs immediatelyrepos-stale-fetch.test.ts(2) andrepos.test.ts(22) pass unchangedpnpm test/pnpm build— left to CIAI Review Report
Reviewed with Claude Code. Checks:
repos:changedburst goes from N+1 overlapping all-host loads (O(N²) host fetches) to ~2 loads. Single-flight state is plain booleans no component selects, so no extra renders.Notes
Method-local single-flight (chosen over extending #7024's generation guard to avoid the shared-counter cross-cancellation it documented). Supersedes the broader #7083 for the all-host slice; #7083 left open for now.
X: @mark86202384100