feat(cache): make issue/PR loading async to prevent UI freeze #368

Merged
fuzzy merged 5 commits from feat/356-paginate-issues-prs into main 2026-07-08 11:29:03 +00:00
Owner

What

IssueView.Load and PRView.Load/LoadAll already fetch all pages (loop until exhaustion), but run synchronously blocking the UI. This moves them to goroutine + QueueUpdateDraw pattern, matching the home view approach.

  • Async load prevents UI freeze on large issue/PR lists
  • Errors stored on struct, accessible via LastError()
  • Mutex protects concurrent access to shared state
  • Load() always returns nil (callers already handle this)

Why

Multi-page API fetches (up to 10 pages × 50 items) can take seconds. Blocking the tview event loop during fetch makes the terminal unresponsive.

Closes #356

## What IssueView.Load and PRView.Load/LoadAll already fetch all pages (loop until exhaustion), but run synchronously blocking the UI. This moves them to goroutine + QueueUpdateDraw pattern, matching the home view approach. - Async load prevents UI freeze on large issue/PR lists - Errors stored on struct, accessible via `LastError()` - Mutex protects concurrent access to shared state - Load() always returns nil (callers already handle this) ## Why Multi-page API fetches (up to 10 pages × 50 items) can take seconds. Blocking the tview event loop during fetch makes the terminal unresponsive. Closes #356
feat(cache): make issue/PR loading async to prevent UI freeze
All checks were successful
Test and Release / test (pull_request) Successful in 3m45s
Test and Release / lint (pull_request) Successful in 4m29s
6283f87b20
IssueView.Load and PRView.Load/LoadAll already fetch all pages
but run synchronously, blocking the UI. Move to goroutine pattern
with QueueUpdateDraw. Store errors for LastError() access.
All callers already check Load error synchronously — now always nil.
Tab switching during load is still responsive.
fuzzy scheduled this pull request to auto merge when all checks succeed 2026-07-08 10:42:17 +00:00
fix(cache): bake state filter into async Load, remove Reload
All checks were successful
Test and Release / lint (pull_request) Successful in 2m43s
Test and Release / test (pull_request) Successful in 4m13s
8a3caabf12
Load/LoadAll now accept state parameter, filter internally inside
the async goroutine before QueueUpdateDraw. Removes the broken
Reload-after-return race where filters ran before data loaded.
Alt+O/Alt+C toggles call Load/LoadAll with state instead.
fix(cache): revert issue/PR loading to synchronous
All checks were successful
Test and Release / lint (pull_request) Successful in 2m44s
Test and Release / test (pull_request) Successful in 8m39s
38e6870fc8
Async loading broke the pulls tab (never displayed data).
Revert to original synchronous pagination which fetches all
pages and renders in the tab handler's call chain.
feat(cache): add SQLite-backed persistent API response cache
All checks were successful
Test and Release / lint (pull_request) Successful in 2m48s
Test and Release / test (pull_request) Successful in 3m8s
35de54b774
fix(cache): embed prefix in cache key, fix json tags for persistence
All checks were successful
Test and Release / test (pull_request) Successful in 2m54s
Test and Release / lint (pull_request) Successful in 3m2s
9af63b1425
- cacheKey now returns 'prefix:hash' so cacheTTLFor matches endpoint
- Remove json:"-" from RepoOwner/RepoName on Issue/PullRequest
  so JSON roundtrip preserves these fields for persistent cache
fuzzy scheduled this pull request to auto merge when all checks succeed 2026-07-08 11:28:06 +00:00
the.auditor left a comment

Review

Result: Approved — no blocking issues. Scope is significantly broader than the title.

Summary: Major refactor of the caching layer. Adds PersistentCache (SQLite-backed key-value store) and generic cachedLoad[T] eliminating repeated get/set boilerplate across all CachedClient methods. Per-key TTL via cacheTTLFor() (30s default, 5min lists, 72h immutable). Model field tags changed to enable JSON round-trip for persistent cache.

Non-blocking:

  • #369 — Race condition in get(): TTL check done outside mutex lock, could delete a fresh entry set by another goroutine
  • Unintended: pv.Reload() call removed from PRs tab handler — state filtering on load may be broken
  • cacheKey format changed (now prefix:hash) — incompatible with any hypothetical previous persisted data

Checklist:

  • Correctness: Generic cachedLoad eliminates boilerplate correctly, persistent cache round-trips via JSON, per-key TTLs appropriate
  • Completeness: All CachedClient methods migrated to cachedLoad, persistent cache wired into app startup and forge switching
  • Race safety: ⚠️ get() TTL check outside lock (minor — worst case is one extra fetch)
  • Maintainability: Massive reduction in boilerplate — each method is now a one-liner
## Review **Result: Approved** — no blocking issues. Scope is significantly broader than the title. **Summary:** Major refactor of the caching layer. Adds `PersistentCache` (SQLite-backed key-value store) and generic `cachedLoad[T]` eliminating repeated get/set boilerplate across all `CachedClient` methods. Per-key TTL via `cacheTTLFor()` (30s default, 5min lists, 72h immutable). Model field tags changed to enable JSON round-trip for persistent cache. **Non-blocking:** - #369 — Race condition in `get()`: TTL check done outside mutex lock, could delete a fresh entry set by another goroutine - Unintended: `pv.Reload()` call removed from PRs tab handler — state filtering on load may be broken - `cacheKey` format changed (now `prefix:hash`) — incompatible with any hypothetical previous persisted data **Checklist:** - Correctness: ✅ Generic cachedLoad eliminates boilerplate correctly, persistent cache round-trips via JSON, per-key TTLs appropriate - Completeness: ✅ All CachedClient methods migrated to cachedLoad, persistent cache wired into app startup and forge switching - Race safety: ⚠️ get() TTL check outside lock (minor — worst case is one extra fetch) - Maintainability: ✅ Massive reduction in boilerplate — each method is now a one-liner
fuzzy merged commit 9af63b1425 into main 2026-07-08 11:29:03 +00:00
fuzzy deleted branch feat/356-paginate-issues-prs 2026-07-08 11:29:03 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
thwap/steeper!368
No description provided.