diff --git a/LeStorage/ApiCallCollection.swift b/LeStorage/ApiCallCollection.swift index 710aea4..f89d6fb 100644 --- a/LeStorage/ApiCallCollection.swift +++ b/LeStorage/ApiCallCollection.swift @@ -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: 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: SomeCallCollection { } } } - + /// Starts the JSON file decoding synchronously or asynchronously /// Reschedule Api calls if not empty func loadFromFile() throws { @@ -157,12 +157,12 @@ actor ApiCallCollection: 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: 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: 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: 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? { -// -// 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 { -// -// // 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 { - // 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 - 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 = try self._createCall(method, instance: instance, transactionId: transactionId) self._prepareCall(apiCall: call) - + return call } - + fileprivate func _deleteCalls(_ calls: [ApiCall]) { for call in calls { self.deleteById(call.id) @@ -298,7 +263,7 @@ actor ApiCallCollection: SomeCallCollection { fileprivate func _createGetCall() throws -> ApiCall { 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 { if let instance { @@ -328,7 +293,7 @@ actor ApiCallCollection: SomeCallCollection { } func executeBatch(_ batch: OperationBatch) async throws -> [T] { - + var apiCalls: [ApiCall] = [] let transactionId = Store.randomId() for insert in batch.inserts { @@ -345,7 +310,7 @@ actor ApiCallCollection: SomeCallCollection { } return try await self._executeApiCalls(apiCalls) } - + // /// Initiates the process of sending the data with the server //<<<<<<< HEAD // fileprivate func _sendServerRequest(_ method: HTTPMethod, instance: T? = nil) async throws -> V? { @@ -365,13 +330,13 @@ actor ApiCallCollection: 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(_ apiCall: ApiCall) 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]) async throws -> [T] { diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index 219b38f..9f2f58d 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -951,7 +951,7 @@ public class StoreCenter { if let logs = self._logs { return logs } else { - let logsCollection: StoredCollection = Store.main.registerCollection(limit: 1000) + let logsCollection: StoredCollection = Store.main.registerCollection(limit: 50) self._logs = logsCollection return logsCollection } diff --git a/LeStorage/StoredCollection+Sync.swift b/LeStorage/StoredCollection+Sync.swift index bd256be..e2a5f17 100644 --- a/LeStorage/StoredCollection+Sync.swift +++ b/LeStorage/StoredCollection+Sync.swift @@ -49,10 +49,10 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { self.addOrUpdateNoSync(contentOfs: items) } } + self.setAsLoaded() } catch { Logger.error(error) } - self.setAsLoaded() } /// Updates a local item from a server instance. This method is typically used when the server makes update diff --git a/LeStorage/StoredCollection.swift b/LeStorage/StoredCollection.swift index cd9547c..a0ceb3b 100644 --- a/LeStorage/StoredCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -153,7 +153,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti self._indexes = self.items.dictionary { $0.id } } } - + // MARK: - Basic operations /// Adds or updates the provided instance inside the collection diff --git a/LeStorageTests/ApiCallTests.swift b/LeStorageTests/ApiCallTests.swift index bd5a7eb..b2c594f 100644 --- a/LeStorageTests/ApiCallTests.swift +++ b/LeStorageTests/ApiCallTests.swift @@ -37,10 +37,13 @@ struct ApiCallTests { thing.name = "woo" let _ = try await collection.sendUpdate(thing) - await #expect(collection.items.count == 1) + await #expect(collection.items.count == 2) // one post and one put if let apiCall = await collection.items.first { #expect(apiCall.method == .post) } + if let apiCall = await collection.items.last { + #expect(apiCall.method == .put) + } let _ = try await collection.sendDeletion(thing) await #expect(collection.items.count == 1) @@ -60,6 +63,8 @@ struct ApiCallTests { thing.name = "woo" let _ = try await collection.sendUpdate(thing) + let _ = try await collection.sendUpdate(thing) + let _ = try await collection.sendUpdate(thing) await #expect(collection.items.count == 1) if let apiCall = await collection.items.first { #expect(apiCall.method == .put)