From 1f78cc7be47c61e07eb6e5f123cccc32fd1893a0 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 12 May 2025 16:47:50 +0200 Subject: [PATCH] improves delete dependencies system --- LeStorage/BaseCollection.swift | 30 ++--- LeStorage/ModelObject.swift | 2 +- LeStorage/Storable.swift | 2 +- LeStorage/Store.swift | 56 ++++++--- LeStorage/StoreCenter.swift | 19 +-- LeStorage/SyncedCollection.swift | 197 ++++++++++++------------------- 6 files changed, 139 insertions(+), 167 deletions(-) diff --git a/LeStorage/BaseCollection.swift b/LeStorage/BaseCollection.swift index e628170..15d0f3f 100644 --- a/LeStorage/BaseCollection.swift +++ b/LeStorage/BaseCollection.swift @@ -272,7 +272,7 @@ public class BaseCollection: SomeCollection, CollectionHolder { @discardableResult func addItem(instance: T, checkLoaded: Bool = true, shouldBeSynchronized: Bool = false) -> Bool { if checkLoaded && !self.hasLoaded { - self._addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized) + self.addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized) return false } @@ -288,7 +288,7 @@ public class BaseCollection: SomeCollection, CollectionHolder { @discardableResult func updateItem(_ instance: T, index: Int, checkLoaded: Bool = true, shouldBeSynchronized: Bool = false) -> Bool { if checkLoaded && !self.hasLoaded { - self._addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized) + self.addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized) return false } @@ -306,11 +306,14 @@ public class BaseCollection: SomeCollection, CollectionHolder { @discardableResult func deleteItem(_ instance: T, shouldBeSynchronized: Bool = false) -> Bool { if !self.hasLoaded { - self._addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized) + self.addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized) return false } - instance.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized) + if shouldBeSynchronized { // when user initiated, we want to cascade delete, but when synchronized, we want the delete notifications to make the job and be sure everything works + instance.deleteDependencies(store: self.store, shouldBeSynchronized: shouldBeSynchronized) + } + self.items.removeAll { $0.id == instance.id } self._indexes?.removeValue(forKey: instance.id) return true @@ -337,13 +340,12 @@ public class BaseCollection: SomeCollection, CollectionHolder { defer { self._hasChanged = true } - let itemsArray = Array(items) // fix error if items is self.items + let itemsArray = Array(items) // fix error if items is self.items for item in itemsArray { if let index = self.items.firstIndex(where: { $0.id == item.id }) { self.items.remove(at: index) } } - } /// Proceeds to delete all instance of the collection, properly cleaning up dependencies and sending API calls @@ -353,14 +355,14 @@ public class BaseCollection: SomeCollection, CollectionHolder { // MARK: - Pending operations - fileprivate func _addPendingOperation(method: StorageMethod, instance: T, shouldBeSynchronized: Bool) { + 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) + self._addPendingOperationIfPossible(method: method, instance: instance, shouldBeSynchronized: false) } - func addPendingOperation(method: StorageMethod, instance: T, shouldBeSynchronized: Bool) { + fileprivate func _addPendingOperationIfPossible(method: StorageMethod, instance: T, shouldBeSynchronized: Bool) { self.pendingOperationManager?.addPendingOperation(method: method, instance: instance, shouldBeSynchronized: shouldBeSynchronized) } @@ -383,16 +385,6 @@ public class BaseCollection: SomeCollection, CollectionHolder { } } -// 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 } diff --git a/LeStorage/ModelObject.swift b/LeStorage/ModelObject.swift index 1c934d5..0c5f32a 100644 --- a/LeStorage/ModelObject.swift +++ b/LeStorage/ModelObject.swift @@ -15,7 +15,7 @@ open class ModelObject: NSObject { public override init() { } - open func deleteDependencies(shouldBeSynchronized: Bool) { + open func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { } diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index 0bfff97..e901387 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(shouldBeSynchronized: Bool) + func deleteDependencies(store: Store, 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/Store.swift b/LeStorage/Store.swift index aa895aa..f6eb5d8 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -17,6 +17,7 @@ public enum StoreError: Error, LocalizedError { case collectionNotRegistered(type: String) case apiCallCollectionNotRegistered(type: String) case synchronizationInactive + case storeNotRegistered(id: String) public var errorDescription: String? { switch self { @@ -36,6 +37,8 @@ public enum StoreError: Error, LocalizedError { return "The api call collection has not been registered for \(type)" case .synchronizationInactive: return "The synchronization is not active on this StoreCenter" + case .storeNotRegistered(let id): + return "The store with identifier \(id) is not registered" } } @@ -45,15 +48,9 @@ final public class Store { fileprivate(set) var storeCenter: StoreCenter - /// The Store singleton -// public static let main = Store() - /// The dictionary of registered collections fileprivate var _collections: [String : any SomeCollection] = [:] -// /// The name of the directory to store the json files -// static let storageDirectory = "storage" - /// The store identifier, used to name the store directory, and to perform filtering requests to the server public fileprivate(set) var identifier: String? = nil @@ -71,6 +68,10 @@ final public class Store { public static var main: Store { return StoreCenter.main.mainStore } + public func alternateStore(identifier: String) throws -> Store { + return try self.storeCenter.store(identifier: identifier) + } + /// Creates the store directory /// - Parameters: /// - directory: the name of the directory @@ -159,17 +160,6 @@ final public class Store { return collection.findById(id) } - /// Filters a collection by predicate - /// - Parameters: - /// - isIncluded: a predicate to returns if a data should be filtered in -// public func filter(isIncluded: (T) throws -> (Bool)) rethrows -> [T] { -// do { -// return try self.collection().filter(isIncluded) -// } catch { -// return [] -// } -// } - /// Returns a collection by type func syncedCollection() throws -> SyncedCollection { if let collection = self._collections[T.resourceName()] as? SyncedCollection { @@ -265,6 +255,38 @@ final public class Store { return count } + public func deleteAllDependencies(type: T.Type, shouldBeSynchronized: Bool) { + do { + let collection: BaseCollection = try self.collection() + try self._deleteDependencies(Array(collection.items), shouldBeSynchronized: shouldBeSynchronized) + } catch { + Logger.error(error) + } + } + + public func deleteDependencies(type: T.Type, shouldBeSynchronized: Bool, _ handler: (T) throws -> Bool) { + do { + let collection: BaseCollection = try self.collection() + let items = try collection.items.filter(handler) + try self._deleteDependencies(items, shouldBeSynchronized: shouldBeSynchronized) + } catch { + Logger.error(error) + } + + } + + fileprivate func _deleteDependencies(_ items: [T], shouldBeSynchronized: Bool) throws { + do { + let collection: BaseCollection = try self.collection() + for item in items { + item.deleteDependencies(store: self, shouldBeSynchronized: shouldBeSynchronized) + } + collection.deleteDependencies(collection.items) + } catch { + Logger.error(error) + } + } + // MARK: - Write /// Returns the directory URL of the store diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index 964e494..f45f493 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -200,7 +200,7 @@ public class StoreCenter { /// - Parameters: /// - identifier: The store identifer /// - parameter: The parameter name used to filter data on the server - public func store(identifier: String) -> Store { + public func requestStore(identifier: String) -> Store { if let store = self._stores[identifier] { return store } else { @@ -209,6 +209,13 @@ public class StoreCenter { return store } } + + public func store(identifier: String) throws -> Store { + if let store = self._stores[identifier] { + return store + } + throw StoreError.storeNotRegistered(id: identifier) + } // MARK: - Settings @@ -797,13 +804,9 @@ public class StoreCenter { /// Creates a delete log for an instance func createDeleteLog(_ instance: T) { - self._addDataLog(instance, method: .delete) - } - - /// Adds a datalog for an instance with the associated method - fileprivate func _addDataLog(_ instance: T, method: HTTPMethod) { - let dataLog = DataLog( - dataId: instance.stringId, modelName: String(describing: T.self), operation: method) + let dataLog = DataLog(dataId: instance.stringId, + modelName: String(describing: T.self), + operation: .delete) self._deleteLogs.addOrUpdate(instance: dataLog) } diff --git a/LeStorage/SyncedCollection.swift b/LeStorage/SyncedCollection.swift index 03704b4..09b55f5 100644 --- a/LeStorage/SyncedCollection.swift +++ b/LeStorage/SyncedCollection.swift @@ -40,10 +40,6 @@ public class SyncedCollection: BaseCollection, SomeSynced } } -// func loadDataFromServerIfAllowed() async throws { -// try await self.loadDataFromServerIfAllowed(clear: false) -// } - /// Retrieves the data from the server and loads it into the items array public func loadDataFromServerIfAllowed(clear: Bool = false) async throws { do { @@ -67,11 +63,23 @@ public class SyncedCollection: BaseCollection, SomeSynced return } - DispatchQueue.main.async { - if let localInstance = self.findById(serverInstance.id) { - localInstance.copy(from: serverInstance) - self.setChanged() - } + Task { + await _updateLocalInstance(serverInstance) + } + +// DispatchQueue.main.async { +// if let localInstance = self.findById(serverInstance.id) { +// localInstance.copy(from: serverInstance) +// self.setChanged() +// } +// } + } + + @MainActor + fileprivate func _updateLocalInstance(_ serverInstance: T) { + if let localInstance = self.findById(serverInstance.id) { + localInstance.copy(from: serverInstance) + self.setChanged() } } @@ -89,17 +97,6 @@ public class SyncedCollection: BaseCollection, SomeSynced // MARK: - Basic operations with sync - /// Adds or update an instance asynchronously and waits for network operations - func addOrUpdateAsync(instance: T) async throws { - if let result = _addOrUpdateCore(instance: instance) { - if result.isNewItem { - try await self._executeBatchOnce(OperationBatch(insert: result.item)) - } else { - try await self._executeBatchOnce(OperationBatch(update: result.item)) - } - } - } - /// Adds or update an instance synchronously, dispatching network operations to background tasks public override func addOrUpdate(instance: T) { if let result = _addOrUpdateCore(instance: instance) { @@ -128,37 +125,6 @@ public class SyncedCollection: BaseCollection, SomeSynced return nil } -// func addOrUpdateAsync(instance: T) async { -// instance.lastUpdate = Date() -// if let index = self.items.firstIndex(where: { $0.id == instance.id }) { -// if self.updateItem(instance, index: index, shouldBeSynchronized: true) { -// await self._sendUpdate(instance) -// self.setChanged() -// } -// } else { -// if self.addItem(instance: instance, shouldBeSynchronized: true) { -// await self._sendInsertion(instance) -// self.setChanged() -// } -// } -// } -// -// /// Adds or update an instance and writes -// public override func addOrUpdate(instance: T) { -// instance.lastUpdate = Date() -// if let index = self.items.firstIndex(where: { $0.id == instance.id }) { -// if self.updateItem(instance, index: index, shouldBeSynchronized: true) { -// Task { await self._sendUpdate(instance) } -// self.setChanged() -// } -// } else { -// if self.addItem(instance: instance, shouldBeSynchronized: true) { -// Task { await self._sendInsertion(instance) } -// self.setChanged() -// } -// } -// } - fileprivate func _addOrUpdateCore(contentOfs sequence: any Sequence) -> OperationBatch { defer { @@ -190,11 +156,6 @@ public class SyncedCollection: BaseCollection, SomeSynced Task { await self._sendOperationBatch(batch) } } - func addOrUpdateAsync(contentOfs sequence: any Sequence) async throws { - let batch = self._addOrUpdateCore(contentOfs: sequence) - try await self._executeBatchOnce(batch) - } - /// Proceeds to delete all instance of the collection, properly cleaning up dependencies and sending API calls override public func deleteAll() throws { self.delete(contentOfs: self.items) @@ -228,37 +189,23 @@ public class SyncedCollection: BaseCollection, SomeSynced Task { await self._sendOperationBatch(batch) } } - /// Deletes all items of the sequence by id and sets the collection as changed to trigger a write - public func deleteAsync(contentOfs sequence: any RandomAccessCollection) async throws{ - guard sequence.isNotEmpty else { return } - let batch = self._deleteCore(contentOfs: sequence) - try await self._executeBatchOnce(batch) - } - - /// Deletes an instance and writes - func deleteAsync(instance: T) async throws{ - defer { - self.setChanged() - } - self._deleteNoWrite(instance: instance) - try await self._executeBatchOnce(OperationBatch(delete: instance)) - } - /// Deletes an instance and writes override public func delete(instance: T) { defer { self.setChanged() } - self._deleteNoWrite(instance: instance) + self.deleteItem(instance, shouldBeSynchronized: true) + self.storeCenter.createDeleteLog(instance) + Task { await self._sendDeletion(instance) } } /// Deletes an instance without writing, logs the operation and sends an API call - fileprivate func _deleteNoWrite(instance: T) { - self.deleteItem(instance, shouldBeSynchronized: true) - self.storeCenter.createDeleteLog(instance) -// await self._sendDeletion(instance) - } +// fileprivate func _deleteNoWrite(instance: T) { +// self.deleteItem(instance, shouldBeSynchronized: true) +// self.storeCenter.createDeleteLog(instance) +//// await self._sendDeletion(instance) +// } public func deleteDependencies(_ items: any RandomAccessCollection, shouldBeSynchronized: Bool) { guard items.isNotEmpty else { return } @@ -269,6 +216,57 @@ public class SyncedCollection: BaseCollection, SomeSynced } } + public override func deleteDependencies(_ items: any Sequence) { + super.deleteDependencies(items) + + let batch = OperationBatch() + batch.deletes = Array(items) + Task { await self._sendOperationBatch(batch) } + } + + public func deleteDependenciesAsync(_ items: any Sequence) async { + super.deleteDependencies(items) + + let batch = OperationBatch() + batch.deletes = Array(items) + await self._sendOperationBatch(batch) + } + + // MARK: - Asynchronous operations + + /// Adds or update an instance asynchronously and waits for network operations + func addOrUpdateAsync(instance: T) async throws { + if let result = _addOrUpdateCore(instance: instance) { + if result.isNewItem { + try await self._executeBatchOnce(OperationBatch(insert: result.item)) + } else { + try await self._executeBatchOnce(OperationBatch(update: result.item)) + } + } + } + + func addOrUpdateAsync(contentOfs sequence: any Sequence) async throws { + let batch = self._addOrUpdateCore(contentOfs: sequence) + try await self._executeBatchOnce(batch) + } + + /// Deletes all items of the sequence by id and sets the collection as changed to trigger a write + public func deleteAsync(contentOfs sequence: any RandomAccessCollection) async throws{ + guard sequence.isNotEmpty else { return } + let batch = self._deleteCore(contentOfs: sequence) + try await self._executeBatchOnce(batch) + } + + /// Deletes an instance and writes + func deleteAsync(instance: T) async throws { + defer { + self.setChanged() + } + self.deleteItem(instance, shouldBeSynchronized: true) + self.storeCenter.createDeleteLog(instance) + try await self._executeBatchOnce(OperationBatch(delete: instance)) + } + // MARK: - Basic operations without sync /// Adds or update an instance without synchronizing it @@ -350,49 +348,6 @@ public class SyncedCollection: BaseCollection, SomeSynced } } - /// 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