ConfigRealtime.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. // Copyright 2025 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 FirebaseCore
  15. import FirebaseInstallations
  16. import Foundation
  17. @_implementationOnly import GoogleUtilities
  18. #if canImport(UIKit) // iOS/tvOS/watchOS
  19. import UIKit
  20. #endif
  21. #if canImport(AppKit) // macOS
  22. import AppKit
  23. #endif
  24. // URL params
  25. private let serverURLDomain = "firebaseremoteconfigrealtime.googleapis.com"
  26. // Realtime API enablement
  27. private let serverForbiddenStatusCode = "\"code\": 403"
  28. // Header names
  29. private let httpMethodPost = "POST"
  30. private let contentTypeHeaderName = "Content-Type"
  31. private let contentEncodingHeaderName = "Content-Encoding"
  32. private let acceptEncodingHeaderName = "Accept"
  33. private let etagHeaderName = "etag"
  34. private let ifNoneMatchETagHeaderName = "if-none-match"
  35. private let installationsAuthTokenHeaderName = "x-goog-firebase-installations-auth"
  36. // Sends the bundle ID. Refer to b/130301479 for details.
  37. private let iOSBundleIdentifierHeaderName = "X-Ios-Bundle-Identifier"
  38. // Retryable HTTP status code.
  39. private let fetchResponseHTTPStatusOK = 200
  40. private let fetchResponseHTTPStatusTooManyRequests = 429
  41. private let fetchResponseHTTPStatusCodeBadGateway = 502
  42. private let fetchResponseHTTPStatusCodeServiceUnavailable = 503
  43. private let fetchResponseHTTPStatusCodeGatewayTimeout = 504
  44. // Invalidation message field names.
  45. private let templateVersionNumberKey = "latestTemplateVersionNumber"
  46. private let featureDisabledKey = "featureDisabled"
  47. private let timeoutSeconds: TimeInterval = 330
  48. private let fetchAttempts = 3
  49. private let applicationJSON = "application/json"
  50. private let gzip = "gzip"
  51. private let canRetry = "X-Google-GFE-Can-Retry"
  52. // Retry parameters
  53. private let maxRetries = 7
  54. /// Listener registration returned by `addOnConfigUpdateListener`. Calling its method `remove` stops
  55. /// the associated listener from receiving config updates and unregisters itself.
  56. ///
  57. /// If `remove` is called and no other listener registrations remain, the connection to the
  58. /// real-time connection.
  59. @objc(FIRConfigUpdateListenerRegistration) public
  60. final class ConfigUpdateListenerRegistration: NSObject, Sendable {
  61. let completionHandler: @Sendable (RemoteConfigUpdate?, Error?) -> Void
  62. private let realtimeClient: ConfigRealtime?
  63. @objc public
  64. init(client: ConfigRealtime,
  65. completionHandler: @escaping @Sendable (RemoteConfigUpdate?, Error?) -> Void) {
  66. realtimeClient = client
  67. self.completionHandler = completionHandler
  68. }
  69. @objc public
  70. func remove() {
  71. realtimeClient?.removeConfigUpdateListener(completionHandler)
  72. }
  73. }
  74. @objc(RCNConfigRealtime) public
  75. class ConfigRealtime: NSObject, URLSessionDataDelegate {
  76. private var listeners = NSOrderedSet()
  77. private let realtimeLockQueue = DispatchQueue(label: "com.google.firebase.remoteconfig.realtime")
  78. private let notificationCenter = NotificationCenter.default
  79. private let request: URLRequest
  80. private var session: URLSession?
  81. private var dataTask: URLSessionDataTask?
  82. private let configFetch: ConfigFetch
  83. private let settings: ConfigSettings
  84. private let options: FirebaseOptions
  85. private let namespace: String
  86. var remainingRetryCount: Int
  87. private var isRequestInProgress: Bool
  88. var isInBackground: Bool
  89. var isRealtimeDisabled: Bool
  90. public var installations: (any InstallationsProtocol)?
  91. @objc public
  92. init(configFetch: ConfigFetch,
  93. settings: ConfigSettings,
  94. namespace: String,
  95. options: FirebaseOptions,
  96. installations: InstallationsProtocol? = nil) {
  97. self.configFetch = configFetch
  98. self.settings = settings
  99. self.options = options
  100. self.namespace = namespace
  101. remainingRetryCount = max(maxRetries - settings.realtimeRetryCount, 1)
  102. isRequestInProgress = false
  103. isRealtimeDisabled = false
  104. isInBackground = false
  105. self.installations = if let installations {
  106. installations
  107. } else if
  108. let appName = namespace.components(separatedBy: ":").last,
  109. let app = FirebaseApp.app(name: appName) {
  110. Installations.installations(app: app)
  111. } else {
  112. nil as InstallationsProtocol?
  113. }
  114. request = ConfigRealtime.setupHTTPRequest(options, namespace)
  115. super.init()
  116. session = setupSession()
  117. backgroundChangeListener()
  118. }
  119. deinit {
  120. dataTask?.cancel() // Ensure the task is cancelled when the object is deallocated
  121. session?.invalidateAndCancel()
  122. }
  123. private static func setupHTTPRequest(_ options: FirebaseOptions,
  124. _ namespace: String) -> URLRequest {
  125. let url = Utils.constructServerURL(
  126. domain: serverURLDomain,
  127. apiKey: options.apiKey,
  128. optionsID: options.gcmSenderID,
  129. namespace: namespace
  130. )
  131. var request = URLRequest(url: url,
  132. cachePolicy: .reloadIgnoringLocalCacheData,
  133. timeoutInterval: timeoutSeconds)
  134. request.httpMethod = httpMethodPost
  135. request.setValue(applicationJSON, forHTTPHeaderField: contentTypeHeaderName)
  136. request.setValue(applicationJSON, forHTTPHeaderField: acceptEncodingHeaderName)
  137. request.setValue(gzip, forHTTPHeaderField: contentEncodingHeaderName)
  138. request.setValue("true", forHTTPHeaderField: canRetry)
  139. request.setValue(options.apiKey, forHTTPHeaderField: "X-Goog-Api-Key")
  140. request.setValue(
  141. Bundle.main.bundleIdentifier,
  142. forHTTPHeaderField: iOSBundleIdentifierHeaderName
  143. )
  144. return request
  145. }
  146. private func setupSession() -> URLSession {
  147. let config = URLSessionConfiguration.default
  148. config.timeoutIntervalForResource = timeoutSeconds
  149. config.timeoutIntervalForRequest = timeoutSeconds
  150. return URLSession(configuration: config, delegate: self, delegateQueue: .main)
  151. }
  152. private func propagateErrors(_ error: Error) {
  153. realtimeLockQueue.async { [weak self] in
  154. guard let self else { return }
  155. for listener in self.listeners {
  156. if let listener = listener as? (RemoteConfigUpdate?, Error?) -> Void {
  157. listener(nil, error)
  158. }
  159. }
  160. }
  161. }
  162. // TESTING ONLY
  163. @objc func triggerListenerForTesting(listener: @escaping (RemoteConfigUpdate?, Error?) -> Void) {
  164. DispatchQueue.main.async {
  165. listener(RemoteConfigUpdate(), nil)
  166. }
  167. }
  168. // MARK: - HTTP Helpers
  169. private func appName(fromFullyQualifiedNamespace fullyQualifiedNamespace: String) -> String {
  170. return String(fullyQualifiedNamespace.split(separator: ":").last ?? "")
  171. }
  172. private func reportCompletion(onHandler completionHandler: (
  173. (RemoteConfigFetchStatus, Error?) -> Void
  174. )?,
  175. withStatus status: RemoteConfigFetchStatus,
  176. withError error: Error?) {
  177. guard let completionHandler = completionHandler else { return }
  178. realtimeLockQueue.async {
  179. completionHandler(status, error)
  180. }
  181. }
  182. private func refreshInstallationsToken(completionHandler: (
  183. (RemoteConfigFetchStatus, Error?) -> Void
  184. )?) {
  185. guard let installations, !options.gcmSenderID.isEmpty else {
  186. let errorDescription = "Failed to get GCMSenderID"
  187. RCLog.error("I-RCN000074", errorDescription)
  188. settings.isFetchInProgress = false
  189. reportCompletion(
  190. onHandler: completionHandler,
  191. withStatus: .failure,
  192. withError: NSError(
  193. domain: ConfigConstants.remoteConfigErrorDomain,
  194. code: RemoteConfigError.internalError.rawValue,
  195. userInfo: [NSLocalizedDescriptionKey: errorDescription]
  196. )
  197. )
  198. return
  199. }
  200. RCLog.debug("I-RCN000039", "Starting requesting token.")
  201. installations.authToken { [weak self] result, error in
  202. guard let self else { return }
  203. if let error = error {
  204. let errorDescription = "Failed to get installations token. Error : \(error)."
  205. RCLog.error("I-RCN000073", errorDescription)
  206. self.isRequestInProgress = false
  207. var userInfo = [String: Any]()
  208. userInfo[NSLocalizedDescriptionKey] = errorDescription
  209. userInfo[NSUnderlyingErrorKey] = error
  210. self.reportCompletion(
  211. onHandler: completionHandler,
  212. withStatus: .failure,
  213. withError: NSError(domain: ConfigConstants.remoteConfigErrorDomain,
  214. code: RemoteConfigError.internalError.rawValue,
  215. userInfo: userInfo)
  216. )
  217. return
  218. }
  219. guard let tokenResult = result else {
  220. let errorDescription = "Failed to get installations token"
  221. RCLog.error("I-RCN000073", errorDescription)
  222. self.isRequestInProgress = false
  223. reportCompletion(onHandler: completionHandler,
  224. withStatus: .failure,
  225. withError: NSError(domain: ConfigConstants.remoteConfigErrorDomain,
  226. code: RemoteConfigError.internalError.rawValue,
  227. userInfo: [
  228. NSLocalizedDescriptionKey: errorDescription,
  229. ]))
  230. return
  231. }
  232. /// We have a valid token. Get the backing installationID.
  233. installations.installationID { [weak self] identifier, error in
  234. guard let self else { return }
  235. // Dispatch to the RC serial queue to update settings on the queue.
  236. self.realtimeLockQueue.async {
  237. /// Update config settings with the IID and token.
  238. self.settings.configInstallationsToken = tokenResult.authToken
  239. self.settings.configInstallationsIdentifier = identifier ?? ""
  240. if let error = error {
  241. let errorDescription = "Error getting iid : \(error)."
  242. RCLog.error("I-RCN000055", errorDescription)
  243. self.isRequestInProgress = false
  244. var userInfo = [String: Any]()
  245. userInfo[NSLocalizedDescriptionKey] = errorDescription
  246. userInfo[NSUnderlyingErrorKey] = error
  247. self.reportCompletion(
  248. onHandler: completionHandler,
  249. withStatus: .failure,
  250. withError: NSError(domain: ConfigConstants.remoteConfigErrorDomain,
  251. code: RemoteConfigError.internalError.rawValue,
  252. userInfo: userInfo)
  253. )
  254. } else if let identifier = identifier {
  255. RCLog.info("I-RCN000022", "Success to get iid : \(identifier)")
  256. self.reportCompletion(onHandler: completionHandler,
  257. withStatus: .noFetchYet,
  258. withError: nil)
  259. }
  260. }
  261. }
  262. }
  263. }
  264. @objc public
  265. func createRequestBody(completion: @escaping (Data) -> Void) {
  266. refreshInstallationsToken { status, error in
  267. if self.settings.configInstallationsIdentifier.isEmpty {
  268. RCLog.debug(
  269. "I-RCN000013",
  270. "Installation token retrieval failed. Realtime connection will not include " +
  271. "valid installations token."
  272. )
  273. }
  274. var request = self.request
  275. request.setValue(self.settings.configInstallationsToken,
  276. forHTTPHeaderField: installationsAuthTokenHeaderName)
  277. if let etag = self.settings.lastETag {
  278. request.setValue(etag, forHTTPHeaderField: ifNoneMatchETagHeaderName)
  279. }
  280. let postBody = """
  281. {
  282. project:'\(self.options.gcmSenderID)',
  283. namespace:'\(Utils.namespaceOnly(self.namespace))',
  284. lastKnownVersionNumber:'\(self.configFetch.templateVersionNumber)',
  285. appId:'\(self.options.googleAppID)',
  286. sdkVersion:'\(Device.remoteConfigPodVersion())',
  287. appInstanceId:'\(self.settings.configInstallationsIdentifier)'
  288. }
  289. """
  290. do {
  291. if let postData = postBody.data(using: .utf8) {
  292. let compressedData = try NSData.gul_data(byGzippingData: postData)
  293. completion(compressedData)
  294. } else {
  295. RCLog.error("I-RCN000090", "Error creating fetch body for realtime")
  296. completion(Data())
  297. }
  298. } catch {
  299. RCLog.error("I-RCN000091", "Error compressing fetch body for realtime \(error)")
  300. completion(Data())
  301. }
  302. }
  303. }
  304. // MARK: - Retry Helpers
  305. func canMakeConnection() -> Bool {
  306. let noRunningConnection = dataTask == nil || dataTask?.state != .running
  307. return noRunningConnection && listeners.count > 0 && !isInBackground && !isRealtimeDisabled
  308. }
  309. func retryHTTPConnection() {
  310. realtimeLockQueue.async { [weak self] in
  311. guard let self, !self.isInBackground else { return }
  312. guard self.remainingRetryCount > 0 else {
  313. let error = NSError(domain: ConfigConstants.remoteConfigUpdateErrorDomain,
  314. code: RemoteConfigUpdateError.streamError.rawValue,
  315. userInfo: [
  316. NSLocalizedDescriptionKey: "Unable to connect to the server. Check your connection and try again.",
  317. ])
  318. RCLog.error("I-RCN000014", "Cannot establish connection. Error: \(error)")
  319. self.propagateErrors(error)
  320. return
  321. }
  322. if self.canMakeConnection() {
  323. self.remainingRetryCount -= 1
  324. self.settings.realtimeRetryCount += 1
  325. let backoffInterval = self.settings.realtimeBackoffInterval()
  326. self.realtimeLockQueue.asyncAfter(deadline: .now() + backoffInterval) {
  327. self.beginRealtimeStream()
  328. }
  329. }
  330. }
  331. }
  332. private func backgroundChangeListener() {
  333. #if canImport(UIKit)
  334. NotificationCenter.default.addObserver(self,
  335. selector: #selector(willEnterForeground),
  336. name: UIApplication
  337. .willEnterForegroundNotification,
  338. object: nil)
  339. NotificationCenter.default.addObserver(self,
  340. selector: #selector(didEnterBackground),
  341. name: UIApplication.didEnterBackgroundNotification,
  342. object: nil)
  343. #elseif canImport(AppKit)
  344. NotificationCenter.default.addObserver(self,
  345. selector: #selector(willEnterForeground),
  346. name: NSApplication.willBecomeActiveNotification,
  347. object: nil)
  348. NotificationCenter.default.addObserver(self,
  349. selector: #selector(didEnterBackground),
  350. name: NSApplication.didResignActiveNotification,
  351. object: nil)
  352. #endif
  353. }
  354. @objc private func willEnterForeground() {
  355. realtimeLockQueue.async { [weak self] in
  356. guard let self else { return }
  357. self.isInBackground = false
  358. self.beginRealtimeStream()
  359. }
  360. }
  361. @objc private func didEnterBackground() {
  362. realtimeLockQueue.async { [weak self] in
  363. guard let self else { return }
  364. self.pauseRealtimeStream()
  365. self.isInBackground = true
  366. }
  367. }
  368. // MARK: - Autofetch Helpers
  369. @objc(fetchLatestConfig:targetVersion:) public
  370. func fetchLatestConfig(remainingAttempts: Int, targetVersion: Int) {
  371. realtimeLockQueue.async { [weak self] in
  372. guard let self else { return }
  373. let attempts = remainingAttempts - 1
  374. self.configFetch.realtimeFetchConfig(fetchAttemptNumber: fetchAttempts - attempts) {
  375. status, update, error in
  376. if let error = error {
  377. RCLog.error("I-RCN000010",
  378. "Failed to retrieve config due to fetch error. Error: \(error)")
  379. self.propagateErrors(error)
  380. return
  381. }
  382. if status == .success {
  383. if Int(self.configFetch.templateVersionNumber) ?? 0 >= targetVersion {
  384. // Only notify listeners if there is a change.
  385. if let update = update, !update.updatedKeys.isEmpty {
  386. self.realtimeLockQueue.async { [weak self] in
  387. guard let self else { return }
  388. for listener in self.listeners {
  389. if let l = listener as? (RemoteConfigUpdate?, Error?) -> Void {
  390. l(update, nil)
  391. }
  392. }
  393. }
  394. }
  395. } else {
  396. RCLog.debug("I-RCN000016",
  397. "Fetched config's template version is outdated, re-fetching")
  398. self.autoFetch(attempts: attempts, targetVersion: targetVersion)
  399. }
  400. } else {
  401. RCLog.debug("I-RCN000016",
  402. "Fetched config's template version is outdated, re-fetching")
  403. self.autoFetch(attempts: attempts, targetVersion: targetVersion)
  404. }
  405. }
  406. }
  407. }
  408. @objc(scheduleFetch:targetVersion:) public
  409. func scheduleFetch(remainingAttempts: Int, targetVersion: Int) {
  410. // Needs fetch to occur between 0 - 3 seconds. Randomize to not cause DDoS
  411. // alerts in backend.
  412. let delay = TimeInterval.random(in: 0 ... 3) // Random delay between 0 and 3 seconds
  413. realtimeLockQueue.asyncAfter(deadline: .now() + delay) {
  414. self.fetchLatestConfig(remainingAttempts: remainingAttempts, targetVersion: targetVersion)
  415. }
  416. }
  417. /// Perform fetch and handle developers callbacks.
  418. @objc(autoFetch:targetVersion:) public
  419. func autoFetch(attempts: Int, targetVersion: Int) {
  420. realtimeLockQueue.async { [weak self] in
  421. guard let self else { return }
  422. guard attempts > 0 else {
  423. let error = NSError(domain: ConfigConstants.remoteConfigUpdateErrorDomain,
  424. code: RemoteConfigUpdateError.notFetched.rawValue,
  425. userInfo: [
  426. NSLocalizedDescriptionKey: "Unable to fetch the latest version of the template.",
  427. ])
  428. RCLog.error("I-RCN000011", "Ran out of fetch attempts, cannot find target config version.")
  429. self.propagateErrors(error)
  430. return
  431. }
  432. self.scheduleFetch(remainingAttempts: attempts, targetVersion: targetVersion)
  433. }
  434. }
  435. // MARK: - URLSessionDataDelegate
  436. /// Delegate to asynchronously handle every new notification that comes over
  437. /// the wire. Auto-fetches and runs callback for each new notification.
  438. public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask,
  439. didReceive data: Data) {
  440. let strData = String(data: data, encoding: .utf8) ?? ""
  441. // If response data contains the API enablement link, return the entire
  442. // message to the user in the form of a error.
  443. if strData.contains(serverForbiddenStatusCode) {
  444. let error = NSError(domain: ConfigConstants.remoteConfigUpdateErrorDomain,
  445. code: RemoteConfigUpdateError.streamError.rawValue,
  446. userInfo: [NSLocalizedDescriptionKey: strData])
  447. RCLog.error("I-RCN000021", "Cannot establish connection. \(error)")
  448. propagateErrors(error)
  449. return
  450. }
  451. if let beginRange = strData.range(of: "{"),
  452. let endRange = strData.range(of: "}") {
  453. RCLog.debug("I-RCN000015", "Received config update message on stream.")
  454. let msgRange = Range(uncheckedBounds: (lower: beginRange.lowerBound,
  455. upper: strData.index(after: endRange.upperBound)))
  456. let jsonData = String(strData[msgRange]).data(using: .utf8)!
  457. do {
  458. if let response = try JSONSerialization.jsonObject(with: jsonData,
  459. options: []) as? [String: Any] {
  460. evaluateStreamResponse(response)
  461. }
  462. } catch {
  463. let wrappedError =
  464. NSError(domain: ConfigConstants.remoteConfigUpdateErrorDomain,
  465. code: RemoteConfigUpdateError.messageInvalid.rawValue,
  466. userInfo: [
  467. NSLocalizedDescriptionKey: "Unable to parse ConfigUpdate. \(strData)",
  468. NSUnderlyingErrorKey: error,
  469. ])
  470. propagateErrors(wrappedError)
  471. return
  472. }
  473. }
  474. }
  475. @objc public
  476. func evaluateStreamResponse(_ response: [String: Any]) {
  477. var updateTemplateVersion = 1
  478. if let version = response[templateVersionNumberKey] as? Int {
  479. updateTemplateVersion = version
  480. }
  481. if let isDisabled = response[featureDisabledKey] as? Bool {
  482. isRealtimeDisabled = isDisabled
  483. }
  484. if isRealtimeDisabled {
  485. pauseRealtimeStream()
  486. let error =
  487. NSError(domain: ConfigConstants.remoteConfigUpdateErrorDomain,
  488. code: RemoteConfigUpdateError.unavailable.rawValue,
  489. userInfo: [
  490. NSLocalizedDescriptionKey: "The server is temporarily unavailable. Try again in a few minutes.",
  491. ])
  492. propagateErrors(error)
  493. } else {
  494. let clientTemplateVersion = Int(configFetch.templateVersionNumber) ?? 0
  495. if updateTemplateVersion > clientTemplateVersion {
  496. autoFetch(attempts: fetchAttempts, targetVersion: updateTemplateVersion)
  497. }
  498. }
  499. }
  500. func isStatusCodeRetryable(_ statusCode: Int) -> Bool {
  501. return statusCode == fetchResponseHTTPStatusTooManyRequests ||
  502. statusCode == fetchResponseHTTPStatusCodeServiceUnavailable ||
  503. statusCode == fetchResponseHTTPStatusCodeBadGateway ||
  504. statusCode == fetchResponseHTTPStatusCodeGatewayTimeout
  505. }
  506. /// Delegate to handle initial reply from the server.
  507. public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask,
  508. didReceive response: URLResponse,
  509. completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
  510. isRequestInProgress = false
  511. if let httpResponse = response as? HTTPURLResponse {
  512. let statusCode = httpResponse.statusCode
  513. if statusCode == 403 {
  514. completionHandler(.allow)
  515. return
  516. }
  517. if statusCode != fetchResponseHTTPStatusOK {
  518. settings.updateRealtimeExponentialBackoffTime()
  519. pauseRealtimeStream()
  520. if isStatusCodeRetryable(statusCode) {
  521. retryHTTPConnection()
  522. } else {
  523. let error = NSError(
  524. domain: ConfigConstants.remoteConfigUpdateErrorDomain,
  525. code: RemoteConfigUpdateError.streamError.rawValue,
  526. userInfo: [
  527. NSLocalizedDescriptionKey:
  528. "Unable to connect to the server. Try again in a few minutes. HTTP Status code: \(statusCode)",
  529. ]
  530. )
  531. RCLog.error("I-RCN000021", "Cannot establish connection. Error: \(error)")
  532. propagateErrors(error)
  533. }
  534. } else {
  535. // On success, reset retry parameters.
  536. remainingRetryCount = maxRetries
  537. settings.realtimeRetryCount = 0
  538. }
  539. completionHandler(.allow)
  540. }
  541. }
  542. /// Delegate to handle data task completion.
  543. public func urlSession(_ session: URLSession, task: URLSessionTask,
  544. didCompleteWithError error: Error?) {
  545. if !session.isEqual(self.session) {
  546. return
  547. }
  548. isRequestInProgress = false
  549. if let error = error, error._code != NSURLErrorCancelled {
  550. settings.updateRealtimeExponentialBackoffTime()
  551. }
  552. pauseRealtimeStream()
  553. retryHTTPConnection()
  554. }
  555. /// Delegate to handle session invalidation.
  556. public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
  557. if !isRequestInProgress {
  558. if let _ = error {
  559. settings.updateRealtimeExponentialBackoffTime()
  560. }
  561. pauseRealtimeStream()
  562. retryHTTPConnection()
  563. }
  564. }
  565. // MARK: - Top Level Methods
  566. @objc public
  567. func beginRealtimeStream() {
  568. realtimeLockQueue.async { [weak self] in
  569. guard let self else { return }
  570. guard self.settings.realtimeBackoffInterval() <= 0.0 else {
  571. self.retryHTTPConnection()
  572. return
  573. }
  574. if self.canMakeConnection() {
  575. self.createRequestBody { [weak self] requestBody in
  576. guard let self else { return }
  577. var request = self.request
  578. request.httpBody = requestBody
  579. self.isRequestInProgress = true
  580. self.dataTask = self.session?.dataTask(with: request)
  581. self.dataTask?.resume()
  582. }
  583. }
  584. }
  585. }
  586. @objc public
  587. func pauseRealtimeStream() {
  588. realtimeLockQueue.async { [weak self] in
  589. guard let self else { return }
  590. if let task = self.dataTask {
  591. task.cancel()
  592. self.dataTask = nil
  593. }
  594. }
  595. }
  596. @discardableResult
  597. @objc public func addConfigUpdateListener(_ listener: @Sendable @escaping (RemoteConfigUpdate?,
  598. Error?) -> Void)
  599. -> ConfigUpdateListenerRegistration {
  600. realtimeLockQueue.async { [weak self] in
  601. guard let self else { return }
  602. let temp = self.listeners.mutableCopy() as! NSMutableOrderedSet
  603. temp.add(listener)
  604. self.listeners = temp
  605. self.beginRealtimeStream()
  606. }
  607. return ConfigUpdateListenerRegistration(client: self, completionHandler: listener)
  608. }
  609. @objc public func removeConfigUpdateListener(_ listener: @escaping (RemoteConfigUpdate?, Error?)
  610. -> Void) {
  611. realtimeLockQueue.async { [weak self] in
  612. guard let self else { return }
  613. let temp: NSMutableOrderedSet = self.listeners.mutableCopy() as! NSMutableOrderedSet
  614. temp.remove(listener)
  615. self.listeners = temp
  616. if self.listeners.count == 0 {
  617. self.pauseRealtimeStream()
  618. }
  619. }
  620. }
  621. }