// ── 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 } } }