Conversation
WalkthroughError handling in EventManager's trigger pipeline was modified to return previously cached calendars and events on failure instead of empty arrays. A separate catch block was removed and error handling consolidated, with current values retrieved upfront as a fallback mechanism. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR modifies the EventManager's refresh error handling to preserve the last known calendars and events instead of clearing them when a fetch fails. This prevents data loss from transient network or API errors.
Changes:
- Capture current calendars and events state before attempting fetch operations
- Return captured state on error instead of empty arrays
- Remove redundant downstream
.catchblock that was also returning empty data
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let current = await MainActor.run { (self.calendars, self.events) } | ||
| do { | ||
| let cals = try await self.provider.fetchAllCalendars() | ||
| let evts = try await self.fetchEvents(fromCalendars: cals) | ||
| promise(.success((cals, evts))) | ||
| } catch { | ||
| NSLog("EventManager refresh failed: \(error)") | ||
| promise(.success(([], []))) | ||
| promise(.success(current)) |
There was a problem hiding this comment.
There's a potential race condition where the captured current state may become stale before being used. The current tuple is captured at the start of the Task, but if a concurrent refresh successfully completes and updates self.calendars and self.events on the main actor before this refresh fails, the error handler will overwrite the newer values with the older current snapshot.
This can occur when multiple refresh triggers fire in quick succession (e.g., manual refresh + timer trigger + defaults change). The second refresh could succeed and update the published properties, but then the first refresh's failure handler would revert them to the older state.
Consider either:
- Not updating the properties at all on error (remove the
.sinkupdate when the values matchcurrent), or - Implementing a serial queue or cancellation mechanism to ensure only one refresh operation runs at a time
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| let current = await MainActor.run { (self.calendars, self.events) } | ||
| do { | ||
| let cals = try await self.provider.fetchAllCalendars() | ||
| let evts = try await self.fetchEvents(fromCalendars: cals) | ||
| promise(.success((cals, evts))) | ||
| } catch { | ||
| NSLog("EventManager refresh failed: \(error)") | ||
| promise(.success(([], []))) | ||
| promise(.success(current)) |
There was a problem hiding this comment.
The new error-handling behavior (keeping last known values on refresh failure) lacks test coverage. The existing tests in EventManagerTests.swift only cover successful fetch scenarios and don't test what happens when fetchAllCalendars() or fetchEvents() throw errors.
Consider adding a test case that:
- Sets up FakeEventStore to return initial successful data
- Verifies the initial data is published
- Modifies FakeEventStore to throw errors on subsequent fetches
- Triggers a refresh
- Verifies that calendars and events still contain the initial data (not empty arrays)
This would validate that the new fallback-to-current behavior works as intended.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
MeetingBar/Core/Managers/EventManager.swift (1)
170-189: Fix failure-type mismatch: flatMap closure return type must beNever, notError.The upstream
triggerpublisher (merged fromdefaultsPub,timerPub, andmanualPub) hasFailure == Never. When you declare theflatMapclosure's return type asAnyPublisher<([MBCalendar], [MBEvent]), Never>on line 170, Combine requires the actual returned publisher to haveFailure == Neveras well. However, line 175 definesFuture<([MBCalendar], [MBEvent]), Error>, which hasFailure == Error. After.eraseToAnyPublisher()on line 189, this becomesAnyPublisher<..., Error>—a type mismatch that prevents compilation. Change theFuture's failure type toNever:Suggested fix
- return Deferred { - Future<([MBCalendar], [MBEvent]), Error> { promise in + return Deferred { + Future<([MBCalendar], [MBEvent]), Never> { promise in Task { let current = await MainActor.run { (self.calendars, self.events) } do { let cals = try await self.provider.fetchAllCalendars() let evts = try await self.fetchEvents(fromCalendars: cals) promise(.success((cals, evts))) } catch { NSLog("EventManager refresh failed: \(error)") promise(.success(current)) } } } }
🤖 Fix all issues with AI agents
In `@MeetingBar/Core/Managers/EventManager.swift`:
- Around line 176-185: The code captures a stale snapshot into `current` before
doing `provider.fetchAllCalendars()` and `fetchEvents(fromCalendars:)`, which
lets a slower failing refresh overwrite newer data; to fix, stop reading
`self.calendars`/`self.events` before the fetch and instead, inside the `catch`
block, obtain the latest values via `await MainActor.run { (self.calendars,
self.events) }` and pass those to `promise(.success(...))`, leaving the
happy-path success case unchanged.
| Task { | ||
| let current = await MainActor.run { (self.calendars, self.events) } | ||
| do { | ||
| let cals = try await self.provider.fetchAllCalendars() | ||
| let evts = try await self.fetchEvents(fromCalendars: cals) | ||
| promise(.success((cals, evts))) | ||
| } catch { | ||
| NSLog("EventManager refresh failed: \(error)") | ||
| promise(.success(([], []))) | ||
| promise(.success(current)) | ||
| } |
There was a problem hiding this comment.
Avoid stale fallback on overlapping refreshes.
Capturing current before the fetch means a slower failing refresh can overwrite newer data from a later successful refresh. Read current in the catch block instead, so the fallback always uses the latest cached values.
✅ Suggested fix
- Task {
- let current = await MainActor.run { (self.calendars, self.events) }
- do {
+ Task {
+ do {
let cals = try await self.provider.fetchAllCalendars()
let evts = try await self.fetchEvents(fromCalendars: cals)
promise(.success((cals, evts)))
} catch {
NSLog("EventManager refresh failed: \(error)")
+ let current = await MainActor.run { (self.calendars, self.events) }
promise(.success(current))
}
}🤖 Prompt for AI Agents
In `@MeetingBar/Core/Managers/EventManager.swift` around lines 176 - 185, The code
captures a stale snapshot into `current` before doing
`provider.fetchAllCalendars()` and `fetchEvents(fromCalendars:)`, which lets a
slower failing refresh overwrite newer data; to fix, stop reading
`self.calendars`/`self.events` before the fetch and instead, inside the `catch`
block, obtain the latest values via `await MainActor.run { (self.calendars,
self.events) }` and pass those to `promise(.success(...))`, leaving the
happy-path success case unchanged.
|
@copilot open a new pull request to apply changes based on the comments in this thread |
Motivation
Description
MeetingBar/Core/Managers/EventManager.swiftcapture the currentself.calendarsandself.eventson theMainActorbefore creating theFuture, and return thatcurrenttuple from thecatchblock instead of([], []), and remove the downstream.catchthat converted errors into an empty publisher.Testing
Codex Task
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.