From b5b32892dc244e84a9a165fcea1541fa749dd2c4 Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 25 Oct 2024 17:23:01 +0200 Subject: [PATCH] Adds network monitor to resume api calls --- LeStorage.xcodeproj/project.pbxproj | 4 ++ LeStorage/ApiCallCollection.swift | 39 ++++++++++--------- LeStorage/NetworkMonitor.swift | 59 +++++++++++++++++++++++++++++ LeStorage/StoreCenter.swift | 17 +++++++++ 4 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 LeStorage/NetworkMonitor.swift diff --git a/LeStorage.xcodeproj/project.pbxproj b/LeStorage.xcodeproj/project.pbxproj index 0f43c63..f30e894 100644 --- a/LeStorage.xcodeproj/project.pbxproj +++ b/LeStorage.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ C425D4582B6D2519002A7B48 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4572B6D2519002A7B48 /* Store.swift */; }; C456EFE22BE52379007388E2 /* StoredSingleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C456EFE12BE52379007388E2 /* StoredSingleton.swift */; }; C45D35912C0A1DB5000F379F /* FailedAPICall.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D35902C0A1DB5000F379F /* FailedAPICall.swift */; }; + C488C8802CCBDC210082001F /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C488C87F2CCBDC210082001F /* NetworkMonitor.swift */; }; C49B6E502C2089B6002BDE1B /* ApiCallCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */; }; C49EF0242BD6BDC50077B5AA /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */; }; C4A47D4F2B6D280200ADC637 /* StoredCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */; }; @@ -59,6 +60,7 @@ C425D4572B6D2519002A7B48 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; C456EFE12BE52379007388E2 /* StoredSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredSingleton.swift; sourceTree = ""; }; C45D35902C0A1DB5000F379F /* FailedAPICall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedAPICall.swift; sourceTree = ""; }; + C488C87F2CCBDC210082001F /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCallCollection.swift; sourceTree = ""; }; C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extensions.swift"; sourceTree = ""; }; C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredCollection.swift; sourceTree = ""; }; @@ -135,6 +137,7 @@ C4A47D9D2B7CFFF500ADC637 /* Codables */, C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */, C4A47D6C2B71364600ADC637 /* ModelObject.swift */, + C488C87F2CCBDC210082001F /* NetworkMonitor.swift */, C4A47D602B6D3C1300ADC637 /* Services.swift */, C425D4572B6D2519002A7B48 /* Store.swift */, C4FC2E282C2B2EC30021F3BF /* StoreCenter.swift */, @@ -316,6 +319,7 @@ C456EFE22BE52379007388E2 /* StoredSingleton.swift in Sources */, C4A47D652B6E92FE00ADC637 /* Storable.swift in Sources */, C4D477972CB66EEA0077713D /* Date+Extensions.swift in Sources */, + C488C8802CCBDC210082001F /* NetworkMonitor.swift in Sources */, C4A47D6D2B71364600ADC637 /* ModelObject.swift in Sources */, C400D7232CC2AF560092237C /* GetSyncData.swift in Sources */, C4A47D4F2B6D280200ADC637 /* StoredCollection.swift in Sources */, diff --git a/LeStorage/ApiCallCollection.swift b/LeStorage/ApiCallCollection.swift index f025430..43728e7 100644 --- a/LeStorage/ApiCallCollection.swift +++ b/LeStorage/ApiCallCollection.swift @@ -16,6 +16,7 @@ protocol SomeCallCollection { func contentOfFile() async -> String? func reset() async + func resumeApiCalls() async } @@ -33,6 +34,8 @@ actor ApiCallCollection: SomeCallCollection { /// Indicates if the collection is currently retrying ApiCalls fileprivate var _isRescheduling: Bool = false + fileprivate var _schedulingTask: Task<(), Never>? = nil + /// Indicates whether the collection content has changed /// Initiates a write when true fileprivate var _hasChanged: Bool = false { @@ -140,23 +143,15 @@ actor ApiCallCollection: SomeCallCollection { } } - /// Wait for an exponentionnaly long time depending on the number of attemps - fileprivate func _wait() async { - - let delay = pow(2, self._attemptLoops) - let seconds = NSDecimalNumber(decimal: delay).intValue - Logger.log("\(T.resourceName()): wait for \(seconds) sec") - do { - try await Task.sleep(until: .now + .seconds(seconds)) - } catch { - Logger.w("*** WAITING CRASHED !!!") - Logger.error(error) - } + func resumeApiCalls() { + self._schedulingTask?.cancel() + self._attemptLoops = -1 + self.rescheduleApiCallsIfNecessary() } /// Reschedule API calls if necessary func rescheduleApiCallsIfNecessary() { - Task { + self._schedulingTask = Task { await self._rescheduleApiCalls() } } @@ -195,8 +190,6 @@ actor ApiCallCollection: SomeCallCollection { } else { let _: [T] = try await self._executeApiCall(apiCall) } - // process GET - // what if it is a sync GET } } catch { // Logger.log("\(T.resourceName()) > API CALL RETRY ERROR:") @@ -204,8 +197,6 @@ actor ApiCallCollection: SomeCallCollection { } } -// Logger.log("\(T.resourceName()) > STOP RESCHED") - self._isRescheduling = false if self.items.isNotEmpty { await self._rescheduleApiCalls() @@ -214,6 +205,20 @@ actor ApiCallCollection: SomeCallCollection { // Logger.log("\(T.resourceName()) > isRescheduling = \(self._isRescheduling)") } + /// Wait for an exponentionnaly long time depending on the number of attemps + fileprivate func _wait() async { + + let delay = pow(2, self._attemptLoops) + let seconds = NSDecimalNumber(decimal: delay).intValue + Logger.log("\(T.resourceName()): wait for \(seconds) sec") + do { + try await Task.sleep(until: .now + .seconds(seconds)) + } catch { + Logger.w("*** WAITING CRASHED !!!") + Logger.error(error) + } + } + // MARK: - Synchronization /// Returns an APICall instance for the Storable [instance] and an HTTP [method] diff --git a/LeStorage/NetworkMonitor.swift b/LeStorage/NetworkMonitor.swift new file mode 100644 index 0000000..aada980 --- /dev/null +++ b/LeStorage/NetworkMonitor.swift @@ -0,0 +1,59 @@ +// +// NetworkMonitor.swift +// LeStorage +// +// Created by Laurent Morvillier on 25/10/2024. +// + +import Network +import Foundation + +public class NetworkMonitor { + + public static let shared = NetworkMonitor() + private var monitor: NWPathMonitor + private var queue = DispatchQueue(label: "NetworkMonitor") + + public var isConnected: Bool { + get { + return status == .satisfied + } + } + + private(set) var status: NWPath.Status = .requiresConnection + + // Closure to be called when connection is established + var onConnectionEstablished: (() -> Void)? + + private init() { + monitor = NWPathMonitor() + self._startMonitoring() + } + + private func _startMonitoring() { + monitor.pathUpdateHandler = { [weak self] path in + guard let self = self else { return } + + // Update status + self.status = path.status + + // Print status for debugging + Logger.log("Network Status: \(path.status)") + + // Handle connection established + if path.status == .satisfied { + DispatchQueue.main.async { + self.onConnectionEstablished?() + } + } + + } + + monitor.start(queue: queue) + } + + func stopMonitoring() { + monitor.cancel() + } + +} diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index fd8babf..a0d974d 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -56,6 +56,10 @@ public class StoreCenter { init() { self._dataLogs = Store.main.registerCollection() self._setupNotifications() + + NetworkMonitor.shared.onConnectionEstablished = { + self._resumeApiCalls() + } } /// Returns the service instance @@ -266,6 +270,19 @@ public class StoreCenter { // MARK: - Api call rescheduling + /// Retry API calls immediately + fileprivate func _resumeApiCalls() { + guard self.collectionsCanSynchronize else { + return + } + Logger.log("_resumeApiCalls") + Task { + for collection in self._apiCallCollections.values { + await collection.resumeApiCalls() + } + } + } + /// Reschedule an ApiCall by id func rescheduleApiCalls(id: String, type: T.Type) async throws { guard self.collectionsCanSynchronize else {