Batches api calls by transactionId

sync2
Laurent 10 months ago
parent 4423d3f52a
commit b8077f231c
  1. 158
      LeStorage/ApiCallCollection.swift
  2. 8
      LeStorage/Codables/ApiCall.swift
  3. 200
      LeStorage/Services.swift
  4. 34
      LeStorage/Store.swift
  5. 66
      LeStorage/StoreCenter.swift
  6. 157
      LeStorage/StoredCollection+Sync.swift

@ -183,34 +183,48 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
await self._wait() await self._wait()
let apiCallsCopy = self.items let batches = Dictionary(grouping: self.items, by: { $0.transactionId })
for apiCall in apiCallsCopy {
apiCall.attemptsCount += 1
apiCall.lastAttemptDate = Date()
for batch in batches.values {
do { do {
switch apiCall.method { if batch.count == 1, let apiCall = batch.first, apiCall.method == .get {
case .post: let _: Empty = try await self._executeGetCall(apiCall)
let result: T = try await self._executeApiCall(apiCall) } else {
StoreCenter.main.updateFromServerInstance(result) try await self._executeApiCalls(batch)
// 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 { } 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 self._isRescheduling = false
if self.items.isNotEmpty { if self.items.isNotEmpty {
await self._rescheduleApiCalls() await self._rescheduleApiCalls()
@ -271,10 +285,10 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
} }
/// Creates an API call for the Storable [instance] and an HTTP [method] /// Creates an API call for the Storable [instance] and an HTTP [method]
fileprivate func _createCall(_ method: HTTPMethod, instance: T? = nil) throws -> ApiCall<T> { fileprivate func _createCall(_ method: HTTPMethod, instance: T? = nil, transactionId: String? = nil) throws -> ApiCall<T> {
if let instance { if let instance {
let jsonString = try instance.jsonString() 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 { } else {
return ApiCall(method: .get) return ApiCall(method: .get)
} }
@ -299,43 +313,65 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
} }
} }
/// Sends an insert api call for the provided [instance] func executeBatch(_ batch: BatchPreparation<T>) async throws {
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] var apiCalls: [ApiCall<T>] = []
func sendUpdate(_ instance: T) async throws -> T? { let transactionId = Store.randomId()
do { for insert in batch.inserts {
return try await self._sendServerRequest(HTTPMethod.put, instance: instance) let call = try self._createCall(.post, instance: insert, transactionId: transactionId)
} catch { self._prepareCall(apiCall: call)
self.rescheduleApiCallsIfNecessary() apiCalls.append(call)
StoreCenter.main.log(message: "PUT failed for \(instance): \(error.localizedDescription)")
Logger.error(error)
} }
return nil for update in batch.updates {
} let call = try self._createCall(.put, instance: update, transactionId: transactionId)
self._prepareCall(apiCall: call)
/// Sends an delete api call for the provided [instance] apiCalls.append(call)
func sendDeletion(_ instance: T) async throws { }
do { for delete in batch.deletes {
let _: Empty? = try await self._sendServerRequest(HTTPMethod.delete, instance: instance) let call = try self._createCall(.delete, instance: delete, transactionId: transactionId)
} catch { self._prepareCall(apiCall: call)
self.rescheduleApiCallsIfNecessary() apiCalls.append(call)
StoreCenter.main.log(message: "DELETE failed for \(instance): \(error.localizedDescription)")
Logger.error(error)
} }
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 /// Initiates the process of sending the data with the server
fileprivate func _sendServerRequest<V: Decodable>(_ method: HTTPMethod, instance: T? = nil) async throws -> V? { fileprivate func _sendServerRequest<V: Decodable>(_ method: HTTPMethod, instance: T? = nil) async throws -> V? {
if let apiCall = try self._call(method: method, instance: instance) { if let apiCall = try self._call(method: method, instance: instance) {
@ -349,13 +385,19 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
fileprivate func _prepareAndSendCall<V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V? { fileprivate func _prepareAndSendCall<V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V? {
self._prepareCall(apiCall: apiCall) 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<V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V {
return try await StoreCenter.main.executeGet(apiCall: apiCall)
} }
/// 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<V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V { fileprivate func _executeApiCalls(_ apiCalls: [ApiCall<T>]) async throws {
return try await StoreCenter.main.execute(apiCall: apiCall) try await StoreCenter.main.execute(apiCalls: apiCalls)
} }
/// Returns the content of the API call file as a String /// Returns the content of the API call file as a String

@ -20,6 +20,9 @@ class ApiCall<T: Storable>: ModelObject, Storable, SomeCall {
var id: String = Store.randomId() var id: String = Store.randomId()
/// The transactionId to group calls together
var transactionId: String = Store.randomId()
/// Creation date of the call /// Creation date of the call
var creationDate: Date? = Date() var creationDate: Date? = Date()
@ -41,10 +44,13 @@ class ApiCall<T: Storable>: ModelObject, Storable, SomeCall {
/// The parameters to add in the URL to obtain : "?p1=v1&p2=v2" /// The parameters to add in the URL to obtain : "?p1=v1&p2=v2"
var urlParameters: [String : String]? = nil 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.method = method
self.dataId = dataId self.dataId = dataId
self.body = body self.body = body
if let transactionId {
self.transactionId = transactionId
}
} }
func copy(from other: any Storable) { func copy(from other: any Storable) {

@ -79,6 +79,64 @@ public class Services {
return try await _runRequest(request) 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<T: SyncedStorable>(
_ 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<T> = 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 /// Runs a request using a traditional URLRequest
/// - Parameters: /// - Parameters:
/// - request: the URLRequest to run /// - request: the URLRequest to run
@ -112,7 +170,7 @@ public class Services {
errorMessage = message errorMessage = message
} }
try await StoreCenter.main.rescheduleApiCalls(id: apiCall.id, type: T.self) try await StoreCenter.main.rescheduleApiCalls(type: T.self)
StoreCenter.main.logFailedAPICall( StoreCenter.main.logFailedAPICall(
apiCall.id, request: request, collectionName: T.resourceName(), apiCall.id, request: request, collectionName: T.resourceName(),
error: errorMessage.message) error: errorMessage.message)
@ -265,7 +323,7 @@ public class Services {
/// Returns the URLRequest for an ApiCall /// Returns the URLRequest for an ApiCall
/// - Parameters: /// - Parameters:
/// - apiCall: An ApiCall instance to configure the returned request /// - apiCall: An ApiCall instance to configure the returned request
fileprivate func _syncRequest<T: SyncedStorable>(from apiCall: ApiCall<T>) throws -> URLRequest { fileprivate func _syncGetRequest<T: SyncedStorable>(from apiCall: ApiCall<T>) throws -> URLRequest {
var urlString = baseURL + "data/" var urlString = baseURL + "data/"
if let urlParameters = apiCall.formattedURLParameters() { if let urlParameters = apiCall.formattedURLParameters() {
@ -277,36 +335,54 @@ public class Services {
} }
var request = URLRequest(url: url) var request = URLRequest(url: url)
if apiCall.method == .get { request.httpMethod = HTTPMethod.get.rawValue
request.httpMethod = HTTPMethod.get.rawValue request.setValue("application/json", forHTTPHeaderField: "Content-Type")
} else {
request.httpMethod = HTTPMethod.post.rawValue 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<T: SyncedStorable>(from apiCalls: [ApiCall<T>]) 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") request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let token = try self.keychainStore.getValue()
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
if let body = apiCall.body { let modelName = String(describing: T.self)
if let data = body.data(using: .utf8) { let operations = try apiCalls.map { apiCall in
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)
if let body = apiCall.body, let data = body.data(using: .utf8) {
let object = try JSON.decoder.decode(T.self, from: data)
return Operation(apiCallId: apiCall.id,
operation: apiCall.method.rawValue,
modelName: modelName,
data: object,
storeId: object.getStoreId())
} else { } else {
throw ServiceError.cantDecodeData(resource: T.resourceName(), method: apiCall.method.rawValue, content: apiCall.body) 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 return request
} }
@ -388,44 +464,41 @@ public class Services {
} }
/// Executes a POST request /// Executes a POST request
public func post<T: Storable>(_ instance: T) async throws -> T { // public func post<T: Storable>(_ instance: T) async throws -> T {
//
let method: HTTPMethod = .post // let method: HTTPMethod = .post
let payload = SyncPayload( // let payload = SyncPayload(
operation: method.rawValue, // operation: method.rawValue,
modelName: String(describing: T.self), // modelName: String(describing: T.self),
data: instance, // data: instance,
deviceId: StoreCenter.main.deviceId()) // deviceId: StoreCenter.main.deviceId())
let syncRequest = try self._baseSyncRequest(method: .post, payload: payload) // let syncRequest = try self._baseSyncRequest(method: .post, payload: payload)
return try await self._runRequest(syncRequest) // return try await self._runRequest(syncRequest)
// }
// var postRequest = try self._postRequest(type: T.self) //
// postRequest.httpBody = try jsonEncoder.encode(instance) // /// Executes a PUT request
// return try await self._runRequest(postRequest) // public func put<T: SyncedStorable>(_ instance: T) async throws -> T {
} //
// let method: HTTPMethod = .put
/// Executes a PUT request // let payload = SyncPayload(
public func put<T: SyncedStorable>(_ instance: T) async throws -> T { // operation: method.rawValue,
// modelName: String(describing: T.self),
let method: HTTPMethod = .put // data: instance,
let payload = SyncPayload( // deviceId: StoreCenter.main.deviceId())
operation: method.rawValue, // let syncRequest = try self._baseSyncRequest(method: .post, payload: payload)
modelName: String(describing: T.self), // return try await self._runRequest(syncRequest)
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) /// Executes an ApiCall
// postRequest.httpBody = try jsonEncoder.encode(instance) func runGetApiCall<T: SyncedStorable, V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V {
// return try await self._runRequest(postRequest) let request = try self._syncGetRequest(from: apiCall)
return try await self._runRequest(request, apiCall: apiCall)
} }
/// Executes an ApiCall /// Executes an ApiCall
func runApiCall<T: SyncedStorable, V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V { func runApiCalls<T: SyncedStorable>(_ apiCalls: [ApiCall<T>]) async throws {
let request = try self._syncRequest(from: apiCall) let request = try self._syncPostRequest(from: apiCalls)
// print("HTTP \(request.httpMethod ?? "") : id = \(apiCall.dataId)") try await self._runSyncPostRequest(request, type: T.self)
return try await self._runRequest(request, apiCall: apiCall)
} }
/// Returns the URLRequest for an ApiCall /// Returns the URLRequest for an ApiCall
@ -649,11 +722,26 @@ public class Services {
} }
struct SyncPayload<T: Encodable>: Encodable { struct SyncPayload<T: Encodable>: Encodable {
var operations: [Operation<T>]
var deviceId: String?
}
struct Operation<T: Encodable>: Encodable {
var apiCallId: String
var operation: String var operation: String
var modelName: String var modelName: String
var data: T var data: T
var storeId: String? var storeId: String?
var deviceId: String? }
struct BatchResponse<T: Decodable>: Decodable {
var results: [OperationResult<T>]
}
struct OperationResult<T: Decodable>: Decodable {
var apiCallId: String
var status: Int
var data: T
} }
struct ErrorMessage { struct ErrorMessage {

@ -287,23 +287,23 @@ final public class Store {
/// Requests an insertion to the StoreCenter /// Requests an insertion to the StoreCenter
/// - Parameters: /// - Parameters:
/// - instance: an object to insert /// - instance: an object to insert
func sendInsertion<T: SyncedStorable>(_ instance: T) async throws -> T? { // func sendInsertion<T: SyncedStorable>(_ instance: T) async throws -> T? {
return try await StoreCenter.main.sendInsertion(instance) // return try await StoreCenter.main.sendInsertion(instance)
} // }
//
/// Requests an update to the StoreCenter // /// Requests an update to the StoreCenter
/// - Parameters: // /// - Parameters:
/// - instance: an object to update // /// - instance: an object to update
@discardableResult func sendUpdate<T: SyncedStorable>(_ instance: T) async throws -> T? { // @discardableResult func sendUpdate<T: SyncedStorable>(_ instance: T) async throws -> T? {
return try await StoreCenter.main.sendUpdate(instance) // return try await StoreCenter.main.sendUpdate(instance)
} // }
//
/// Requests a deletion to the StoreCenter // /// Requests a deletion to the StoreCenter
/// - Parameters: // /// - Parameters:
/// - instance: an object to delete // /// - instance: an object to delete
func sendDeletion<T: SyncedStorable>(_ instance: T) async throws { // func sendDeletion<T: SyncedStorable>(_ instance: T) async throws {
return try await StoreCenter.main.sendDeletion(instance) // return try await StoreCenter.main.sendDeletion(instance)
} // }
/// Returns whether all collections have loaded locally /// Returns whether all collections have loaded locally
public func fileCollectionsAllLoaded() -> Bool { public func fileCollectionsAllLoaded() -> Bool {

@ -344,7 +344,7 @@ public class StoreCenter {
} }
/// Reschedule an ApiCall by id /// Reschedule an ApiCall by id
func rescheduleApiCalls<T: SyncedStorable>(id: String, type: T.Type) async throws { func rescheduleApiCalls<T: SyncedStorable>(type: T.Type) async throws {
guard self.collectionsCanSynchronize else { guard self.collectionsCanSynchronize else {
return return
} }
@ -360,8 +360,13 @@ public class StoreCenter {
// } // }
/// Executes an API call /// Executes an API call
func execute<T: SyncedStorable, V: Decodable>(apiCall: ApiCall<T>) async throws -> V { func executeGet<T: SyncedStorable, V: Decodable>(apiCall: ApiCall<T>) async throws -> V {
return try await self.service().runApiCall(apiCall) return try await self.service().runGetApiCall(apiCall)
}
/// Executes an API call
func execute<T: SyncedStorable>(apiCalls: [ApiCall<T>]) async throws {
try await self.service().runApiCalls(apiCalls)
} }
// MARK: - Api calls // MARK: - Api calls
@ -372,35 +377,44 @@ public class StoreCenter {
&& self.userIsAllowed() && self.userIsAllowed()
} }
/// Transmit the insertion request to the ApiCall collection func prepareOperationBatch<T: SyncedStorable>(_ batch: BatchPreparation<T>) {
/// - Parameters:
/// - instance: an object to insert
func sendInsertion<T: SyncedStorable>(_ instance: T) async throws -> T? {
guard self._canSynchronise() else { guard self._canSynchronise() else {
return nil return
} }
return try await self.apiCallCollection().sendInsertion(instance) Task {
} try await self.apiCallCollection().executeBatch(batch)
/// Transmit the update request to the ApiCall collection
/// - Parameters:
/// - instance: an object to update
func sendUpdate<T: SyncedStorable>(_ 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 /// Transmit the insertion request to the ApiCall collection
/// - Parameters: /// - Parameters:
/// - instance: an object to delete /// - instance: an object to insert
func sendDeletion<T: SyncedStorable>(_ instance: T) async throws { // func sendInsertion<T: SyncedStorable>(_ instance: T) async throws -> T? {
guard self._canSynchronise() else { // guard self._canSynchronise() else {
return // return nil
} // }
try await self.apiCallCollection().sendDeletion(instance) // return try await self.apiCallCollection().sendInsertion(instance)
} // }
//
// /// Transmit the update request to the ApiCall collection
// /// - Parameters:
// /// - instance: an object to update
// func sendUpdate<T: SyncedStorable>(_ 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<T: SyncedStorable>(_ instance: T) async throws {
// guard self._canSynchronise() else {
// return
// }
// try await self.apiCallCollection().sendDeletion(instance)
// }
/// Retrieves all the items on the server /// Retrieves all the items on the server
func getItems<T: SyncedStorable>(identifier: String? = nil) async throws -> [T] { func getItems<T: SyncedStorable>(identifier: String? = nil) async throws -> [T] {

@ -102,10 +102,10 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
instance.lastUpdate = Date() instance.lastUpdate = Date()
if let index = self.items.firstIndex(where: { $0.id == instance.id }) { if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
self.updateItem(instance, index: index) self.updateItem(instance, index: index)
self._sendUpdateIfNecessary(instance) self._sendUpdate(instance)
} else { } else {
self.addItem(instance: instance) self.addItem(instance: instance)
self._sendInsertionIfNecessary(instance) self._sendInsertion(instance)
} }
} }
@ -115,17 +115,24 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
self.setChanged() self.setChanged()
} }
let date = Date()
let batch = BatchPreparation<T>()
for instance in sequence { for instance in sequence {
instance.lastUpdate = Date() instance.lastUpdate = date
if let index = self.items.firstIndex(where: { $0.id == instance.id }) { if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
self.updateItem(instance, index: index) self.updateItem(instance, index: index)
self._sendUpdateIfNecessary(instance) batch.addUpdate(instance)
// self._sendUpdateIfNecessary(instance)
} else { // insert } else { // insert
self.addItem(instance: instance) self.addItem(instance: instance)
self._sendInsertionIfNecessary(instance) batch.addInsert(instance)
// self._sendInsertionIfNecessary(instance)
} }
} }
self._prepareBatch(batch)
} }
/// Proceeds to delete all instance of the collection, properly cleaning up dependencies and sending API calls /// Proceeds to delete all instance of the collection, properly cleaning up dependencies and sending API calls
@ -141,8 +148,13 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
} }
for instance in sequence { for instance in sequence {
self._deleteNoWrite(instance: instance) self.deleteItem(instance)
StoreCenter.main.createDeleteLog(instance)
} }
let batch = BatchPreparation<T>()
batch.deletes = sequence
self._prepareBatch(batch)
} }
/// Deletes an instance and writes /// Deletes an instance and writes
@ -157,54 +169,71 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
fileprivate func _deleteNoWrite(instance: T) { fileprivate func _deleteNoWrite(instance: T) {
self.deleteItem(instance) self.deleteItem(instance)
StoreCenter.main.createDeleteLog(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 fileprivate func _sendInsertion(_ instance: T) {
/// Calls copyFromServerInstance on the instance with the result of the HTTP call self._prepareBatch(BatchPreparation(insert: instance))
/// - Parameters: }
/// - instance: the object to POST
fileprivate func _sendInsertionIfNecessary(_ instance: T) {
Task { fileprivate func _sendUpdate(_ instance: T) {
do { self._prepareBatch(BatchPreparation(update: instance))
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] fileprivate func _sendDeletion(_ instance: T) {
/// - Parameters: self._prepareBatch(BatchPreparation(delete: instance))
/// - 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] fileprivate func _prepareBatch(_ batch: BatchPreparation<T>) {
/// - Parameters: StoreCenter.main.prepareOperationBatch(batch)
/// - instance: the object to DELETE
fileprivate func _sendDeletionIfNecessary(_ instance: T) {
Task {
do {
try await self.store.sendDeletion(instance)
} catch {
Logger.error(error)
}
}
} }
/// 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 // MARK: - Synchronization
/// Adds or update an instance if it is newer than the local instance /// Adds or update an instance if it is newer than the local instance
@ -229,24 +258,38 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
// MARK: - Migrations // 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 /// Sends a POST request for the instance, and changes the collection to perform a write
public func writeChangeAndInsertOnServer(instance: T) { public func writeChangeAndInsertOnServer(instance: T) {
defer { defer {
self.setChanged() self.setChanged()
} }
self._sendInsertionIfNecessary(instance) self._sendInsertion(instance)
} }
} }
class BatchPreparation<T> {
var inserts: [T] = []
var updates: [T] = []
var deletes: any Sequence<T> = []
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)
}
}

Loading…
Cancel
Save