mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-10 22:45:55 +05:45
* refactor(remote-config): introduce config lifecycle observers * refactor(remote-config): simplify SDK settings delivery * test(remote-config): cover lifecycle terminal failure
146 lines
5.0 KiB
Swift
146 lines
5.0 KiB
Swift
//
|
|
// AudiencesConfigProvider.swift
|
|
// RevenueCat
|
|
//
|
|
// Created by Cesar de la Vega.
|
|
// Copyright © 2026 RevenueCat, Inc. All rights reserved.
|
|
|
|
import Foundation
|
|
|
|
protocol AudiencesConfigProviderType {
|
|
|
|
func configuration() async throws -> AudienceConfigurationSnapshot?
|
|
func isCurrent(_ snapshot: AudienceConfigurationSnapshot) -> Bool
|
|
|
|
}
|
|
|
|
/// One immutable view of the audience rules served by the `audiences` topic.
|
|
struct AudienceConfigurationSnapshot: Equatable, Sendable {
|
|
|
|
let audiences: [String: Audience]
|
|
let configGeneration: Int
|
|
|
|
}
|
|
|
|
/// The topic-specific front door for canonical audience configuration.
|
|
///
|
|
/// All published audience rules live in the immutable `default` blob.
|
|
final class AudiencesConfigProvider: AudiencesConfigProviderType, RemoteConfigLifecycleObserver {
|
|
|
|
private let manager: RemoteConfigManagerType
|
|
private let cachedConfiguration = GenerationGuardedCache<
|
|
RemoteConfiguration.ConfigTopic,
|
|
AudienceConfigurationSnapshot
|
|
>()
|
|
|
|
init(manager: RemoteConfigManagerType) {
|
|
self.manager = manager
|
|
}
|
|
|
|
func remoteConfigEventReceived(_ event: RemoteConfigLifecycleEvent) {
|
|
switch event {
|
|
case .observerRegistered, .committed:
|
|
Task { [weak self] in
|
|
await self?.warm()
|
|
}
|
|
case .refreshFinished:
|
|
break
|
|
}
|
|
}
|
|
|
|
func warm() async {
|
|
guard let snapshot = await self.manager.committedTopicCacheSnapshot(.audiences),
|
|
snapshot.key[Self.audiencesBlobItemKey]?.prefetch == true,
|
|
let blob = await self.manager.blobData(
|
|
for: .audiences,
|
|
itemKey: Self.audiencesBlobItemKey,
|
|
policy: .cachedOnly
|
|
) else { return }
|
|
|
|
guard let audiences = try? Self.decodeAudiences(from: blob),
|
|
self.manager.configGeneration == snapshot.generation else { return }
|
|
self.cachedConfiguration.store(
|
|
.init(audiences: audiences, configGeneration: snapshot.generation),
|
|
for: snapshot
|
|
)
|
|
}
|
|
|
|
func configuration() async throws -> AudienceConfigurationSnapshot? {
|
|
if let cached = self.cachedConfiguration.value(currentGeneration: self.manager.configGeneration) {
|
|
return cached
|
|
}
|
|
return try await self.manager.readConsistent {
|
|
try await self.loadConfiguration()
|
|
}
|
|
}
|
|
|
|
private func loadConfiguration() async throws -> AudienceConfigurationSnapshot? {
|
|
try Task.checkCancellation()
|
|
guard let topic = await self.manager.awaitTopicAndPrefetchBlobsReady(.audiences) else { return nil }
|
|
let topicSnapshot = GenerationGuardedCacheSnapshot(
|
|
generation: self.manager.configGeneration,
|
|
key: topic
|
|
)
|
|
guard await self.manager.isCurrent(topicSnapshot, for: .audiences) else { return nil }
|
|
if let cached = self.cachedConfiguration.value(for: topicSnapshot) { return cached }
|
|
|
|
do {
|
|
guard topicSnapshot.key[Self.audiencesBlobItemKey] != nil,
|
|
let blob = await self.manager.blobData(
|
|
for: .audiences,
|
|
itemKey: Self.audiencesBlobItemKey
|
|
) else {
|
|
return nil
|
|
}
|
|
let configuration = AudienceConfigurationSnapshot(
|
|
audiences: try Self.decodeAudiences(from: blob),
|
|
configGeneration: topicSnapshot.generation
|
|
)
|
|
guard await self.manager.isCurrent(topicSnapshot, for: .audiences) else { return nil }
|
|
self.cachedConfiguration.store(configuration, for: topicSnapshot)
|
|
return configuration
|
|
} catch {
|
|
guard await self.manager.isCurrent(topicSnapshot, for: .audiences) else { return nil }
|
|
Logger.error(Strings.remoteConfig.audienceConfigurationDecodeFailed(error))
|
|
throw error
|
|
}
|
|
}
|
|
|
|
func isCurrent(_ snapshot: AudienceConfigurationSnapshot) -> Bool {
|
|
return self.manager.configGeneration == snapshot.configGeneration
|
|
}
|
|
|
|
private static func decodeAudiences(from data: Data) throws -> [String: Audience] {
|
|
let entries = try JSONDecoder.default.decode([String: FailableAudience].self, from: data)
|
|
|
|
return entries.reduce(into: [:]) { audiences, entry in
|
|
let (mapKey, decoded) = entry
|
|
switch decoded.result {
|
|
case .success(let audience):
|
|
audiences[mapKey] = audience
|
|
case .failure(let error):
|
|
Logger.error(Strings.remoteConfig.audienceDecodeFailed(identifier: mapKey, error: error))
|
|
}
|
|
}
|
|
}
|
|
|
|
private static let audiencesBlobItemKey = "default"
|
|
|
|
}
|
|
|
|
extension AudiencesConfigProvider: @unchecked Sendable {}
|
|
|
|
private struct FailableAudience: Decodable {
|
|
|
|
let result: Result<Audience, Error>
|
|
|
|
init(from decoder: Decoder) throws {
|
|
do {
|
|
self.result = .success(try Audience(from: decoder))
|
|
} catch {
|
|
self.result = .failure(error)
|
|
}
|
|
}
|
|
|
|
}
|