diff --git a/LeStorage/BaseCollection.swift b/LeStorage/BaseCollection.swift index 355b724..55c5c8c 100644 --- a/LeStorage/BaseCollection.swift +++ b/LeStorage/BaseCollection.swift @@ -41,7 +41,7 @@ public class BaseCollection: SomeCollection, CollectionHolder { fileprivate var _indexes: [T.ID: T]? = nil /// A PendingOperationManager instance that manages operations while the collection is not loaded - fileprivate var _pendingOperationManager: PendingOperationManager? = nil + fileprivate(set) var pendingOperationManager: PendingOperationManager? = nil /// Indicates whether the collection has changed, thus requiring a write operation fileprivate var _hasChanged: Bool = false { @@ -145,24 +145,6 @@ public class BaseCollection: SomeCollection, CollectionHolder { name: NSNotification.Name.CollectionDidLoad, object: self) } - fileprivate func _mergePendingOperations() { - - guard let manager = self._pendingOperationManager, manager.items.isNotEmpty else { return } - - Logger.log(">>> Merge pending: \(manager.items.count)") - let methodGroups = manager.items.group { $0.method } - for (method, group) in methodGroups { - let dataArray = group.map { $0.data } - switch method { - case .addOrUpdate: - self.addOrUpdate(contentOfs: dataArray) - case .delete: - self.delete(contentOfs: dataArray) - } - } - self._pendingOperationManager = nil - } - /// Sets a collection of items and indexes them func setItems(_ items: [T]) { for item in items { @@ -272,10 +254,10 @@ public class BaseCollection: SomeCollection, CollectionHolder { } /// Adds an instance to the collection - @discardableResult func addItem(instance: T, checkLoaded: Bool = true) -> Bool { + @discardableResult func addItem(instance: T, checkLoaded: Bool = true, shouldBeSynchronized: Bool = false) -> Bool { if checkLoaded && !self.hasLoaded { - self._addPendingOperation(method: .addOrUpdate, instance: instance) + self._addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized) return false } @@ -288,10 +270,10 @@ public class BaseCollection: SomeCollection, CollectionHolder { } /// Updates an instance to the collection by index - @discardableResult func updateItem(_ instance: T, index: Int, checkLoaded: Bool = true) -> Bool { + @discardableResult func updateItem(_ instance: T, index: Int, checkLoaded: Bool = true, shouldBeSynchronized: Bool = false) -> Bool { if checkLoaded && !self.hasLoaded { - self._addPendingOperation(method: .addOrUpdate, instance: instance) + self._addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized) return false } @@ -306,26 +288,19 @@ public class BaseCollection: SomeCollection, CollectionHolder { } /// Deletes an instance from the collection - @discardableResult func deleteItem(_ instance: T) -> Bool { + @discardableResult func deleteItem(_ instance: T, shouldBeSynchronized: Bool = false) -> Bool { if !self.hasLoaded { - self._addPendingOperation(method: .addOrUpdate, instance: instance) + self._addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized) return false } - instance.deleteDependencies() + instance.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) self.items.removeAll { $0.id == instance.id } self._indexes?.removeValue(forKey: instance.id) return true } - fileprivate func _addPendingOperation(method: StorageMethod, instance: T) { - if self._pendingOperationManager == nil { - self._pendingOperationManager = PendingOperationManager(store: self.store, inMemory: self.inMemory) - } - self._pendingOperationManager?.addPendingOperation(method: method, instance: instance) - } - /// If the collection has more instance that its limit, remove the surplus fileprivate func _applyLimitIfPresent() { if let limit { @@ -361,6 +336,51 @@ public class BaseCollection: SomeCollection, CollectionHolder { self.delete(contentOfs: self.items) } + // MARK: - Pending operations + + fileprivate func _addPendingOperation(method: StorageMethod, instance: T, shouldBeSynchronized: Bool) { + if self.pendingOperationManager == nil { + self.pendingOperationManager = PendingOperationManager(store: self.store, inMemory: self.inMemory) + } + self.addPendingOperation(method: method, instance: instance, shouldBeSynchronized: false) + } + + func addPendingOperation(method: StorageMethod, instance: T, shouldBeSynchronized: Bool) { + self.pendingOperationManager?.addPendingOperation(method: method, instance: instance, shouldBeSynchronized: shouldBeSynchronized) + } + + fileprivate func _mergePendingOperations() { + + guard let manager = self.pendingOperationManager, manager.items.isNotEmpty else { return } + + Logger.log(">>> Merge pending: \(manager.items.count)") + for item in manager.items { + let data = item.data + switch (item.method, item.shouldBeSynchronized) { + case (.addOrUpdate, true): + self.addOrUpdate(instance: data) + case (.addOrUpdate, false): + self.addOrUpdateItem(instance: data) + case (.delete, true): + self.delete(instance: data) + case (.delete, false): + self.deleteItem(data) + } + } + +// let methodGroups = manager.items.group { $0.method } +// for (method, group) in methodGroups { +// let dataArray = group.map { $0.data } +// switch method { +// case .addOrUpdate: +// self.addOrUpdate(contentOfs: dataArray) +// case .delete: +// self.delete(contentOfs: dataArray) +// } +// } + self.pendingOperationManager = nil + } + // MARK: - File access /// Schedules a write operation diff --git a/LeStorage/Codables/PendingOperation.swift b/LeStorage/Codables/PendingOperation.swift index e3c64ed..b96136d 100644 --- a/LeStorage/Codables/PendingOperation.swift +++ b/LeStorage/Codables/PendingOperation.swift @@ -17,14 +17,12 @@ class PendingOperation: Codable, Equatable { var id: String = Store.randomId() var method: StorageMethod var data: T -// var modelName: String -// var storeId: String? + var shouldBeSynchronized: Bool - init(method: StorageMethod, data: T) { -// self.modelName = modelName -// self.storeId = storeId + init(method: StorageMethod, data: T, shouldBeSynchronized: Bool) { self.method = method self.data = data + self.shouldBeSynchronized = shouldBeSynchronized } static func == (lhs: PendingOperation, rhs: PendingOperation) -> Bool { diff --git a/LeStorage/ModelObject.swift b/LeStorage/ModelObject.swift index f7f5160..99e785f 100644 --- a/LeStorage/ModelObject.swift +++ b/LeStorage/ModelObject.swift @@ -15,7 +15,7 @@ open class ModelObject: NSObject { public override init() { } - open func deleteDependencies() { + open func deleteDependencies(shouldBeSynchronized: Bool) { } diff --git a/LeStorage/PendingOperationManager.swift b/LeStorage/PendingOperationManager.swift index 16f370d..8a354c2 100644 --- a/LeStorage/PendingOperationManager.swift +++ b/LeStorage/PendingOperationManager.swift @@ -32,10 +32,10 @@ class PendingOperationManager { } } - func addPendingOperation(method: StorageMethod, instance: T) { + func addPendingOperation(method: StorageMethod, instance: T, shouldBeSynchronized: Bool) { Logger.log("addPendingOperation: \(method), \(instance)") - let operation = PendingOperation(method: method, data: instance) + let operation = PendingOperation(method: method, data: instance, shouldBeSynchronized: shouldBeSynchronized) self.items.append(operation) self._writeIfNecessary() diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index 90b6f99..0938ff5 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -34,7 +34,7 @@ let changePasswordCall: ServiceCall = ServiceCall( let postDeviceTokenCall: ServiceCall = ServiceCall( path: "device-token/", method: .post, requiresToken: true) let getUserDataAccessCall: ServiceCall = ServiceCall( - path: "data-access/", method: .get, requiresToken: true) + path: "data-access-content/", method: .get, requiresToken: true) let userNamesCall: ServiceCall = ServiceCall( path: "user-names/", method: .get, requiresToken: true) @@ -632,7 +632,7 @@ public class Services { } /// Returns the list of DataAccess - public func getUserDataAccess() async throws { + func getUserDataAccess() async throws { let request = try self._baseRequest(call: getUserDataAccessCall) if let data = try await self._runRequest(request) { await StoreCenter.main.userDataAccessRetrieved(data) diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index ee9a3d2..eb87d65 100644 --- a/LeStorage/Storable.swift +++ b/LeStorage/Storable.swift @@ -21,7 +21,7 @@ public protocol Storable: Codable, Identifiable, NSObjectProtocol { /// 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() + func deleteDependencies(shouldBeSynchronized: Bool) /// 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 diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index 39f0ea6..3af7d2b 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -577,6 +577,23 @@ public class StoreCenter { if let revocations = json["revocations"] as? [String: Any] { try self._parseSyncRevocations(revocations, parents: json["revocation_parents"] as? [[String: Any]]) } + + // Data access events + if let rs = json["relationship_sets"] as? [String: Any] { + try self._parseSyncUpdates(rs) + } + + if let rr = json["relationship_removals"] as? [String: Any] { + try self._parseSyncDeletions(rr) + } + + if let srs = json["shared_relationship_sets"] as? [String: Any] { + try self._parseSyncUpdates(srs, shared: true) + } + + if let srm = json["shared_relationship_removals"] as? [String: Any] { + self._synchronizationRevoke(items: srm) + } if let dateString = json["date"] as? String { Logger.log("Sets sync date = \(dateString)") @@ -671,23 +688,28 @@ public class StoreCenter { if let parents { for level in parents { - for (className, parentData) in level { - guard let parentItems = parentData as? [Any] else { - Logger.w("Invalid update data for \(className): \(parentData)") - continue - } - for parentItem in parentItems { - do { - let data = try JSONSerialization.data(withJSONObject: parentItem, options: []) - let revokedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) - StoreCenter.main.synchronizationRevoke(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId) - } catch { - Logger.error(error) - } - } + self._synchronizationRevoke(items: level) + } + } + } + + fileprivate func _synchronizationRevoke(items: [String: Any]) { + for (className, parentData) in items { + guard let parentItems = parentData as? [Any] else { + Logger.w("Invalid update data for \(className): \(parentData)") + continue + } + for parentItem in parentItems { + do { + let data = try JSONSerialization.data(withJSONObject: parentItem, options: []) + let revokedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) + StoreCenter.main.synchronizationRevoke(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId) + } catch { + Logger.error(error) } } } + } /// Returns a Type object for a class name @@ -987,6 +1009,7 @@ public class StoreCenter { } else { dataAccess.sharedWith.removeAll() dataAccess.sharedWith = users + dataAccessCollection.addOrUpdate(instance: dataAccess) } } else { let dataAccess = DataAccess(owner: userId, sharedWith: users, modelName: String(describing: type(of: instance)), modelId: instance.stringId) diff --git a/LeStorage/SyncedCollection.swift b/LeStorage/SyncedCollection.swift index e4bbc22..d63a4ce 100644 --- a/LeStorage/SyncedCollection.swift +++ b/LeStorage/SyncedCollection.swift @@ -92,12 +92,12 @@ public class SyncedCollection: BaseCollection, SomeSynced instance.lastUpdate = Date() if let index = self.items.firstIndex(where: { $0.id == instance.id }) { - if self.updateItem(instance, index: index) { + if self.updateItem(instance, index: index, shouldBeSynchronized: true) { self._sendUpdate(instance) self.setChanged() } } else { - if self.addItem(instance: instance) { + if self.addItem(instance: instance, shouldBeSynchronized: true) { self._sendInsertion(instance) self.setChanged() } @@ -117,11 +117,11 @@ public class SyncedCollection: BaseCollection, SomeSynced for instance in sequence { instance.lastUpdate = date if let index = self.items.firstIndex(where: { $0.id == instance.id }) { - if self.updateItem(instance, index: index) { + if self.updateItem(instance, index: index, shouldBeSynchronized: true) { batch.addUpdate(instance) } } else { // insert - if self.addItem(instance: instance) { + if self.addItem(instance: instance, shouldBeSynchronized: true) { batch.addInsert(instance) } } @@ -137,7 +137,7 @@ public class SyncedCollection: BaseCollection, SomeSynced } /// Deletes all items of the sequence by id and sets the collection as changed to trigger a write - override public func delete(contentOfs sequence: any RandomAccessCollection) { + public override func delete(contentOfs sequence: any RandomAccessCollection) { defer { self.setChanged() @@ -148,7 +148,7 @@ public class SyncedCollection: BaseCollection, SomeSynced var deleted: [T] = [] for instance in sequence { - if self.deleteItem(instance) { + if self.deleteItem(instance, shouldBeSynchronized: true) { deleted.append(instance) } StoreCenter.main.createDeleteLog(instance) @@ -169,15 +169,19 @@ public class SyncedCollection: BaseCollection, SomeSynced /// Deletes an instance without writing, logs the operation and sends an API call fileprivate func _deleteNoWrite(instance: T) { - self.deleteItem(instance) + self.deleteItem(instance, shouldBeSynchronized: true) StoreCenter.main.createDeleteLog(instance) self._sendDeletion(instance) } - public func deleteDependencies(_ items: any RandomAccessCollection) { + public func deleteDependencies(_ items: any RandomAccessCollection, shouldBeSynchronized: Bool) { guard items.isNotEmpty else { return } - self.delete(contentOfs: items) + if shouldBeSynchronized { + self.delete(contentOfs: items) + } else { + self.deleteNoSync(contentOfs: items) + } } // MARK: - Basic operations without sync @@ -193,11 +197,21 @@ public class SyncedCollection: BaseCollection, SomeSynced } /// Deletes the instance in the collection without synchronization - func deleteNoSync(instance: T) throws { + func deleteNoSync(contentOfs sequence: any Sequence) { + defer { + self.setChanged() + } + for item in sequence { + self.deleteItem(item, shouldBeSynchronized: false) + } + } + + /// Deletes the instance in the collection without synchronization + func deleteNoSync(instance: T) { defer { self.setChanged() } - self.deleteItem(instance) + self.deleteItem(instance, shouldBeSynchronized: false) } /// Deletes the instance in the collection without synchronization @@ -207,7 +221,7 @@ public class SyncedCollection: BaseCollection, SomeSynced } let realId = T.buildRealId(id: id) if let instance = self.findById(realId) { - self.deleteItem(instance) + self.deleteItem(instance, shouldBeSynchronized: false) } } @@ -311,7 +325,7 @@ public class SyncedCollection: BaseCollection, SomeSynced if shared { instance.shared = true } - self.addItem(instance: instance) + self.addItem(instance: instance, shouldBeSynchronized: false) } } diff --git a/LeStorage/WebSocketManager.swift b/LeStorage/WebSocketManager.swift index 31cbcb1..c3b7c0b 100644 --- a/LeStorage/WebSocketManager.swift +++ b/LeStorage/WebSocketManager.swift @@ -21,7 +21,7 @@ class WebSocketManager: ObservableObject { init(urlString: String) { self._url = urlString -// _setupWebSocket() + _setupWebSocket() } deinit {