bugger/Sources/Services/PollerService.swift

137 lines
3.8 KiB
Swift

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 openId = tokenManager.cachedOpenId
let bugs = try await feishuService.fetchBugs(
config: config,
assigneeName: assignee,
accessToken: token,
userOpenId: openId
)
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
}
}