Skip to content
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

[rc-swift] RemoteConfig.swift #14309

Merged
merged 11 commits into from
Jan 16, 2025
Merged

[rc-swift] RemoteConfig.swift #14309

merged 11 commits into from
Jan 16, 2025

Conversation

paulb777
Copy link
Member

@paulb777 paulb777 commented Jan 7, 2025

FIRRemoteConfig.m -> RemoteConfig.swift

This is a first pass sufficient to pass the fake console integration tests. Open questions include:

  • Keep the Objective-C error domain symbols or not? See the APIBuildTest changes
  • RCNRemoteConfigTest.m is heavily commented out because of mocks and needs to be rethought
  • Queues and synchronization need a deep dive in a Swift 6 Structured Concurrency context
  • Assess client app CI after master rebase

@google-oss-bot
Copy link

1 Warning
⚠️ Did you forget to add a changelog entry? (Add #no-changelog to the PR description to silence this warning.)

Generated by 🚫 Danger

Copy link
Contributor

github-actions bot commented Jan 7, 2025

Apple API Diff Report

Commit: 54267c8
Last updated: Thu Jan 16 14:29 PST 2025
View workflow logs & download artifacts


FirebaseRemoteConfig

Type Definitions

[REMOVED] FIRRemoteConfigFetchAndActivateCompletion
[REMOVED] FIRRemoteConfigFetchAndActivateCompletion
Objective-C:
-  typedef void ( ^ FIRRemoteConfigFetchAndActivateCompletion )( FIRRemoteConfigFetchAndActivateStatus , NSError * _Nullable )
[REMOVED] FIRRemoteConfigFetchCompletion
[REMOVED] FIRRemoteConfigFetchCompletion
Objective-C:
-  typedef void ( ^ FIRRemoteConfigFetchCompletion )( FIRRemoteConfigFetchStatus , NSError * _Nullable )
[REMOVED] FIRRemoteConfigUpdateCompletion
[REMOVED] FIRRemoteConfigUpdateCompletion
[REMOVED] Enumerations
Swift:
-    typealias RemoteConfigError . Code . _ErrorType = RemoteConfigError
-      case unknown = 8001
-      case throttled = 8002
-      case internalError = 8003
-    enum RemoteConfigFetchAndActivateStatus : Int , @unchecked Sendable
-      case successFetchedFromRemote = 0
-      case successUsingPreFetchedData = 1
-      case error = 2
-    enum RemoteConfigFetchStatus : Int , @unchecked Sendable
-      case noFetchYet = 0
-      case success = 1
-      case failure = 2
-      case throttled = 3
-    enum RemoteConfigSource : Int , @unchecked Sendable
-      case remote = 0
-      case ` default ` = 1
-      case ` static ` = 2
-    typealias RemoteConfigUpdateError . Code . _ErrorType = RemoteConfigUpdateError
-      case streamError = 8001
-      case notFetched = 8002
-      case messageInvalid = 8003
-      case unavailable = 8004
Objective-C:
-    enum FIRRemoteConfigError : NSInteger {}
-      FIRRemoteConfigErrorUnknown = 8001
-      FIRRemoteConfigErrorThrottled = 8002
-      FIRRemoteConfigErrorInternalError = 8003
-    enum FIRRemoteConfigFetchAndActivateStatus : NSInteger {}
-      FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote
-      FIRRemoteConfigFetchAndActivateStatusSuccessUsingPreFetchedData
-      FIRRemoteConfigFetchAndActivateStatusError
-    enum FIRRemoteConfigFetchStatus : NSInteger {}
-      FIRRemoteConfigFetchStatusNoFetchYet
-      FIRRemoteConfigFetchStatusSuccess
-      FIRRemoteConfigFetchStatusFailure
-      FIRRemoteConfigFetchStatusThrottled
-    enum FIRRemoteConfigSource : NSInteger {}
-      FIRRemoteConfigSourceRemote
-      FIRRemoteConfigSourceDefault
-      FIRRemoteConfigSourceStatic
-    enum FIRRemoteConfigUpdateError : NSInteger {}
-      FIRRemoteConfigUpdateErrorStreamError = 8001
-      FIRRemoteConfigUpdateErrorNotFetched = 8002
-      FIRRemoteConfigUpdateErrorMessageInvalid = 8003
-      FIRRemoteConfigUpdateErrorUnavailable = 8004

Constants

[REMOVED] FIRRemoteConfigErrorDomain
[REMOVED] FIRRemoteConfigErrorDomain
Swift:
-  let RemoteConfigErrorDomain : String
Objective-C:
-  extern NS_SWIFT_NAME ( RemoteConfigErrorDomain ) NSString * const FIRRemoteConfigErrorDomain
[REMOVED] FIRRemoteConfigUpdateErrorDomain
[REMOVED] FIRRemoteConfigUpdateErrorDomain
Swift:
-  let RemoteConfigUpdateErrorDomain : String
Objective-C:
-  extern NS_SWIFT_NAME ( RemoteConfigUpdateErrorDomain ) NSString * const FIRRemoteConfigUpdateErrorDomain
[REMOVED] Classes
Swift:
-      var lastFetchTime : Date ? { get }
-      var lastFetchStatus : RemoteConfigFetchStatus { get }
-      var configSettings : RemoteConfigSettings { get set }
-      class func remoteConfig () -> RemoteConfig
-      class func remoteConfig ( app : FIRApp ) -> RemoteConfig
-      func ensureInitialized () async throws
-      func fetch () async throws -> RemoteConfigFetchStatus
-      func fetch ( withExpirationDuration expirationDuration : TimeInterval ) async throws -> RemoteConfigFetchStatus
-      func fetchAndActivate () async throws -> RemoteConfigFetchAndActivateStatus
-      func activate () async throws -> Bool
-      subscript ( key : String ) -> FIRRemoteConfigValue { get }
-      func configValue ( forKey key : String ?) -> FIRRemoteConfigValue
-      func configValue ( forKey key : String ?, source : RemoteConfigSource ) -> FIRRemoteConfigValue
-      func allKeys ( from source : RemoteConfigSource ) -> [ String ]
-      func keys ( withPrefix prefix : String ?) -> Set < String >
-      func setDefaults ( _ defaults : [ String : NSObject ]?)
-      func setDefaults ( fromPlist fileName : String ?)
-      func defaultValue ( forKey key : String ?) -> FIRRemoteConfigValue ?
-      func addOnConfigUpdateListener ( remoteConfigUpdateCompletion listener : @escaping ( FIRRemoteConfigUpdate ?, ( any Error )?) -> Void ) -> FIRConfigUpdateListenerRegistration
-      var configRealtime : RCNConfigRealtime { get set }
-      var settings : RCNConfigSettings { get }
-      init ( appName : String , firOptions options : FIROptions , namespace FIRNamespace : String , dbManager DBManager : RCNConfigDBManager , configContent : RCNConfigContent , analytics : ( any FIRAnalyticsInterop )?)
-      init ( appName : String , firOptions options : FIROptions , namespace FIRNamespace : String , dbManager DBManager : RCNConfigDBManager , configContent : RCNConfigContent , userDefaults : UserDefaults ?, analytics : ( any FIRAnalyticsInterop )?, configFetch : RCNConfigFetch ?, configRealtime : RCNConfigRealtime ?)
-      func addInteropSubscriber ( _ subscriber : any FIRRolloutsStateSubscriber )
-    class RemoteConfigSettings : NSObject
-      var minimumFetchInterval : TimeInterval { get set }
-      var fetchTimeout : TimeInterval { get set }
Objective-C:
-      @property ( nonatomic , strong , readonly , nullable ) NSDate * lastFetchTime ;
-      @property ( nonatomic , readonly ) FIRRemoteConfigFetchStatus lastFetchStatus ;
-      @property ( nonatomic , strong , nonnull ) FIRRemoteConfigSettings * configSettings ;
-      + ( nonnull FIRRemoteConfig * ) remoteConfig ;
-      + ( nonnull FIRRemoteConfig * ) remoteConfigWithApp :( nonnull FIRApp * ) app ;
-      - ( nonnull instancetype ) init ;
-      - ( void ) ensureInitializedWithCompletionHandler : ( void ( ^ _Nonnull )( NSError * _Nullable )) completionHandler ;
-      - ( void ) fetchWithCompletionHandler : ( void ( ^ _Nullable )( FIRRemoteConfigFetchStatus , NSError * _Nullable )) completionHandler ;
-      - ( void ) fetchWithExpirationDuration :( NSTimeInterval ) expirationDuration completionHandler : ( void ( ^ _Nullable )( FIRRemoteConfigFetchStatus , NSError * _Nullable )) completionHandler ;
-      - ( void ) fetchAndActivateWithCompletionHandler : ( void ( ^ _Nullable )( FIRRemoteConfigFetchAndActivateStatus , NSError * _Nullable )) completionHandler ;
-      - ( void ) activateWithCompletion : ( void ( ^ _Nullable )( BOOL , NSError * _Nullable )) completion ;
-      - ( nonnull FIRRemoteConfigValue * ) objectForKeyedSubscript : ( nonnull NSString * ) key ;
-      - ( nonnull FIRRemoteConfigValue * ) configValueForKey :( nullable NSString * ) key ;
-      - ( nonnull FIRRemoteConfigValue * ) configValueForKey :( nullable NSString * ) key source : ( FIRRemoteConfigSource ) source ;
-      - ( nonnull NSArray < NSString *> * ) allKeysFromSource : ( FIRRemoteConfigSource ) source ;
-      - ( nonnull NSSet < NSString *> * ) keysWithPrefix :( nullable NSString * ) prefix ;
-      - ( void ) setDefaults :( nullable NSDictionary < NSString * , NSObject *> * ) defaults ;
-      - ( void ) setDefaultsFromPlistFileName :( nullable NSString * ) fileName ;
-      - ( nullable FIRRemoteConfigValue * ) defaultValueForKey :( nullable NSString * ) key ;
-      - ( FIRConfigUpdateListenerRegistration * _Nonnull ) addOnConfigUpdateListener : ( FIRRemoteConfigUpdateCompletion _Nonnull ) listener ;
-      @property ( nonatomic , readwrite , strong , nonnull ) RCNConfigRealtime * configRealtime
-      @property ( nonatomic , readonly , strong ) RCNConfigSettings * settings
-      - ( nonnull instancetype ) initWithAppName :( nonnull NSString * ) appName FIROptions :( nonnull FIROptions * ) options namespace :( nonnull NSString * ) FIRNamespace DBManager :( nonnull RCNConfigDBManager * ) DBManager configContent :( nonnull RCNConfigContent * ) configContent analytics :( nullable id < FIRAnalyticsInterop > ) analytics ;
-      - ( instancetype ) initWithAppName :( NSString * ) appName FIROptions :( FIROptions * ) options namespace :( NSString * ) FIRNamespace DBManager :( RCNConfigDBManager * ) DBManager configContent :( RCNConfigContent * ) configContent userDefaults :( nullable NSUserDefaults * ) userDefaults analytics :( nullable id < FIRAnalyticsInterop > ) analytics configFetch :( nullable RCNConfigFetch * ) configFetch configRealtime :( nullable RCNConfigRealtime * ) configRealtime ;
-      - ( void ) addRemoteConfigInteropSubscriber : ( id < FIRRolloutsStateSubscriber > _Nonnull ) subscriber ;
-    @interface FIRRemoteConfigSettings : NSObject
-      @property ( nonatomic ) NSTimeInterval minimumFetchInterval ;
-      @property ( nonatomic ) NSTimeInterval fetchTimeout ;

FirebaseRemoteConfig/SwiftNew/ConfigConstants.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/ConfigConstants.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m Outdated Show resolved Hide resolved
configRealtime: nil
)
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

init() was made unavailable in ObjC. Can clients use the inherited init()? If so, we may need to add:

/// Unavailable. Use `remoteConfig()`  instead.
@available(*, unavailable)
@objc public init() {}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, deferring for now.

Screenshot 2025-01-16 at 1 44 46 PM

FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
RCLog.debug("I-RCN987366",
"Successfully read configSettings. Minimum Fetch Interval: " +
"\(minimumFetchInterval), Fetch timeout: \(fetchTimeout)")
return settings
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ObjC implementation would recreate a network session before returning the settings.

  /// The NSURLSession needs to be recreated whenever the fetch timeout may be updated.
  [_configFetch recreateNetworkSession];
  return settings;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why the get needs to do this, but will add it back for now to be consistent.

FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
Comment on lines 391 to 416
NSError(
domain: ConfigConstants.RemoteConfigErrorDomain,
code: RemoteConfigError.internalError.rawValue,
userInfo: [NSLocalizedDescriptionKey: "Timed out waiting for database load."]
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be an RC-typed error ?

Comment on lines 402 to 435
@objc public func addListener(_ listener: @escaping RemoteConfigListener) {
queue.async {
self.listeners.append(listener)
}
}

private func callListeners(key: String, config: [String: RemoteConfigValue]) {
queue.async { [weak self] in
guard let self = self else { return }
for listener in self.listeners {
listener(key, config)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No changes required, but the ObjC impl. synchronized with a lock on the listeners property rather than the serial queue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I can't think of advantages of a separate dispatch_async for each listener

FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
Copy link
Member Author

@paulb777 paulb777 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough review so far

Comment on lines 25 to 31
public let FIRNamespaceGoogleMobilePlatform = "firebase"

public let FIRRemoteConfigThrottledEndTimeInSecondsKey = "error_throttled_end_time_seconds"

public let FIRRemoteConfigActivateNotification =
Notification.Name("FIRRemoteConfigActivateNotification")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes for some. Restored objc version of FIRRemoteConfigThrottledEndTimeInSecondsKey and FIRNamespaceGoogleMobilePlatform

FirebaseRemoteConfig/SwiftNew/ConfigConstants.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/ConfigConstants.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
@paulb777
Copy link
Member Author

@ncooke3 I've only processed your first batch so far - will start on second batch now.

Copy link
Member Author

@paulb777 paulb777 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should now have responded to all review comments

FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
RCLog.debug("I-RCN987366",
"Successfully read configSettings. Minimum Fetch Interval: " +
"\(minimumFetchInterval), Fetch timeout: \(fetchTimeout)")
return settings
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why the get needs to do this, but will add it back for now to be consistent.

FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift Outdated Show resolved Hide resolved
Comment on lines 402 to 435
@objc public func addListener(_ listener: @escaping RemoteConfigListener) {
queue.async {
self.listeners.append(listener)
}
}

private func callListeners(key: String, config: [String: RemoteConfigValue]) {
queue.async { [weak self] in
guard let self = self else { return }
for listener in self.listeners {
listener(key, config)
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I can't think of advantages of a separate dispatch_async for each listener

@paulb777
Copy link
Member Author

I'm going to merge here and rebase with main. Let's address any additional comments and open items in subsequent PRs.

@paulb777 paulb777 merged commit 1d5e80c into rc-swift Jan 16, 2025
36 of 46 checks passed
@paulb777 paulb777 deleted the pb-rc branch January 16, 2025 23:13
paulb777 added a commit that referenced this pull request Jan 17, 2025
Copy link
Member

@ncooke3 ncooke3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for resolving everything! I'm leaving one final batch, and I can open a PR to address these points. Nothing critical or problematic found.

((RemoteConfigFetchAndActivateStatus, Error?) -> Void)? =
nil) {
fetch { [weak self] fetchStatus, error in
guard let self = self else { return }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
guard let self = self else { return }
guard let self else { return }

Comment on lines +515 to +542
_ = try await fetch()
do {
try await activate()
return .successFetchedFromRemote
} catch {
return .successUsingPreFetchedData
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This impl. looks good to me, but it may be worth it to wrap a continuation around a call to the completion handler API below. So both API's share the same impl.

/// - Parameter completion Activate operation callback with changed and error parameters.
@objc public func activate(completion: ((Bool, Error?) -> Void)? = nil) {
queue.async { [weak self] in
guard let self = self else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
guard let self = self else {
guard let self else {

Comment on lines +613 to +618
RCLog.error("I-RCN000068", "Internal error activating config.")
if let completion {
DispatchQueue.main.async {
completion(false, error)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably doesn't matter, but ObjC logged after the async dispatch

        if let completion {
          DispatchQueue.main.async {
            completion(false, error)
          }
        }
        RCLog.error("I-RCN000068", "Internal error activating config.")

)
}

// MARK: helpers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  // MARK: - Helpers

Comment on lines +775 to +787
var keys = Set<String>()
queue.sync {
let fullyQualifiedNamespace = self.fullyQualifiedNamespace(FIRNamespace)

if let config = configContent.activeConfig()[fullyQualifiedNamespace] {
if let prefix = prefix, !prefix.isEmpty {
keys = Set(config.keys.filter { $0.hasPrefix(prefix) })
} else {
keys = Set(config.keys)
}
}
}
return keys
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably could return the value from the sync block, rather than passing it through the context.

Comment on lines +793 to +801
var count = 0
queue.sync {
let fullyQualifiedNamespace = self.fullyQualifiedNamespace(FIRNamespace)

if let config = configContent.activeConfig()[fullyQualifiedNamespace] as? NSDictionary {
count = config.countByEnumerating(with: state, objects: buffer, count: len)
}
}
return count
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably could return the value from the sync block, rather than passing it through the context.

Comment on lines +806 to +807
/// Sets config defaults for parameter keys and values in the default namespace config.
/// - Parameter defaults A dictionary mapping a NSString * key to a NSObject * value.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing

Comment on lines +850 to +862
let fullyQualifiedNamespace = self.fullyQualifiedNamespace(FIRNamespace)
var value: RemoteConfigValue?
queue.sync {
if let config = configContent.defaultConfig()[fullyQualifiedNamespace] {
value = config[key]
if let value, value.source != .default {
RCLog.error("I-RCN000002",
"Key \(key) should come from source: \(RemoteConfigSource.default.rawValue)" +
"instead coming from source: \(value.source.rawValue)")
}
}
}
return value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably could return the value from the sync block, rather than passing it through the context.

/// `RemoteConfig.remoteConfig().setDefaults(fromPlist: "defaultSamples")`
@objc(setDefaultsFromPlistFileName:)
public func setDefaults(fromPlist fileName: String?) {
guard let fileName = fileName, !fileName.isEmpty else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guard let fileName, !fileName.isEmpty else {

@paulb777
Copy link
Member Author

Agree with all comments.

ncooke3 added a commit that referenced this pull request Jan 22, 2025
ncooke3 added a commit that referenced this pull request Jan 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants