87 lines
2.5 KiB
Swift
87 lines
2.5 KiB
Swift
// ── Feature Flag Client ──────────────────────────────────────
|
|
// Generic feature flag polling from platform-service /api/flags/poll.
|
|
// Flags cached in memory, re-polled at configurable interval.
|
|
// Matches the platform-service flags module API.
|
|
|
|
import Foundation
|
|
|
|
/// Generic feature flag client for all ByteLyst iOS apps.
|
|
/// Polls platform-service and caches flag values in memory.
|
|
public final class BLFeatureFlagClient {
|
|
|
|
private let config: BLPlatformConfig
|
|
private let client: BLPlatformClient
|
|
private let pollIntervalSec: TimeInterval
|
|
|
|
private var flags: [String: Bool] = [:]
|
|
private let flagsLock = NSLock()
|
|
private var pollTimer: Timer?
|
|
|
|
public init(
|
|
config: BLPlatformConfig,
|
|
client: BLPlatformClient,
|
|
pollIntervalSec: TimeInterval = 5 * 60
|
|
) {
|
|
self.config = config
|
|
self.client = client
|
|
self.pollIntervalSec = pollIntervalSec
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
/// Start polling for feature flags.
|
|
public 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 { await self.fetchFlags(userId: userId) }
|
|
}
|
|
}
|
|
|
|
/// Stop polling.
|
|
public func stop() {
|
|
pollTimer?.invalidate()
|
|
pollTimer = nil
|
|
}
|
|
|
|
// MARK: - Query
|
|
|
|
/// Check if a feature flag is enabled.
|
|
public func isEnabled(_ key: String) -> Bool {
|
|
flagsLock.lock()
|
|
defer { flagsLock.unlock() }
|
|
return flags[key] == true
|
|
}
|
|
|
|
/// Get all current flag values.
|
|
public func allFlags() -> [String: Bool] {
|
|
flagsLock.lock()
|
|
defer { flagsLock.unlock() }
|
|
return flags
|
|
}
|
|
|
|
// MARK: - Fetch
|
|
|
|
private struct FlagsResponse: Codable {
|
|
let flags: [String: Bool]
|
|
}
|
|
|
|
private func fetchFlags(userId: String? = nil) async {
|
|
var path = "/api/flags/poll?platform=\(config.platform)"
|
|
if let userId { path += "&userId=\(userId)" }
|
|
|
|
do {
|
|
let result = try await client.request(
|
|
path: path,
|
|
responseType: FlagsResponse.self
|
|
)
|
|
flagsLock.lock()
|
|
flags = result.flags
|
|
flagsLock.unlock()
|
|
} catch {
|
|
// Keep existing flags on error — silent failure
|
|
}
|
|
}
|
|
}
|