@@ -189,6 +189,177 @@ swiftlint --strict && swiftformat .
189189- Mark ` @Observable ` classes with ` @MainActor `
190190- Never use ` DispatchQueue ` — use ` async ` /` await ` , ` MainActor `
191191
192+ ### Common Bug Patterns to Avoid
193+
194+ These patterns have caused bugs in this codebase. ** Always check for these during code review.**
195+
196+ #### ❌ Fire-and-Forget Tasks
197+
198+ ``` swift
199+ // ❌ BAD: Task not tracked, errors lost, can't cancel
200+ func likeTrack () {
201+ Task { await api.like (trackId) }
202+ }
203+
204+ // ✅ GOOD: Track task, handle errors, support cancellation
205+ private var likeTask: Task<Void , Error >?
206+
207+ func likeTrack () async throws {
208+ likeTask? .cancel ()
209+ likeTask = Task {
210+ try await api.like (trackId)
211+ }
212+ try await likeTask? .value
213+ }
214+ ```
215+
216+ #### ❌ Optimistic Updates Without Proper Rollback
217+
218+ ``` swift
219+ // ❌ BAD: CancellationError not handled, cache permanently wrong
220+ func rate (_ song : Song, status : LikeStatus) async {
221+ let previous = cache[song.id ]
222+ cache[song.id ] = status // Optimistic update
223+ do {
224+ try await api.rate (song.id , status)
225+ } catch {
226+ cache[song.id ] = previous // Doesn't run on cancellation!
227+ }
228+ }
229+
230+ // ✅ GOOD: Handle ALL errors including cancellation
231+ func rate (_ song : Song, status : LikeStatus) async {
232+ let previous = cache[song.id ]
233+ cache[song.id ] = status
234+ do {
235+ try await api.rate (song.id , status)
236+ } catch let error as CancellationError {
237+ cache[song.id ] = previous // Rollback on cancel
238+ throw error // Propagate original cancellation
239+ } catch {
240+ cache[song.id ] = previous // Rollback on error
241+ throw error
242+ }
243+ }
244+ ```
245+
246+ #### ❌ Static Shared Singletons with Mutable Assignment
247+
248+ ``` swift
249+ // ❌ BAD: Race condition if multiple instances created
250+ class LibraryViewModel {
251+ static var shared: LibraryViewModel?
252+ init () { Self .shared = self } // Overwrites previous!
253+ }
254+
255+ // ✅ GOOD: Use SwiftUI Environment for dependency injection
256+ @Observable @MainActor
257+ class LibraryViewModel { /* ... */ }
258+
259+ // In parent view:
260+ .environment (libraryViewModel)
261+
262+ // In child view:
263+ @Environment (LibraryViewModel.self ) var viewModel
264+ ```
265+
266+ #### ❌ ` .onAppear ` Instead of ` .task ` for Async Work
267+
268+ ``` swift
269+ // ❌ BAD: Task not cancelled on disappear, can update stale view
270+ .onAppear {
271+ Task { await viewModel.load () }
272+ }
273+
274+ // ✅ GOOD: Lifecycle-managed, auto-cancelled on disappear
275+ .task {
276+ await viewModel.load ()
277+ }
278+
279+ // ✅ GOOD: With ID for re-execution on change
280+ .task (id : playlistId) {
281+ await viewModel.load (playlistId)
282+ }
283+ ```
284+
285+ #### ❌ ForEach with Unstable Identity
286+
287+ ``` swift
288+ // ❌ BAD: Index-based identity causes wrong views during mutations
289+ ForEach (tracks.indices , id : \.self ) { index in
290+ TrackRow (track : tracks[index])
291+ }
292+
293+ // ❌ BAD: Array enumeration recreates identity on every change
294+ ForEach (Array (tracks.enumerated ()), id : \.offset ) { index, track in
295+ TrackRow (track : track, rank : index + 1 )
296+ }
297+
298+ // ✅ GOOD: Use stable model identity
299+ ForEach (tracks) { track in
300+ TrackRow (track : track)
301+ }
302+
303+ // ✅ GOOD: If you need index for display (charts), use element ID
304+ ForEach (Array (tracks.enumerated ()), id : \.element .id ) { index, track in
305+ TrackRow (track : track, rank : index + 1 )
306+ }
307+ ```
308+
309+ #### ❌ Background Tasks Not Cancelled on Deinit
310+
311+ ``` swift
312+ // ❌ BAD: Task continues after ViewModel is deallocated
313+ @Observable @MainActor
314+ class HomeViewModel {
315+ private var backgroundTask: Task<Void , Never >?
316+
317+ func startLoading () {
318+ backgroundTask = Task { /* ... */ }
319+ }
320+ // Missing deinit cleanup!
321+ }
322+
323+ // ✅ GOOD: Cancel tasks in deinit
324+ @Observable @MainActor
325+ class HomeViewModel {
326+ private var backgroundTask: Task<Void , Never >?
327+
328+ func startLoading () {
329+ backgroundTask? .cancel ()
330+ backgroundTask = Task { [weak self ] in
331+ guard ! Task.isCancelled else { return }
332+ // ...
333+ }
334+ }
335+
336+ deinit {
337+ backgroundTask? .cancel ()
338+ }
339+ }
340+ ```
341+
342+ #### ❌ Shared Continuation Tokens Across Different Requests
343+
344+ ``` swift
345+ // ❌ BAD: Single token for all search types causes conflicts
346+ class YTMusicClient {
347+ private var searchContinuationToken: String ? // Shared!
348+
349+ func searchSongs () { /* sets token */ }
350+ func searchAlbums () { /* overwrites token! */ }
351+ }
352+
353+ // ✅ GOOD: Scope tokens by request type or return in response
354+ class YTMusicClient {
355+ private var continuationTokens: [String : String ] = [: ]
356+
357+ func searchSongs () -> (songs: [Song], continuation: String ? ) {
358+ // Return token with response, let caller manage
359+ }
360+ }
361+ ```
362+
192363### WebKit Patterns
193364
194365- Use ` WebKitManager ` 's shared ` WKWebsiteDataStore ` for cookie persistence
@@ -264,6 +435,19 @@ Before completing non-trivial features, verify:
264435- [ ] ForEach uses stable identity (avoid ` Array(enumerated()) ` unless rank is needed)
265436- [ ] Frequently updating UI (e.g., progress) caches formatted strings
266437
438+ ## Concurrency Safety Checklist
439+
440+ Before completing features with async code, verify:
441+
442+ - [ ] No fire-and-forget ` Task { } ` without error handling
443+ - [ ] Optimistic updates handle ` CancellationError ` explicitly
444+ - [ ] Background tasks cancelled in ` deinit `
445+ - [ ] Using ` .task ` instead of ` .onAppear { Task { } } `
446+ - [ ] Continuation tokens scoped per-request (not shared across types)
447+ - [ ] No ` static var shared ` pattern with mutable assignment in ` init `
448+ - [ ] WebView message handlers removed in ` dismantleNSView `
449+ - [ ] ` WKNavigationDelegate ` implements ` webViewWebContentProcessDidTerminate `
450+
267451> 📚 See [ docs/architecture.md#performance-guidelines] ( docs/architecture.md#performance-guidelines ) for detailed patterns.
268452
269453## Task Planning: Phases with Exit Criteria
0 commit comments