72 lines
2.4 KiB
Swift
72 lines
2.4 KiB
Swift
// ── Feature Flag Client ───────────────────────────────────────
|
|
// Polls platform-service /flags/poll for feature flags.
|
|
// Flags cached in memory, re-polled every 5 minutes.
|
|
// Consumers call FeatureFlagService.shared.isEnabled("flag_key").
|
|
|
|
import Foundation
|
|
|
|
@MainActor
|
|
final class FeatureFlagService: ObservableObject {
|
|
static let shared = FeatureFlagService()
|
|
|
|
@Published private(set) var flags: [String: Bool] = [:]
|
|
private var pollTimer: Timer?
|
|
private let pollIntervalSec: TimeInterval = 5 * 60 // 5 minutes
|
|
private let productId = "chronomind"
|
|
|
|
private var baseURL: String {
|
|
Bundle.main.object(forInfoDictionaryKey: "PLATFORM_SERVICE_URL") as? String
|
|
?? "https://api.chronomind.app"
|
|
}
|
|
|
|
private init() {}
|
|
|
|
// MARK: - Public API
|
|
|
|
func start(userId: String? = nil) {
|
|
Task { await fetchFlags(userId: userId) }
|
|
pollTimer?.invalidate()
|
|
pollTimer = Timer.scheduledTimer(withTimeInterval: pollIntervalSec, repeats: true) { [weak self] _ in
|
|
guard let self else { return }
|
|
Task { @MainActor in await self.fetchFlags(userId: userId) }
|
|
}
|
|
}
|
|
|
|
func stop() {
|
|
pollTimer?.invalidate()
|
|
pollTimer = nil
|
|
}
|
|
|
|
func isEnabled(_ key: String) -> Bool {
|
|
flags[key] == true
|
|
}
|
|
|
|
// MARK: - Fetch
|
|
|
|
private func fetchFlags(userId: String? = nil) async {
|
|
var components = URLComponents(string: "\(baseURL)/api/flags/poll")
|
|
var queryItems: [URLQueryItem] = [.init(name: "platform", value: "ios")]
|
|
if let userId { queryItems.append(.init(name: "userId", value: userId)) }
|
|
components?.queryItems = queryItems
|
|
|
|
guard let url = components?.url else { return }
|
|
|
|
var request = URLRequest(url: url)
|
|
request.setValue(productId, forHTTPHeaderField: "X-Product-Id")
|
|
request.timeoutInterval = 10
|
|
|
|
do {
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return }
|
|
|
|
struct FlagsResponse: Codable {
|
|
let flags: [String: Bool]
|
|
}
|
|
let parsed = try JSONDecoder().decode(FlagsResponse.self, from: data)
|
|
flags = parsed.flags
|
|
} catch {
|
|
// Keep existing flags on error
|
|
}
|
|
}
|
|
}
|