|
|
|
|
@ -22,7 +22,7 @@ protocol SomeCallCollection { |
|
|
|
|
|
|
|
|
|
enum ApiCallError: Error, LocalizedError { |
|
|
|
|
case encodingError(id: String, type: String) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var errorDescription: String? { |
|
|
|
|
switch self { |
|
|
|
|
case .encodingError(let id, let type): |
|
|
|
|
@ -46,7 +46,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection { |
|
|
|
|
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 { |
|
|
|
|
@ -57,7 +57,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Starts the JSON file decoding synchronously or asynchronously |
|
|
|
|
/// Reschedule Api calls if not empty |
|
|
|
|
func loadFromFile() throws { |
|
|
|
|
@ -157,12 +157,12 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection { |
|
|
|
|
self.rescheduleApiCallsIfNecessary() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func rescheduleImmediately() { |
|
|
|
|
self._attemptLoops = -1 |
|
|
|
|
self.rescheduleApiCallsIfNecessary() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Reschedule API calls if necessary |
|
|
|
|
func rescheduleApiCallsIfNecessary() { |
|
|
|
|
if self.items.isNotEmpty && !self._isRescheduling { |
|
|
|
|
@ -174,19 +174,19 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection { |
|
|
|
|
|
|
|
|
|
/// Reschedule the execution of API calls |
|
|
|
|
fileprivate func _waitAndExecuteApiCalls() async { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Logger.log("\(T.resourceName()) > RESCHED") |
|
|
|
|
guard !self._isRescheduling, StoreCenter.main.collectionsCanSynchronize else { return } |
|
|
|
|
guard self.items.isNotEmpty else { return } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self._isRescheduling = true |
|
|
|
|
|
|
|
|
|
self._attemptLoops += 1 |
|
|
|
|
|
|
|
|
|
await self._wait() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let batches = Dictionary(grouping: self.items, by: { $0.transactionId }) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for batch in batches.values { |
|
|
|
|
do { |
|
|
|
|
if batch.count == 1, let apiCall = batch.first, apiCall.method == .get { |
|
|
|
|
@ -212,7 +212,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection { |
|
|
|
|
|
|
|
|
|
/// Wait for an exponentionnaly long time depending on the number of attemps |
|
|
|
|
fileprivate func _wait() async { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#if DEBUG |
|
|
|
|
let seconds = self._attemptLoops |
|
|
|
|
#else |
|
|
|
|
@ -227,68 +227,33 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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 _call(method: HTTPMethod, instance: T? = nil) async throws -> ApiCall<T>? { |
|
|
|
|
// |
|
|
|
|
// if let instance { |
|
|
|
|
// return try await self._callForInstance(instance, method: method) |
|
|
|
|
// } else { |
|
|
|
|
// if self.items.contains(where: { $0.method == .get }) { |
|
|
|
|
// return nil |
|
|
|
|
// } else { |
|
|
|
|
// return try self._createGetCall() |
|
|
|
|
// } |
|
|
|
|
// } |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
// fileprivate func _callForInstance(_ instance: T, method: HTTPMethod, transactionId: String? = nil) async throws -> ApiCall<T> { |
|
|
|
|
// |
|
|
|
|
// // cleanup |
|
|
|
|
// let existingCalls = self.items.filter { $0.data?.id == instance.id } |
|
|
|
|
// self._deleteCalls(existingCalls) |
|
|
|
|
// |
|
|
|
|
// // create |
|
|
|
|
// let call = try self._createCall(method, instance: instance, transactionId: transactionId) |
|
|
|
|
// self._prepareCall(apiCall: call) |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
/// The method makes some clean up when necessary: |
|
|
|
|
/// - When deleting, we delete other calls as they are unecessary |
|
|
|
|
/// - When updating, we delete other PUT as we don't want them to be executed in random orders |
|
|
|
|
func callForInstance(_ instance: T, method: HTTPMethod, transactionId: String? = nil) throws -> ApiCall<T> { |
|
|
|
|
|
|
|
|
|
// cleanup |
|
|
|
|
let existingCalls = self.items.filter { $0.data?.stringId == instance.stringId } |
|
|
|
|
if existingCalls.count > 1 { |
|
|
|
|
StoreCenter.main.log(message: "There are multiple calls registered for a single item: \(T.resourceName()), id = \(instance.stringId)") |
|
|
|
|
// cleanup if necessary |
|
|
|
|
switch method { |
|
|
|
|
case .delete: // we don't want anything else than a DELETE in the queue |
|
|
|
|
let existingCalls = self.items.filter { $0.data?.stringId == instance.stringId } |
|
|
|
|
self._deleteCalls(existingCalls) |
|
|
|
|
case .put: // we don't want mixed PUT calls so we delete the others |
|
|
|
|
let existingPuts = self.items.filter { $0.data?.stringId == instance.stringId && $0.method == .put } |
|
|
|
|
self._deleteCalls(existingPuts) |
|
|
|
|
default: |
|
|
|
|
break |
|
|
|
|
} |
|
|
|
|
let currentHTTPMethod = existingCalls.first?.method |
|
|
|
|
let call: ApiCall<T> |
|
|
|
|
if let currentHTTPMethod { |
|
|
|
|
switch (currentHTTPMethod, method) { |
|
|
|
|
case (.post, .put): |
|
|
|
|
call = try self._createCall(.post, instance: instance, transactionId: transactionId) |
|
|
|
|
case (.post, .delete): |
|
|
|
|
call = try self._createCall(.delete, instance: instance, transactionId: transactionId) |
|
|
|
|
case (.put, .put): |
|
|
|
|
call = try self._createCall(.put, instance: instance, transactionId: transactionId) |
|
|
|
|
case (.put, .delete): |
|
|
|
|
call = try self._createCall(.delete, instance: instance, transactionId: transactionId) |
|
|
|
|
default: |
|
|
|
|
call = try self._createCall(method, instance: instance, transactionId: transactionId) |
|
|
|
|
StoreCenter.main.log(message: "case \(currentHTTPMethod) : \(method) should not happen") |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
call = try self._createCall(method, instance: instance, transactionId: transactionId) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self._deleteCalls(existingCalls) |
|
|
|
|
|
|
|
|
|
let call: ApiCall<T> = try self._createCall(method, instance: instance, transactionId: transactionId) |
|
|
|
|
self._prepareCall(apiCall: call) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return call |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fileprivate func _deleteCalls(_ calls: [ApiCall<T>]) { |
|
|
|
|
for call in calls { |
|
|
|
|
self.deleteById(call.id) |
|
|
|
|
@ -298,7 +263,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection { |
|
|
|
|
fileprivate func _createGetCall() throws -> ApiCall<T> { |
|
|
|
|
return try self._createCall(.get, instance: nil) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Creates an API call for the Storable [instance] and an HTTP [method] |
|
|
|
|
fileprivate func _createCall(_ method: HTTPMethod, instance: T?, transactionId: String? = nil) throws -> ApiCall<T> { |
|
|
|
|
if let instance { |
|
|
|
|
@ -328,7 +293,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func executeBatch(_ batch: OperationBatch<T>) async throws -> [T] { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var apiCalls: [ApiCall<T>] = [] |
|
|
|
|
let transactionId = Store.randomId() |
|
|
|
|
for insert in batch.inserts { |
|
|
|
|
@ -345,7 +310,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection { |
|
|
|
|
} |
|
|
|
|
return try await self._executeApiCalls(apiCalls) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// /// Initiates the process of sending the data with the server |
|
|
|
|
//<<<<<<< HEAD |
|
|
|
|
// fileprivate func _sendServerRequest<V: Decodable>(_ method: HTTPMethod, instance: T? = nil) async throws -> V? { |
|
|
|
|
@ -365,13 +330,13 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection { |
|
|
|
|
self._prepareCall(apiCall: apiCall) |
|
|
|
|
return try await self._executeGetCall(apiCall) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Executes an API call |
|
|
|
|
/// For POST requests, potentially copies additional data coming from the server during the insert |
|
|
|
|
fileprivate func _executeGetCall<V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V { |
|
|
|
|
return try await StoreCenter.main.executeGet(apiCall: apiCall) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Executes an API call |
|
|
|
|
/// For POST requests, potentially copies additional data coming from the server during the insert |
|
|
|
|
fileprivate func _executeApiCalls(_ apiCalls: [ApiCall<T>]) async throws -> [T] { |
|
|
|
|
|