mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 23:24:32 +00:00
BugFix: Reminder Memory Leak
This commit is contained in:
parent
4e0659d4cc
commit
39673e9130
8 changed files with 401 additions and 456 deletions
|
|
@ -203,4 +203,9 @@ class CalendarManager: ObservableObject {
|
|||
)
|
||||
self.events = eventsResult
|
||||
}
|
||||
|
||||
func setReminderCompleted(reminderID: String, completed: Bool) async {
|
||||
await calendarService.setReminderCompleted(reminderID: reminderID, completed: completed)
|
||||
await updateEvents()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import Combine
|
||||
import Defaults
|
||||
import EventKit
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import os
|
||||
|
|
@ -36,59 +35,23 @@ final class ReminderLiveActivityManager: ObservableObject {
|
|||
|
||||
private let logger: os.Logger = os.Logger(subsystem: "com.ebullioscopic.Atoll", category: "ReminderLiveActivity")
|
||||
|
||||
private enum RefreshReason: String {
|
||||
case initialization
|
||||
case defaults
|
||||
case manual
|
||||
case eventStoreChange
|
||||
case calendarListChange
|
||||
case calendarEventsUpdate
|
||||
case evaluation
|
||||
case hover
|
||||
case ticker
|
||||
case other
|
||||
}
|
||||
|
||||
private var nextReminder: ReminderEntry?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var tickerTask: Task<Void, Never>? { didSet { oldValue?.cancel() } }
|
||||
private var evaluationTask: Task<Void, Never>?
|
||||
private var refreshTask: Task<Void, Never>?
|
||||
private var pendingRefreshTask: Task<Void, Never>?
|
||||
private var pendingRefreshForce = false
|
||||
private var pendingRefreshToken = UUID()
|
||||
private var pendingRefreshReason: RefreshReason = .other
|
||||
private var lastKnownCalendarIDs = Set<String>()
|
||||
private var lastRefreshDate: Date?
|
||||
private var lastRefreshCompletionDate: Date?
|
||||
private let minimumRefreshInterval: TimeInterval = 60
|
||||
private let refreshDebounceInterval: TimeInterval = 0.3
|
||||
private var refreshTaskToken = UUID()
|
||||
private var hasShownCriticalSneakPeek = false
|
||||
private let eventStoreChangeNoiseInterval: TimeInterval = 4
|
||||
private let eventStoreForceThrottleInterval: TimeInterval = 20
|
||||
private let manualRefreshSuppressionInterval: TimeInterval = 20
|
||||
private var lastEventStoreForceRefreshDate: Date?
|
||||
private var lastManualRefreshDate: Date?
|
||||
private var pendingEventStoreChange: (date: Date, reason: RefreshReason)?
|
||||
private var reminderCompletionSuppressionDeadline: Date?
|
||||
private let reminderCompletionSuppressionInterval: TimeInterval = 15
|
||||
private var lastUpcomingSignature: [String] = []
|
||||
private var lastNoopEventStoreRefreshDate: Date?
|
||||
private let noopEventStoreSuppressionInterval: TimeInterval = 30
|
||||
private var lastCalendarEventSnapshot: [EventModel] = []
|
||||
private var lastCalendarSnapshotDate: Date?
|
||||
private var lastCalendarEventSnapshotSignature: [String] = []
|
||||
private var latestEvents: [EventModel] = []
|
||||
|
||||
private let calendarService: CalendarServiceProviding
|
||||
private let calendarManager = CalendarManager.shared
|
||||
|
||||
var isActive: Bool { activeReminder != nil }
|
||||
|
||||
private init(calendarService: CalendarServiceProviding = CalendarService()) {
|
||||
self.calendarService = calendarService
|
||||
private init() {
|
||||
latestEvents = calendarManager.events
|
||||
setupObservers()
|
||||
scheduleRefresh(force: true, reason: .initialization)
|
||||
if !latestEvents.isEmpty {
|
||||
recalculateUpcomingEntries(reason: "initialization")
|
||||
}
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
|
|
@ -96,7 +59,7 @@ final class ReminderLiveActivityManager: ObservableObject {
|
|||
.sink { [weak self] change in
|
||||
guard let self else { return }
|
||||
if change.newValue {
|
||||
self.scheduleRefresh(force: true, reason: .defaults)
|
||||
self.recalculateUpcomingEntries(reason: "defaults-toggle")
|
||||
} else {
|
||||
self.deactivateReminder()
|
||||
}
|
||||
|
|
@ -106,7 +69,19 @@ final class ReminderLiveActivityManager: ObservableObject {
|
|||
Defaults.publisher(.reminderLeadTime, options: [])
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.scheduleRefresh(force: true, reason: .defaults)
|
||||
self.recalculateUpcomingEntries(reason: "lead-time")
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
Defaults.publisher(.hideAllDayEvents, options: [])
|
||||
.sink { [weak self] _ in
|
||||
self?.recalculateUpcomingEntries(reason: "hide-all-day")
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
Defaults.publisher(.hideCompletedReminders, options: [])
|
||||
.sink { [weak self] _ in
|
||||
self?.recalculateUpcomingEntries(reason: "hide-completed")
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
|
@ -120,16 +95,6 @@ final class ReminderLiveActivityManager: ObservableObject {
|
|||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
calendarManager.$allCalendars
|
||||
.sink { [weak self] calendars in
|
||||
guard let self else { return }
|
||||
let identifiers = Set(calendars.map { $0.id })
|
||||
guard identifiers != self.lastKnownCalendarIDs else { return }
|
||||
self.lastKnownCalendarIDs = identifiers
|
||||
self.handleEventStoreDrivenChange(reason: .calendarListChange)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
calendarManager.$events
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] events in
|
||||
|
|
@ -137,26 +102,13 @@ final class ReminderLiveActivityManager: ObservableObject {
|
|||
self.handleCalendarEventsUpdate(events)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
NotificationCenter.default.publisher(for: .EKEventStoreChanged)
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.handleEventStoreDrivenChange(reason: .eventStoreChange)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func cancelAllTimers() {
|
||||
tickerTask = nil
|
||||
evaluationTask?.cancel()
|
||||
evaluationTask = nil
|
||||
pendingRefreshTask?.cancel()
|
||||
pendingRefreshTask = nil
|
||||
pendingRefreshForce = false
|
||||
refreshTask?.cancel()
|
||||
refreshTask = nil
|
||||
hasShownCriticalSneakPeek = false
|
||||
pendingEventStoreChange = nil
|
||||
}
|
||||
|
||||
private func deactivateReminder() {
|
||||
|
|
@ -165,32 +117,13 @@ final class ReminderLiveActivityManager: ObservableObject {
|
|||
upcomingEntries = []
|
||||
activeWindowReminders = []
|
||||
cancelAllTimers()
|
||||
lastUpcomingSignature = []
|
||||
lastNoopEventStoreRefreshDate = nil
|
||||
}
|
||||
|
||||
private func selectedCalendarIDs() -> [String] {
|
||||
calendarManager.allCalendars
|
||||
.filter { calendarManager.getCalendarSelected($0) }
|
||||
.map { $0.id }
|
||||
}
|
||||
|
||||
private func handleCalendarEventsUpdate(_ events: [EventModel]) {
|
||||
let now = Date()
|
||||
let signature = events.map { $0.id }
|
||||
if signature == lastCalendarEventSnapshotSignature,
|
||||
let snapshotDate = lastCalendarSnapshotDate,
|
||||
now.timeIntervalSince(snapshotDate) < 5 {
|
||||
return
|
||||
}
|
||||
|
||||
lastCalendarEventSnapshot = events
|
||||
lastCalendarSnapshotDate = now
|
||||
lastCalendarEventSnapshotSignature = signature
|
||||
|
||||
latestEvents = events
|
||||
guard Defaults[.enableReminderLiveActivity] else { return }
|
||||
logger.debug("[Reminder] Applying calendar snapshot update (events=\(events.count, privacy: .public))")
|
||||
refreshFromEvents(events, referenceDate: now, reason: .calendarEventsUpdate)
|
||||
recalculateUpcomingEntries(reason: "calendar-events")
|
||||
}
|
||||
|
||||
private func shouldHide(_ event: EventModel) -> Bool {
|
||||
|
|
@ -211,6 +144,45 @@ final class ReminderLiveActivityManager: ObservableObject {
|
|||
return ReminderEntry(event: event, triggerDate: trigger, leadTime: TimeInterval(leadSeconds))
|
||||
}
|
||||
|
||||
private func recalculateUpcomingEntries(referenceDate: Date = Date(), reason: String) {
|
||||
guard Defaults[.enableReminderLiveActivity] else {
|
||||
deactivateReminder()
|
||||
return
|
||||
}
|
||||
|
||||
guard calendarManager.hasReminderAccess else {
|
||||
deactivateReminder()
|
||||
return
|
||||
}
|
||||
|
||||
let leadMinutes = Defaults[.reminderLeadTime]
|
||||
let upcoming = latestEvents
|
||||
.filter { !shouldHide($0) }
|
||||
.compactMap { makeEntry(from: $0, leadMinutes: leadMinutes, referenceDate: referenceDate) }
|
||||
.sorted { $0.triggerDate < $1.triggerDate }
|
||||
|
||||
upcomingEntries = upcoming
|
||||
updateActiveWindowReminders(for: referenceDate)
|
||||
|
||||
guard let first = upcoming.first else {
|
||||
clearActiveReminderState()
|
||||
logger.debug("[Reminder] No upcoming reminders found (reason=\(reason, privacy: .public))")
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug("[Reminder] Next reminder ‘\(first.event.title, privacy: .public)’ (reason=\(reason, privacy: .public))")
|
||||
handleEntrySelection(first, referenceDate: referenceDate)
|
||||
}
|
||||
|
||||
private func clearActiveReminderState() {
|
||||
nextReminder = nil
|
||||
if activeReminder != nil {
|
||||
activeReminder = nil
|
||||
}
|
||||
activeWindowReminders = []
|
||||
cancelAllTimers()
|
||||
}
|
||||
|
||||
private func scheduleEvaluation(at date: Date) {
|
||||
evaluationTask?.cancel()
|
||||
let delay = date.timeIntervalSinceNow
|
||||
|
|
@ -226,88 +198,6 @@ final class ReminderLiveActivityManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func scheduleRefresh(force: Bool, reason: RefreshReason = .other) {
|
||||
let wasPending = pendingRefreshTask != nil
|
||||
pendingRefreshForce = pendingRefreshForce || force
|
||||
pendingRefreshToken = UUID()
|
||||
pendingRefreshReason = reason
|
||||
let token = pendingRefreshToken
|
||||
|
||||
logger.debug("[Reminder] scheduleRefresh reason=\(reason.rawValue, privacy: .public) force=\(force, privacy: .public) pending=\(wasPending, privacy: .public)")
|
||||
|
||||
refreshTask?.cancel()
|
||||
pendingRefreshTask?.cancel()
|
||||
pendingRefreshTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: UInt64(self.refreshDebounceInterval * 1_000_000_000))
|
||||
} catch {
|
||||
self.logger.debug("[Reminder] Debounced refresh for reason=\(reason.rawValue, privacy: .public) was cancelled")
|
||||
return
|
||||
}
|
||||
await self.executeScheduledRefresh(token: token)
|
||||
}
|
||||
}
|
||||
|
||||
private func executeScheduledRefresh(token: UUID) async {
|
||||
guard pendingRefreshToken == token else { return }
|
||||
pendingRefreshTask = nil
|
||||
|
||||
if Task.isCancelled {
|
||||
logger.debug("[Reminder] Cancelled refresh token=\(token.uuidString, privacy: .public)")
|
||||
return
|
||||
}
|
||||
|
||||
let reason = pendingRefreshReason
|
||||
let now = Date()
|
||||
let force = pendingRefreshForce
|
||||
if !force, let last = lastRefreshDate {
|
||||
let elapsed = now.timeIntervalSince(last)
|
||||
if elapsed < minimumRefreshInterval {
|
||||
let remaining = max(minimumRefreshInterval - elapsed, refreshDebounceInterval)
|
||||
logger.debug("[Reminder] Deferring refresh reason=\(reason.rawValue, privacy: .public); next attempt in \(remaining, format: .fixed(precision: 2))s")
|
||||
pendingRefreshToken = UUID()
|
||||
let nextToken = pendingRefreshToken
|
||||
pendingRefreshTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000))
|
||||
} catch {
|
||||
self.logger.debug("[Reminder] Deferred refresh for reason=\(reason.rawValue, privacy: .public) was cancelled")
|
||||
return
|
||||
}
|
||||
await self.executeScheduledRefresh(token: nextToken)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pendingRefreshForce = false
|
||||
lastRefreshDate = now
|
||||
|
||||
let startTime = Date()
|
||||
logger.debug("[Reminder] Starting refresh reason=\(reason.rawValue, privacy: .public) force=\(force, privacy: .public)")
|
||||
|
||||
refreshTask?.cancel()
|
||||
let taskToken = UUID()
|
||||
refreshTaskToken = taskToken
|
||||
let task = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
await self.refreshUpcomingReminder(force: force, reason: reason)
|
||||
}
|
||||
refreshTask = task
|
||||
defer {
|
||||
if refreshTaskToken == taskToken {
|
||||
refreshTask = nil
|
||||
lastRefreshCompletionDate = Date()
|
||||
let duration = Date().timeIntervalSince(startTime)
|
||||
logger.debug("[Reminder] Finished refresh reason=\(reason.rawValue, privacy: .public) duration=\(duration, format: .fixed(precision: 2))s upcoming=\(self.upcomingEntries.count, privacy: .public)")
|
||||
processPendingEventStoreChangeIfNeeded()
|
||||
}
|
||||
}
|
||||
_ = try? await task.value
|
||||
}
|
||||
|
||||
private func startTickerIfNeeded() {
|
||||
guard tickerTask == nil else { return }
|
||||
tickerTask = Task { [weak self] in
|
||||
|
|
@ -328,109 +218,16 @@ final class ReminderLiveActivityManager: ObservableObject {
|
|||
}
|
||||
|
||||
private func handleEntrySelection(_ entry: ReminderEntry?, referenceDate: Date) {
|
||||
guard nextReminder != entry else {
|
||||
Task { await self.evaluateCurrentState(at: referenceDate) }
|
||||
return
|
||||
}
|
||||
|
||||
nextReminder = entry
|
||||
hasShownCriticalSneakPeek = false
|
||||
Task { await self.evaluateCurrentState(at: referenceDate) }
|
||||
}
|
||||
|
||||
private func refreshFromEvents(_ events: [EventModel], referenceDate: Date, reason: RefreshReason) {
|
||||
let leadMinutes = Defaults[.reminderLeadTime]
|
||||
let upcoming = events
|
||||
.filter { !shouldHide($0) }
|
||||
.compactMap { makeEntry(from: $0, leadMinutes: leadMinutes, referenceDate: referenceDate) }
|
||||
.sorted { $0.triggerDate < $1.triggerDate }
|
||||
|
||||
upcomingEntries = upcoming
|
||||
updateActiveWindowReminders(for: referenceDate)
|
||||
|
||||
logger.debug("[Reminder] Reduced \(events.count, privacy: .public) events to \(upcoming.count, privacy: .public) upcoming reminders")
|
||||
|
||||
guard let first = upcoming.first else {
|
||||
deactivateReminder()
|
||||
logger.debug("[Reminder] No upcoming reminders found; awaiting next calendar update")
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug("[Reminder] Next reminder ‘\(first.event.title, privacy: .public)’ at \(first.triggerDate.timeIntervalSinceReferenceDate, privacy: .public)")
|
||||
|
||||
handleEntrySelection(first, referenceDate: referenceDate)
|
||||
|
||||
let signature = upcoming.map { $0.event.id }
|
||||
if lastUpcomingSignature == signature {
|
||||
if reason == .eventStoreChange {
|
||||
lastNoopEventStoreRefreshDate = Date()
|
||||
}
|
||||
} else {
|
||||
lastNoopEventStoreRefreshDate = nil
|
||||
}
|
||||
lastUpcomingSignature = signature
|
||||
}
|
||||
|
||||
func requestRefresh(force: Bool = false) {
|
||||
let reason: RefreshReason = force ? .manual : .other
|
||||
if force {
|
||||
let now = Date()
|
||||
if let lastManualRefreshDate,
|
||||
now.timeIntervalSince(lastManualRefreshDate) < manualRefreshSuppressionInterval {
|
||||
let delta = now.timeIntervalSince(lastManualRefreshDate)
|
||||
logger.debug("[Reminder] Skipping manual refresh; last manual was \(delta, format: .fixed(precision: 2))s ago")
|
||||
return
|
||||
}
|
||||
lastManualRefreshDate = now
|
||||
}
|
||||
scheduleRefresh(force: force, reason: reason)
|
||||
}
|
||||
|
||||
private func refreshUpcomingReminder(force: Bool = false, reason: RefreshReason = .other) async {
|
||||
guard Defaults[.enableReminderLiveActivity] else {
|
||||
deactivateReminder()
|
||||
logger.debug("[Reminder] Refresh aborted (feature disabled)")
|
||||
return
|
||||
}
|
||||
|
||||
guard calendarManager.hasReminderAccess else {
|
||||
deactivateReminder()
|
||||
logger.debug("[Reminder] Refresh aborted; reminder permissions missing")
|
||||
return
|
||||
}
|
||||
|
||||
let now = Date()
|
||||
|
||||
if !force, let entry = nextReminder, entry.event.start > now {
|
||||
logger.debug("[Reminder] Skipping fetch; cached entry \(entry.event.title, privacy: .public) still valid for reason=\(reason.rawValue, privacy: .public)")
|
||||
await evaluateCurrentState(at: now)
|
||||
return
|
||||
}
|
||||
|
||||
if let snapshotDate = lastCalendarSnapshotDate,
|
||||
now.timeIntervalSince(snapshotDate) < 5,
|
||||
!lastCalendarEventSnapshot.isEmpty {
|
||||
logger.debug("[Reminder] Using calendar snapshot (age=\(now.timeIntervalSince(snapshotDate), format: .fixed(precision: 2))s) for reason=\(reason.rawValue, privacy: .public)")
|
||||
refreshFromEvents(lastCalendarEventSnapshot, referenceDate: now, reason: reason)
|
||||
return
|
||||
}
|
||||
|
||||
let calendars = selectedCalendarIDs()
|
||||
guard !calendars.isEmpty else {
|
||||
deactivateReminder()
|
||||
logger.debug("[Reminder] Refresh aborted; no calendars selected")
|
||||
return
|
||||
}
|
||||
|
||||
let windowEnd = Calendar.current.date(byAdding: .hour, value: 24, to: now) ?? now.addingTimeInterval(24 * 60 * 60)
|
||||
logger.debug("[Reminder] Fetching reminders (reason=\(reason.rawValue, privacy: .public), force=\(force, privacy: .public), calendars=\(calendars.count, privacy: .public))")
|
||||
let fetchStart = Date()
|
||||
let events = await calendarService.events(from: now, to: windowEnd, calendars: calendars)
|
||||
let fetchDuration = Date().timeIntervalSince(fetchStart)
|
||||
logger.debug("[Reminder] EventKit fetch completed (events=\(events.count, privacy: .public)) in \(fetchDuration, format: .fixed(precision: 2))s")
|
||||
lastCalendarEventSnapshot = events
|
||||
lastCalendarSnapshotDate = now
|
||||
lastCalendarEventSnapshotSignature = events.map { $0.id }
|
||||
await MainActor.run {
|
||||
self.refreshFromEvents(events, referenceDate: now, reason: reason)
|
||||
}
|
||||
}
|
||||
|
||||
func evaluateCurrentState(at date: Date) async {
|
||||
guard Defaults[.enableReminderLiveActivity] else {
|
||||
deactivateReminder()
|
||||
|
|
@ -450,13 +247,9 @@ final class ReminderLiveActivityManager: ObservableObject {
|
|||
}
|
||||
|
||||
if entry.event.start <= date {
|
||||
activeReminder = nil
|
||||
nextReminder = nil
|
||||
stopTicker()
|
||||
hasShownCriticalSneakPeek = false
|
||||
logger.debug("[Reminder] Reminder reached start time; requesting evaluation refresh")
|
||||
reminderCompletionSuppressionDeadline = Date().addingTimeInterval(reminderCompletionSuppressionInterval)
|
||||
scheduleRefresh(force: true, reason: .evaluation)
|
||||
clearActiveReminderState()
|
||||
logger.debug("[Reminder] Reminder reached start time; reevaluating reminders from cache")
|
||||
recalculateUpcomingEntries(referenceDate: date, reason: "evaluation-complete")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -528,83 +321,6 @@ final class ReminderLiveActivityManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func handleEventStoreDrivenChange(reason: RefreshReason) {
|
||||
processEventStoreChange(at: Date(), reason: reason)
|
||||
}
|
||||
|
||||
private func processEventStoreChange(at date: Date, reason: RefreshReason) {
|
||||
if let manual = lastManualRefreshDate {
|
||||
let delta = date.timeIntervalSince(manual)
|
||||
if delta < manualRefreshSuppressionInterval {
|
||||
logger.debug("[Reminder] Ignoring \(reason.rawValue, privacy: .public) change; manual refresh cooldown \(delta, format: .fixed(precision: 2))s")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let lastRefreshStart = lastRefreshDate {
|
||||
let sinceStart = date.timeIntervalSince(lastRefreshStart)
|
||||
if sinceStart >= 0 && sinceStart < eventStoreChangeNoiseInterval {
|
||||
logger.debug("[Reminder] Ignoring \(reason.rawValue, privacy: .public); \(sinceStart, format: .fixed(precision: 2))s since start")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let lastCompletion = lastRefreshCompletionDate {
|
||||
let sinceCompletion = abs(date.timeIntervalSince(lastCompletion))
|
||||
if sinceCompletion < eventStoreChangeNoiseInterval {
|
||||
logger.debug("[Reminder] Ignoring \(reason.rawValue, privacy: .public); \(sinceCompletion, format: .fixed(precision: 2))s since completion")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if reason == .eventStoreChange,
|
||||
let suppressionDeadline = reminderCompletionSuppressionDeadline,
|
||||
date <= suppressionDeadline {
|
||||
let remaining = suppressionDeadline.timeIntervalSince(date)
|
||||
logger.debug("[Reminder] Ignoring \(reason.rawValue, privacy: .public); reminder completion suppression \(remaining, format: .fixed(precision: 2))s")
|
||||
return
|
||||
}
|
||||
|
||||
if reason == .eventStoreChange,
|
||||
let noopDate = lastNoopEventStoreRefreshDate,
|
||||
date.timeIntervalSince(noopDate) < noopEventStoreSuppressionInterval {
|
||||
let remaining = noopEventStoreSuppressionInterval - date.timeIntervalSince(noopDate)
|
||||
logger.debug("[Reminder] Ignoring \(reason.rawValue, privacy: .public); identical upcoming entries \(remaining, format: .fixed(precision: 2))s window")
|
||||
return
|
||||
}
|
||||
|
||||
if let lastForce = lastEventStoreForceRefreshDate {
|
||||
let sinceForce = date.timeIntervalSince(lastForce)
|
||||
if sinceForce < self.eventStoreForceThrottleInterval {
|
||||
logger.debug("[Reminder] Ignoring \(reason.rawValue, privacy: .public); throttle window remaining \(self.eventStoreForceThrottleInterval - sinceForce, format: .fixed(precision: 2))s")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if refreshTask != nil {
|
||||
pendingEventStoreChange = (date, reason)
|
||||
logger.debug("[Reminder] Queued \(reason.rawValue, privacy: .public) change while refresh is running")
|
||||
return
|
||||
}
|
||||
|
||||
triggerEventStoreForcedRefresh(at: date, reason: reason)
|
||||
}
|
||||
|
||||
private func processPendingEventStoreChangeIfNeeded() {
|
||||
guard let pendingChange = pendingEventStoreChange else { return }
|
||||
pendingEventStoreChange = nil
|
||||
processEventStoreChange(at: pendingChange.date, reason: pendingChange.reason)
|
||||
}
|
||||
|
||||
private func triggerEventStoreForcedRefresh(at date: Date = Date(), reason: RefreshReason) {
|
||||
lastEventStoreForceRefreshDate = date
|
||||
if reason == .eventStoreChange, reminderCompletionSuppressionDeadline != nil {
|
||||
reminderCompletionSuppressionDeadline = nil
|
||||
}
|
||||
logger.debug("[Reminder] Triggering event-store refresh for \(reason.rawValue, privacy: .public)")
|
||||
scheduleRefresh(force: true, reason: reason)
|
||||
}
|
||||
|
||||
static func additionalHeight(forRowCount rowCount: Int) -> CGFloat {
|
||||
guard rowCount > 0 else { return 0 }
|
||||
let rows = CGFloat(rowCount)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue