| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143 |
- //
- // TaskScheduler.swift
- // LoopFollow
- //
- // Created by Jonas Björkert on 2025-01-10.
- // Copyright © 2025 Jon Fawcett. All rights reserved.
- //
- import Foundation
- import UIKit
- enum TaskID {
- case profile
- case deviceStatus
- case treatments
- case fetchBG
- case minAgoUpdate
- case calendarWrite
- case alarmCheck
- }
- struct ScheduledTask {
- var nextRun: Date
- var action: () -> Void
- }
- class TaskScheduler {
- static let shared = TaskScheduler()
- // Thread-safety: a serial queue so we don’t manipulate tasks from multiple threads at once
- private let queue = DispatchQueue(label: "com.LoopFollow.TaskSchedulerQueue")
- private var tasks: [TaskID: ScheduledTask] = [:]
- private var currentTimer: Timer?
- private init() {}
- // MARK: - Public API
- func scheduleTask(id: TaskID, nextRun: Date, action: @escaping () -> Void) {
- queue.async {
- let timeString = self.formatTime(nextRun)
- LogManager.shared.log(category: .taskScheduler, message: "scheduleTask(\(id)): nextRun = \(timeString)")
-
- self.tasks[id] = ScheduledTask(nextRun: nextRun, action: action)
- self.rescheduleTimer()
- }
- }
- func rescheduleTask(id: TaskID, to newRunDate: Date) {
- queue.async {
- guard var existingTask = self.tasks[id] else {
- return
- }
- //let timeString = self.formatTime(newRunDate)
- //LogManager.shared.log(category: .taskScheduler, message: "Reschedule Task \(id), nextRun = \(timeString)")
- existingTask.nextRun = newRunDate
- self.tasks[id] = existingTask
- self.rescheduleTimer()
- }
- }
- func checkTasksNow() {
- queue.async {
- LogManager.shared.log(category: .taskScheduler, message: "CheckTasksNow, Forcing immediate check.")
- self.fireOverdueTasks()
- self.rescheduleTimer()
- }
- }
- // MARK: - Private
- /// Updated signature to include info about who called us, and which task triggered it (if any).
- private func rescheduleTimer() {
- // Invalidate any existing timer
- currentTimer?.invalidate()
- currentTimer = nil
- guard let (_, earliestTask) = tasks.min(by: { $0.value.nextRun < $1.value.nextRun }) else {
- LogManager.shared.log(
- category: .taskScheduler,
- message: "No tasks, no timer scheduled."
- )
- return
- }
- let interval = earliestTask.nextRun.timeIntervalSinceNow
- let safeInterval = max(interval, 0)
- DispatchQueue.main.async {
- self.currentTimer = Timer.scheduledTimer(withTimeInterval: safeInterval, repeats: false) { [weak self] _ in
- guard let self = self else { return }
- self.queue.async {
- self.fireOverdueTasks()
- self.rescheduleTimer()
- }
- }
- }
- }
- private func fireOverdueTasks() {
- let now = Date()
- for (id, task) in tasks {
- if task.nextRun <= now {
- // let scheduledTimeString = formatTime(task.nextRun)
- // let diffSeconds = Int(now.timeIntervalSince(task.nextRun))
- // let diffInfo = diffSeconds > 0 ? "\(diffSeconds)s late" : "\(abs(diffSeconds))s early"
- // LogManager.shared.log(category: .taskScheduler, message: "Executing \(id) scheduled for \(scheduledTimeString) (\(diffInfo))")
- var updatedTask = task
- updatedTask.nextRun = .distantFuture
- tasks[id] = updatedTask
- DispatchQueue.main.async {
- task.action()
- }
- }
- }
- }
- private func formatTime(_ date: Date) -> String {
- let formatter = DateFormatter()
- formatter.dateStyle = .none
- formatter.timeStyle = .medium
- return formatter.string(from: date)
- }
- }
- private extension TaskID {
- var description: String {
- switch self {
- case .profile: return "profile"
- case .deviceStatus: return "deviceStatus"
- case .fetchBG: return "fetchBG"
- case .treatments: return "treatments"
- case .calendarWrite: return "calendarWrite"
- case .minAgoUpdate: return "minAgoUpdate"
- case .alarmCheck: return "alarmCheck"
- }
- }
- }
|