ConfigSettings.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. // Copyright 2024 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import Foundation
  15. @_implementationOnly import GoogleUtilities
  16. // TODO(ncooke3): Once Obj-C tests are ported, all `public` access modifers can be removed.
  17. private let kRCNGroupPrefix = "frc.group."
  18. private let kRCNUserDefaultsKeyNamelastETag = "lastETag"
  19. private let kRCNUserDefaultsKeyNameLastSuccessfulFetchTime = "lastSuccessfulFetchTime"
  20. private let kRCNAnalyticsFirstOpenTimePropertyName = "_fot"
  21. private let kRCNExponentialBackoffMinimumInterval = 60 * 2 // 2 mins.
  22. private let kRCNExponentialBackoffMaximumInterval = 60 * 60 * 4 // 4 hours.
  23. let RCNHTTPDefaultConnectionTimeout: TimeInterval = 60
  24. /// This internal class contains a set of variables that are unique among all the config instances.
  25. /// It also handles all metadata. This class is not thread safe and does not
  26. /// inherently allow for synchronized access. Callers are responsible for synchronization
  27. /// (currently using serial dispatch queues).
  28. @objc(RCNConfigSettings) public class ConfigSettings: NSObject {
  29. // MARK: - Private Properties
  30. /// A list of successful fetch timestamps in seconds.
  31. private var _successFetchTimes: [TimeInterval] = []
  32. /// A list of failed fetch timestamps in seconds.
  33. private var _failureFetchTimes: [TimeInterval] = []
  34. /// Device conditions since last successful fetch from the backend. Device conditions including
  35. /// app version, iOS version, device locale, language, GMP project ID and Game project ID.
  36. /// Used for determining whether to throttle.
  37. @objc public private(set) var deviceContext: [String: String] = [:]
  38. /// Custom variables (aka App context digest). This is the pending custom variables
  39. /// request before fetching.
  40. private var _customVariables: [String: Sendable] = [:]
  41. /// Last fetch status.
  42. @objc public var lastFetchStatus: RemoteConfigFetchStatus = .noFetchYet
  43. /// Last fetch Error.
  44. private var _lastFetchError: RemoteConfigError
  45. /// The time of last apply timestamp.
  46. private var _lastApplyTimeInterval: TimeInterval = 0
  47. /// The time of last setDefaults timestamp.
  48. private var _lastSetDefaultsTimeInterval: TimeInterval = 0
  49. /// The database manager.
  50. private var _DBManager: ConfigDBManager
  51. /// The namespace for this instance.
  52. private let _FIRNamespace: String
  53. /// The Google App ID of the configured FIRApp.
  54. private let _googleAppID: String
  55. /// The user defaults manager scoped to this RC instance of FIRApp and namespace.
  56. private var _userDefaultsManager: UserDefaultsManager
  57. // MARK: - Data required by config request.
  58. // TODO(ncooke3): This property was atomic in ObjC.
  59. /// InstallationsID.
  60. /// - Note: The property is atomic because it is accessed across multiple threads.
  61. @objc public var configInstallationsIdentifier = ""
  62. // TODO(ncooke3): This property was atomic in ObjC.
  63. /// Installations token.
  64. /// - Note: The property is atomic because it is accessed across multiple threads.
  65. @objc public var configInstallationsToken: String?
  66. /// Bundle Identifier
  67. public let bundleIdentifier: String
  68. /// Last fetched template version.
  69. @objc public var lastFetchedTemplateVersion: String
  70. /// Last active template version.
  71. @objc public var lastActiveTemplateVersion: String
  72. // MARK: - Throttling Properties
  73. // TODO(ncooke3): This property was atomic in ObjC.
  74. /// Throttling intervals are based on https://cloud.google.com/storage/docs/exponential-backoff
  75. /// Returns true if client has fetched config and has not got back from server. This is used to
  76. /// determine whether there is another config task infight when fetching.
  77. @objc public var isFetchInProgress: Bool
  78. /// Returns the current retry interval in seconds set for exponential backoff.
  79. @objc public var exponentialBackoffRetryInterval: Double
  80. /// Returns the time in seconds until the next request is allowed while in exponential backoff
  81. /// mode.
  82. @objc public var exponentialBackoffThrottleEndTime: TimeInterval
  83. /// Returns the current retry interval in seconds set for exponential backoff for the Realtime
  84. /// service.
  85. @objc public var realtimeExponentialBackoffRetryInterval: Double
  86. /// Returns the time in seconds until the next request is allowed while in
  87. /// exponential backoff mode for the Realtime service.
  88. public var realtimeExponentialBackoffThrottleEndTime: TimeInterval
  89. /// Realtime connection attempts.
  90. @objc public var realtimeRetryCount: Int
  91. // MARK: - Initializers
  92. /// Designated initializer.
  93. @objc public init(databaseManager: ConfigDBManager,
  94. namespace: String,
  95. firebaseAppName: String,
  96. googleAppID: String,
  97. userDefaults: UserDefaults?) {
  98. _FIRNamespace = namespace
  99. _googleAppID = googleAppID
  100. bundleIdentifier = Bundle.main.bundleIdentifier ?? ""
  101. if bundleIdentifier.isEmpty {
  102. RCLog.notice(
  103. "I-RCN000038",
  104. "Main bundle identifier is missing. Remote Config might not work properly."
  105. )
  106. }
  107. _minimumFetchInterval = ConfigConstants.defaultMinimumFetchInterval
  108. deviceContext = [:]
  109. _customVariables = [:]
  110. _successFetchTimes = []
  111. _failureFetchTimes = []
  112. _DBManager = databaseManager
  113. _userDefaultsManager = UserDefaultsManager(
  114. appName: firebaseAppName,
  115. bundleID: bundleIdentifier,
  116. namespace: _FIRNamespace,
  117. userDefaults: userDefaults
  118. )
  119. // Check if the config database is new. If so, clear the configs saved in userDefaults.
  120. if _DBManager.isNewDatabase {
  121. RCLog.notice("I-RCN000072", "New config database created. Resetting user defaults.")
  122. _userDefaultsManager.resetUserDefaults()
  123. }
  124. isFetchInProgress = false
  125. lastFetchedTemplateVersion = _userDefaultsManager.lastFetchedTemplateVersion
  126. lastActiveTemplateVersion = _userDefaultsManager.lastActiveTemplateVersion
  127. realtimeExponentialBackoffRetryInterval = _userDefaultsManager
  128. .currentRealtimeThrottlingRetryIntervalSeconds
  129. realtimeExponentialBackoffThrottleEndTime = _userDefaultsManager
  130. .currentRealtimeThrottlingRetryIntervalSeconds
  131. realtimeRetryCount = _userDefaultsManager.realtimeRetryCount
  132. _lastFetchError = .unknown
  133. exponentialBackoffRetryInterval = 0
  134. _fetchTimeout = 0
  135. exponentialBackoffThrottleEndTime = 0
  136. super.init()
  137. }
  138. @objc public convenience init(databaseManager: ConfigDBManager,
  139. namespace: String,
  140. firebaseAppName: String,
  141. googleAppID: String) {
  142. self.init(
  143. databaseManager: databaseManager,
  144. namespace: namespace,
  145. firebaseAppName: firebaseAppName,
  146. googleAppID: googleAppID,
  147. userDefaults: nil
  148. )
  149. }
  150. // MARK: - Read / Update User Defaults
  151. /// The latest eTag value stored from the last successful response.
  152. @objc public var lastETag: String? {
  153. get { _userDefaultsManager.lastETag }
  154. set {
  155. lastETagUpdateTime = Date().timeIntervalSince1970
  156. _userDefaultsManager.lastETag = newValue
  157. }
  158. }
  159. /// The time of last successful config fetch.
  160. @objc public var lastFetchTimeInterval: TimeInterval {
  161. _userDefaultsManager.lastFetchTime
  162. }
  163. /// The timestamp of the last eTag update.
  164. @objc public var lastETagUpdateTime: TimeInterval {
  165. get { _userDefaultsManager.lastETagUpdateTime }
  166. set { _userDefaultsManager.lastETagUpdateTime = newValue }
  167. }
  168. // TODO: Update logic for app extensions as required.
  169. private func updateLastFetchTimeInterval(_ lastFetchTimeInternal: TimeInterval) {
  170. _userDefaultsManager.lastFetchTime = lastFetchTimeInternal
  171. }
  172. // MARK: - Load from Database
  173. /// Returns metadata from metadata table.
  174. @objc public func loadConfigFromMetadataTable() {
  175. _DBManager
  176. .loadMetadata(
  177. withBundleIdentifier: bundleIdentifier,
  178. namespace: _FIRNamespace
  179. ) { metadata in
  180. // TODO: Remove (all metadata in general) once ready to
  181. // migrate to user defaults completely.
  182. if let deviceContext = metadata[RCNKeyDeviceContext] as? [String: String] {
  183. self.deviceContext = deviceContext
  184. }
  185. if let customVariables = metadata[RCNKeyAppContext] as? [String: Sendable] {
  186. self._customVariables = customVariables
  187. }
  188. if let successFetchTimes = metadata[RCNKeySuccessFetchTime] as? [TimeInterval] {
  189. self._successFetchTimes = successFetchTimes
  190. }
  191. if let failureFetchTimes = metadata[RCNKeyFailureFetchTime] as? [TimeInterval] {
  192. self._failureFetchTimes = failureFetchTimes
  193. }
  194. if let lastFetchStatus = metadata[RCNKeyLastFetchStatus] as? RemoteConfigFetchStatus {
  195. self.lastFetchStatus = lastFetchStatus
  196. }
  197. if let lastFetchError = metadata[RCNKeyLastFetchError] as? RemoteConfigError {
  198. self._lastFetchError = lastFetchError
  199. }
  200. if let lastApplyTimeInterval = metadata[RCNKeyLastApplyTime] as? TimeInterval {
  201. self._lastApplyTimeInterval = lastApplyTimeInterval
  202. }
  203. if let lastSetDefaultsTimeInterval = metadata[RCNKeyLastFetchStatus] as? TimeInterval {
  204. self._lastSetDefaultsTimeInterval = lastSetDefaultsTimeInterval
  205. }
  206. }
  207. }
  208. // MARK: - Update Database/Cache
  209. /// If the last fetch was not successful, update the (exponential backoff)
  210. /// period that we wait until fetching again. Any subsequent fetch requests
  211. /// will be checked and allowed only if past this throttle end time.
  212. @objc public func updateExponentialBackoffTime() {
  213. if lastFetchStatus == .success {
  214. RCLog.debug("I-RCN000057", "Throttling: Entering exponential backoff mode.")
  215. exponentialBackoffRetryInterval = Double(kRCNExponentialBackoffMinimumInterval)
  216. } else {
  217. RCLog.debug("I-RCN000057", "Throttling: Updating throttling interval.")
  218. // Double the retry interval until we hit the truncated exponential backoff. More info here:
  219. // https://cloud.google.com/storage/docs/exponential-backoff
  220. exponentialBackoffRetryInterval = if exponentialBackoffRetryInterval * 2 <
  221. Double(kRCNExponentialBackoffMaximumInterval) {
  222. exponentialBackoffRetryInterval * 2
  223. } else {
  224. exponentialBackoffRetryInterval
  225. }
  226. }
  227. // Randomize the next retry interval.
  228. let randomPlusMinusInterval = Bool.random() ? -0.5 : 0.5
  229. let randomizedRetryInterval = exponentialBackoffRetryInterval +
  230. (exponentialBackoffRetryInterval * randomPlusMinusInterval)
  231. exponentialBackoffThrottleEndTime = Date().timeIntervalSince1970 + randomizedRetryInterval
  232. }
  233. /// Increases the throttling time for Realtime. Should only be called if the Realtime error
  234. /// indicates a server issue.
  235. @objc public func updateRealtimeExponentialBackoffTime() {
  236. // If there was only one stream attempt before, reset the retry interval.
  237. if realtimeRetryCount == 0 {
  238. RCLog.debug("I-RCN000058", "Throttling: Entering exponential Realtime backoff mode.")
  239. realtimeExponentialBackoffRetryInterval = Double(kRCNExponentialBackoffMinimumInterval)
  240. } else {
  241. RCLog.debug("I-RCN000058", "Throttling: Updating Realtime throttling interval.")
  242. // Double the retry interval until we hit the truncated exponential backoff. More info here:
  243. // https://cloud.google.com/storage/docs/exponential-backoff
  244. realtimeExponentialBackoffRetryInterval = if (realtimeExponentialBackoffRetryInterval * 2) <
  245. Double(kRCNExponentialBackoffMaximumInterval) {
  246. realtimeExponentialBackoffRetryInterval * 2
  247. } else {
  248. realtimeExponentialBackoffRetryInterval
  249. }
  250. }
  251. // Randomize the next retry interval.
  252. let randomPlusMinusInterval = Bool.random() ? -0.5 : 0.5
  253. let randomizedRetryInterval = realtimeExponentialBackoffRetryInterval +
  254. (realtimeExponentialBackoffRetryInterval * randomPlusMinusInterval)
  255. realtimeExponentialBackoffThrottleEndTime = Date()
  256. .timeIntervalSince1970 + randomizedRetryInterval
  257. _userDefaultsManager.realtimeThrottleEndTime = realtimeExponentialBackoffThrottleEndTime
  258. _userDefaultsManager
  259. .currentRealtimeThrottlingRetryIntervalSeconds = realtimeExponentialBackoffRetryInterval
  260. }
  261. func setRealtimeRetryCount(_ retryCount: Int) {
  262. realtimeRetryCount = retryCount
  263. _userDefaultsManager.realtimeRetryCount = realtimeRetryCount
  264. }
  265. /// Returns the difference between the Realtime backoff end time and the current time in a
  266. /// NSTimeInterval format.
  267. @objc public func realtimeBackoffInterval() -> TimeInterval {
  268. let now = Date().timeIntervalSince1970
  269. return realtimeExponentialBackoffThrottleEndTime - now
  270. }
  271. /// Updates the metadata table with the current fetch status.
  272. /// @param fetchSuccess True if fetch was successful.
  273. @objc public func updateMetadata(withFetchSuccessStatus fetchSuccess: Bool,
  274. templateVersion: String?) {
  275. RCLog.debug("I-RCN000056", "Updating metadata with fetch result: \(fetchSuccess).")
  276. updateFetchTime(success: fetchSuccess)
  277. lastFetchStatus = fetchSuccess ? .success : .failure
  278. _lastFetchError = fetchSuccess ? .unknown : .internalError
  279. if fetchSuccess, let templateVersion {
  280. updateLastFetchTimeInterval(Date().timeIntervalSince1970)
  281. // Note: We expect the googleAppID to always be available.
  282. deviceContext = Device.remoteConfigDeviceContext(with: _googleAppID)
  283. lastFetchedTemplateVersion = templateVersion
  284. _userDefaultsManager.lastFetchedTemplateVersion = templateVersion
  285. }
  286. updateMetadataTable()
  287. }
  288. private func updateFetchTime(success: Bool) {
  289. let epochTimeInterval = Date().timeIntervalSince1970
  290. if success {
  291. _successFetchTimes.append(epochTimeInterval)
  292. } else {
  293. _failureFetchTimes.append(epochTimeInterval)
  294. }
  295. }
  296. private func updateMetadataTable() {
  297. _DBManager.deleteRecord(withBundleIdentifier: bundleIdentifier, namespace: _FIRNamespace)
  298. guard JSONSerialization.isValidJSONObject(_customVariables) else {
  299. RCLog.error("I-RCN000028", "Invalid custom variables to be serialized.")
  300. return
  301. }
  302. guard JSONSerialization.isValidJSONObject(deviceContext) else {
  303. RCLog.error("I-RCN000029", "Invalid device context to be serialized.")
  304. return
  305. }
  306. guard JSONSerialization.isValidJSONObject(_successFetchTimes) else {
  307. RCLog.error("I-RCN000031", "Invalid success fetch times to be serialized.")
  308. return
  309. }
  310. guard JSONSerialization.isValidJSONObject(_failureFetchTimes) else {
  311. RCLog.error("I-RCN000032", "Invalid failure fetch times to be serialized.")
  312. return
  313. }
  314. let serializedAppContext = try? JSONSerialization.data(withJSONObject: _customVariables,
  315. options: [.prettyPrinted])
  316. let serializedDeviceContext = try? JSONSerialization.data(withJSONObject: deviceContext,
  317. options: [.prettyPrinted])
  318. // The digestPerNamespace is not used and only meant for backwards DB compatibility.
  319. let serializedDigestPerNamespace = try? JSONSerialization.data(withJSONObject: [:],
  320. options: [.prettyPrinted])
  321. let serializedSuccessTime = try? JSONSerialization.data(withJSONObject: _successFetchTimes,
  322. options: [.prettyPrinted])
  323. let serializedFailureTime = try? JSONSerialization.data(withJSONObject: _failureFetchTimes,
  324. options: [.prettyPrinted])
  325. guard let serializedDigestPerNamespace = serializedDigestPerNamespace,
  326. let serializedDeviceContext = serializedDeviceContext,
  327. let serializedAppContext = serializedAppContext,
  328. let serializedSuccessTime = serializedSuccessTime,
  329. let serializedFailureTime = serializedFailureTime else {
  330. return
  331. }
  332. let columnNameToValue: [String: Any] = [
  333. RCNKeyBundleIdentifier: bundleIdentifier,
  334. RCNKeyNamespace: _FIRNamespace,
  335. RCNKeyFetchTime: lastFetchTimeInterval,
  336. RCNKeyDigestPerNamespace: serializedDigestPerNamespace,
  337. RCNKeyDeviceContext: serializedDeviceContext,
  338. RCNKeyAppContext: serializedAppContext,
  339. RCNKeySuccessFetchTime: serializedSuccessTime,
  340. RCNKeyFailureFetchTime: serializedFailureTime,
  341. RCNKeyLastFetchStatus: lastFetchStatus.rawValue,
  342. RCNKeyLastFetchError: _lastFetchError.rawValue,
  343. RCNKeyLastApplyTime: _lastApplyTimeInterval,
  344. RCNKeyLastSetDefaultsTime: _lastSetDefaultsTimeInterval,
  345. ]
  346. _DBManager.insertMetadataTable(withValues: columnNameToValue)
  347. }
  348. /// Update last active template version from last fetched template version.
  349. @objc public func updateLastActiveTemplateVersion() {
  350. lastActiveTemplateVersion = lastFetchedTemplateVersion
  351. _userDefaultsManager.lastActiveTemplateVersion = lastActiveTemplateVersion
  352. }
  353. // MARK: - Fetch Request
  354. /// Returns a fetch request with the latest device and config change.
  355. /// Whenever user issues a fetch api call, collect the latest request.
  356. /// - Parameter userProperties: User properties to set to config request.
  357. /// - Returns: Config fetch request string
  358. @objc public func nextRequest(withUserProperties userProperties: [String: Any]?) -> String {
  359. var request = "{"
  360. request += "app_instance_id:'\(configInstallationsIdentifier)'"
  361. request += ", app_instance_id_token:'\(configInstallationsToken ?? "")'"
  362. request += ", app_id:'\(_googleAppID)'"
  363. request += ", country_code:'\(Device.remoteConfigDeviceCountry())'"
  364. request += ", language_code:'\(Device.remoteConfigDeviceLocale())'"
  365. request += ", platform_version:'\(GULAppEnvironmentUtil.systemVersion())'"
  366. request += ", time_zone:'\(Device.remoteConfigTimezone())'"
  367. request += ", package_name:'\(bundleIdentifier)'"
  368. request += ", app_version:'\(Device.remoteConfigAppVersion())'"
  369. request += ", app_build:'\(Device.remoteConfigAppBuildVersion())'"
  370. request += ", sdk_version:'\(Device.remoteConfigPodVersion())'"
  371. if let userProperties, !userProperties.isEmpty {
  372. // Extract first open time from user properties and send as a separate field
  373. var remainingUserProperties = userProperties
  374. if let firstOpenTime = userProperties[kRCNAnalyticsFirstOpenTimePropertyName] as? NSNumber {
  375. let date = Date(timeIntervalSince1970: firstOpenTime.doubleValue / 1000)
  376. let formatter = ISO8601DateFormatter()
  377. let firstOpenTimeISOString = formatter.string(from: date)
  378. request += ", first_open_time:'\(firstOpenTimeISOString)'"
  379. remainingUserProperties.removeValue(forKey: kRCNAnalyticsFirstOpenTimePropertyName)
  380. }
  381. if !remainingUserProperties.isEmpty {
  382. do {
  383. let jsonData = try JSONSerialization.data(
  384. withJSONObject: remainingUserProperties,
  385. options: []
  386. )
  387. if let jsonString = String(data: jsonData, encoding: .utf8) {
  388. request += ", analytics_user_properties:\(jsonString)"
  389. }
  390. } catch {
  391. // Ignore JSON serialization error.
  392. }
  393. }
  394. if customSignals.count > 0,
  395. let jsonData = try? JSONSerialization.data(withJSONObject: customSignals),
  396. let jsonString = String(data: jsonData, encoding: .utf8) {
  397. request += ", custom_signals:\(jsonString)"
  398. // Log the keys of the custom signals sent during fetch.
  399. RCLog.debug("I-RCN000078", "Keys of custom signals during fetch: \(customSignals.keys)")
  400. }
  401. }
  402. request += "}"
  403. return request
  404. }
  405. // MARK: - Getter/Setter
  406. /// The reason that last fetch failed.
  407. @objc public var lastFetchError: RemoteConfigError {
  408. get { _lastFetchError }
  409. set {
  410. _lastFetchError = newValue
  411. _DBManager
  412. .updateMetadata(
  413. withOption: .fetchStatus,
  414. namespace: _FIRNamespace,
  415. values: [lastFetchStatus, _lastFetchError]
  416. )
  417. }
  418. }
  419. private var _minimumFetchInterval: TimeInterval
  420. /// The time interval that config data stays fresh.
  421. @objc public var minimumFetchInterval: TimeInterval {
  422. get { _minimumFetchInterval }
  423. set { _minimumFetchInterval = max(0, newValue) }
  424. }
  425. private var _fetchTimeout: TimeInterval
  426. /// The timeout to set for outgoing fetch requests.
  427. @objc public var fetchTimeout: TimeInterval {
  428. get { _fetchTimeout }
  429. set {
  430. if newValue <= 0 {
  431. _fetchTimeout = RCNHTTPDefaultConnectionTimeout
  432. } else {
  433. _fetchTimeout = newValue
  434. }
  435. }
  436. }
  437. /// The time of last apply timestamp.
  438. @objc public var lastApplyTimeInterval: TimeInterval {
  439. get { _lastApplyTimeInterval }
  440. set {
  441. _lastApplyTimeInterval = newValue
  442. _DBManager
  443. .updateMetadata(withOption: .applyTime, namespace: _FIRNamespace, values: [newValue])
  444. }
  445. }
  446. /// The time of last setDefaults timestamp.
  447. @objc public var lastSetDefaultsTimeInterval: TimeInterval {
  448. get { _lastSetDefaultsTimeInterval }
  449. set {
  450. _lastSetDefaultsTimeInterval = newValue
  451. _DBManager.updateMetadata(
  452. withOption: .defaultTime,
  453. namespace: _FIRNamespace,
  454. values: [newValue]
  455. )
  456. }
  457. }
  458. /// A dictionary to hold custom signals set by the developer.
  459. @objc public var customSignals: [String: String] {
  460. get { _userDefaultsManager.customSignals }
  461. set {
  462. _userDefaultsManager.customSignals = newValue
  463. }
  464. }
  465. // MARK: - Throttling
  466. /// Returns true if the last fetch is outside the minimum fetch interval supplied.
  467. @objc public func hasMinimumFetchIntervalElapsed(_ minimumFetchInterval: TimeInterval) -> Bool {
  468. if lastFetchTimeInterval == 0 {
  469. return true
  470. }
  471. // Check if last config fetch is within minimum fetch interval in seconds.
  472. let diffInSeconds = Date().timeIntervalSince1970 - lastFetchTimeInterval
  473. return diffInSeconds > minimumFetchInterval
  474. }
  475. /// Returns true if we are in exponential backoff mode and it is not yet the next request time.
  476. @objc public func shouldThrottle() -> Bool {
  477. let now = Date().timeIntervalSince1970
  478. return lastFetchTimeInterval > 0 && lastFetchStatus != .success &&
  479. exponentialBackoffThrottleEndTime - now > 0
  480. }
  481. }