diff --git a/LeStorage/ApiCallCollection.swift b/LeStorage/ApiCallCollection.swift index 65bf79a..0f3e842 100644 --- a/LeStorage/ApiCallCollection.swift +++ b/LeStorage/ApiCallCollection.swift @@ -183,33 +183,47 @@ actor ApiCallCollection: SomeCallCollection { await self._wait() - let apiCallsCopy = self.items - for apiCall in apiCallsCopy { - apiCall.attemptsCount += 1 - apiCall.lastAttemptDate = Date() - + let batches = Dictionary(grouping: self.items, by: { $0.transactionId }) + + for batch in batches.values { do { - 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: - if T.self == GetSyncData.self { - let _: Empty = try await self._executeApiCall(apiCall) - } else { - let _: [T] = try await self._executeApiCall(apiCall) - } + if batch.count == 1, let apiCall = batch.first, apiCall.method == .get { + let _: Empty = try await self._executeGetCall(apiCall) + } else { + try await self._executeApiCalls(batch) } } catch { -// Logger.log("\(T.resourceName()) > API CALL RETRY ERROR:") -// Logger.error(error) + Logger.error(error) } } + +// let apiCallsCopy = self.items +// for apiCall in apiCallsCopy { +// apiCall.attemptsCount += 1 +// apiCall.lastAttemptDate = Date() +// +// do { +// 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: +// if T.self == GetSyncData.self { +// let _: Empty = try await self._executeApiCall(apiCall) +// } else { +// let _: [T] = try await self._executeApiCall(apiCall) +// } +// } +// } catch { +//// Logger.log("\(T.resourceName()) > API CALL RETRY ERROR:") +//// Logger.error(error) +// } +// } self._isRescheduling = false if self.items.isNotEmpty { @@ -271,10 +285,10 @@ actor ApiCallCollection: SomeCallCollection { } /// Creates an API call for the Storable [instance] and an HTTP [method] - fileprivate func _createCall(_ method: HTTPMethod, instance: T? = nil) throws -> ApiCall { + fileprivate func _createCall(_ method: HTTPMethod, instance: T? = nil, transactionId: String? = nil) throws -> ApiCall { if let instance { let jsonString = try instance.jsonString() - return ApiCall(method: method, dataId: instance.stringId, body: jsonString) + return ApiCall(method: method, dataId: instance.stringId, body: jsonString, transactionId: transactionId) } else { return ApiCall(method: .get) } @@ -299,42 +313,64 @@ actor ApiCallCollection: SomeCallCollection { } } - /// Sends an insert api call for the provided [instance] - func sendInsertion(_ instance: T) async throws -> T? { - do { - return try await self._sendServerRequest(HTTPMethod.post, instance: instance) - } catch { - self.rescheduleApiCallsIfNecessary() - StoreCenter.main.log(message: "POST failed for \(instance): \(error.localizedDescription)") - Logger.error(error) + func executeBatch(_ batch: BatchPreparation) async throws { + + var apiCalls: [ApiCall] = [] + let transactionId = Store.randomId() + for insert in batch.inserts { + let call = try self._createCall(.post, instance: insert, transactionId: transactionId) + self._prepareCall(apiCall: call) + apiCalls.append(call) } - return nil - - } - - /// Sends an update api call for the provided [instance] - func sendUpdate(_ instance: T) async throws -> T? { - do { - return try await self._sendServerRequest(HTTPMethod.put, instance: instance) - } catch { - self.rescheduleApiCallsIfNecessary() - StoreCenter.main.log(message: "PUT failed for \(instance): \(error.localizedDescription)") - Logger.error(error) + for update in batch.updates { + let call = try self._createCall(.put, instance: update, transactionId: transactionId) + self._prepareCall(apiCall: call) + apiCalls.append(call) } - return nil - } - - /// Sends an delete api call for the provided [instance] - func sendDeletion(_ instance: T) async throws { - do { - let _: Empty? = try await self._sendServerRequest(HTTPMethod.delete, instance: instance) - } catch { - self.rescheduleApiCallsIfNecessary() - StoreCenter.main.log(message: "DELETE failed for \(instance): \(error.localizedDescription)") - Logger.error(error) + for delete in batch.deletes { + let call = try self._createCall(.delete, instance: delete, transactionId: transactionId) + self._prepareCall(apiCall: call) + apiCalls.append(call) } - return + try await self._executeApiCalls(apiCalls) } + + /// Sends an insert api call for the provided [instance] +// func sendInsertion(_ instance: T) async throws -> T? { +// do { +// return try await self._sendServerRequest(HTTPMethod.post, instance: instance) +// } catch { +// self.rescheduleApiCallsIfNecessary() +// StoreCenter.main.log(message: "POST failed for \(instance): \(error.localizedDescription)") +// Logger.error(error) +// } +// return nil +// +// } +// +// /// Sends an update api call for the provided [instance] +// func sendUpdate(_ instance: T) async throws -> T? { +// do { +// return try await self._sendServerRequest(HTTPMethod.put, instance: instance) +// } catch { +// self.rescheduleApiCallsIfNecessary() +// StoreCenter.main.log(message: "PUT failed for \(instance): \(error.localizedDescription)") +// Logger.error(error) +// } +// return nil +// } +// +// /// Sends an delete api call for the provided [instance] +// func sendDeletion(_ instance: T) async throws { +// do { +// let _: Empty? = try await self._sendServerRequest(HTTPMethod.delete, instance: instance) +// } catch { +// self.rescheduleApiCallsIfNecessary() +// StoreCenter.main.log(message: "DELETE failed for \(instance): \(error.localizedDescription)") +// Logger.error(error) +// } +// return +// } /// Initiates the process of sending the data with the server fileprivate func _sendServerRequest(_ method: HTTPMethod, instance: T? = nil) async throws -> V? { @@ -349,13 +385,19 @@ actor ApiCallCollection: SomeCallCollection { fileprivate func _prepareAndSendCall(_ apiCall: ApiCall) async throws -> V? { self._prepareCall(apiCall: apiCall) - return try await self._executeApiCall(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 _executeApiCall(_ apiCall: ApiCall) async throws -> V { - return try await StoreCenter.main.execute(apiCall: apiCall) + fileprivate func _executeApiCalls(_ apiCalls: [ApiCall]) async throws { + try await StoreCenter.main.execute(apiCalls: apiCalls) } /// Returns the content of the API call file as a String diff --git a/LeStorage/Codables/ApiCall.swift b/LeStorage/Codables/ApiCall.swift index cd41fdd..f87dc0e 100644 --- a/LeStorage/Codables/ApiCall.swift +++ b/LeStorage/Codables/ApiCall.swift @@ -19,6 +19,9 @@ class ApiCall: ModelObject, Storable, SomeCall { static func tokenExemptedMethods() -> [HTTPMethod] { return [] } var id: String = Store.randomId() + + /// The transactionId to group calls together + var transactionId: String = Store.randomId() /// Creation date of the call var creationDate: Date? = Date() @@ -41,10 +44,13 @@ class ApiCall: ModelObject, Storable, SomeCall { /// The parameters to add in the URL to obtain : "?p1=v1&p2=v2" var urlParameters: [String : String]? = nil - init(method: HTTPMethod, dataId: String? = nil, body: String? = nil) { + init(method: HTTPMethod, dataId: String? = nil, body: String? = nil, transactionId: String? = nil) { self.method = method self.dataId = dataId self.body = body + if let transactionId { + self.transactionId = transactionId + } } func copy(from other: any Storable) { diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index 8530aeb..88a7a85 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -79,6 +79,64 @@ public class Services { 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 _runSyncPostRequest( + _ request: URLRequest, type: T.Type) async throws { + let debugURL = request.url?.absoluteString ?? "" + // print("Run \(request.httpMethod ?? "") \(debugURL)") + let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) + print("sync POST \(String(describing: T.self)) => \(String(data: task.0, encoding: .utf8) ?? "")") + + var rescheduleApiCalls: Bool = false + + if let response = task.1 as? HTTPURLResponse { + let statusCode = response.statusCode + print("\(debugURL) ended, status code = \(statusCode)") + switch statusCode { + case 200..<300: // success + + let decoded: BatchResponse = try self._decode(data: task.0) + for result in decoded.results { + switch result.status { + case 200..<300: + + try await StoreCenter.main.deleteApiCallById(type: T.self, id: result.apiCallId) + default: + rescheduleApiCalls = true + } + } + + 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 + } + try await StoreCenter.main.rescheduleApiCalls(type: T.self) + + // StoreCenter.main.logFailedAPICall( + // apiCall.id, request: request, collectionName: T.resourceName(), + // error: 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) + } + + if rescheduleApiCalls { + try await StoreCenter.main.rescheduleApiCalls(type: T.self) + } + } + /// Runs a request using a traditional URLRequest /// - Parameters: /// - request: the URLRequest to run @@ -112,7 +170,7 @@ public class Services { errorMessage = message } - try await StoreCenter.main.rescheduleApiCalls(id: apiCall.id, type: T.self) + try await StoreCenter.main.rescheduleApiCalls(type: T.self) StoreCenter.main.logFailedAPICall( apiCall.id, request: request, collectionName: T.resourceName(), error: errorMessage.message) @@ -265,7 +323,7 @@ public class Services { /// Returns the URLRequest for an ApiCall /// - Parameters: /// - apiCall: An ApiCall instance to configure the returned request - fileprivate func _syncRequest(from apiCall: ApiCall) throws -> URLRequest { + fileprivate func _syncGetRequest(from apiCall: ApiCall) throws -> URLRequest { var urlString = baseURL + "data/" if let urlParameters = apiCall.formattedURLParameters() { @@ -277,39 +335,57 @@ public class Services { } var request = URLRequest(url: url) - if apiCall.method == .get { - request.httpMethod = HTTPMethod.get.rawValue - } else { - request.httpMethod = HTTPMethod.post.rawValue + request.httpMethod = HTTPMethod.get.rawValue + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + if self._isTokenRequired(type: T.self, method: apiCall.method) { + let token = try self.keychainStore.getValue() + request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") + } + + return request + } + + /// Returns the URLRequest for an ApiCall + /// - Parameters: + /// - apiCall: An ApiCall instance to configure the returned request + fileprivate func _syncPostRequest(from apiCalls: [ApiCall]) throws -> URLRequest { + + let urlString = baseURL + "data/" + + guard let url = URL(string: urlString) else { + throw ServiceError.urlCreationError(url: urlString) } + var request = URLRequest(url: url) + request.httpMethod = HTTPMethod.post.rawValue request.setValue("application/json", forHTTPHeaderField: "Content-Type") + let token = try self.keychainStore.getValue() + request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") - if let body = apiCall.body { - if let data = body.data(using: .utf8) { + let modelName = String(describing: T.self) + let operations = try apiCalls.map { apiCall in + + if let body = apiCall.body, let data = body.data(using: .utf8) { let object = try JSON.decoder.decode(T.self, from: data) - let modelName = String(describing: T.self) - let payload = SyncPayload( - operation: apiCall.method.rawValue, - modelName: modelName, - data: object, - storeId: object.getStoreId(), - deviceId: StoreCenter.main.deviceId()) - request.httpBody = try JSON.encoder.encode(payload) - + return Operation(apiCallId: apiCall.id, + operation: apiCall.method.rawValue, + modelName: modelName, + data: object, + storeId: object.getStoreId()) } else { throw ServiceError.cantDecodeData(resource: T.resourceName(), method: apiCall.method.rawValue, content: apiCall.body) } + } - - if self._isTokenRequired(type: T.self, method: apiCall.method) { - let token = try self.keychainStore.getValue() - request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") - } - + + let payload = SyncPayload(operations: operations, + deviceId: StoreCenter.main.deviceId()) + request.httpBody = try JSON.encoder.encode(payload) + return request } - + /// Starts a request to retrieve the synchronization updates /// - Parameters: /// - since: The date from which updates are retrieved @@ -388,44 +464,41 @@ public class Services { } /// Executes a POST request - public func post(_ instance: T) async throws -> T { - - let method: HTTPMethod = .post - let payload = SyncPayload( - operation: method.rawValue, - modelName: String(describing: T.self), - data: instance, - deviceId: StoreCenter.main.deviceId()) - let syncRequest = try self._baseSyncRequest(method: .post, payload: payload) - return try await self._runRequest(syncRequest) - - // var postRequest = try self._postRequest(type: T.self) - // postRequest.httpBody = try jsonEncoder.encode(instance) - // return try await self._runRequest(postRequest) - } - - /// Executes a PUT request - public func put(_ instance: T) async throws -> T { - - let method: HTTPMethod = .put - let payload = SyncPayload( - operation: method.rawValue, - modelName: String(describing: T.self), - data: instance, - deviceId: StoreCenter.main.deviceId()) - let syncRequest = try self._baseSyncRequest(method: .post, payload: payload) - return try await self._runRequest(syncRequest) +// public func post(_ instance: T) async throws -> T { +// +// let method: HTTPMethod = .post +// let payload = SyncPayload( +// operation: method.rawValue, +// modelName: String(describing: T.self), +// data: instance, +// deviceId: StoreCenter.main.deviceId()) +// let syncRequest = try self._baseSyncRequest(method: .post, payload: payload) +// return try await self._runRequest(syncRequest) +// } +// +// /// Executes a PUT request +// public func put(_ instance: T) async throws -> T { +// +// let method: HTTPMethod = .put +// let payload = SyncPayload( +// operation: method.rawValue, +// modelName: String(describing: T.self), +// data: instance, +// deviceId: StoreCenter.main.deviceId()) +// let syncRequest = try self._baseSyncRequest(method: .post, payload: payload) +// return try await self._runRequest(syncRequest) +// } - // var postRequest = try self._putRequest(type: T.self, id: instance.stringId) - // postRequest.httpBody = try jsonEncoder.encode(instance) - // return try await self._runRequest(postRequest) + /// Executes an ApiCall + func runGetApiCall(_ apiCall: ApiCall) async throws -> V { + let request = try self._syncGetRequest(from: apiCall) + return try await self._runRequest(request, apiCall: apiCall) } /// Executes an ApiCall - func runApiCall(_ apiCall: ApiCall) async throws -> V { - let request = try self._syncRequest(from: apiCall) -// print("HTTP \(request.httpMethod ?? "") : id = \(apiCall.dataId)") - return try await self._runRequest(request, apiCall: apiCall) + func runApiCalls(_ apiCalls: [ApiCall]) async throws { + let request = try self._syncPostRequest(from: apiCalls) + try await self._runSyncPostRequest(request, type: T.self) } /// Returns the URLRequest for an ApiCall @@ -649,11 +722,26 @@ public class Services { } struct SyncPayload: Encodable { + var operations: [Operation] + var deviceId: String? +} + +struct Operation: Encodable { + var apiCallId: String var operation: String var modelName: String var data: T var storeId: String? - var deviceId: String? +} + +struct BatchResponse: Decodable { + var results: [OperationResult] +} + +struct OperationResult: Decodable { + var apiCallId: String + var status: Int + var data: T } struct ErrorMessage { diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index d984899..3770931 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -287,23 +287,23 @@ final public class Store { /// Requests an insertion to the StoreCenter /// - Parameters: /// - instance: an object to insert - func sendInsertion(_ instance: T) async throws -> T? { - return try await StoreCenter.main.sendInsertion(instance) - } - - /// Requests an update to the StoreCenter - /// - Parameters: - /// - instance: an object to update - @discardableResult func sendUpdate(_ instance: T) async throws -> T? { - return try await StoreCenter.main.sendUpdate(instance) - } - - /// Requests a deletion to the StoreCenter - /// - Parameters: - /// - instance: an object to delete - func sendDeletion(_ instance: T) async throws { - return try await StoreCenter.main.sendDeletion(instance) - } +// func sendInsertion(_ instance: T) async throws -> T? { +// return try await StoreCenter.main.sendInsertion(instance) +// } +// +// /// Requests an update to the StoreCenter +// /// - Parameters: +// /// - instance: an object to update +// @discardableResult func sendUpdate(_ instance: T) async throws -> T? { +// return try await StoreCenter.main.sendUpdate(instance) +// } +// +// /// Requests a deletion to the StoreCenter +// /// - Parameters: +// /// - instance: an object to delete +// func sendDeletion(_ instance: T) async throws { +// return try await StoreCenter.main.sendDeletion(instance) +// } /// Returns whether all collections have loaded locally public func fileCollectionsAllLoaded() -> Bool { diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index dae2628..87ca587 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -344,7 +344,7 @@ public class StoreCenter { } /// Reschedule an ApiCall by id - func rescheduleApiCalls(id: String, type: T.Type) async throws { + func rescheduleApiCalls(type: T.Type) async throws { guard self.collectionsCanSynchronize else { return } @@ -360,8 +360,13 @@ public class StoreCenter { // } /// Executes an API call - func execute(apiCall: ApiCall) async throws -> V { - return try await self.service().runApiCall(apiCall) + func executeGet(apiCall: ApiCall) async throws -> V { + return try await self.service().runGetApiCall(apiCall) + } + + /// Executes an API call + func execute(apiCalls: [ApiCall]) async throws { + try await self.service().runApiCalls(apiCalls) } // MARK: - Api calls @@ -372,35 +377,44 @@ public class StoreCenter { && self.userIsAllowed() } - /// Transmit the insertion request to the ApiCall collection - /// - Parameters: - /// - instance: an object to insert - func sendInsertion(_ instance: T) async throws -> T? { + func prepareOperationBatch(_ batch: BatchPreparation) { guard self._canSynchronise() else { - return nil + return } - return try await self.apiCallCollection().sendInsertion(instance) - } - - /// Transmit the update request to the ApiCall collection - /// - Parameters: - /// - instance: an object to update - func sendUpdate(_ instance: T) async throws -> T? { - guard self._canSynchronise() else { - return nil + Task { + try await self.apiCallCollection().executeBatch(batch) } - return try await self.apiCallCollection().sendUpdate(instance) } - - /// Transmit the deletion request to the ApiCall collection + + /// Transmit the insertion request to the ApiCall collection /// - Parameters: - /// - instance: an object to delete - func sendDeletion(_ instance: T) async throws { - guard self._canSynchronise() else { - return - } - try await self.apiCallCollection().sendDeletion(instance) - } + /// - instance: an object to insert +// func sendInsertion(_ instance: T) async throws -> T? { +// guard self._canSynchronise() else { +// return nil +// } +// return try await self.apiCallCollection().sendInsertion(instance) +// } +// +// /// Transmit the update request to the ApiCall collection +// /// - Parameters: +// /// - instance: an object to update +// func sendUpdate(_ instance: T) async throws -> T? { +// guard self._canSynchronise() else { +// return nil +// } +// return try await self.apiCallCollection().sendUpdate(instance) +// } +// +// /// Transmit the deletion request to the ApiCall collection +// /// - Parameters: +// /// - instance: an object to delete +// func sendDeletion(_ instance: T) async throws { +// guard self._canSynchronise() else { +// return +// } +// try await self.apiCallCollection().sendDeletion(instance) +// } /// Retrieves all the items on the server func getItems(identifier: String? = nil) async throws -> [T] { diff --git a/LeStorage/StoredCollection+Sync.swift b/LeStorage/StoredCollection+Sync.swift index 4cf840d..e8abb46 100644 --- a/LeStorage/StoredCollection+Sync.swift +++ b/LeStorage/StoredCollection+Sync.swift @@ -102,10 +102,10 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { instance.lastUpdate = Date() if let index = self.items.firstIndex(where: { $0.id == instance.id }) { self.updateItem(instance, index: index) - self._sendUpdateIfNecessary(instance) + self._sendUpdate(instance) } else { self.addItem(instance: instance) - self._sendInsertionIfNecessary(instance) + self._sendInsertion(instance) } } @@ -115,16 +115,23 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { self.setChanged() } + let date = Date() + let batch = BatchPreparation() + for instance in sequence { - instance.lastUpdate = Date() + instance.lastUpdate = date if let index = self.items.firstIndex(where: { $0.id == instance.id }) { self.updateItem(instance, index: index) - self._sendUpdateIfNecessary(instance) + batch.addUpdate(instance) +// self._sendUpdateIfNecessary(instance) } else { // insert self.addItem(instance: instance) - self._sendInsertionIfNecessary(instance) + batch.addInsert(instance) +// self._sendInsertionIfNecessary(instance) } } + + self._prepareBatch(batch) } @@ -141,8 +148,13 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { } for instance in sequence { - self._deleteNoWrite(instance: instance) + self.deleteItem(instance) + StoreCenter.main.createDeleteLog(instance) } + + let batch = BatchPreparation() + batch.deletes = sequence + self._prepareBatch(batch) } /// Deletes an instance and writes @@ -157,54 +169,71 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { fileprivate func _deleteNoWrite(instance: T) { self.deleteItem(instance) StoreCenter.main.createDeleteLog(instance) - self._sendDeletionIfNecessary(instance) + + self._sendDeletion(instance) } - // MARK: - Reschedule calls + // MARK: - Send requests - /// Sends an insert api call for the provided - /// Calls copyFromServerInstance on the instance with the result of the HTTP call - /// - Parameters: - /// - instance: the object to POST - fileprivate func _sendInsertionIfNecessary(_ instance: T) { - - Task { - do { - if let result = try await self.store.sendInsertion(instance) { - self.updateFromServerInstance(result) - } - } catch { - Logger.error(error) - } - } + fileprivate func _sendInsertion(_ instance: T) { + self._prepareBatch(BatchPreparation(insert: instance)) } - /// Sends an update api call for the provided [instance] - /// - Parameters: - /// - instance: the object to PUT - fileprivate func _sendUpdateIfNecessary(_ instance: T) { - Task { - do { - try await self.store.sendUpdate(instance) - } catch { - Logger.error(error) - } - } + fileprivate func _sendUpdate(_ instance: T) { + self._prepareBatch(BatchPreparation(update: instance)) } - /// Sends an delete api call for the provided [instance] - /// - Parameters: - /// - instance: the object to DELETE - fileprivate func _sendDeletionIfNecessary(_ instance: T) { - Task { - do { - try await self.store.sendDeletion(instance) - } catch { - Logger.error(error) - } - } + fileprivate func _sendDeletion(_ instance: T) { + self._prepareBatch(BatchPreparation(delete: instance)) } + fileprivate func _prepareBatch(_ batch: BatchPreparation) { + StoreCenter.main.prepareOperationBatch(batch) + } + + /// Sends an insert api call for the provided + /// Calls copyFromServerInstance on the instance with the result of the HTTP call + /// - Parameters: + /// - instance: the object to POST +// fileprivate func _sendInsertionIfNecessary(_ instance: T) { +// +// Task { +// do { +// if let result = try await self.store.sendInsertion(instance) { +// self.updateFromServerInstance(result) +// } +// } catch { +// Logger.error(error) +// } +// } +// } +// +// /// Sends an update api call for the provided [instance] +// /// - Parameters: +// /// - instance: the object to PUT +// fileprivate func _sendUpdateIfNecessary(_ instance: T) { +// Task { +// do { +// try await self.store.sendUpdate(instance) +// } catch { +// Logger.error(error) +// } +// } +// } +// +// /// Sends an delete api call for the provided [instance] +// /// - Parameters: +// /// - instance: the object to DELETE +// fileprivate func _sendDeletionIfNecessary(_ instance: T) { +// Task { +// do { +// try await self.store.sendDeletion(instance) +// } catch { +// Logger.error(error) +// } +// } +// } + // MARK: - Synchronization /// Adds or update an instance if it is newer than the local instance @@ -228,25 +257,39 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { } // MARK: - Migrations - - /// Makes POST ApiCall for all items in the collection - public func insertAllIntoCurrentService() { - for item in self.items { - self._sendInsertionIfNecessary(item) - } - } - - /// Makes POST ApiCall for the provided item - public func insertIntoCurrentService(item: T) { - self._sendInsertionIfNecessary(item) - } /// Sends a POST request for the instance, and changes the collection to perform a write public func writeChangeAndInsertOnServer(instance: T) { defer { self.setChanged() } - self._sendInsertionIfNecessary(instance) + self._sendInsertion(instance) } } + +class BatchPreparation { + var inserts: [T] = [] + var updates: [T] = [] + var deletes: any Sequence = [] + + init() { + + } + init(insert: T) { + self.inserts = [insert] + } + init(update: T) { + self.updates = [update] + } + init(delete: T) { + self.deletes = [delete] + } + + func addInsert(_ instance: T) { + self.inserts.append(instance) + } + func addUpdate(_ instance: T) { + self.updates.append(instance) + } +}