import AppKit import Foundation @Observable final class PollerService { static let shared = PollerService() private let bugStore = BugStore.shared private let feishuService = FeishuService() private let tokenManager = TokenManager.shared private let configService = AppStateService.shared private var timer: Timer? private var isFetching = false private var lastFetchTime: Date? private(set) var isRunning = false private init() {} func startIfConfigured() async { guard let config = configService.config, config.isConfigured, tokenManager.isAuthenticated else { return } start(interval: TimeInterval(config.pollIntervalSeconds), fetchImmediately: config.refreshOnStart) } func start(interval: TimeInterval, fetchImmediately: Bool = true) { guard !isRunning else { return } isRunning = true if fetchImmediately { Task { await performFetch() } } if interval < 0 { // Daily schedule mode: check every 60s, fire at configured times timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in self?.checkDailySchedule() } timer?.tolerance = 5 } else { timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in Task { await self?.performFetch() } } timer?.tolerance = interval * 0.1 } } func stop() { timer?.invalidate() timer = nil isRunning = false } func restart(interval: TimeInterval) { stop() start(interval: interval) } func fetchNow() async { await performFetch() } // MARK: - Daily schedule private func checkDailySchedule() { guard let config = configService.config else { return } let formatter = DateFormatter() formatter.dateFormat = "HH:mm" let now = formatter.string(from: Date()) guard config.dailyRefreshTimes.contains(now) else { return } // Avoid firing twice in the same minute if let last = lastFetchTime, Date().timeIntervalSince(last) < 120 { return } Task { await performFetch() } } // MARK: - Fetch private func performFetch() async { guard !isFetching else { return } isFetching = true defer { isFetching = false } lastFetchTime = Date() await MainActor.run { bugStore.setLoading(true) bugStore.setError(nil) } do { let config = try getConfig() let token = try await tokenManager.getAccessToken() let assignee = try await tokenManager.resolveAssigneeName(config: config, accessToken: token) let bugs = try await feishuService.fetchBugs( config: config, assigneeName: assignee, accessToken: token ) await MainActor.run { bugStore.update(with: bugs) bugStore.setLoading(false) AppDelegate.shared?.updateBadge(count: bugStore.activeBugs.count) } } catch FeishuError.unauthorized { await MainActor.run { bugStore.setLoading(false) tokenManager.clearTokens() } } catch { await MainActor.run { bugStore.setLoading(false) bugStore.setError(error) } } } private func getConfig() throws -> AppConfig { guard let config = configService.config, config.isConfigured else { throw FeishuError.notConfigured } return config } }