diff --git a/LeStorage/ApiCallCollection.swift b/LeStorage/ApiCallCollection.swift index 0f3e842..403836c 100644 --- a/LeStorage/ApiCallCollection.swift +++ b/LeStorage/ApiCallCollection.swift @@ -190,7 +190,10 @@ actor ApiCallCollection: SomeCallCollection { if batch.count == 1, let apiCall = batch.first, apiCall.method == .get { let _: Empty = try await self._executeGetCall(apiCall) } else { - try await self._executeApiCalls(batch) + let success = try await self._executeApiCalls(batch) + if T.copyServerResponse { + StoreCenter.main.updateLocalInstances(success) + } } } catch { Logger.error(error) @@ -313,7 +316,7 @@ actor ApiCallCollection: SomeCallCollection { } } - func executeBatch(_ batch: BatchPreparation) async throws { + func executeBatch(_ batch: OperationBatch) async throws -> [T] { var apiCalls: [ApiCall] = [] let transactionId = Store.randomId() @@ -332,7 +335,7 @@ actor ApiCallCollection: SomeCallCollection { self._prepareCall(apiCall: call) apiCalls.append(call) } - try await self._executeApiCalls(apiCalls) + return try await self._executeApiCalls(apiCalls) } /// Sends an insert api call for the provided [instance] @@ -396,8 +399,8 @@ actor ApiCallCollection: SomeCallCollection { /// 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 { - try await StoreCenter.main.execute(apiCalls: apiCalls) + fileprivate func _executeApiCalls(_ apiCalls: [ApiCall]) async throws -> [T] { + return try await StoreCenter.main.execute(apiCalls: apiCalls) } /// Returns the content of the API call file as a String diff --git a/LeStorage/ModelObject.swift b/LeStorage/ModelObject.swift index ef9aa90..f7f5160 100644 --- a/LeStorage/ModelObject.swift +++ b/LeStorage/ModelObject.swift @@ -19,10 +19,6 @@ open class ModelObject: NSObject { } - open func copyFromServerInstance(_ instance: any Storable) -> Bool { - return false - } - static var relationshipNames: [String] = [] } @@ -57,6 +53,10 @@ open class SyncedModelObject: BaseModelObject { public var lastUpdate: Date = Date() public var shared: Bool? + open func copyFromServerInstance(_ instance: any Storable) -> Bool { + return false + } + public override init() { super.init() } diff --git a/LeStorage/Relationship.swift b/LeStorage/Relationship.swift index 3074981..a3b58fe 100644 --- a/LeStorage/Relationship.swift +++ b/LeStorage/Relationship.swift @@ -12,6 +12,9 @@ public struct Relationship { self.keyPath = keyPath } + /// The type of the relationship var type: any Storable.Type + + /// the keyPath to access the relationship var keyPath: AnyKeyPath } diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index 41a3fc5..2fb5a50 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -40,6 +40,9 @@ let userNamesCall: ServiceCall = ServiceCall( /// A class used to send HTTP request to the django server public class Services { + /// The base API URL to send requests + fileprivate(set) var baseURL: String + /// A KeychainStore object used to store the user's token let keychainStore: KeychainStore @@ -47,13 +50,8 @@ public class Services { self.baseURL = url self.keychainStore = KeychainStore(serverId: url) Logger.log("create keystore with id: \(url)") - } - /// The base API URL to send requests - fileprivate(set) var baseURL: String - - // MARK: - Base /// Runs a request using a configuration object @@ -61,8 +59,7 @@ public class Services { /// - serviceConf: A instance of ServiceConf /// - apiCallId: an optional id referencing an ApiCall fileprivate func _runRequest(serviceCall: ServiceCall) - async throws -> U - { + async throws -> U { let request = try self._baseRequest(call: serviceCall) return try await _runRequest(request) } @@ -84,13 +81,14 @@ public class Services { /// - 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 { + _ request: URLRequest, type: T.Type) async throws -> [T] { 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 + var success: [T] = [] if let response = task.1 as? HTTPURLResponse { let statusCode = response.statusCode @@ -100,8 +98,12 @@ public class Services { let decoded: BatchResponse = try self._decode(data: task.0) for result in decoded.results { + switch result.status { case 200..<300: + if let data = result.data { + success.append(data) + } try await StoreCenter.main.deleteApiCallById(type: T.self, id: result.apiCallId) default: @@ -109,7 +111,7 @@ public class Services { } } - default: // error + default: // error Logger.log( "Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") let errorString: String = String(data: task.0, encoding: .utf8) ?? "" @@ -133,8 +135,10 @@ public class Services { } if rescheduleApiCalls { - try await StoreCenter.main.rescheduleApiCalls(type: T.self) + try? await StoreCenter.main.rescheduleApiCalls(type: T.self) } + + return success } /// Runs a request using a traditional URLRequest @@ -496,9 +500,9 @@ public class Services { } /// Executes an ApiCall - func runApiCalls(_ apiCalls: [ApiCall]) async throws { + func runApiCalls(_ apiCalls: [ApiCall]) async throws -> [T] { let request = try self._syncPostRequest(from: apiCalls) - try await self._runSyncPostRequest(request, type: T.self) + return try await self._runSyncPostRequest(request, type: T.self) } /// Returns the URLRequest for an ApiCall diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index 2c08e3e..ee9a3d2 100644 --- a/LeStorage/Storable.swift +++ b/LeStorage/Storable.swift @@ -18,13 +18,16 @@ public protocol Storable: Codable, Identifiable, NSObjectProtocol { static func resourceName() -> String /// A method that deletes the local dependencies of the resource - /// Mimics the behavior the cascading delete on the django server + /// Mimics the behavior of the cascading delete on the django server /// Typically when we delete a resource, we automatically delete items that depends on it, /// so when we do that on the server, we also need to do it locally func deleteDependencies() + /// Copies the content of another item into the instance + /// This behavior has been made to get live updates when looking at properties in SwiftUI screens func copy(from other: any Storable) + /// This method returns RelationShips objects of the type static func relationships() -> [Relationship] } diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index 87ca587..6254da2 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -365,8 +365,8 @@ public class StoreCenter { } /// Executes an API call - func execute(apiCalls: [ApiCall]) async throws { - try await self.service().runApiCalls(apiCalls) + func execute(apiCalls: [ApiCall]) async throws -> [T] { + return try await self.service().runApiCalls(apiCalls) } // MARK: - Api calls @@ -377,13 +377,11 @@ public class StoreCenter { && self.userIsAllowed() } - func prepareOperationBatch(_ batch: BatchPreparation) { + func sendOperationBatch(_ batch: OperationBatch) async throws -> [T] { guard self._canSynchronise() else { - return - } - Task { - try await self.apiCallCollection().executeBatch(batch) + return [] } + return try await self.apiCallCollection().executeBatch(batch) } /// Transmit the insertion request to the ApiCall collection @@ -836,13 +834,24 @@ public class StoreCenter { // MARK: - Instant update /// Updates a local object with a server instance - func updateFromServerInstance(_ result: T) { - if let storedCollection: StoredCollection = self.collectionOfInstance(result) { - if storedCollection.findById(result.id) != nil { - storedCollection.updateFromServerInstance(result) + func updateLocalInstances(_ results: [T]) { + for result in results { + if let storedCollection: StoredCollection = self.collectionOfInstance(result) { + if storedCollection.findById(result.id) != nil { + storedCollection.updateFromServerInstance(result) + } } } } + + /// Updates a local object with a server instance +// func updateFromServerInstance(_ result: T) { +// if let storedCollection: StoredCollection = self.collectionOfInstance(result) { +// if storedCollection.findById(result.id) != nil { +// storedCollection.updateFromServerInstance(result) +// } +// } +// } /// Returns the collection hosting an instance func collectionOfInstance(_ instance: T) -> StoredCollection? { diff --git a/LeStorage/StoredCollection+Sync.swift b/LeStorage/StoredCollection+Sync.swift index e8abb46..d83cf14 100644 --- a/LeStorage/StoredCollection+Sync.swift +++ b/LeStorage/StoredCollection+Sync.swift @@ -54,10 +54,12 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { func updateFromServerInstance(_ serverInstance: T) { DispatchQueue.main.async { if let localInstance = self.findById(serverInstance.id) { - let modified = localInstance.copyFromServerInstance(serverInstance) - if modified { - self.setChanged() - } + localInstance.copy(from: serverInstance) + self.setChanged() +// let modified = localInstance.copyFromServerInstance(serverInstance) +// if modified { +// self.setChanged() +// } } } } @@ -116,7 +118,7 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { } let date = Date() - let batch = BatchPreparation() + let batch = OperationBatch() for instance in sequence { instance.lastUpdate = date @@ -131,7 +133,7 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { } } - self._prepareBatch(batch) + self._sendOperationBatch(batch) } @@ -152,9 +154,9 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { StoreCenter.main.createDeleteLog(instance) } - let batch = BatchPreparation() - batch.deletes = sequence - self._prepareBatch(batch) + let batch = OperationBatch() + batch.deletes = Array(sequence) + self._sendOperationBatch(batch) } /// Deletes an instance and writes @@ -176,19 +178,28 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { // MARK: - Send requests fileprivate func _sendInsertion(_ instance: T) { - self._prepareBatch(BatchPreparation(insert: instance)) + self._sendOperationBatch(OperationBatch(insert: instance)) } fileprivate func _sendUpdate(_ instance: T) { - self._prepareBatch(BatchPreparation(update: instance)) + self._sendOperationBatch(OperationBatch(update: instance)) } fileprivate func _sendDeletion(_ instance: T) { - self._prepareBatch(BatchPreparation(delete: instance)) + self._sendOperationBatch(OperationBatch(delete: instance)) } - fileprivate func _prepareBatch(_ batch: BatchPreparation) { - StoreCenter.main.prepareOperationBatch(batch) + fileprivate func _sendOperationBatch(_ batch: OperationBatch) { + Task { + do { + let success = try await StoreCenter.main.sendOperationBatch(batch) + for item in success { + self.updateFromServerInstance(item) + } + } catch { + Logger.error(error) + } + } } /// Sends an insert api call for the provided @@ -268,10 +279,10 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { } -class BatchPreparation { +class OperationBatch { var inserts: [T] = [] var updates: [T] = [] - var deletes: any Sequence = [] + var deletes: [T] = [] init() { @@ -292,4 +303,7 @@ class BatchPreparation { func addUpdate(_ instance: T) { self.updates.append(instance) } + func addDelete(_ instance: T) { + self.deletes.append(instance) + } } diff --git a/LeStorage/SyncedStorable.swift b/LeStorage/SyncedStorable.swift index 6825382..c4ee339 100644 --- a/LeStorage/SyncedStorable.swift +++ b/LeStorage/SyncedStorable.swift @@ -15,12 +15,9 @@ public protocol SyncedStorable: Storable { /// Returns HTTP methods that do not need to pass the token to the request static func tokenExemptedMethods() -> [HTTPMethod] - /// A method called to retrieve data added by the server on a POST request - /// The method will be called after a POST has succeeded, - /// and will provide a copy of what's on the server - /// Should return true to trigger a write on the collection, or false if nothing changed - func copyFromServerInstance(_ instance: any Storable) -> Bool - + /// Returns whether we should copy the server response into the local instance + static var copyServerResponse: Bool { get } + } protocol URLParameterConvertible { @@ -33,6 +30,8 @@ public protocol SideStorable { extension SyncedStorable { + public static var copyServerResponse: Bool { return false } + func getStoreId() -> String? { if let alt = self as? SideStorable { return alt.storeId