135 lines
3.8 KiB
Swift
135 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 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
|
|
}
|
|
}
|