182 lines · 6.5 KB · Swift
RawDownload
import CodexBarCoreimport Foundation private enum ConfigChangeOrigin {    case localUser    case externalSync    case reload} private struct ConfigChangeContext {    let origin: ConfigChangeOrigin    let reason: String     static func local(reason: String) -> Self {        Self(origin: .localUser, reason: reason)    }     static func external(reason: String) -> Self {        Self(origin: .externalSync, reason: reason)    }     static func reload(reason: String) -> Self {        Self(origin: .reload, reason: reason)    }     var shouldBroadcast: Bool {        switch self.origin {        case .localUser:            true        case .externalSync, .reload:            false        }    }} extension SettingsStore {    private func updateConfig(reason: String, mutate: (inout CodexBarConfig) -> Void) {        guard !self.configLoading else { return }        var config = self.config        mutate(&config)        self.config = config.normalized()        self.updateProviderState(config: self.config)        self.schedulePersistConfig()        self.bumpConfigRevision(.local(reason: reason))    }     func updateProviderConfig(provider: UsageProvider, mutate: (inout ProviderConfig) -> Void) {        self.updateConfig(reason: "provider-\(provider.rawValue)") { config in            if let index = config.providers.firstIndex(where: { $0.id == provider }) {                var entry = config.providers[index]                mutate(&entry)                config.providers[index] = entry            } else {                var entry = ProviderConfig(id: provider)                mutate(&entry)                config.providers.append(entry)            }        }    }     func updateProviderTokenAccounts(_ accounts: [UsageProvider: ProviderTokenAccountData]) {        let summary = accounts            .sorted { $0.key.rawValue < $1.key.rawValue }            .map { "\($0.key.rawValue)=\($0.value.accounts.count)" }            .joined(separator: ",")        CodexBarLog.logger(LogCategories.tokenAccounts).info(            "Token accounts updated",            metadata: [                "providers": "\(accounts.count)",                "summary": summary,            ])        self.updateConfig(reason: "token-accounts") { config in            var seen: Set<UsageProvider> = []            for index in config.providers.indices {                let provider = config.providers[index].id                config.providers[index].tokenAccounts = accounts[provider]                seen.insert(provider)            }            for (provider, data) in accounts where !seen.contains(provider) {                config.providers.append(ProviderConfig(id: provider, tokenAccounts: data))            }        }    }     func setProviderOrder(_ order: [UsageProvider]) {        self.updateConfig(reason: "order") { config in            let configsByID = Dictionary(uniqueKeysWithValues: config.providers.map { ($0.id, $0) })            var seen: Set<UsageProvider> = []            var ordered: [ProviderConfig] = []            ordered.reserveCapacity(max(order.count, config.providers.count))             for provider in order {                guard !seen.contains(provider) else { continue }                seen.insert(provider)                ordered.append(configsByID[provider] ?? ProviderConfig(id: provider))            }             for provider in UsageProvider.allCases where !seen.contains(provider) {                ordered.append(configsByID[provider] ?? ProviderConfig(id: provider))            }             config.providers = ordered        }    }     func reloadConfig(reason: String) {        guard !self.configLoading else { return }        do {            guard let loaded = try self.configStore.load() else { return }            self.applyExternalConfig(loaded, reason: "reload-\(reason)")        } catch {            CodexBarLog.logger(LogCategories.configStore).error("Failed to reload config: \(error)")        }    }     func applyExternalConfig(_ config: CodexBarConfig, reason: String) {        guard !self.configLoading else { return }        self.configLoading = true        self.config = config        self.updateProviderState(config: config)        self.configLoading = false        self.bumpConfigRevision(.external(reason: "sync-\(reason)"))    }     private func bumpConfigRevision(_ context: ConfigChangeContext) {        // Account routing derives from config paths and source selection. Never let an old        // reconciliation snapshot survive a config reload, even when another provider changed.        self.invalidateCodexAccountReconciliationSnapshotCache()        self.cachedCodexAccountMenuProjection = nil        self.configRevision &+= 1        CodexBarLog.logger(LogCategories.settings)            .debug("Config revision bumped (\(context.reason)) -> \(self.configRevision)")        guard context.shouldBroadcast else { return }        NotificationCenter.default.post(            name: .codexbarProviderConfigDidChange,            object: self,            userInfo: [                "config": self.config,                "reason": context.reason,                "revision": self.configRevision,            ])    }     func normalizedConfigValue(_ raw: String) -> String? {        let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)        return trimmed.isEmpty ? nil : trimmed    }     func schedulePersistConfig() {        guard !self.configLoading else { return }        self.configPersistTask?.cancel()        if Self.isRunningTests {            do {                try self.configStore.save(self.config)            } catch {                CodexBarLog.logger(LogCategories.configStore).error("Failed to persist config: \(error)")            }            return        }        let store = self.configStore        self.configPersistTask = Task { @MainActor in            do {                try await Task.sleep(nanoseconds: 350_000_000)            } catch {                return            }            guard !Task.isCancelled else { return }            let snapshot = self.config            let error: (any Error)? = await Task.detached(priority: .utility) {                do {                    try store.save(snapshot)                    return nil                } catch {                    return error                }            }.value            if let error {                CodexBarLog.logger(LogCategories.configStore).error("Failed to persist config: \(error)")            }        }    }}