From 67f07cfb6f73e113ecf88aaa94610003e29122d7 Mon Sep 17 00:00:00 2001 From: Laurent Date: Sat, 28 Sep 2024 16:21:31 +0200 Subject: [PATCH] Improve request execution to handle various return parameters + fixes --- LeStorage.xcodeproj/project.pbxproj | 2 +- LeStorage/ApiCallCollection.swift | 43 +++++++++++++----- LeStorage/Services.swift | 67 ++++++++++++++++++++--------- LeStorage/Store.swift | 2 +- LeStorage/StoreCenter.swift | 53 ++++++++++++++++++++--- LeStorage/StoredCollection.swift | 12 ++++-- 6 files changed, 137 insertions(+), 42 deletions(-) diff --git a/LeStorage.xcodeproj/project.pbxproj b/LeStorage.xcodeproj/project.pbxproj index 0a30bde..82a5a0f 100644 --- a/LeStorage.xcodeproj/project.pbxproj +++ b/LeStorage.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ diff --git a/LeStorage/ApiCallCollection.swift b/LeStorage/ApiCallCollection.swift index a48ab5d..ccc3664 100644 --- a/LeStorage/ApiCallCollection.swift +++ b/LeStorage/ApiCallCollection.swift @@ -144,6 +144,7 @@ actor ApiCallCollection: SomeCallCollection { do { try await Task.sleep(until: .now + .seconds(seconds)) } catch { + Logger.w("*** WAITING CRASHED !!!") Logger.error(error) } } @@ -158,34 +159,48 @@ actor ApiCallCollection: SomeCallCollection { /// Reschedule the execution of API calls fileprivate func _rescheduleApiCalls() async { + Logger.log("\(T.resourceName()) > RESCHED") guard !self._isRescheduling else { return } + guard self.items.isNotEmpty else { return } + self._isRescheduling = true - guard self.items.isNotEmpty else { - return - } - self._attemptLoops += 1 await self._wait() let apiCallsCopy = self.items - for (index, apiCall) in apiCallsCopy.enumerated() { + for apiCall in apiCallsCopy { apiCall.attemptsCount += 1 apiCall.lastAttemptDate = Date() do { - let _ = try await self._executeApiCall(apiCall) + switch apiCall.method { + case .post: + let result: T = try await self._executeApiCall(apiCall) + StoreCenter.main.updateFromServerInstance(result) + Logger.log("\(T.resourceName()) > SUCCESS!") + case .put: + let _: T = try await self._executeApiCall(apiCall) + case .delete: + let _: Empty = try await self._executeApiCall(apiCall) + case .get: + let _: [T] = try await self._executeApiCall(apiCall) + } } catch { + Logger.log("\(T.resourceName()) > API CALL RETRY ERROR:") Logger.error(error) } } + Logger.log("\(T.resourceName()) > STOP RESCHED") + self._isRescheduling = false if self.items.isNotEmpty { await self._rescheduleApiCalls() } + Logger.log("\(T.resourceName()) > isRescheduling = \(self._isRescheduling)") } // MARK: - Synchronization @@ -249,18 +264,18 @@ actor ApiCallCollection: SomeCallCollection { } /// Sends an delete api call for the provided [instance] - func sendDeletion(_ instance: T) async throws -> T? { + func sendDeletion(_ instance: T) async throws { do { - return try await self._synchronize(instance, method: HTTPMethod.delete) + let _: Empty? = try await self._synchronize(instance, method: HTTPMethod.delete) } catch { self.rescheduleApiCallsIfNecessary() Logger.error(error) } - return nil + return } /// Initiates the process of sending the data with the server - fileprivate func _synchronize(_ instance: T, method: HTTPMethod) async throws -> T? { + fileprivate func _synchronize(_ instance: T, method: HTTPMethod) async throws -> V? { if let apiCall = try self._callForInstance(instance, method: method) { try self._prepareCall(apiCall: apiCall) return try await self._executeApiCall(apiCall) @@ -271,7 +286,13 @@ actor ApiCallCollection: SomeCallCollection { /// Executes an API call /// For POST requests, potentially copies additional data coming from the server during the insert - fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws -> T { +// fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws -> T { +// return try await StoreCenter.main.execute(apiCall: apiCall) +// } + + /// Executes an API call + /// For POST requests, potentially copies additional data coming from the server during the insert + fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws -> V { return try await StoreCenter.main.execute(apiCall: apiCall) } diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index b70b301..4a4db8b 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -100,17 +100,17 @@ public class Services { /// - serviceConf: A instance of ServiceConf /// - payload: a codable value stored in the body of the request /// - apiCallId: an optional id referencing an ApiCall - fileprivate func _runRequest(serviceCall: ServiceCall, payload: T, apiCallId: String? = nil) async throws -> U { + fileprivate func _runRequest(serviceCall: ServiceCall, payload: T) async throws -> U { var request = try self._baseRequest(call: serviceCall) request.httpBody = try jsonEncoder.encode(payload) - return try await _runRequest(request, apiCallId: apiCallId) + return try await _runRequest(request) } /// Runs a request using a traditional URLRequest /// - Parameters: /// - request: the URLRequest to run /// - apiCallId: the id of the ApiCall to delete in case of success, or to schedule for a rerun in case of failure - fileprivate func _runRequest(_ request: URLRequest, apiCallId: String? = nil) async throws -> T { + fileprivate func _runRequest(_ request: URLRequest, apiCall: ApiCall) async throws -> V { let debugURL = request.url?.absoluteString ?? "" print("Run \(request.httpMethod ?? "") \(debugURL)") let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) @@ -121,13 +121,7 @@ public class Services { print("\(debugURL) ended, status code = \(statusCode)") switch statusCode { case 200..<300: // success - if let apiCallId { - if let collectionName = (T.self as? any Storable.Type)?.resourceName() { - try await StoreCenter.main.deleteApiCallById(apiCallId, collectionName: collectionName) - } else { - StoreCenter.main.log(message: "collectionName not found for \(type(of: T.self)), could not delete ApiCall \(apiCallId)") - } - } + try await StoreCenter.main.deleteApiCallById(type: T.self, id: apiCall.id) default: // error Logger.log("Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") let errorString: String = String(data: task.0, encoding: .utf8) ?? "" @@ -137,12 +131,8 @@ public class Services { errorMessage = message } - if let apiCallId, let type = (T.self as? any Storable.Type) { - try await StoreCenter.main.rescheduleApiCalls(id: apiCallId, type: type) - StoreCenter.main.logFailedAPICall(apiCallId, request: request, collectionName: type.resourceName(), error: errorMessage.message) - } else { - StoreCenter.main.logFailedAPICall(request: request, error: errorMessage.message) - } + try await StoreCenter.main.rescheduleApiCalls(id: apiCall.id, type: T.self) + StoreCenter.main.logFailedAPICall(apiCall.id, request: request, collectionName: T.resourceName(), error: errorMessage.message) throw ServiceError.responseError(response: errorMessage.error) } @@ -151,8 +141,45 @@ public class Services { StoreCenter.main.log(message: message) Logger.w(message) } - - return try jsonDecoder.decode(T.self, from: task.0) + + if !(V.self is Empty?.Type) { + return try jsonDecoder.decode(V.self, from: task.0) + } else { + return try jsonDecoder.decode(V.self, from: "{}".data(using: .utf8)!) + } + } + + /// Runs a request using a traditional URLRequest + /// - Parameters: + /// - request: the URLRequest to run + /// - apiCallId: the id of the ApiCall to delete in case of success, or to schedule for a rerun in case of failure + fileprivate func _runRequest(_ request: URLRequest) async throws -> V { + let debugURL = request.url?.absoluteString ?? "" + print("Run \(request.httpMethod ?? "") \(debugURL)") + let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) + print("response = \(String(data: task.0, encoding: .utf8) ?? "")") + + if let response = task.1 as? HTTPURLResponse { + let statusCode = response.statusCode + print("\(debugURL) ended, status code = \(statusCode)") + switch statusCode { + case 200..<300: // success + break + default: // error + Logger.log("Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") + let errorString: String = String(data: task.0, encoding: .utf8) ?? "" + var errorMessage = ErrorMessage(error: errorString, domain: "") + if let message = self.errorMessageFromResponse(data: task.0) { + errorMessage = message + } + throw ServiceError.responseError(response: errorMessage.error) + } + } else { + let message: String = "Unexpected and unmanaged URL Response \(task.1)" + StoreCenter.main.log(message: message) + Logger.w(message) + } + return try jsonDecoder.decode(V.self, from: task.0) } /// Returns if the token is required for a request @@ -254,10 +281,10 @@ public class Services { } /// Executes an ApiCall - func runApiCall(_ apiCall: ApiCall) async throws -> T { + func runApiCall(_ apiCall: ApiCall) async throws -> V { let request = try self._request(from: apiCall) print("HTTP \(request.httpMethod ?? "") : id = \(apiCall.dataId)") - return try await self._runRequest(request, apiCallId: apiCall.id) + return try await self._runRequest(request, apiCall: apiCall) } /// Returns the URLRequest for an ApiCall diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 824143b..2bbca5b 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -233,7 +233,7 @@ open class Store { /// Requests a deletion to the StoreCenter /// - Parameters: /// - instance: an object to delete - @discardableResult func sendDeletion(_ instance: T) async throws -> T? { + func sendDeletion(_ instance: T) async throws { return try await StoreCenter.main.sendDeletion(instance) } diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index aa91214..19610a2 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -217,12 +217,22 @@ public class StoreCenter { } /// Executes an ApiCall - fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws -> T { +// fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws -> T { +// return try await self.service().runApiCall(apiCall) +// } + + /// Executes an ApiCall + fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws -> V { return try await self.service().runApiCall(apiCall) } /// Executes an API call - func execute(apiCall: ApiCall) async throws -> T { +// func execute(apiCall: ApiCall) async throws -> T { +// return try await self._executeApiCall(apiCall) +// } + + /// Executes an API call + func execute(apiCall: ApiCall) async throws -> V { return try await self._executeApiCall(apiCall) } @@ -385,7 +395,7 @@ public class StoreCenter { /// Transmit the update request to the ApiCall collection /// - Parameters: /// - instance: an object to update - func sendUpdate(_ instance: T) async throws -> T? { + func sendUpdate(_ instance: T) async throws -> T? { guard self._canSynchronise() else { return nil } @@ -395,11 +405,42 @@ public class StoreCenter { /// Transmit the deletion request to the ApiCall collection /// - Parameters: /// - instance: an object to delete - func sendDeletion(_ instance: T) async throws -> T? { + func sendDeletion(_ instance: T) async throws { guard self._canSynchronise() else { - return nil + return + } + try await self.apiCallCollection().sendDeletion(instance) + } + + func updateFromServerInstance(_ result: T) { + if let storedCollection: StoredCollection = self.collectionOfInstance(result) { + if storedCollection.findById(result.id) != nil { + storedCollection.updateFromServerInstance(result) + } + } + } + + func collectionOfInstance(_ instance: T) -> StoredCollection? { + do { + let storedCollection: StoredCollection = try Store.main.collection() + if storedCollection.findById(instance.id) != nil { + return storedCollection + } else { + return self.collectionOfInstanceInSubStores(instance) + } + } catch { + return self.collectionOfInstanceInSubStores(instance) + } + } + + func collectionOfInstanceInSubStores(_ instance: T) -> StoredCollection? { + for store in self._stores.values { + let storedCollection: StoredCollection? = try? store.collection() + if storedCollection?.findById(instance.id) != nil { + return storedCollection + } } - return try await self.apiCallCollection().sendDeletion(instance) + return nil } // MARK: - Logs diff --git a/LeStorage/StoredCollection.swift b/LeStorage/StoredCollection.swift index 2a0a557..8701e41 100644 --- a/LeStorage/StoredCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -429,9 +429,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti Task { do { if let result = try await self._store.sendInsertion(instance) { - DispatchQueue.main.async { - self._hasChanged = instance.copyFromServerInstance(result) - } + self.updateFromServerInstance(result) } } catch { Logger.error(error) @@ -439,6 +437,14 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti } } + func updateFromServerInstance(_ serverInstance: T) { + DispatchQueue.main.async { + if let localInstance = self.findById(serverInstance.id) { + self._hasChanged = localInstance.copyFromServerInstance(serverInstance) + } + } + } + /// Sends an update api call for the provided [instance] /// - Parameters: /// - instance: the object to PUT