Skip to content

fix: address critical concurrency bugs and architectural anti-patterns#77

Merged
sozercan merged 3 commits intomainfrom
bugbash
Jan 17, 2026
Merged

fix: address critical concurrency bugs and architectural anti-patterns#77
sozercan merged 3 commits intomainfrom
bugbash

Conversation

@sozercan
Copy link
Owner

@sozercan sozercan commented Jan 17, 2026

Description

Bug Fixes

1. FavoritesManager: Save debouncing and race condition prevention

  • Cancel pending save tasks before starting new ones to prevent race conditions
  • Add 100ms debounce delay to coalesce rapid successive saves
  • Check for task cancellation before writing to disk

2. SongLikeStatusManager: CancellationError handling in optimistic updates

  • Properly rollback cache state when rating task is cancelled
  • Prevents permanently incorrect cache state on rapid user interactions
  • Add debug logging for cancelled rating operations

3. LibraryViewModel: Remove static shared singleton anti-pattern

  • Remove static var shared with mutable assignment in init()
  • Migrate to proper SwiftUI @Environment dependency injection
  • Update MainWindow to inject LibraryViewModel via .environment()
  • Update PlaylistDetailView, SearchView, SongActionsHelper to accept injected VM

4. LikedMusicView: Fix ForEach unstable identity

  • Change from index-based identity (.indices, id: \.self)
  • Use stable element identity (id: \.element.id) with enumeration
  • Prevents wrong views during list mutations

5. MiniPlayerWebView: WebView crash recovery and handler cleanup

  • Implement webViewWebContentProcessDidTerminate for crash recovery
  • Remove message handlers in dismantleNSView to prevent memory leaks
  • Add recovery task for WebView process termination

6. AppDelegate: System sleep/wake notification handling

  • Register for NSWorkspace sleep/wake notifications
  • Pause playback before system sleep
  • Track playback state for potential resume after wake

Documentation

  • Add "Common Bug Patterns to Avoid" section to AGENTS.md with examples:

    • Fire-and-forget tasks
    • Optimistic updates without proper rollback
    • Static shared singletons with mutable assignment
    • .onAppear instead of .task for async work
    • ForEach with unstable identity
    • Background tasks not cancelled on deinit
    • Shared continuation tokens across different requests
  • Add "Concurrency Safety Checklist" to AGENTS.md for code review

AI Prompt (Optional)

🤖 AI Prompt Used
<!-- Paste your prompt here, or write "N/A - Manual implementation" -->

AI Tool:

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to change)
  • 📚 Documentation update
  • 🎨 UI/UX improvement
  • ♻️ Refactoring (no functional changes)
  • 🧪 Test update
  • 🔧 Build/CI configuration

Related Issues

Changes Made

Testing

  • Unit tests pass (xcodebuild test -only-testing:KasetTests)
  • Manual testing performed
  • UI tested on macOS 26+

Checklist

  • My code follows the project's style guidelines
  • I have run swiftlint --strict && swiftformat .
  • I have added tests that prove my fix/feature works
  • New and existing unit tests pass locally
  • I have updated documentation if needed
  • I have checked for any performance implications
  • My changes generate no new warnings

Screenshots

Additional Notes

Signed-off-by: Sertac Ozercan <sozercan@gmail.com>
Copilot AI review requested due to automatic review settings January 17, 2026 05:52
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses critical concurrency bugs and architectural anti-patterns in the codebase, focusing on proper task cancellation, optimistic update rollback, and dependency injection. The changes improve app stability and maintainability through better concurrency patterns and comprehensive documentation.

Changes:

  • Removed static shared singleton anti-pattern from LibraryViewModel and migrated to SwiftUI environment-based dependency injection
  • Added proper CancellationError handling and rollback in SongLikeStatusManager and FavoritesManager
  • Implemented WebView crash recovery with webViewWebContentProcessDidTerminate delegate method
  • Fixed ForEach identity stability issues in PlaylistDetailView and LikedMusicView using element-based IDs
  • Added system sleep/wake notification handling in AppDelegate to gracefully pause playback
  • Added comprehensive documentation in AGENTS.md with common bug patterns and concurrency safety checklist

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Core/ViewModels/LibraryViewModel.swift Removed static var shared singleton pattern to enable proper dependency injection
Views/macOS/SharedViews/SongActionsHelper.swift Added libraryViewModel parameter to playlist management functions for dependency injection
Views/macOS/SearchView.swift Added environment injection for LibraryViewModel and updated SongActionsHelper calls
Views/macOS/PlaylistDetailView.swift Added environment injection and fixed ForEach to use stable element IDs instead of indices
Views/macOS/LikedMusicView.swift Fixed ForEach to use stable element IDs with enumeration
Core/Services/FavoritesManager.swift Added debouncing and race condition prevention with cancellable save tasks
Core/Services/SongLikeStatusManager.swift Added explicit CancellationError handling to rollback optimistic cache updates
Views/macOS/MiniPlayerWebView.swift Added crash recovery handler and proper message handler cleanup in dismantleNSView
App/AppDelegate.swift Added system sleep/wake notification handlers to pause playback before sleep
Views/macOS/MainWindow.swift Wrapped viewForNavigationItem in Group to inject LibraryViewModel via environment
AGENTS.md Added extensive documentation on common bug patterns, examples, and concurrency safety checklist
Comments suppressed due to low confidence (1)

Core/Services/FavoritesManager.swift:113

  • The FavoritesManager now tracks a saveTask that can be running when the app terminates. While FavoritesManager is a singleton that typically lives for the entire app lifetime, it's good practice to add a deinit that cancels any pending saveTask to ensure clean shutdown, especially for test instances. Consider adding:
deinit {
    saveTask?.cancel()
}

This aligns with the new "Background tasks cancelled in deinit" checklist item in AGENTS.md.

    private func save() {
        // Skip persistence for test instances to avoid overwriting user data
        guard !self.skipPersistence else { return }

        // Cancel any pending save to avoid race conditions
        self.saveTask?.cancel()

        // Capture current state for background write
        let itemsSnapshot = self.items
        let targetURL = self.fileURL

        // Perform disk I/O off the main actor with debounce.
        // Slight delay coalesces rapid successive saves.
        self.saveTask = Task(priority: .utility) {
            // Debounce: wait briefly to coalesce rapid changes
            try? await Task.sleep(for: .milliseconds(100))

            // Check if cancelled (a newer save superseded this one)
            guard !Task.isCancelled else { return }

            do {
                // Ensure directory exists
                let directory = targetURL.deletingLastPathComponent()
                try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)

                let data = try JSONEncoder().encode(itemsSnapshot)
                try data.write(to: targetURL, options: .atomic)
                DiagnosticsLogger.ui.debug("Saved \(itemsSnapshot.count) favorite items")
            } catch {
                DiagnosticsLogger.ui.error("Failed to save favorites: \(error.localizedDescription)")
            }
        }
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 207 to 218
func likeTrack() async throws {
likeTask?.cancel()
likeTask = Task {
do {
try await api.like(trackId)
} catch {
// Handle error, update UI
throw error
}
}
try await likeTask?.value
}
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example code for "GOOD" fire-and-forget tasks has a logical issue. The function is marked as async throws, but inside it creates a non-throwing Task (Task<Void, Never>), then tries to throw errors from within that Task. This won't work as expected because errors thrown inside a Task<Void, Never> are not propagated to the caller.

The example should either:

  1. Use Task<Void, Error> and await the task properly, or
  2. Not mark the function as async throws and handle errors internally

Copilot uses AI. Check for mistakes.
AGENTS.md Outdated
Comment on lines 241 to 243
} catch is CancellationError {
cache[song.id] = previous // Rollback on cancel
throw CancellationError()
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example code shows throw CancellationError() after handling cancellation. In Swift concurrency, CancellationError is typically not re-thrown after being caught and handled. The cancellation has already been rolled back, and re-throwing would require the catch block to be marked as throwing. Consider removing the throw statement or clarifying the intent.

Suggested change
} catch is CancellationError {
cache[song.id] = previous // Rollback on cancel
throw CancellationError()
} catch let error as CancellationError {
cache[song.id] = previous // Rollback on cancel
throw error // Propagate original cancellation

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +54
private func registerForSleepWakeNotifications() {
let notificationCenter = NSWorkspace.shared.notificationCenter

notificationCenter.addObserver(
self,
selector: #selector(self.systemWillSleep),
name: NSWorkspace.willSleepNotification,
object: nil
)

notificationCenter.addObserver(
self,
selector: #selector(self.systemDidWake),
name: NSWorkspace.didWakeNotification,
object: nil
)
}
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AppDelegate registers for system sleep/wake notifications but never removes these observers. While AppDelegate lives for the entire app lifetime, it's best practice to clean up observers in deinit to prevent potential issues if the AppDelegate is ever deallocated or during testing scenarios. Consider adding a deinit method that calls NSWorkspace.shared.notificationCenter.removeObserver(self).

Copilot uses AI. Check for mistakes.
Signed-off-by: Sertac Ozercan <sozercan@gmail.com>
@sozercan sozercan merged commit 5d6740f into main Jan 17, 2026
6 checks passed
@sozercan sozercan deleted the bugbash branch January 17, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants