Finally fixed: added explicit cancellation checks, need to verify by independent testers

This commit is contained in:
Hariharan Mudaliar 2025-12-07 23:54:10 +05:30
commit b8f403d537
7 changed files with 527 additions and 185 deletions

View file

@ -29,6 +29,12 @@ class CalendarManager: ObservableObject {
private var lastEventsFetchDate: Date?
private let reloadRefreshInterval: TimeInterval = 15
private var eventStoreChangedObserver: NSObjectProtocol?
private var pendingEventStoreRefreshTask: Task<Void, Never>?
private var nextAllowedEventStoreRefresh: Date = .distantPast
private var ignoreEventStoreChangesUntil: Date = .distantPast
private let eventStoreChangeThrottle: TimeInterval = 20
private let selfInducedChangeSuppression: TimeInterval = 6
private let eventFetchLimiter = EventFetchLimiter()
var hasCalendarAccess: Bool { isAuthorized(calendarAuthorizationStatus) }
var hasReminderAccess: Bool { isAuthorized(reminderAuthorizationStatus) }
@ -45,6 +51,7 @@ class CalendarManager: ObservableObject {
if let observer = eventStoreChangedObserver {
NotificationCenter.default.removeObserver(observer)
}
pendingEventStoreRefreshTask?.cancel()
}
private func setupEventStoreChangedObserver() {
@ -53,14 +60,46 @@ class CalendarManager: ObservableObject {
object: nil,
queue: .main
) { [weak self] _ in
Task {
guard let self else { return }
await self.reloadCalendarAndReminderLists()
await self.maybeRefreshEventsAfterReload()
}
self?.handleEventStoreChanged()
}
}
private func handleEventStoreChanged() {
Logger.log("CalendarManager: Event store changed notification received", category: .lifecycle)
let now = Date()
guard now >= ignoreEventStoreChangesUntil else { return }
if now < nextAllowedEventStoreRefresh {
let delay = max(nextAllowedEventStoreRefresh.timeIntervalSince(now), 0.05)
scheduleEventStoreRefresh(after: delay)
return
}
nextAllowedEventStoreRefresh = now.addingTimeInterval(eventStoreChangeThrottle)
scheduleEventStoreRefresh(after: 0)
}
private func scheduleEventStoreRefresh(after delay: TimeInterval) {
pendingEventStoreRefreshTask?.cancel()
pendingEventStoreRefreshTask = Task { [weak self] in
guard let self else { return }
if delay > 0 {
let nanoseconds = UInt64(delay * 1_000_000_000)
try? await Task.sleep(nanoseconds: nanoseconds)
}
await self.performEventStoreRefresh()
}
}
@MainActor
private func performEventStoreRefresh() async {
pendingEventStoreRefreshTask = nil
await reloadCalendarAndReminderLists()
await maybeRefreshEventsAfterReload()
nextAllowedEventStoreRefresh = Date().addingTimeInterval(eventStoreChangeThrottle)
ignoreEventStoreChangesUntil = Date().addingTimeInterval(selfInducedChangeSuppression)
}
@MainActor
func reloadCalendarAndReminderLists() async {
let allCalendars = await calendarService.calendars()
@ -99,13 +138,13 @@ class CalendarManager: ObservableObject {
calendarAuthorizationStatus = granted ? .fullAccess : .denied
if granted {
await reloadCalendarAndReminderLists()
await updateEvents()
await updateEvents(force: true)
}
case .restricted, .denied:
NSLog("Calendar access denied or restricted")
case .authorized, .fullAccess:
await reloadCalendarAndReminderLists()
await updateEvents()
await updateEvents(force: true)
case .writeOnly:
NSLog("Calendar write only")
@unknown default:
@ -175,7 +214,7 @@ class CalendarManager: ObservableObject {
Defaults[.calendarSelectionState] = selectionState
updateSelectedCalendars()
await updateEvents()
await updateEvents(force: true)
}
static func startOfDay(_ date: Date) -> Date {
@ -184,22 +223,70 @@ class CalendarManager: ObservableObject {
func updateCurrentDate(_ date: Date) async {
currentWeekStartDate = Calendar.current.startOfDay(for: date)
await updateEvents()
await updateEvents(force: true)
}
private func updateEvents() async {
private func updateEvents(force: Bool = false) async {
let now = Date()
if !force, let lastFetch = lastEventsFetchDate, now.timeIntervalSince(lastFetch) < reloadRefreshInterval {
return
}
Logger.log("CalendarManager: Updating events (force: \(force))", category: .lifecycle)
let calendarIDs = selectedCalendars.map { $0.id }
let events = await calendarService.events(
from: currentWeekStartDate,
to: Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)!,
calendars: calendarIDs
)
let startDate = currentWeekStartDate
guard let endDate = Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate) else { return }
let service = calendarService
let events = await eventFetchLimiter.run {
await service.events(
from: startDate,
to: endDate,
calendars: calendarIDs
)
}
self.events = events
lastEventsFetchDate = Date()
}
func setReminderCompleted(reminderID: String, completed: Bool) async {
await calendarService.setReminderCompleted(reminderID: reminderID, completed: completed)
await updateEvents()
await updateEvents(force: true)
}
}
// MARK: - Event Fetch Limiter
private actor EventFetchLimiter {
private var waiters: [CheckedContinuation<Void, Never>] = []
private var isRunning = false
func run<T>(_ operation: @escaping @Sendable () async -> T) async -> T {
await waitTurn()
defer { resumeNext() }
return await operation()
}
private func waitTurn() async {
if !isRunning {
isRunning = true
return
}
await withCheckedContinuation { continuation in
waiters.append(continuation)
}
}
private func resumeNext() {
if waiters.isEmpty {
isRunning = false
return
}
let continuation = waiters.removeFirst()
continuation.resume()
}
}