Files
yellowjacket/.planning/milestones/v1.0-phases/07-backend-performance/07-02-PLAN.md
T
yonlu 6ce0661fca chore: complete v1.0 Consolidation milestone
Archive milestone artifacts:
- milestones/v1.0-ROADMAP.md (full roadmap archive)
- milestones/v1.0-REQUIREMENTS.md (26/26 requirements complete)
- milestones/v1.0-phases/ (8 phase directories with plans, summaries, verifications)

Updated:
- PROJECT.md: full evolution review, all consolidation requirements validated
- ROADMAP.md: collapsed to milestone summary with archive link
- STATE.md: reset for next milestone
- MILESTONES.md: created with stats and accomplishments
- RETROSPECTIVE.md: created with lessons learned

Deleted:
- REQUIREMENTS.md (archived, fresh for next milestone)

8 phases, 17 plans, 34 tasks, 84 tests added, 6 days
2026-03-05 09:34:43 -05:00

182 lines
7.3 KiB
Markdown

---
phase: 07-backend-performance
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- frontend/src/store/library-store.ts
autonomous: true
requirements:
- PERF-03
must_haves:
truths:
- "LibraryStore constructor does NOT call eagerFetch() — app shell renders instantly"
- "After DOM is ready, eagerFetch() is called — all 4 data types (tracks, albums, artists, genres) are still loaded eagerly"
- "Views display loading state while data arrives (existing isTracksLoading/isAlbumsLoading/etc. flags)"
- "Post-scan invalidation still calls eagerFetch() to re-fetch everything"
- "First view switch after startup has data available (no empty views)"
artifacts:
- path: "frontend/src/store/library-store.ts"
provides: "Deferred eagerFetch — constructor omits data fetch, Wails DomReady event or document ready triggers it"
contains: "EventsOn"
key_links:
- from: "frontend/src/store/library-store.ts (constructor)"
to: "frontend/src/store/library-store.ts (eagerFetch)"
via: "Wails EventsOnce for dom-ready event OR document.readyState listener"
pattern: "eagerFetch"
---
<objective>
Defer library data loading from constructor time to after DOM is ready, so the app shell renders instantly without blocking on backend data fetches.
Purpose: Currently, `LibraryStore`'s constructor calls `eagerFetch()` which immediately fires 4 async Wails binding calls (`GetAllTracks`, `GetAllAlbums`, `GetAllArtists`, `GetAllGenresWithCounts`). Since the store singleton is instantiated during ES module evaluation (at import time), these 4 backend roundtrips begin before the DOM has even finished rendering, competing with the app shell paint. Moving `eagerFetch()` to after DOM ready means the app shell renders first, then data loads begin. The user still gets all 4 data types eagerly loaded — the change is WHEN, not WHETHER.
Output: Modified library-store.ts with deferred eagerFetch trigger.
</objective>
<execution_context>
@/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md
@/home/caleb/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@frontend/src/store/library-store.ts
@frontend/index.ts
</context>
<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
From frontend/src/store/library-store.ts:
```typescript
class LibraryStore {
constructor() {
EventsOn(Events.LibraryScanComplete, () => {
this.invalidate();
});
this.loadCoverSize();
this.eagerFetch(); // <-- THIS LINE MUST BE REMOVED FROM CONSTRUCTOR
}
private eagerFetch(): void {
void this.getTracks();
void this.getAlbums();
void this.getArtists();
void this.getGenres();
}
private invalidate(): void {
this.tracks = null;
this.albums = null;
this.artists = null;
this.genres = null;
this.scrollPositions = { tracks: 0, albums: 0, artists: 0, genres: 0 };
this.notify();
this.eagerFetch(); // <-- THIS CALL IN invalidate() MUST REMAIN
}
}
```
From frontend/index.ts:
```typescript
// At the bottom of index.ts, after all imports and setup:
void Player.EmitCurrentState();
void Queue.EmitCurrentState();
// Library data fetching should happen around this point (after DOM is ready)
```
</interfaces>
<tasks>
<task type="auto">
<name>Task 1: Defer eagerFetch from constructor to post-DOM-ready</name>
<files>frontend/src/store/library-store.ts</files>
<action>
**Modify the `LibraryStore` constructor to NOT call `eagerFetch()`:**
1. Remove the `this.eagerFetch()` line from the constructor. The constructor should only do:
- Register the `LibraryScanComplete` event listener
- Call `this.loadCoverSize()`
2. **Add a deferred fetch trigger.** The best mechanism for this Wails app is to check `document.readyState` and either call immediately or listen for the load event. Since the LibraryStore singleton is instantiated during module evaluation (import time), the DOM may or may not be ready:
```typescript
constructor() {
EventsOn(Events.LibraryScanComplete, () => {
this.invalidate();
});
this.loadCoverSize();
this.deferEagerFetch();
}
private deferEagerFetch(): void {
if (document.readyState === 'complete') {
// DOM already ready (shouldn't happen during module eval, but safe)
this.eagerFetch();
} else {
// Wait for DOM to be ready, then fetch
window.addEventListener('load', () => {
this.eagerFetch();
}, { once: true });
}
}
```
**Why `load` event and not `DOMContentLoaded`:** The `DOMContentLoaded` event fires when the HTML is parsed but before stylesheets, images, and subframes finish loading. The `load` event fires after everything is ready. Using `load` ensures the app shell has fully rendered (CSS applied, layout complete) before data fetches compete for resources. This is the mechanism that ensures the fastest visual shell render.
**Alternative (Claude's discretion):** If `load` causes a noticeable delay in data availability (because it waits for ALL resources), `DOMContentLoaded` is acceptable — it fires earlier and still defers past the initial module evaluation. Use judgment based on what feels right, but do NOT use `requestAnimationFrame` or `setTimeout` hacks.
3. **Keep `eagerFetch()` call in `invalidate()` unchanged** — post-scan invalidation should still eagerly re-fetch everything immediately (the app is already running and rendered at that point).
4. **Keep `eagerFetch()` method itself unchanged** — it should still call all 4 getters (`getTracks`, `getAlbums`, `getArtists`, `getGenres`).
5. **Keep all `isTracksLoading()` / `isAlbumsLoading()` / etc. accessors unchanged** — views already use these for loading states. When the deferred fetch runs, these flags will be set to true and views will show loading state naturally.
**What NOT to change:**
- Do NOT make loading per-view or lazy-per-access — user explicitly wants ALL views pre-loaded
- Do NOT change `invalidate()` behavior
- Do NOT change the data access methods (`getTracks`, `getAlbums`, etc.)
- Do NOT remove `eagerFetch` method — just defer WHEN it's first called
</action>
<verify>
cd frontend && npx tsc --noEmit
</verify>
<done>
- LibraryStore constructor no longer calls eagerFetch() directly
- eagerFetch() is deferred to after DOM ready (via load or DOMContentLoaded event)
- invalidate() still calls eagerFetch() immediately (for post-scan refresh)
- All 4 data types still loaded eagerly once triggered
- TypeScript compiles without errors
</done>
</task>
</tasks>
<verification>
```bash
# TypeScript compiles
cd frontend && npx tsc --noEmit
# Frontend builds
cd frontend && npx vite build
```
</verification>
<success_criteria>
- LibraryStore constructor does NOT call eagerFetch()
- eagerFetch() is triggered after DOM is ready
- All 4 data types (tracks, albums, artists, genres) are still eagerly loaded once DOM is ready
- Post-scan invalidation behavior is unchanged
- TypeScript compiles and frontend builds
</success_criteria>
<output>
After completion, create `.planning/phases/07-backend-performance/07-02-SUMMARY.md`
</output>