Skip to content

Config service improvements #49

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/swift-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
schedule:
- cron: '0 0 * * *'
push:
branches: [ '*' ]
branches: [ master ]
paths-ignore:
- '**.md'
tags: [ '[0-9]+.[0-9]+.[0-9]+' ]
Expand Down
2 changes: 1 addition & 1 deletion ConfigCat.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |spec|

spec.name = "ConfigCat"
spec.version = "11.2.0"
spec.version = "11.3.0"
spec.summary = "ConfigCat Swift SDK"
spec.swift_version = "5.0"

Expand Down
2 changes: 1 addition & 1 deletion ConfigCat.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator watchos watchsimulator app
SWIFT_VERSION = 5.0

// ConfigCat SDK version
MARKETING_VERSION = 11.2.0
MARKETING_VERSION = 11.3.0
18 changes: 12 additions & 6 deletions ConfigCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@
F10AC6D02A93B02B006FA496 /* CacheTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AC6CF2A93B02B006FA496 /* CacheTest.swift */; };
F10AC6D22A950197006FA496 /* FlagEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AC6D12A950197006FA496 /* FlagEvaluator.swift */; };
F10AC6D32A9507C4006FA496 /* FlagEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AC6D12A950197006FA496 /* FlagEvaluator.swift */; };
F10AC6D52A9516F5006FA496 /* ConfigCatSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AC6D42A9516F5006FA496 /* ConfigCatSnapshot.swift */; };
F10AC6D62A9516F5006FA496 /* ConfigCatSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AC6D42A9516F5006FA496 /* ConfigCatSnapshot.swift */; };
F10AC6D52A9516F5006FA496 /* ConfigCatClientSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AC6D42A9516F5006FA496 /* ConfigCatClientSnapshot.swift */; };
F10AC6D62A9516F5006FA496 /* ConfigCatClientSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = F10AC6D42A9516F5006FA496 /* ConfigCatClientSnapshot.swift */; };
F1109D0C2DF99347000EF1AA /* SnapshotBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1109D0B2DF9933F000EF1AA /* SnapshotBuilder.swift */; };
F1109D0D2DF99347000EF1AA /* SnapshotBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1109D0B2DF9933F000EF1AA /* SnapshotBuilder.swift */; };
F11F76BC288AD6CA0097939F /* AsyncAwaitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11F76BB288AD6CA0097939F /* AsyncAwaitTests.swift */; };
F11F76BE288AE7540097939F /* SnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11F76BD288AE7540097939F /* SnapshotTests.swift */; };
F11F76C0288AE7650097939F /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11F76BF288AE7640097939F /* Extensions.swift */; };
Expand Down Expand Up @@ -135,8 +137,9 @@
C4FA1B3F278D953300BFA8C3 /* OverrideBehaviour.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverrideBehaviour.swift; sourceTree = "<group>"; };
F10AC6CF2A93B02B006FA496 /* CacheTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheTest.swift; sourceTree = "<group>"; };
F10AC6D12A950197006FA496 /* FlagEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagEvaluator.swift; sourceTree = "<group>"; };
F10AC6D42A9516F5006FA496 /* ConfigCatSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigCatSnapshot.swift; sourceTree = "<group>"; };
F10AC6D42A9516F5006FA496 /* ConfigCatClientSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigCatClientSnapshot.swift; sourceTree = "<group>"; };
F10F787D2528950D0021F468 /* DataGovernanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataGovernanceTests.swift; sourceTree = "<group>"; };
F1109D0B2DF9933F000EF1AA /* SnapshotBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotBuilder.swift; sourceTree = "<group>"; };
F11F76BB288AD6CA0097939F /* AsyncAwaitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncAwaitTests.swift; sourceTree = "<group>"; };
F11F76BD288AE7540097939F /* SnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotTests.swift; sourceTree = "<group>"; };
F11F76BF288AE7640097939F /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -234,6 +237,7 @@
3F880A42207BE91900087A6B /* Sources */ = {
isa = PBXGroup;
children = (
F1109D0B2DF9933F000EF1AA /* SnapshotBuilder.swift */,
F1B1D8B428FF2C830034165E /* ConfigCatOptions.swift */,
F1BC414428E1D54800F2230A /* EvaluationDetails.swift */,
F11F76BF288AE7640097939F /* Extensions.swift */,
Expand All @@ -258,7 +262,7 @@
F1E1180A257532D700DA245A /* Log.swift */,
3F880A41207BE91300087A6B /* Resources */,
F10AC6D12A950197006FA496 /* FlagEvaluator.swift */,
F10AC6D42A9516F5006FA496 /* ConfigCatSnapshot.swift */,
F10AC6D42A9516F5006FA496 /* ConfigCatClientSnapshot.swift */,
F1CBE12E2B8CC81700CD2FF9 /* EvaluationLogger.swift */,
);
name = Sources;
Expand Down Expand Up @@ -447,10 +451,11 @@
F17DEE28288876F7009C3E48 /* MutableQueue.swift in Sources */,
F1BC414728E1D54800F2230A /* EvaluationDetails.swift in Sources */,
C4FA1B3D278D919A00BFA8C3 /* OverrideDataSource.swift in Sources */,
F10AC6D52A9516F5006FA496 /* ConfigCatSnapshot.swift in Sources */,
F10AC6D52A9516F5006FA496 /* ConfigCatClientSnapshot.swift in Sources */,
B4BD2AB6258CA6FF007371E2 /* Log.swift in Sources */,
B4BD2AB8258CA6FF007371E2 /* KeyValue.swift in Sources */,
F10AC6D32A9507C4006FA496 /* FlagEvaluator.swift in Sources */,
F1109D0D2DF99347000EF1AA /* SnapshotBuilder.swift in Sources */,
B4BD2AB9258CA6FF007371E2 /* Config.swift in Sources */,
B4BD2ABA258CA6FF007371E2 /* RolloutEvaluator.swift in Sources */,
B4BD2ABE258CA6FF007371E2 /* Synced.swift in Sources */,
Expand All @@ -473,7 +478,7 @@
F1BC414828E1D54800F2230A /* EvaluationDetails.swift in Sources */,
C40CF51527B557EE00D9F88A /* LocalDictionaryDataSource.swift in Sources */,
F1B1D8B628FF2C830034165E /* ConfigCatOptions.swift in Sources */,
F10AC6D62A9516F5006FA496 /* ConfigCatSnapshot.swift in Sources */,
F10AC6D62A9516F5006FA496 /* ConfigCatClientSnapshot.swift in Sources */,
F10AC6D22A950197006FA496 /* FlagEvaluator.swift in Sources */,
B4BD2AE6258CA7DF007371E2 /* ConfigFetcher.swift in Sources */,
B4BD2AE7258CA7DF007371E2 /* RolloutIntegrationV1Tests.swift in Sources */,
Expand Down Expand Up @@ -509,6 +514,7 @@
F183A2082B9096A30015967E /* EvaluationLogTests.swift in Sources */,
B4BD2B00258CA7DF007371E2 /* Mock.swift in Sources */,
F11F76BE288AE7540097939F /* SnapshotTests.swift in Sources */,
F1109D0C2DF99347000EF1AA /* SnapshotBuilder.swift in Sources */,
C40CF51227B5533800D9F88A /* LocalTests.swift in Sources */,
B4BD2B02258CA7DF007371E2 /* ManualPollingTests.swift in Sources */,
F17DEE29288876F7009C3E48 /* MutableQueue.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ The following device platform versions are supported:

``` swift
dependencies: [
.package(url: "https://github.com/configcat/swift-sdk", from: "11.2.0")
.package(url: "https://github.com/configcat/swift-sdk", from: "11.3.0")
]
```

Expand Down
101 changes: 51 additions & 50 deletions Sources/ConfigCat/ConfigCatClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
private let configService: ConfigService?
private let sdkKey: String
private let overrideDataSource: OverrideDataSource?
private var snapshotBuilder: SnapshotBuilderProtocol
private var closed: Bool = false
private var defaultUser: ConfigCatUser?

private static let mutex = Mutex()
private static var instances: [String: Weak<ConfigCatClient>] = [:]
Expand All @@ -37,19 +37,19 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {

self.sdkKey = sdkKey
self.hooks = hooks ?? Hooks()
self.defaultUser = defaultUser
log = InternalLogger(log: logger, level: logLevel, hooks: self.hooks)
overrideDataSource = flagOverrides
flagEvaluator = FlagEvaluator(log: log, evaluator: RolloutEvaluator(logger: log), hooks: self.hooks)

self.snapshotBuilder = SnapshotBuilder(flagEvaluator: flagEvaluator, defaultUser: defaultUser, overrideDataSource: overrideDataSource, log: log)

if let overrideDataSource = overrideDataSource, overrideDataSource.behaviour == .localOnly {
// configService is not needed in localOnly mode
configService = nil
hooks?.invokeOnReady(state: .hasLocalOverrideFlagDataOnly)
hooks?.invokeOnReady(snapshotBuilder: snapshotBuilder, inMemoryResult: InMemoryResult(entry: .empty, cacheState: .hasLocalOverrideFlagDataOnly))
} else if !Utils.validateSdkKey(sdkKey: sdkKey, isCustomUrl: !baseUrl.isEmpty) {
log.error(eventId: 0, message: "ConfigCat SDK Key '\(sdkKey)' is invalid.")
configService = nil
hooks?.invokeOnReady(state: .noFlagData)
hooks?.invokeOnReady(snapshotBuilder: snapshotBuilder, inMemoryResult: InMemoryResult(entry: .empty, cacheState: .noFlagData))
} else {
let fetcher = ConfigFetcher(httpEngine: httpEngine ?? URLSessionEngine(session: URLSession(configuration: URLSessionConfiguration.default)),
logger: log,
Expand All @@ -58,7 +58,8 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
dataGovernance: dataGovernance,
baseUrl: baseUrl)

configService = ConfigService(log: log,
configService = ConfigService(snapshotBuilder: snapshotBuilder,
log: log,
fetcher: fetcher,
cache: configCache,
pollingMode: pollingMode,
Expand All @@ -69,7 +70,7 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
}

/**
Creates a new or gets an already existing `ConfigCatClient` for the given sdkKey.
Creates a new or gets an already existing `ConfigCatClient` for the given `sdkKey`.

- Parameters:
- sdkKey: The SDK Key for to communicate with the ConfigCat services.
Expand Down Expand Up @@ -112,7 +113,7 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
}

/**
Creates a new or gets an already existing ConfigCatClient for the given sdkKey.
Creates a new or gets an already existing ConfigCatClient for the given `sdkKey`.

- Parameters:
- sdkKey: The SDK Key for to communicate with the ConfigCat services.
Expand Down Expand Up @@ -174,7 +175,7 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
*/
public func getValue<Value>(for key: String, defaultValue: Value, user: ConfigCatUser? = nil, completion: @escaping (Value) -> ()) {
assert(!key.isEmpty, "key cannot be empty")
let evalUser = user ?? defaultUser
let evalUser = user ?? snapshotBuilder.defaultUser

if let _ = flagEvaluator.validateFlagType(of: Value.self, key: key, defaultValue: defaultValue, user: evalUser) {
completion(defaultValue)
Expand All @@ -197,10 +198,10 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
*/
public func getValueDetails<Value>(for key: String, defaultValue: Value, user: ConfigCatUser? = nil, completion: @escaping (TypedEvaluationDetails<Value>) -> ()) {
assert(!key.isEmpty, "key cannot be empty")
let evalUser = user ?? defaultUser
let evalUser = user ?? snapshotBuilder.defaultUser

if let error = flagEvaluator.validateFlagType(of: Value.self, key: key, defaultValue: defaultValue, user: evalUser) {
completion(TypedEvaluationDetails<Value>.fromError(key: key, value: defaultValue, error: error, user: evalUser))
completion(TypedEvaluationDetails<Value>.fromError(value: defaultValue, details: error))
return
}

Expand Down Expand Up @@ -228,7 +229,7 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
guard let setting = result.settings[key] else {
continue
}
if let details = self.flagEvaluator.evaluateFlag(for: setting, key: key, user: user ?? self.defaultUser, fetchTime: result.fetchTime, settings: result.settings) {
if let details = self.flagEvaluator.evaluateFlag(for: setting, key: key, user: user ?? self.snapshotBuilder.defaultUser, fetchTime: result.fetchTime, settings: result.settings) {
detailsResult.append(details)
}
}
Expand Down Expand Up @@ -320,7 +321,7 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
guard let setting = result.settings[key] else {
continue
}
if let details = self.flagEvaluator.evaluateFlag(for: setting, key: key, user: user ?? self.defaultUser, fetchTime: result.fetchTime, settings: result.settings) {
if let details = self.flagEvaluator.evaluateFlag(for: setting, key: key, user: user ?? self.snapshotBuilder.defaultUser, fetchTime: result.fetchTime, settings: result.settings) {
allValues[key] = details.value
}
}
Expand All @@ -329,28 +330,54 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
}

/**
Initiates a force refresh asynchronously on the cached configuration.
Updates the internally cached config by synchronizing with the external cache (if any),
then by fetching the latest version from the ConfigCat CDN (provided that the client is online).

- Parameter completion: The function which will be called when refresh completed successfully.
*/
@objc public func forceRefresh(completion: @escaping (RefreshResult) -> ()) {
if let configService = configService {
configService.refresh(completion: completion)
} else {
let message = "Client is configured to use local-only mode, thus `.refresh()` has no effect."
let message = "Client is configured to use the localOnly override behavior, which prevents synchronization with external cache and making HTTP requests."
log.warning(eventId: 3202, message: message)
completion(RefreshResult(success: false, error: message))
completion(RefreshResult(success: false, errorCode: .localOnlyClient, error: message))
}
}

@objc public func snapshot() -> ConfigCatSnapshot {
return ConfigCatSnapshot(flagEvaluator: flagEvaluator, settingsSnapshot: getInMemorySettings(), defaultUser: defaultUser, log: log)
/**
Captures the current state of the client.
The resulting snapshot can be used to synchronously evaluate feature flags and settings based on the captured state.

The operation captures the internally cached config data.
It does not attempt to update it by synchronizing with the external cache or by fetching the latest version from the ConfigCat CDN.

Therefore, it is recommended to use snapshots in conjunction with the Auto Polling mode,
where the SDK automatically updates the internal cache in the background.

For other polling modes, you will need to manually initiate a cache
update by invoking `.forceRefresh()`.
*/
@objc public func snapshot() -> ConfigCatClientSnapshot {
return snapshotBuilder.buildSnapshot(inMemoryResult: configService?.inMemory)
}

#if compiler(>=5.5) && canImport(_Concurrency)
/**
Waits for the client to reach the ready state, i.e. to complete initialization.

Ready state is reached as soon as the initial sync with the external cache (if any) completes.
If this does not produce up-to-date config data, and the client is online (i.e. HTTP requests are allowed),
the first config fetch operation is also awaited in Auto Polling mode before ready state is reported.

That is, reaching the ready state usually means the client is ready to evaluate feature flags and settings.
However, please note that this is not guaranteed. In case of initialization failure or timeout,
the internal cache may be empty or expired even after the ready state is reported. You can verify this by
checking the return value.
*/
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
@discardableResult
public func waitForReady() async -> ClientReadyState {
public func waitForReady() async -> ClientCacheState {
// withCheckedContinuation sometimes crashes on iOS 18.0. See https://github.com/RevenueCat/purchases-ios/pull/4286
await withUnsafeContinuation { continuation in
guard let configService = self.configService else {
Expand Down Expand Up @@ -396,53 +423,27 @@ public final class ConfigCatClient: NSObject, ConfigCatClientProtocol {
}
}

func getInMemorySettings() -> SettingsResult {
if let overrideDataSource = overrideDataSource, overrideDataSource.behaviour == .localOnly {
return SettingsResult(settings: overrideDataSource.getOverrides(), fetchTime: .distantPast)
}
guard let configService = configService else {
return .empty
}

let inMemory = configService.inMemory

if let overrideDataSource = overrideDataSource {
if overrideDataSource.behaviour == .localOverRemote {
return SettingsResult(settings: inMemory.config.settings.merging(overrideDataSource.getOverrides()) { (_, new) in
new
}, fetchTime: inMemory.fetchTime)
}
if overrideDataSource.behaviour == .remoteOverLocal {
return SettingsResult(settings: inMemory.config.settings.merging(overrideDataSource.getOverrides()) { (current, _) in
current
}, fetchTime: inMemory.fetchTime)
}
}

return SettingsResult(settings: inMemory.config.settings, fetchTime: inMemory.fetchTime)
}

/// Sets the default user.
@objc public func setDefaultUser(user: ConfigCatUser) {
defaultUser = user
snapshotBuilder.defaultUser = user
}

/// Sets the default user to null.
@objc public func clearDefaultUser() {
defaultUser = nil
snapshotBuilder.defaultUser = nil
}

/// Configures the SDK to allow HTTP requests.
/// Configures the client to allow HTTP requests.
@objc public func setOnline() {
configService?.setOnline()
}

/// Configures the SDK to not initiate HTTP requests.
/// Configures the client to not initiate HTTP requests but work using the cache only.
@objc public func setOffline() {
configService?.setOffline()
}

/// True when the SDK is configured not to initiate HTTP requests, otherwise false.
/// Returns `true` when the client is configured not to initiate HTTP requests, otherwise `false`.
@objc public var isOffline: Bool {
get {
configService?.isOffline ?? true
Expand Down
Loading