From 2191733d975c27e338a94caade6dc34ce71e0c3f Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 18 Jun 2024 11:08:21 +0200 Subject: [PATCH] Refactor asynchronicity for API calls collections --- LeStorage.xcodeproj/project.pbxproj | 4 + LeStorage/ApiCallCollection.swift | 260 ++++++++++++++++++++++++ LeStorage/Services.swift | 4 +- LeStorage/Storable.swift | 16 ++ LeStorage/Store.swift | 43 ++-- LeStorage/StoredCollection.swift | 299 +++++++++++----------------- 6 files changed, 428 insertions(+), 198 deletions(-) create mode 100644 LeStorage/ApiCallCollection.swift diff --git a/LeStorage.xcodeproj/project.pbxproj b/LeStorage.xcodeproj/project.pbxproj index 79fe587..1c6055f 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 */; }; + 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 */; }; C4A47D512B6D2C4E00ADC637 /* Codable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D502B6D2C4E00ADC637 /* Codable+Extensions.swift */; }; @@ -51,6 +52,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 = ""; }; + 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 = ""; }; C4A47D502B6D2C4E00ADC637 /* Codable+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Codable+Extensions.swift"; sourceTree = ""; }; @@ -113,6 +115,7 @@ C425D4372B6D24E1002A7B48 /* LeStorage.h */, C425D4382B6D24E1002A7B48 /* LeStorage.docc */, C4A47D9D2B7CFFF500ADC637 /* Codables */, + C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */, C4A47D6C2B71364600ADC637 /* ModelObject.swift */, C4A47D602B6D3C1300ADC637 /* Services.swift */, C425D4572B6D2519002A7B48 /* Store.swift */, @@ -294,6 +297,7 @@ C49EF0242BD6BDC50077B5AA /* FileManager+Extensions.swift in Sources */, C425D4582B6D2519002A7B48 /* Store.swift in Sources */, C45D35912C0A1DB5000F379F /* FailedAPICall.swift in Sources */, + C49B6E502C2089B6002BDE1B /* ApiCallCollection.swift in Sources */, C4A47D6B2B71244100ADC637 /* Collection+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/LeStorage/ApiCallCollection.swift b/LeStorage/ApiCallCollection.swift new file mode 100644 index 0000000..f9e7111 --- /dev/null +++ b/LeStorage/ApiCallCollection.swift @@ -0,0 +1,260 @@ +// +// SafeCollection.swift +// LeStorage +// +// Created by Laurent Morvillier on 17/06/2024. +// + +import Foundation + +actor ApiCallCollection { + + /// The reference to the Store + fileprivate var _store: Store + + fileprivate(set) var items: [ApiCall] = [] + + /// number of time an execution loop has been called + fileprivate var _attemptLoops: Int = 0 + + /// Indicates if the collection is currently retrying ApiCalls + fileprivate var _isRetryingCalls: Bool = false + + fileprivate var _hasChanged: Bool = false { + didSet { + self._write() + } + } + + init(store: Store) { + self._store = store + } + + /// Starts the JSON file decoding synchronously or asynchronously + func loadFromFile() throws { + try self._decodeJSONFile() + } + + fileprivate func _urlForJSONFile() throws -> URL { + return try ApiCall.urlForJSONFile() + } + + /// Decodes the json file into the items array + fileprivate func _decodeJSONFile() throws { + let fileURL = try self._urlForJSONFile() + + if FileManager.default.fileExists(atPath: fileURL.path()) { + let jsonString: String = try FileUtils.readFile(fileURL: fileURL) + let decoded: [ApiCall] = try jsonString.decodeArray() ?? [] + Logger.log("loaded \(T.fileName()) with \(decoded.count) items") + self.items = decoded + + self.rescheduleApiCallsIfNecessary() + + } + + } + + fileprivate func _write() { + let fileName = ApiCall.fileName() + DispatchQueue(label: "lestorage.queue.write", qos: .utility).asyncAndWait { + Logger.log("Start write to \(fileName)...") + do { + let jsonString: String = try self.items.jsonString() + try T.writeToStorageDirectory(content: jsonString, fileName: fileName) + } catch { + Logger.error(error) + } + Logger.log("End write") + } + } + + func addOrUpdate(_ instance: ApiCall) { + if let index = self.items.firstIndex(where: { $0.id == instance.id }) { + self.items[index] = instance + } else { + self.items.append(instance) + } + self._hasChanged = true + } + + /// Deletes an API call by [id] + func deleteById(_ id: String) { + self.items.removeAll(where: { $0.id == id }) + self._hasChanged = true + } + + func deleteByDataId(_ id: String) { + if let apiCallIndex = self.items.firstIndex(where: { $0.dataId == id }) { + self.items.remove(at: apiCallIndex) + self._hasChanged = true + } + } + + func findById(_ id: String) -> ApiCall? { + return self.items.first(where: { $0.id == id }) + } + + func reset() { + self.items.removeAll() + + do { + let url: URL = try self._urlForJSONFile() + if FileManager.default.fileExists(atPath: url.path()) { + try FileManager.default.removeItem(at: url) + } + } catch { + Logger.error(error) + } + } + + fileprivate func _rescheduleApiCalls() { + + guard self.items.isNotEmpty else { + return + } + + self._isRetryingCalls = true + self._attemptLoops += 1 + + Task { + + let delay = pow(2, self._attemptLoops) + let seconds = NSDecimalNumber(decimal: delay).intValue + Logger.log("wait for \(seconds) sec") + try await Task.sleep(until: .now + .seconds(seconds)) + + let apiCallsCopy = self.items + for apiCall in apiCallsCopy { + apiCall.attemptsCount += 1 + apiCall.lastAttemptDate = Date() + + do { + try await self._executeApiCall(apiCall) +// let _ = try await Store.main.execute(apiCall: apiCall) + } catch { + Logger.error(error) + } + } + + if self.items.isEmpty { + self._isRetryingCalls = false + } else { + self._rescheduleApiCalls() + } + + } + + } + + // MARK: - Synchronization + + /// Returns an APICall instance for the Storable [instance] and an HTTP [method] + /// The method updates existing calls or creates a new one + fileprivate func _callForInstance(_ instance: T, method: HTTPMethod) throws -> ApiCall? { + + if let existingCall = self.items.first(where: { $0.dataId == instance.id }) { + switch method { + case .delete: + self.deleteById(existingCall.id) // delete the existing call as we don't need it + if existingCall.method == HTTPMethod.post { + return nil // if the post has not been done, we can just stop here + } else { + return try self._createCall(instance, method: method) // otherwise it's a put and we want to send the delete + } + default: // here we should only trying to PUT, so we update the existing POST/PUT with the instance new values + existingCall.body = try instance.jsonString() + return existingCall + } + } else { + return try self._createCall(instance, method: method) + } + } + + /// Creates an API call for the Storable [instance] and an HTTP [method] + fileprivate func _createCall(_ instance: T, method: HTTPMethod) throws -> ApiCall { + let jsonString = try instance.jsonString() + return ApiCall(method: method, dataId: String(instance.id), body: jsonString) + } + + /// Prepares a call for execution by updating its properties and adding it to its collection for storage + fileprivate func _prepareCall(apiCall: ApiCall) throws { + apiCall.lastAttemptDate = Date() + apiCall.attemptsCount += 1 + self.addOrUpdate(apiCall) + } + + /// Reschedule API calls if necessary + func rescheduleApiCallsIfNecessary() { + if !self._isRetryingCalls { + self._rescheduleApiCalls() + } + } + + /// Sends an insert api call for the provided [instance] + func sendInsertion(_ instance: T) { + Task { + do { + try await self._synchronize(instance, method: HTTPMethod.post) + } catch { + self.rescheduleApiCallsIfNecessary() + Logger.error(error) + } + } + } + + /// Sends an update api call for the provided [instance] + func sendUpdate(_ instance: T) { + Task { + do { + try await self._synchronize(instance, method: HTTPMethod.put) + } catch { + self.rescheduleApiCallsIfNecessary() + Logger.error(error) + } + } + + } + + /// Sends an delete api call for the provided [instance] + func sendDeletion(_ instance: T) { + Task { + do { + try await self._synchronize(instance, method: HTTPMethod.delete) + } catch { + self.rescheduleApiCallsIfNecessary() + Logger.error(error) + } + } + + } + + fileprivate func _synchronize(_ instance: T, method: HTTPMethod) async throws { + if let apiCall = try self._callForInstance(instance, method: method) { + try self._prepareCall(apiCall: apiCall) + try await self._executeApiCall(apiCall) + } + } + + fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws { + let result = try await self._store.execute(apiCall: apiCall) + switch apiCall.method { + case .post: + if let instance = self.findById(result.stringId) { + self._hasChanged = instance.copyFromServerInstance(result) + } + default: + break + } + Logger.log("") + } + + func contentOfApiCallFile() -> String? { + guard let fileURL = try? self._urlForJSONFile() else { return nil } + if FileManager.default.fileExists(atPath: fileURL.path()) { + return try? FileUtils.readFile(fileURL: fileURL) + } + return nil + } + +} diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index 1f2cf08..9d06665 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -102,9 +102,7 @@ public class Services { case 200..<300: if let apiCallId, let collectionName = (T.self as? any Storable.Type)?.resourceName() { - try await MainActor.run { - try Store.main.deleteApiCallById(apiCallId, collectionName: collectionName) - } + try await Store.main.deleteApiCallById(apiCallId, collectionName: collectionName) } default: /* diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index 319712e..dd2a9af 100644 --- a/LeStorage/Storable.swift +++ b/LeStorage/Storable.swift @@ -47,4 +47,20 @@ extension Storable { return path } + static func storageDirectoryPath() throws -> URL { + return try FileUtils.pathForDirectoryInDocuments(directory: Store.storageDirectory) + } + + static func writeToStorageDirectory(content: String, fileName: String) throws { + var fileURL = try self.storageDirectoryPath() + fileURL.append(component: fileName) + try content.write(to: fileURL, atomically: false, encoding: .utf8) + } + + static func urlForJSONFile() throws -> URL { + var storageDirectory = try self.storageDirectoryPath() + storageDirectory.append(component: self.fileName()) + return storageDirectory + } + } diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 16025c0..9d1921b 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -199,9 +199,9 @@ public class Store { // MARK: - Api call rescheduling /// Deletes an ApiCall by [id] and [collectionName] - func deleteApiCallById(_ id: String, collectionName: String) throws { + func deleteApiCallById(_ id: String, collectionName: String) async throws { if let collection = self._collections[collectionName] { - try collection.deleteApiCallById(id) + try await collection.deleteApiCallById(id) } else { throw StoreError.collectionNotRegistered(type: collectionName) } @@ -257,8 +257,13 @@ public class Store { } /// Returns whether any collection has pending API calls - public func hasPendingAPICalls() -> Bool { - return self._collections.values.contains(where: { $0.hasPendingAPICalls() }) + public func hasPendingAPICalls() async -> Bool { + for collection in self._collections.values { + if await collection.hasPendingAPICalls() { + return true + } + } + return false } /// Returns the names of all collections @@ -267,8 +272,8 @@ public class Store { } /// Returns the content of the api call file - public func apiCallsFile(resourceName: String) -> String { - return self._collections[resourceName]?.contentOfApiCallFile() ?? "" + public func apiCallsFileContent(resourceName: String) async -> String { + return await self._collections[resourceName]?.contentOfApiCallFile() ?? "" } /// This method triggers the framework to save and send failed api calls @@ -282,23 +287,29 @@ public class Store { guard let failedAPICallsCollection = self._failedAPICallsCollection, let collection = self._collections[collectionName], - collectionName != FailedAPICall.resourceName(), - let apiCall = try? collection.apiCallById(apiCallId) else { + collectionName != FailedAPICall.resourceName() + else { return } - if !failedAPICallsCollection.contains(where: { $0.callId == apiCallId }) && apiCall.attemptsCount > 6 { + Task { + if let apiCall = await collection.apiCallById(apiCallId) { + + if !failedAPICallsCollection.contains(where: { $0.callId == apiCallId }) && apiCall.attemptsCount > 6 { - do { - let authValue = request.allHTTPHeaderFields?["Authorization"] - let string = try apiCall.jsonString() - let failedAPICall = FailedAPICall(callId: apiCall.id, type: collectionName, apiCall: string, error: error, authentication: authValue) - try failedAPICallsCollection.addOrUpdate(instance: failedAPICall) - } catch { - Logger.error(error) + do { + let authValue = request.allHTTPHeaderFields?["Authorization"] + let string = try apiCall.jsonString() + let failedAPICall = FailedAPICall(callId: apiCall.id, type: collectionName, apiCall: string, error: error, authentication: authValue) + try failedAPICallsCollection.addOrUpdate(instance: failedAPICall) + } catch { + Logger.error(error) + } + } } } + } func logFailedAPICall(request: URLRequest, error: String) { diff --git a/LeStorage/StoredCollection.swift b/LeStorage/StoredCollection.swift index ac38547..f21f683 100644 --- a/LeStorage/StoredCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -13,22 +13,34 @@ enum StoredCollectionError: Error { case missingInstance } +protocol CollectionHolder { + associatedtype Item + + var items: [Item] { get } +} + +extension CollectionHolder { + +} + protocol SomeCollection: Identifiable { var resourceName: String { get } var synchronized: Bool { get } func allItems() -> [any Storable] func deleteById(_ id: String) throws - func deleteApiCallById(_ id: String) throws func loadDataFromServerIfAllowed() async throws - func hasPendingAPICalls() -> Bool - func contentOfApiCallFile() -> String? - func reset() func resetApiCalls() - func apiCallById(_ id: String) throws -> (any SomeCall)? + + func deleteApiCallById(_ id: String) async throws + func apiCallById(_ id: String) async -> (any SomeCall)? + + func hasPendingAPICalls() async -> Bool + func contentOfApiCallFile() async -> String? + } extension Notification.Name { @@ -36,7 +48,7 @@ extension Notification.Name { public static let CollectionDidChange: Notification.Name = Notification.Name.init("notification.collectionDidChange") } -public class StoredCollection: RandomAccessCollection, SomeCollection { +public class StoredCollection: RandomAccessCollection, SomeCollection, CollectionHolder { /// If true, will synchronize the data with the provided server located at the Store's synchronizationApiURL let synchronized: Bool @@ -60,7 +72,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti fileprivate var _indexes: [String : T]? = nil /// Collection of API calls used to store HTTP calls - fileprivate var apiCallsCollection: StoredCollection>? = nil + fileprivate var apiCallsCollection: ApiCallCollection? = nil /// Indicates whether the collection has changed, thus requiring a write operation fileprivate var _hasChanged: Bool = false { @@ -94,9 +106,20 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti self.loadCompletion = loadCompletion if synchronized { - self.apiCallsCollection = StoredCollection>(synchronized: false, store: store, loadCompletion: { apiCallCollection in - self._rescheduleApiCalls() - }) + let apiCallCollection = ApiCallCollection(store: store) + self.apiCallsCollection = apiCallCollection + Task { + do { + try await apiCallCollection.loadFromFile() + } catch { + Logger.error(error) + } + } + + +// self.apiCallsCollection = StoredCollection>(synchronized: false, store: store, loadCompletion: { apiCallCollection in +// self._rescheduleApiCalls() +// }) } self._load() @@ -216,20 +239,20 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti // update if let index = self.items.firstIndex(where: { $0.id == instance.id }) { self.items[index] = instance - try self._sendUpdateIfNecessary(instance) + self._sendUpdateIfNecessary(instance) } else { // insert self.items.append(instance) - try self._sendInsertionIfNecessary(instance) + self._sendInsertionIfNecessary(instance) } self._indexes?[instance.stringId] = instance } - public func writeChangeAndInsertOnServer(instance: T) throws { + public func writeChangeAndInsertOnServer(instance: T) { defer { self._hasChanged = true } - try self._sendInsertionIfNecessary(instance) + self._sendInsertionIfNecessary(instance) } /// A method the treat the collection as a single instance holder @@ -251,8 +274,8 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti try instance.deleteDependencies() self.items.removeAll { $0.id == instance.id } self._indexes?.removeValue(forKey: instance.stringId) - try self._sendDeletionIfNecessary(instance) + self._sendDeletionIfNecessary(instance) } /// Deletes all items of the sequence by id @@ -266,7 +289,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti try instance.deleteDependencies() self.items.removeAll { $0.id == instance.id } self._indexes?.removeValue(forKey: instance.stringId) - try self._sendDeletionIfNecessary(instance) + self._sendDeletionIfNecessary(instance) } } @@ -285,12 +308,12 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti if let index = self.items.firstIndex(where: { $0.id == instance.id }) { self.items[index] = instance if shouldSync { - try self._sendUpdateIfNecessary(instance) + self._sendUpdateIfNecessary(instance) } } else { // insert self.items.append(instance) if shouldSync { - try self._sendInsertionIfNecessary(instance) + self._sendInsertionIfNecessary(instance) } } self._indexes?[instance.stringId] = instance @@ -322,9 +345,9 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti for item in items { self.items.removeAll(where: { $0.id == item.id }) - /// remove related API call if existing - if let apiCallIndex = self.apiCallsCollection?.firstIndex(where: { $0.dataId == item.id }) { - self.apiCallsCollection?.items.remove(at: apiCallIndex) + Task { + /// Remove related API call if existing + await self.apiCallsCollection?.deleteByDataId(item.stringId) } } @@ -335,6 +358,16 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti try self.delete(contentOfs: self.items) } + // MARK: - Some Collection + + func deleteApiCallById(_ id: String) async throws { + await self.apiCallsCollection?.deleteById(id) + } + + func apiCallById(_ id: String) async -> (any SomeCall)? { + return await self.apiCallsCollection?.findById(id) + } + // MARK: - SomeCall /// Returns the collection items as [any Storable] @@ -395,202 +428,110 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti /// Removes the collection related API calls collection public func resetApiCalls() { if let apiCallsCollection = self.apiCallsCollection { - apiCallsCollection.reset() - } - } - - // MARK: - Synchronization - - /// Returns an APICall instance for the Storable [instance] and an HTTP [method] - /// The method updates existing calls or creates a new one - fileprivate func _callForInstance(_ instance: T, method: HTTPMethod) throws -> ApiCall? { - guard let apiCallCollection = self.apiCallsCollection else { - throw StoredCollectionError.missingApiCallCollection - } - - if let existingCall = apiCallCollection.first(where: { $0.dataId == instance.id }) { - switch method { - case .delete: - try self.deleteApiCallById(existingCall.id) // delete the existing call as we don't need it - if existingCall.method == HTTPMethod.post { - return nil // if the post has not been done, we can just stop here - } else { - return try self._createCall(instance, method: method) // otherwise it's a put and we want to send the delete - } - default: // here we should only trying to PUT, so we update the existing POST/PUT with the instance new values - existingCall.body = try instance.jsonString() - return existingCall + Task { + await apiCallsCollection.reset() } - } else { - return try self._createCall(instance, method: method) } } - /// Creates an API call for the Storable [instance] and an HTTP [method] - fileprivate func _createCall(_ instance: T, method: HTTPMethod) throws -> ApiCall { - let jsonString = try instance.jsonString() - return ApiCall(method: method, dataId: String(instance.id), body: jsonString) - } - - /// Prepares a call for execution by updating its properties and adding it to its collection for storage - fileprivate func _prepareCall(apiCall: ApiCall) throws { - apiCall.lastAttemptDate = Date() - apiCall.attemptsCount += 1 - try self.apiCallsCollection?.addOrUpdate(instance: apiCall) - } + // MARK: - Reschedule calls /// Sends an insert api call for the provided [instance] - fileprivate func _sendInsertionIfNecessary(_ instance: T) throws { + fileprivate func _sendInsertionIfNecessary(_ instance: T) { guard self.synchronized, Store.main.collectionsCanSynchronize else { return } - Task { - do { - try await self._synchronize(instance, method: HTTPMethod.post) - } catch { - self.rescheduleApiCallsIfNecessary() - Logger.error(error) - } + await self.apiCallsCollection?.sendInsertion(instance) } } /// Sends an update api call for the provided [instance] - fileprivate func _sendUpdateIfNecessary(_ instance: T) throws { + fileprivate func _sendUpdateIfNecessary(_ instance: T) { guard self.synchronized, self._sendsUpdate, Store.main.collectionsCanSynchronize else { return } Task { - do { - try await self._synchronize(instance, method: HTTPMethod.put) - } catch { - self.rescheduleApiCallsIfNecessary() - Logger.error(error) - } + await self.apiCallsCollection?.sendUpdate(instance) } - } /// Sends an delete api call for the provided [instance] - fileprivate func _sendDeletionIfNecessary(_ instance: T) throws { + fileprivate func _sendDeletionIfNecessary(_ instance: T) { guard self.synchronized, Store.main.collectionsCanSynchronize else { return } Task { - do { - try await self._synchronize(instance, method: HTTPMethod.delete) - } catch { - self.rescheduleApiCallsIfNecessary() - Logger.error(error) - } - } - - } - - fileprivate func _synchronize(_ instance: T, method: HTTPMethod) async throws { - if let apiCall = try self._callForInstance(instance, method: method) { - try self._prepareCall(apiCall: apiCall) - try await self._executeApiCall(apiCall) - } - } - - fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws { - let result = try await self._store.execute(apiCall: apiCall) - switch apiCall.method { - case .post: -// DispatchQueue.main.async { - if let instance = self.findById(result.stringId) { - self._hasChanged = instance.copyFromServerInstance(result) - } -// } - default: - break + await self.apiCallsCollection?.sendDeletion(instance) } - Logger.log("") } - // MARK: - Reschedule calls - - /// number of time an execution loop has been called - fileprivate var _attemptLoops: Int = 0 - - /// Indicates if the collection is currently retrying ApiCalls - fileprivate var _isRetryingCalls: Bool = false - - /// Reschedule API calls if necessary func rescheduleApiCallsIfNecessary() { - if !self._isRetryingCalls { - self._rescheduleApiCalls() - } - } - - /// Reschedule API calls - fileprivate func _rescheduleApiCalls() { - - guard let apiCallsCollection, apiCallsCollection.isNotEmpty else { - return - } - - self._isRetryingCalls = true - self._attemptLoops += 1 - Task { - - let delay = pow(2, self._attemptLoops) - let seconds = NSDecimalNumber(decimal: delay).intValue - Logger.log("wait for \(seconds) sec") - try await Task.sleep(until: .now + .seconds(seconds)) - - let apiCallsCopy = apiCallsCollection.items - for apiCall in apiCallsCopy { - apiCall.attemptsCount += 1 - apiCall.lastAttemptDate = Date() - - do { - try await self._executeApiCall(apiCall) -// let _ = try await Store.main.execute(apiCall: apiCall) - } catch { - Logger.error(error) - } - } - - if apiCallsCollection.isEmpty { - self._isRetryingCalls = false - } else { - self._rescheduleApiCalls() - } - + await self.apiCallsCollection?.rescheduleApiCallsIfNecessary() } - - } - - /// Deletes an API call by [id] - func deleteApiCallById(_ id: String) throws { - guard let apiCallsCollection else { - throw StoreError.apiCallCollectionNotRegistered(type: T.resourceName()) - } - try apiCallsCollection.deleteById(id) } - func apiCallById(_ id: String) throws -> (any SomeCall)? { - guard let apiCallsCollection else { - throw StoreError.apiCallCollectionNotRegistered(type: T.resourceName()) - } - return apiCallsCollection.findById(id) +// +// /// number of time an execution loop has been called +// fileprivate var _attemptLoops: Int = 0 +// +// /// Indicates if the collection is currently retrying ApiCalls +// fileprivate var _isRetryingCalls: Bool = false +// +// /// Reschedule API calls +// fileprivate func _rescheduleApiCalls() { +// +// guard let apiCallsCollection, apiCallsCollection.isNotEmpty else { +// return +// } +// +// self._isRetryingCalls = true +// self._attemptLoops += 1 +// +// Task { +// +// let delay = pow(2, self._attemptLoops) +// let seconds = NSDecimalNumber(decimal: delay).intValue +// Logger.log("wait for \(seconds) sec") +// try await Task.sleep(until: .now + .seconds(seconds)) +// +// let apiCallsCopy = apiCallsCollection.items +// for apiCall in apiCallsCopy { +// apiCall.attemptsCount += 1 +// apiCall.lastAttemptDate = Date() +// +// do { +// try await self._executeApiCall(apiCall) +//// let _ = try await Store.main.execute(apiCall: apiCall) +// } catch { +// Logger.error(error) +// } +// } +// +// if apiCallsCollection.isEmpty { +// self._isRetryingCalls = false +// } else { +// self._rescheduleApiCalls() +// } +// +// } +// +// } + + func contentOfApiCallFile() async -> String? { + return await self.apiCallsCollection?.contentOfApiCallFile() +// guard let fileURL = try? self.apiCallsCollection?._urlForJSONFile() else { return nil } +// if FileManager.default.fileExists(atPath: fileURL.path()) { +// return try? FileUtils.readFile(fileURL: fileURL) +// } +// return nil } /// Returns if the API call collection is not empty - func hasPendingAPICalls() -> Bool { + func hasPendingAPICalls() async -> Bool { guard let apiCallsCollection else { return false } - return apiCallsCollection.isNotEmpty - } - - func contentOfApiCallFile() -> String? { - guard let fileURL = try? self.apiCallsCollection?._urlForJSONFile() else { return nil } - if FileManager.default.fileExists(atPath: fileURL.path()) { - return try? FileUtils.readFile(fileURL: fileURL) - } - return nil + return await apiCallsCollection.items.isNotEmpty } // MARK: - RandomAccessCollection