-
Notifications
You must be signed in to change notification settings - Fork 28
fix: address critical concurrency bugs and architectural anti-patterns #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -189,6 +189,182 @@ swiftlint --strict && swiftformat . | |||||||||||||
| - Mark `@Observable` classes with `@MainActor` | ||||||||||||||
| - Never use `DispatchQueue` — use `async`/`await`, `MainActor` | ||||||||||||||
|
|
||||||||||||||
| ### Common Bug Patterns to Avoid | ||||||||||||||
|
|
||||||||||||||
| These patterns have caused bugs in this codebase. **Always check for these during code review.** | ||||||||||||||
|
|
||||||||||||||
| #### ❌ Fire-and-Forget Tasks | ||||||||||||||
|
|
||||||||||||||
| ```swift | ||||||||||||||
| // ❌ BAD: Task not tracked, errors lost, can't cancel | ||||||||||||||
| func likeTrack() { | ||||||||||||||
| Task { await api.like(trackId) } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // ✅ GOOD: Track task, handle errors, support cancellation | ||||||||||||||
| private var likeTask: Task<Void, Never>? | ||||||||||||||
|
|
||||||||||||||
| 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 | ||||||||||||||
| } | ||||||||||||||
| ``` | ||||||||||||||
|
|
||||||||||||||
| #### ❌ Optimistic Updates Without Proper Rollback | ||||||||||||||
|
|
||||||||||||||
| ```swift | ||||||||||||||
| // ❌ BAD: CancellationError not handled, cache permanently wrong | ||||||||||||||
| func rate(_ song: Song, status: LikeStatus) async { | ||||||||||||||
| let previous = cache[song.id] | ||||||||||||||
| cache[song.id] = status // Optimistic update | ||||||||||||||
| do { | ||||||||||||||
| try await api.rate(song.id, status) | ||||||||||||||
| } catch { | ||||||||||||||
| cache[song.id] = previous // Doesn't run on cancellation! | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // ✅ GOOD: Handle ALL errors including cancellation | ||||||||||||||
| func rate(_ song: Song, status: LikeStatus) async { | ||||||||||||||
| let previous = cache[song.id] | ||||||||||||||
| cache[song.id] = status | ||||||||||||||
| do { | ||||||||||||||
| try await api.rate(song.id, status) | ||||||||||||||
| } catch is CancellationError { | ||||||||||||||
| cache[song.id] = previous // Rollback on cancel | ||||||||||||||
| throw CancellationError() | ||||||||||||||
|
||||||||||||||
| } 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,47 @@ final class AppDelegate: NSObject, NSApplicationDelegate { | |
| try? await Task.sleep(for: .milliseconds(500)) | ||
| self.setupWindowDelegate() | ||
| } | ||
|
|
||
| // Register for system sleep/wake notifications | ||
| self.registerForSleepWakeNotifications() | ||
| } | ||
|
|
||
| /// Registers for system sleep and wake notifications to handle playback appropriately. | ||
| 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 | ||
| ) | ||
| } | ||
|
Comment on lines
+38
to
+54
|
||
|
|
||
| /// Tracks whether audio was playing before system sleep (for resume on wake). | ||
| private var wasPlayingBeforeSleep: Bool = false | ||
|
|
||
| @objc private func systemWillSleep(_: Notification) { | ||
| // Remember playback state and pause before sleep | ||
| self.wasPlayingBeforeSleep = self.playerService?.isPlaying ?? false | ||
| if self.wasPlayingBeforeSleep { | ||
| DiagnosticsLogger.player.info("System going to sleep, pausing playback") | ||
| SingletonPlayerWebView.shared.pause() | ||
| } | ||
| } | ||
|
|
||
| @objc private func systemDidWake(_: Notification) { | ||
| // Optionally resume playback after wake if it was playing before sleep | ||
| // Note: We don't auto-resume by default as it could be surprising | ||
| // Just log the wake event for now | ||
| DiagnosticsLogger.player.info("System woke from sleep, wasPlayingBeforeSleep: \(self.wasPlayingBeforeSleep)") | ||
| } | ||
|
|
||
| func applicationDidBecomeActive(_: Notification) { | ||
|
|
||
There was a problem hiding this comment.
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:
Task<Void, Error>and await the task properly, orasync throwsand handle errors internally