# Feature Research: Quality Improvements **Domain:** Go/Wails/Lit desktop music player — consolidation milestone **Researched:** 2026-02-27 **Confidence:** HIGH (improvements grounded in codebase analysis + verified patterns) ## Feature Landscape This is a consolidation milestone. "Features" here are quality improvements, not new user-facing functionality. Each improvement addresses a specific concern documented in `.planning/codebase/CONCERNS.md`. --- ### Table Stakes (Must Fix — Codebase Is Unreliable Without These) These are correctness and reliability issues. Leaving them unfixed means the codebase has known race conditions, swallowed errors, and untested critical paths. | Improvement | Why Required | Complexity | Concern Ref | |-------------|-------------|------------|-------------| | **Fix SetContext data races in Queue, Library, Playlist** | `q.ctx`, `l.ctx`, `s.ctx` are written without locks but read under locks. This is a textbook data race detectable by `-race`. Even if startup ordering makes it safe today, any refactoring that changes init order silently introduces corruption. | LOW | Concurrency Concerns | | **Fix package-level `startupErr` variable** | Mutable package-level variable shared between `OnStartup` and `OnDomReady`. Not thread-safe, untestable. Move to `YellowJacketApp` struct field. | LOW | Tech Debt | | **Fix config file permissions (0o666 → 0o644)** | Writing world-writable config files is a security defect. One-line fix. | LOW | Error Handling Gaps | | **Fix swallowed errors in MPRIS lifecycle callbacks** | `_ =` on `Pause()` and `Seek()` errors from OS media controls. Invisible failures. At minimum log; ideally emit frontend notification. | LOW | Error Handling Gaps | | **Fix silently swallowed artist credit link error** | `_, _ = CreateArtistCreditArtist(...)` discards non-duplicate errors. Check error, ignore only UNIQUE constraint violations. | LOW | Error Handling Gaps | | **Separate scan warnings from fatal errors** | `Scan()` returns `errors.Join()` of all errors. Callers cannot distinguish "scan completed with 3 file warnings" from "scan completely failed". Return warnings in metrics, fatal errors as the error return. | MEDIUM | Error Handling Gaps | | **Unit tests for queue operations** | Queue is central to playback — SetQueue, navigation, shuffle, repeat, persistence — all untested. Bugs here cause tracks to skip, repeat wrong, or lose queue on restart. | HIGH | Test Coverage Gaps | | **Unit tests for library scan logic** | Metadata processing, entity cache, orphan cleanup — all untested. Bugs silently drop tracks or create duplicates. | HIGH | Test Coverage Gaps | | **Unit tests for database layer (FTS5, migrations)** | FTS5 edge cases (special chars, empty queries) and migration failures are completely untested. | MEDIUM | Test Coverage Gaps | | **Unit tests for config (load/save roundtrip)** | Config corruption or silent settings loss on upgrade has no safety net. | MEDIUM | Test Coverage Gaps | #### Concurrency Fix Details **Pattern:** For `SetContext` race conditions, the fix is uniform across Queue, Library, and Playlist: ```go // BEFORE (Queue — race condition): func (q *Queue) SetContext(ctx context.Context) { q.ctx = ctx // no lock, but q.ctx read under q.mu elsewhere } // AFTER (correct): func (q *Queue) SetContext(ctx context.Context) { q.mu.Lock() defer q.mu.Unlock() q.ctx = ctx } ``` Player already does this correctly (locks around `p.ctx = ctx` in `SetContext`). Apply the same pattern to Queue, Library, and Playlist. For Library and Playlist which don't currently have a mutex, add one — or document the "set during startup only, before any concurrent access" contract with a comment and `// SAFETY:` annotation. **Recommendation:** Add a `sync.Mutex` to Library and Playlist. The cost is negligible, and it eliminates the `-race` detector finding permanently. Documenting "safe because startup ordering" is fragile — the next developer (or future-you) may change init order. *Confidence: HIGH — standard Go concurrency practice.* #### Testing Strategy Details **In-memory SQLite for DB-dependent tests:** Use `sql.Open("sqlite", ":memory:")` with the `modernc.org/sqlite` driver (already in deps). Apply the same schema migrations used in production. This gives: - Fast test execution (no disk I/O) - Clean state per test (new DB per test function) - Identical query behavior to production **Pattern for queue/library tests:** ```go func setupTestDB(t *testing.T) *database.DB { t.Helper() db, err := database.NewTestDB(t) // in-memory, migrations applied require.NoError(t, err) return db } func TestSetQueueAndNavigate(t *testing.T) { db := setupTestDB(t) q := queue.NewQueue(slog.Default(), db) // No SetContext needed — test without Wails runtime // Test pure queue logic without event emission } ``` **Extract testable pure logic from Player:** Volume math (`UserVolume` → `Volume` conversion), state serialization, and format detection can be tested without audio hardware. Create `volume_test.go` with pure function tests. *Confidence: HIGH — standard Go testing pattern.* **Event-driven testing approach:** For packages that emit events, provide a test double or capture mechanism. Options: 1. Accept an `EventEmitter` interface (allows mock in tests) 2. Make event emission optional when `ctx == nil` (already partially the case — `emit` methods check for nil context) 3. Test state mutations independent of event emission **Recommendation:** Option 2 is already partially implemented. Lean into it: test queue/library state mutations without Wails context, verify state is correct, don't test event emission in unit tests. *Confidence: HIGH.* --- ### Differentiators (Raises Quality Significantly) These improvements go beyond "not broken" to "genuinely well-engineered." They improve performance, maintainability, and user experience noticeably. | Improvement | Value Proposition | Complexity | Concern Ref | |-------------|-------------------|------------|-------------| | **Eliminate duplicated FTS5 JOIN query pattern** | Same 5-table JOIN repeated 5+ times across search functions. Schema changes require updating all copies. Extract into shared constant or consolidate into fewer sqlc queries. | MEDIUM | Code Quality | | **Migrate raw SQL in queue persistence to sqlc** | `lookupChunk` and `insertTrackBatch` use `fmt.Sprintf` for batch operations. Use `sqlc.slice()` for lookups. Batch inserts can remain hand-crafted but documented. | MEDIUM | Code Quality | | **Optimize library store — lazy loading instead of eager fetch** | `eagerFetch()` loads all tracks, albums, artists, genres simultaneously on startup. For 50k+ tracks, this is tens of MB of JS objects loaded before user sees anything. Load only the active view's data. | HIGH | Performance | | **Optimize queue persistence — incremental updates** | Every add/remove/move does DELETE ALL + INSERT ALL. For a 5000-track queue, every single mutation rewrites the entire table. Use INSERT/DELETE for individual operations; reserve full rewrite for SetQueue. | MEDIUM | Performance | | **Fix SetQueue Phase 2 redundant lookups** | Phase 2 re-fetches metadata for ALL file paths including those already resolved in Phase 1. Pass Phase 1 results to Phase 2, only lookup remaining paths. | LOW | Performance | | **Extract testable player logic** | Volume conversion, state serialization, format detection — all testable without audio hardware. Currently locked inside Player struct behind hardware dependency. | LOW | Test Coverage | | **Event name parity validation** | Event names must match exactly between Go and TypeScript. No compile-time or runtime verification. Add a build-time check (code generation or test). | LOW | Fragile Areas | | **Polish UI transitions and visual consistency** | CSS transitions for panel open/close, list item hover states, loading skeletons. Makes the app feel responsive and intentional. | MEDIUM | UX | | **Improve frontend rendering for large libraries** | Even with `lit-virtualizer`, store updates trigger re-renders. Optimize with `repeat()` directive keyed by stable IDs, memoized render functions, and avoiding full-array replacement on updates. | MEDIUM | Performance | #### FTS5 Query Consolidation Details **Current state:** The same JOIN pattern appears in: 1. `SearchFTS()` — 5 columns 2. `SearchFTSByFilename()` — 5 columns (same query, different WHERE) 3. `SearchFTSTracks()` — 16 columns (extended version) 4. `RebuildSearchIndex()` — 5 columns (INSERT INTO ... SELECT) 5. `migration2BasenameAndFTS()` — same pattern in migration **Recommended approach:** Create a SQL view for the common JOIN: ```sql CREATE VIEW IF NOT EXISTS track_metadata_view AS SELECT af.id AS audio_file_id, af.file_path, af.length_milliseconds, COALESCE(r.name, '') AS title, COALESCE(ac.text, '') AS artist, COALESCE(rg.name, '') AS album, r.track_number, r.disc_number, -- ... other fields FROM audio_files af LEFT JOIN recordings r ON af.recording_id = r.id LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id LEFT JOIN ( SELECT recording_id, MIN(release_group_id) AS release_group_id FROM release_group_recordings GROUP BY recording_id ) rgr ON r.id = rgr.recording_id LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id; ``` Then search queries become `SELECT ... FROM search_index si JOIN track_metadata_view tmv ON tmv.audio_file_id = si.rowid WHERE search_index MATCH ?`. Single source of truth for the JOIN pattern. **Alternative:** Extract the JOIN clause as a Go string constant and compose queries from it. Less elegant but simpler to implement. **Recommendation:** Use the SQL view approach. SQLite views are essentially macros — no performance penalty. They can be referenced in sqlc queries. Add the view to the schema, then rewrite search queries against it. *Confidence: MEDIUM — SQLite views in sqlc need verification during implementation. The concept is sound, but sqlc's handling of views with FTS5 virtual tables may have edge cases.* #### Queue Persistence Optimization Details **Current pattern:** ``` Every mutation → commitMutation() → persistTracks() → DELETE ALL + batch INSERT ALL ``` **Improved pattern:** ``` AddTrack → INSERT single row + shift positions RemoveTrack → DELETE single row + shift positions MoveTrack → UPDATE positions for affected range SetQueue / RestoreState → DELETE ALL + batch INSERT ALL (keep current) ``` The sqlc queries `InsertQueueTrack`, `RemoveQueueTrack`, `ShiftQueuePositionsDown`, `ShiftQueuePositionsUp` already exist but aren't used by `commitMutation()`. Wire them up for single-track operations. *Confidence: HIGH — the individual queries already exist in sqlc.* #### Library Store Lazy Loading Details **Current:** Constructor calls `eagerFetch()` → 4 parallel Wails binding calls → 4 full table scans with JOINs → all data in JS memory. **Improved pattern:** ```typescript class LibraryStore { // Load on first access, not constructor async getTracks(): Promise { if (this.tracks !== null) return this.tracks; // ... existing lazy logic (already implemented!) } // Remove eagerFetch() from constructor constructor() { EventsOn(Events.LibraryScanComplete, () => this.invalidate()); this.loadCoverSize(); // Don't call eagerFetch() — let components trigger loading } } ``` The store *already has* lazy loading logic in `getTracks()`, `getAlbums()`, etc. The only change needed is removing the `eagerFetch()` call from the constructor and from `invalidate()`. Components already call the async getters. The eager fetch is redundant. **For even larger libraries (100k+):** Consider pagination. Backend already returns full result sets — add `LIMIT/OFFSET` or cursor-based pagination to the sqlc queries. Frontend virtualizer already handles rendering — it just needs a data provider that fetches pages instead of the full list. *Confidence: HIGH — the lazy loading infrastructure already exists.* #### Frontend Performance Details **Already in place:** `@lit-labs/virtualizer` with `flow` layout for track-list and `grid` layout for cover-grid. This handles DOM virtualization. **Additional optimizations:** 1. **Use `repeat()` with stable keys for virtualized lists.** Lit's `repeat` directive reorders DOM nodes instead of recreating them when list order changes. Use `track.filePath` as key (unique, stable). 2. **Avoid full-array replacement in store updates.** When a scan completes, `invalidate()` sets `tracks = null` forcing a full refetch. Instead, diff the new data against cached data and apply deltas. For scan completion, a full invalidation is appropriate, but for queue mutations, use the delta protocol already in place (`applyTracksDelta`). 3. **Debounce store notifications.** When multiple store properties update in rapid succession (e.g., during scan), batch notifications using `queueMicrotask()` instead of notifying per-property. *Confidence: MEDIUM — `repeat()` performance gains depend on the update patterns. For initially sorted lists that rarely reorder, `map()` is equally fast. For the cover-grid with resize/reflow, `repeat()` is clearly beneficial.* --- ### Anti-Features (Things to Deliberately NOT Do During Refactoring) | Anti-Pattern | Why Tempting | Why Problematic | What to Do Instead | |-------------|-------------|-----------------|-------------------| | **Splitting large files purely for line count** | `playlist.go` (1778 lines) and `library.go` (1328 lines) feel large. Some components exceed 2000 lines. | The project explicitly decided against cosmetic splitting (PROJECT.md: "No cosmetic file splitting"). Splitting for its own sake creates navigation overhead and can break logical grouping. | Extract only when it enables reuse (e.g., shared controllers) or fixes a real problem (e.g., testing). | | **Adding a full ORM or query builder** | Raw SQL in `lookupChunk`/`insertTrackBatch` feels inconsistent with sqlc-generated code. | An ORM would fight the existing sqlc architecture. A query builder adds a dependency for 2-3 queries. The hand-crafted SQL is safe (parameterized) and performant. | Document the hand-crafted queries with `// SAFETY:` comments explaining why they're not in sqlc. Use `sqlc.slice()` where it fits. Accept that batch INSERT with dynamic row count is a legitimate sqlc gap for SQLite. | | **Rewriting the event system** | Event names are fragile strings that must match between Go and TypeScript. A typed event system would be safer. | The current system works. A rewrite touches every component in both frontend and backend. The risk-to-reward ratio is terrible for a consolidation milestone. | Add a build-time parity check (a test or codegen script that compares event constants). Fix the symptom (fragility) not the architecture. | | **Adding frontend unit tests for all components** | No frontend tests exist. The temptation is to add comprehensive Lit component testing. | Large Lit components (1400-2600 lines) are expensive to test in isolation. Testing requires JSDOM or a browser harness, Shadow DOM handling, and Wails binding mocks. The backend is the source of truth — frontend bugs are visual, not data-corruption. | Test frontend-only logic (search ranking, column sorting, selection controller) as pure function tests if extracted. Defer full component testing to a future milestone. | | **Making all queue mutations atomic/transactional from Go to frontend** | The delta protocol between queue store and backend could diverge. Adding sequence numbers or full-state hashes seems robust. | The existing `QueueChanged` event already acts as periodic full-state correction. Adding a sequence protocol adds complexity to every mutation path for a problem that manifests as a temporary visual glitch, self-correcting on the next full emit. | Keep the existing delta + periodic full-state pattern. If divergence becomes a real problem (not theoretical), add a generation counter then. | | **Over-engineering error types** | The project uses sentinel errors and `fmt.Errorf("%w")`. Defining custom error types with fields (e.g., `ScanError{File, Phase, Cause}`) seems more structured. | Custom error types add boilerplate for minimal benefit in a desktop app. The structured logging already captures context via slog key-value pairs. Error types shine in API servers where callers branch on error details — not here. | Keep sentinel errors for `errors.Is()` checks. Keep `fmt.Errorf("%w")` for wrapping with context. Use `errors.Join()` for accumulation. Separate warnings from fatal errors in scan results via the return signature, not error types. | | **Adding connection pooling or health checks for SQLite** | PROJECT.md mentions "No Database Connection Pooling/Health Check" in missing features. | This is a desktop app with a local SQLite file and `SetMaxOpenConns(1)`. Connection pooling is meaningless. Health checks add complexity for a failure mode (corrupt SQLite file) that's better handled by "show error dialog, suggest DB reset." | Leave as-is. This was correctly scoped as out-of-scope in PROJECT.md. | | **Wrapping the entire test suite in Docker for CI** | Integration tests require audio hardware. Docker could theoretically provide a virtual audio device. | Massive CI complexity for marginal benefit. The goal is to make unit tests work without hardware, not to make integration tests work in CI. | Extract testable pure logic. Run unit tests in CI. Keep integration tests as manual/local-only with `YELLOWJACKET_INTEGRATION=1`. | --- ## Feature Dependencies ``` [Fix SetContext races] └── (no deps — standalone fix) [Fix error handling gaps (MPRIS, artist credit, config perms)] └── (no deps — standalone fixes) [Separate scan warnings from fatal errors] └── (no deps — changes Library.Scan return signature) [Add in-memory SQLite test infrastructure] └──requires──> [database.NewTestDB() helper] └──enables──> [Queue unit tests] └──enables──> [Library unit tests] └──enables──> [Database layer tests] └──enables──> [Config tests] [Extract testable player logic] └── (no deps — pure function extraction) └──enables──> [Player pure logic tests] [FTS5 query consolidation (SQL view)] └──should-precede──> [Database layer tests] (test the consolidated queries, not the duplicated ones) [Queue persistence optimization (incremental updates)] └──should-precede──> [Queue unit tests] (test the optimized persistence, not the DELETE-ALL pattern) [Library store lazy loading] └── (no deps — remove eagerFetch() call) [SetQueue Phase 2 optimization] └──requires──> [Queue unit tests] (need tests to verify the optimization doesn't break resolution) [Event name parity validation] └── (no deps — standalone build-time check) [UI polish / transitions] └── (no deps — CSS-only or Lit reactive changes) [Frontend rendering optimization] └──benefits-from──> [Library store lazy loading] (less data in memory = faster re-renders) ``` ### Dependency Notes - **Test infrastructure is the critical enabler:** Almost all other improvements benefit from having tests first (to verify refactoring safety) or should happen before tests (to test the right code). The ordering matters: fix persistence patterns *before* writing persistence tests, consolidate SQL *before* writing SQL tests. - **Concurrency fixes are independent:** They're small, self-contained, and should be done first — they represent known correctness issues. - **Performance optimizations benefit from tests:** The queue persistence optimization and SetQueue Phase 2 fix both modify core queue logic. Having queue tests first provides a safety net. - **Frontend work is independent of backend work:** Library store lazy loading, UI polish, and rendering optimization don't depend on backend changes. --- ## Prioritization ### Phase 1: Correctness & Test Foundation (Do First) Fixes known bugs and establishes the test infrastructure that makes everything else safe. - [ ] Fix SetContext data races (Queue, Library, Playlist) — LOW effort, HIGH value - [ ] Fix package-level `startupErr` → struct field — LOW effort - [ ] Fix config file permissions — LOW effort - [ ] Fix swallowed errors (MPRIS, artist credit) — LOW effort - [ ] Separate scan warnings from fatal errors — MEDIUM effort - [ ] Create in-memory SQLite test helper (`database.NewTestDB()`) — MEDIUM effort - [ ] Extract testable player pure logic (volume, state) — LOW effort ### Phase 2: SQL & Performance Foundations (Do Second) Improves the code that tests will be written against. - [ ] Consolidate FTS5 JOIN pattern (SQL view or constant) — MEDIUM effort - [ ] Migrate queue lookups to `sqlc.slice()` — MEDIUM effort - [ ] Optimize queue persistence (incremental updates) — MEDIUM effort - [ ] Fix SetQueue Phase 2 redundant lookups — LOW effort - [ ] Remove `eagerFetch()` from library store constructor — LOW effort ### Phase 3: Comprehensive Tests (Do Third) Tests verify the improved code from Phases 1-2. - [ ] Queue unit tests (SetQueue, navigation, shuffle, repeat, persistence) — HIGH effort - [ ] Library scan unit tests (metadata, entity cache, orphan cleanup) — HIGH effort - [ ] Database layer tests (FTS5 queries, migrations) — MEDIUM effort - [ ] Config tests (load/save roundtrip, validation, defaults) — MEDIUM effort - [ ] Player pure logic tests (volume math, state serialization) — LOW effort - [ ] Event name parity test — LOW effort ### Phase 4: Polish & Frontend (Do Last) Visual and frontend improvements that don't affect backend correctness. - [ ] UI transitions and responsive feedback — MEDIUM effort - [ ] Frontend rendering optimization (repeat directive, debounced notifications) — MEDIUM effort - [ ] Document intentional exceptions (hand-crafted SQL, singleton store lifecycle) — LOW effort ## Feature Prioritization Matrix | Improvement | Reliability Value | Implementation Cost | Priority | |-------------|-------------------|---------------------|----------| | Fix SetContext data races | HIGH | LOW | **P1** | | Fix startupErr, config perms | HIGH | LOW | **P1** | | Fix swallowed errors | HIGH | LOW | **P1** | | Separate scan warnings/errors | HIGH | MEDIUM | **P1** | | In-memory SQLite test helper | HIGH | MEDIUM | **P1** | | Extract testable player logic | MEDIUM | LOW | **P1** | | FTS5 query consolidation | MEDIUM | MEDIUM | **P2** | | Queue persistence optimization | MEDIUM | MEDIUM | **P2** | | SetQueue Phase 2 fix | MEDIUM | LOW | **P2** | | Library store lazy loading | MEDIUM | LOW | **P2** | | Queue unit tests | HIGH | HIGH | **P2** | | Library unit tests | HIGH | HIGH | **P2** | | Database tests | MEDIUM | MEDIUM | **P2** | | Config tests | MEDIUM | MEDIUM | **P2** | | Event name parity validation | MEDIUM | LOW | **P2** | | Player pure logic tests | MEDIUM | LOW | **P2** | | UI transitions / polish | LOW | MEDIUM | **P3** | | Frontend rendering optimization | LOW | MEDIUM | **P3** | | Migrate queue SQL to sqlc | LOW | MEDIUM | **P3** | **Priority key:** - P1: Must do — correctness issues or critical enablers - P2: Should do — significant quality improvement - P3: Nice to have — polish, can defer if time-constrained ## Sources - Go race detector: https://go.dev/doc/articles/race_detector — HIGH confidence (official Go docs) - sqlc `sqlc.slice()` for SQLite: https://docs.sqlc.dev/en/stable/reference/macros.html — HIGH confidence (official sqlc docs, verified via WebFetch) - sqlc batch operations: https://docs.sqlc.dev/en/stable/howto/select.html#mysql-and-sqlite — HIGH confidence (official docs) - Lit `repeat` directive: https://lit.dev/docs/templates/lists/#the-repeat-directive — HIGH confidence (official Lit docs, verified via WebFetch) - Lit rendering model: https://lit.dev/docs/components/rendering/ — HIGH confidence (official docs) - `@lit-labs/virtualizer` — already in use in codebase (track-list, cover-grid) - `modernc.org/sqlite` in-memory DB — HIGH confidence (`:memory:` is standard SQLite, driver already in deps) - Go `errors.Join()` — HIGH confidence (standard library since Go 1.20, already used in codebase) - Go mutex patterns — HIGH confidence (standard library, matches existing codebase conventions) --- *Feature research for: YellowJacket consolidation milestone* *Researched: 2026-02-27*