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. 67
      LeStorage/Services.swift
  4. 2
      LeStorage/Store.swift
  5. 53
      LeStorage/StoreCenter.swift
  6. 12
      LeStorage/StoredCollection.swift

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

@ -144,6 +144,7 @@ actor ApiCallCollection<T: Storable>: SomeCallCollection {
do {
try await Task.sleep(until: .now + .seconds(seconds))
} catch {
Logger.w("*** WAITING CRASHED !!!")
Logger.error(error)
}
}
@ -158,34 +159,48 @@ actor ApiCallCollection<T: Storable>: 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<T: Storable>: 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<V: Decodable>(_ 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<T: Storable>: SomeCallCollection {
/// Executes an API call
/// 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)
}

@ -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<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)
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<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 ?? ""
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<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
@ -254,10 +281,10 @@ public class Services {
}
/// 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)
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

@ -233,7 +233,7 @@ open class Store {
/// Requests a deletion to the StoreCenter
/// - Parameters:
/// - 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)
}

@ -217,12 +217,22 @@ public class StoreCenter {
}
/// 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)
}
/// 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)
}
@ -385,7 +395,7 @@ public class StoreCenter {
/// Transmit the update request to the ApiCall collection
/// - Parameters:
/// - 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 {
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<T: Storable>(_ instance: T) async throws -> T? {
func sendDeletion<T: Storable>(_ instance: T) async throws {
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

@ -429,9 +429,7 @@ public class StoredCollection<T: Storable>: 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<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]
/// - Parameters:
/// - instance: the object to PUT

Loading…
Cancel
Save