Improve request execution to handle various return parameters + fixes

sync2
Laurent 1 year ago
parent 30306d2d50
commit 67f07cfb6f
  1. 2
      LeStorage.xcodeproj/project.pbxproj
  2. 43
      LeStorage/ApiCallCollection.swift
  3. 65
      LeStorage/Services.swift
  4. 2
      LeStorage/Store.swift
  5. 53
      LeStorage/StoreCenter.swift
  6. 12
      LeStorage/StoredCollection.swift

@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 56; objectVersion = 70;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */

@ -144,6 +144,7 @@ actor ApiCallCollection<T: Storable>: SomeCallCollection {
do { do {
try await Task.sleep(until: .now + .seconds(seconds)) try await Task.sleep(until: .now + .seconds(seconds))
} catch { } catch {
Logger.w("*** WAITING CRASHED !!!")
Logger.error(error) Logger.error(error)
} }
} }
@ -158,34 +159,48 @@ actor ApiCallCollection<T: Storable>: SomeCallCollection {
/// Reschedule the execution of API calls /// Reschedule the execution of API calls
fileprivate func _rescheduleApiCalls() async { fileprivate func _rescheduleApiCalls() async {
Logger.log("\(T.resourceName()) > RESCHED")
guard !self._isRescheduling else { return } guard !self._isRescheduling else { return }
self._isRescheduling = true guard self.items.isNotEmpty else { return }
guard self.items.isNotEmpty else { self._isRescheduling = true
return
}
self._attemptLoops += 1 self._attemptLoops += 1
await self._wait() await self._wait()
let apiCallsCopy = self.items let apiCallsCopy = self.items
for (index, apiCall) in apiCallsCopy.enumerated() { for apiCall in apiCallsCopy {
apiCall.attemptsCount += 1 apiCall.attemptsCount += 1
apiCall.lastAttemptDate = Date() apiCall.lastAttemptDate = Date()
do { 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 { } catch {
Logger.log("\(T.resourceName()) > API CALL RETRY ERROR:")
Logger.error(error) Logger.error(error)
} }
} }
Logger.log("\(T.resourceName()) > STOP RESCHED")
self._isRescheduling = false self._isRescheduling = false
if self.items.isNotEmpty { if self.items.isNotEmpty {
await self._rescheduleApiCalls() await self._rescheduleApiCalls()
} }
Logger.log("\(T.resourceName()) > isRescheduling = \(self._isRescheduling)")
} }
// MARK: - Synchronization // MARK: - Synchronization
@ -249,18 +264,18 @@ actor ApiCallCollection<T: Storable>: SomeCallCollection {
} }
/// Sends an delete api call for the provided [instance] /// Sends an delete api call for the provided [instance]
func sendDeletion(_ instance: T) async throws -> T? { func sendDeletion(_ instance: T) async throws {
do { do {
return try await self._synchronize(instance, method: HTTPMethod.delete) let _: Empty? = try await self._synchronize(instance, method: HTTPMethod.delete)
} catch { } catch {
self.rescheduleApiCallsIfNecessary() self.rescheduleApiCallsIfNecessary()
Logger.error(error) Logger.error(error)
} }
return nil return
} }
/// Initiates the process of sending the data with the server /// Initiates the process of sending the data with the server
fileprivate func _synchronize(_ instance: T, method: HTTPMethod) async throws -> T? { fileprivate func _synchronize<V: Decodable>(_ instance: T, method: HTTPMethod) async throws -> V? {
if let apiCall = try self._callForInstance(instance, method: method) { if let apiCall = try self._callForInstance(instance, method: method) {
try self._prepareCall(apiCall: apiCall) try self._prepareCall(apiCall: apiCall)
return try await self._executeApiCall(apiCall) return try await self._executeApiCall(apiCall)
@ -271,7 +286,13 @@ actor ApiCallCollection<T: Storable>: SomeCallCollection {
/// Executes an API call /// Executes an API call
/// For POST requests, potentially copies additional data coming from the server during the insert /// For POST requests, potentially copies additional data coming from the server during the insert
fileprivate func _executeApiCall(_ apiCall: ApiCall<T>) async throws -> T { // fileprivate func _executeApiCall(_ apiCall: ApiCall<T>) 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<V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V {
return try await StoreCenter.main.execute(apiCall: apiCall) return try await StoreCenter.main.execute(apiCall: apiCall)
} }

@ -100,17 +100,17 @@ public class Services {
/// - serviceConf: A instance of ServiceConf /// - serviceConf: A instance of ServiceConf
/// - payload: a codable value stored in the body of the request /// - payload: a codable value stored in the body of the request
/// - apiCallId: an optional id referencing an ApiCall /// - apiCallId: an optional id referencing an ApiCall
fileprivate func _runRequest<T: Encodable, U: Decodable>(serviceCall: ServiceCall, payload: T, apiCallId: String? = nil) async throws -> U { fileprivate func _runRequest<T: Encodable, U: Decodable>(serviceCall: ServiceCall, payload: T) async throws -> U {
var request = try self._baseRequest(call: serviceCall) var request = try self._baseRequest(call: serviceCall)
request.httpBody = try jsonEncoder.encode(payload) 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 /// Runs a request using a traditional URLRequest
/// - Parameters: /// - Parameters:
/// - request: the URLRequest to run /// - 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 /// - 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<T: Decodable>(_ request: URLRequest, apiCallId: String? = nil) async throws -> T { fileprivate func _runRequest<T: Storable, V: Decodable>(_ request: URLRequest, apiCall: ApiCall<T>) async throws -> V {
let debugURL = request.url?.absoluteString ?? "" let debugURL = request.url?.absoluteString ?? ""
print("Run \(request.httpMethod ?? "") \(debugURL)") print("Run \(request.httpMethod ?? "") \(debugURL)")
let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) let task: (Data, URLResponse) = try await URLSession.shared.data(for: request)
@ -121,13 +121,7 @@ public class Services {
print("\(debugURL) ended, status code = \(statusCode)") print("\(debugURL) ended, status code = \(statusCode)")
switch statusCode { switch statusCode {
case 200..<300: // success case 200..<300: // success
if let apiCallId { try await StoreCenter.main.deleteApiCallById(type: T.self, id: apiCall.id)
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)")
}
}
default: // error default: // error
Logger.log("Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") Logger.log("Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")")
let errorString: String = String(data: task.0, encoding: .utf8) ?? "" let errorString: String = String(data: task.0, encoding: .utf8) ?? ""
@ -137,12 +131,8 @@ public class Services {
errorMessage = message errorMessage = message
} }
if let apiCallId, let type = (T.self as? any Storable.Type) { try await StoreCenter.main.rescheduleApiCalls(id: apiCall.id, type: T.self)
try await StoreCenter.main.rescheduleApiCalls(id: apiCallId, type: type) StoreCenter.main.logFailedAPICall(apiCall.id, request: request, collectionName: T.resourceName(), error: errorMessage.message)
StoreCenter.main.logFailedAPICall(apiCallId, request: request, collectionName: type.resourceName(), error: errorMessage.message)
} else {
StoreCenter.main.logFailedAPICall(request: request, error: errorMessage.message)
}
throw ServiceError.responseError(response: errorMessage.error) throw ServiceError.responseError(response: errorMessage.error)
} }
@ -152,7 +142,44 @@ public class Services {
Logger.w(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<V: Decodable>(_ 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 /// Returns if the token is required for a request
@ -254,10 +281,10 @@ public class Services {
} }
/// Executes an ApiCall /// Executes an ApiCall
func runApiCall<T: Storable>(_ apiCall: ApiCall<T>) async throws -> T { func runApiCall<T: Storable, V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V {
let request = try self._request(from: apiCall) let request = try self._request(from: apiCall)
print("HTTP \(request.httpMethod ?? "") : id = \(apiCall.dataId)") 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 /// Returns the URLRequest for an ApiCall

@ -233,7 +233,7 @@ open class Store {
/// Requests a deletion to the StoreCenter /// Requests a deletion to the StoreCenter
/// - Parameters: /// - Parameters:
/// - instance: an object to delete /// - instance: an object to delete
@discardableResult func sendDeletion<T: Storable>(_ instance: T) async throws -> T? { func sendDeletion<T: Storable>(_ instance: T) async throws {
return try await StoreCenter.main.sendDeletion(instance) return try await StoreCenter.main.sendDeletion(instance)
} }

@ -217,12 +217,22 @@ public class StoreCenter {
} }
/// Executes an ApiCall /// Executes an ApiCall
fileprivate func _executeApiCall<T: Storable>(_ apiCall: ApiCall<T>) async throws -> T { // fileprivate func _executeApiCall<T: Storable>(_ apiCall: ApiCall<T>) async throws -> T {
// return try await self.service().runApiCall(apiCall)
// }
/// Executes an ApiCall
fileprivate func _executeApiCall<T: Storable, V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V {
return try await self.service().runApiCall(apiCall) return try await self.service().runApiCall(apiCall)
} }
/// Executes an API call /// Executes an API call
func execute<T>(apiCall: ApiCall<T>) async throws -> T { // func execute<T>(apiCall: ApiCall<T>) async throws -> T {
// return try await self._executeApiCall(apiCall)
// }
/// Executes an API call
func execute<T, V: Decodable>(apiCall: ApiCall<T>) async throws -> V {
return try await self._executeApiCall(apiCall) return try await self._executeApiCall(apiCall)
} }
@ -385,7 +395,7 @@ public class StoreCenter {
/// Transmit the update request to the ApiCall collection /// Transmit the update request to the ApiCall collection
/// - Parameters: /// - Parameters:
/// - instance: an object to update /// - instance: an object to update
func sendUpdate<T: Storable>(_ instance: T) async throws -> T? { func sendUpdate<T: Storable>(_ instance: T) async throws -> T? {
guard self._canSynchronise() else { guard self._canSynchronise() else {
return nil return nil
} }
@ -395,11 +405,42 @@ public class StoreCenter {
/// Transmit the deletion request to the ApiCall collection /// Transmit the deletion request to the ApiCall collection
/// - Parameters: /// - Parameters:
/// - instance: an object to delete /// - instance: an object to delete
func sendDeletion<T: Storable>(_ instance: T) async throws -> T? { func sendDeletion<T: Storable>(_ instance: T) async throws {
guard self._canSynchronise() else { guard self._canSynchronise() else {
return nil return
}
try await self.apiCallCollection().sendDeletion(instance)
}
func updateFromServerInstance<T: Storable>(_ result: T) {
if let storedCollection: StoredCollection<T> = self.collectionOfInstance(result) {
if storedCollection.findById(result.id) != nil {
storedCollection.updateFromServerInstance(result)
}
}
}
func collectionOfInstance<T: Storable>(_ instance: T) -> StoredCollection<T>? {
do {
let storedCollection: StoredCollection<T> = try Store.main.collection()
if storedCollection.findById(instance.id) != nil {
return storedCollection
} else {
return self.collectionOfInstanceInSubStores(instance)
}
} catch {
return self.collectionOfInstanceInSubStores(instance)
}
}
func collectionOfInstanceInSubStores<T: Storable>(_ instance: T) -> StoredCollection<T>? {
for store in self._stores.values {
let storedCollection: StoredCollection<T>? = try? store.collection()
if storedCollection?.findById(instance.id) != nil {
return storedCollection
}
} }
return try await self.apiCallCollection().sendDeletion(instance) return nil
} }
// MARK: - Logs // MARK: - Logs

@ -429,9 +429,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
Task { Task {
do { do {
if let result = try await self._store.sendInsertion(instance) { if let result = try await self._store.sendInsertion(instance) {
DispatchQueue.main.async { self.updateFromServerInstance(result)
self._hasChanged = instance.copyFromServerInstance(result)
}
} }
} catch { } catch {
Logger.error(error) Logger.error(error)
@ -439,6 +437,14 @@ public class StoredCollection<T: Storable>: 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] /// Sends an update api call for the provided [instance]
/// - Parameters: /// - Parameters:
/// - instance: the object to PUT /// - instance: the object to PUT

Loading…
Cancel
Save