From 007f7d6bf18122239d09b3f2f3b80d98fb088c87 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 12 Jun 2025 12:22:16 +0200 Subject: [PATCH] Avoid doing too many writes when synchronizing --- LeStorage/Store.swift | 64 +++++++++++++- LeStorage/StoreCenter.swift | 138 ++++++++++++++++++++----------- LeStorage/StoredCollection.swift | 38 +++++---- LeStorage/SyncedCollection.swift | 19 ++++- 4 files changed, 187 insertions(+), 72 deletions(-) diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index aece6cc..5683724 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -254,6 +254,20 @@ final public class Store { // MARK: - Synchronization + fileprivate func _requestWrite(type: T.Type) { + self._baseCollections[T.resourceName()]?.requestWrite() + } + + @MainActor + func synchronizationAddOrUpdate(_ instances: [T], shared: SharingStatus?) { + for item in instances { + if !self.storeCenter.hasAlreadyBeenDeleted(item) { + self.addOrUpdateIfNewer(item, shared: shared) + } + } + self._requestWrite(type: T.self) + } + /// Calls addOrUpdateIfNewer from the collection corresponding to the instance @MainActor func addOrUpdateIfNewer(_ instance: T, shared: SharingStatus?) { @@ -261,6 +275,52 @@ final public class Store { collection.addOrUpdateIfNewer(instance, shared: shared) } + @MainActor + func synchronizationDelete(_ identifiers: [ObjectIdentifier], type: T.Type) { + for identifier in identifiers { + do { + try self.deleteNoSyncNoCascadeNoWrite(type: type, id: identifier.modelId) + } catch { + Logger.error(error) + } + self.storeCenter.cleanupDataLog(dataId: identifier.modelId) + } + self._requestWrite(type: T.self) + } + + @MainActor + func synchronizationRevoke(_ identifiers: [ObjectIdentifier], type: T.Type) { + + for identifier in identifiers { + do { + if let instance = self._instance(id: identifier.modelId, type: type) { + if instance.sharing != nil && !self.storeCenter.isReferenced(instance: instance) { + try self.deleteNoSyncNoCascadeNoWrite(type: type, id: identifier.modelId) + } + } + } catch { + Logger.error(error) + } + } + self._requestWrite(type: T.self) + + +// for identifier in identifiers { +// do { +// try self.deleteNoSyncNoCascade(type: type, id: identifier.modelId) +// } catch { +// Logger.error(error) +// } +// self.storeCenter.cleanupDataLog(dataId: identifier.modelId) +// } +// self._requestWrite(type: T.self) + } + + fileprivate func _instance(id: String, type: T.Type) -> T? { + let realId: T.ID = T.buildRealId(id: id) + return self.findById(realId) + } + /// Calls deleteById from the collection corresponding to the instance // func deleteNoSync(instance: T) { // do { @@ -272,9 +332,9 @@ final public class Store { // } /// Calls deleteById from the collection corresponding to the instance - func deleteNoSyncNoCascade(type: T.Type, id: String) throws { + func deleteNoSyncNoCascadeNoWrite(type: T.Type, id: String) throws { let collection: SyncedCollection = try self.syncedCollection() - collection.deleteNoSyncNoCascade(id: id) + collection.deleteByStringId(id, actionOption: .noCascadeNoWrite) } /// Calls deleteById from the collection corresponding to the instance diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index fc751e6..c231a6e 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -680,36 +680,65 @@ public class StoreCenter { /// - updateArrays: the server updates /// - shared: indicates if the content should be flagged as shared @MainActor - func _syncAddOrUpdate(_ updateArrays: [SyncedStorableArray], shared: SharingStatus? = nil) async { + fileprivate func _syncAddOrUpdate(_ updateArrays: [SyncedStorableArray], shared: SharingStatus? = nil) async { for updateArray in updateArrays { - for item in updateArray.items { - let storeId: String? = item.getStoreId() - await self.synchronizationAddOrUpdate(item, storeId: storeId, shared: shared) - } + await self._syncAddOrUpdate(updateArray, type: updateArray.type, shared: shared) +// for item in updateArray.items { +// let storeId: String? = item.getStoreId() +// await self.synchronizationAddOrUpdate(item, storeId: storeId, shared: shared) +// } } } + @MainActor + fileprivate func _syncAddOrUpdate(_ updateArray: SyncedStorableArray, type: T.Type, shared: SharingStatus? = nil) async { + + let itemsByStore = updateArray.items.group { $0.getStoreId() } + for (storeId, items) in itemsByStore { + let store = self._requestStore(id: storeId) + store.synchronizationAddOrUpdate(items as! [T], shared: shared) + } + } + /// Processes data that should be deleted inside the app fileprivate func _syncDelete(_ deletionArrays: [ObjectIdentifierArray]) async { - for deletionArray in deletionArrays { - for deletedObject in deletionArray.items { - await self.synchronizationDelete(id: deletedObject.modelId, type: deletionArray.type, storeId: deletedObject.storeId) - } + await self._syncDelete(deletionArray, type: deletionArray.type) + +// for deletedObject in deletionArray.items { +// await self.synchronizationDelete(id: deletedObject.modelId, type: deletionArray.type, storeId: deletedObject.storeId) +// } } + } + + fileprivate func _syncDelete(_ deletionArray: ObjectIdentifierArray, type: T.Type) async { + for deletedObject in deletionArray.items { + + let itemsByStore = deletionArray.items.group { $0.storeId } + for (storeId, items) in itemsByStore { + let store = self._requestStore(id: storeId) + await store.synchronizationDelete(items, type: T.self) + } + + +// await self.synchronizationDelete(id: deletedObject.modelId, type: deletionArray.type, storeId: deletedObject.storeId) + } } + /// Processes data that has been revoked fileprivate func syncRevoke(_ revokedArrays: [ObjectIdentifierArray], parents: [[ObjectIdentifierArray]]) async { await self._syncRevoke(revokedArrays) for revokedArray in revokedArrays { - for revoked in revokedArray.items { - await self.synchronizationDelete(id: revoked.modelId, type: revokedArray.type, storeId: revoked.storeId) // or synchronizationRevoke ? - } + await self._syncDelete(revokedArray, type: revokedArray.type) + +// for revoked in revokedArray.items { +// await self.synchronizationDelete(id: revoked.modelId, type: revokedArray.type, storeId: revoked.storeId) // or synchronizationRevoke ? +// } } for level in parents { @@ -719,10 +748,28 @@ public class StoreCenter { fileprivate func _syncRevoke(_ revokeArrays: [ObjectIdentifierArray]) async { for revokeArray in revokeArrays { - for revoked in revokeArray.items { - await self.synchronizationRevoke(id: revoked.modelId, type: revokeArray.type, storeId: revoked.storeId) - } + await self._syncRevoke(revokeArray: revokeArray) +// for revoked in revokeArray.items { +// await self.synchronizationRevoke(id: revoked.modelId, type: revokeArray.type, storeId: revoked.storeId) +// } + } + } + + @MainActor + fileprivate func _syncRevoke(revokeArray: ObjectIdentifierArray) async { + + let itemsByStore = revokeArray.items.group { $0.storeId } + for (storeId, items) in itemsByStore { + let store = self._requestStore(id: storeId) + await store.synchronizationRevoke(items, type: revokeArray.type) } + +// for revoked in revokeArray.items { +// +// +// +// } + } /// Returns a Type object for a class name @@ -740,52 +787,45 @@ public class StoreCenter { } /// Returns whether a data has already been deleted by, to avoid inserting it again - fileprivate func _hasAlreadyBeenDeleted(_ instance: T) -> Bool { + func hasAlreadyBeenDeleted(_ instance: T) -> Bool { return self._deleteLogs.contains(where: { $0.dataId == instance.stringId && $0.operation == .delete }) } /// Adds or updates an instance into the store - func synchronizationAddOrUpdate(_ instance: T, storeId: String?, shared: SharingStatus?) async { - let hasAlreadyBeenDeleted: Bool = self._hasAlreadyBeenDeleted(instance) - if !hasAlreadyBeenDeleted { - await self._requestStore(id: storeId).addOrUpdateIfNewer(instance, shared: shared) - } - } +// func synchronizationAddOrUpdate(_ instance: T, storeId: String?, shared: SharingStatus?) async { +// let hasAlreadyBeenDeleted: Bool = self.hasAlreadyBeenDeleted(instance) +// if !hasAlreadyBeenDeleted { +// await self._requestStore(id: storeId).addOrUpdateIfNewer(instance, shared: shared) +// } +// } /// Deletes an instance with the given parameters - @MainActor - func synchronizationDelete(id: String, type: T.Type, storeId: String?) { - do { - try self._store(id: storeId).deleteNoSyncNoCascade(type: type, id: id) - } catch { - Logger.error(error) - } - self._cleanupDataLog(dataId: id) - } +// @MainActor +// func synchronizationDelete(id: String, type: T.Type, storeId: String?) { +// do { +// try self._store(id: storeId).deleteNoSyncNoCascade(type: type, id: id) +// } catch { +// Logger.error(error) +// } +// self.cleanupDataLog(dataId: id) +// } /// Revokes a data that has been shared with the user - @MainActor - func synchronizationRevoke(id: String, type: T.Type, storeId: String?) { - - do { - if let instance = self._instance(id: id, type: type, storeId: storeId) { - if instance.sharing != nil && !self.isReferenced(instance: instance) { - try self._store(id: storeId).deleteNoSyncNoCascade(type: type, id: id) - } - } - -// if self._instanceShared(id: id, type: type, storeId: storeId) { -// let count = self.isReferenced(type: type, id: id) -// if count == 0 { +// @MainActor +// func synchronizationRevoke(id: String, type: T.Type, storeId: String?) { +// +// do { +// if let instance = self._instance(id: id, type: type, storeId: storeId) { +// if instance.sharing != nil && !self.isReferenced(instance: instance) { // try self._store(id: storeId).deleteNoSyncNoCascade(type: type, id: id) // } // } - } catch { - Logger.error(error) - } - } +// } catch { +// Logger.error(error) +// } +// } fileprivate func _instance(id: String, type: T.Type, storeId: String?) -> T? { let realId: T.ID = T.buildRealId(id: id) @@ -800,7 +840,7 @@ public class StoreCenter { // } /// Deletes a data log by data id - fileprivate func _cleanupDataLog(dataId: String) { + func cleanupDataLog(dataId: String) { let logs = self._deleteLogs.filter { $0.dataId == dataId } self._deleteLogs.delete(contentOfs: logs) } diff --git a/LeStorage/StoredCollection.swift b/LeStorage/StoredCollection.swift index 75ec146..0d33968 100644 --- a/LeStorage/StoredCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -25,7 +25,7 @@ public protocol SomeCollection: Identifiable { func deleteDependencies(actionOption: ActionOption, _ isIncluded: (Item) -> Bool) func findById(_ id: Item.ID) -> Item? - + func requestWrite() } protocol CollectionDelegate { @@ -49,11 +49,12 @@ public struct ActionResult { public struct ActionOption: Codable { var synchronize: Bool var cascade: Bool + var write: Bool - static let standard: ActionOption = ActionOption(synchronize: false, cascade: false) - static let cascade: ActionOption = ActionOption(synchronize: false, cascade: true) - static let syncedCascade: ActionOption = ActionOption(synchronize: true, cascade: true) - + static let standard: ActionOption = ActionOption(synchronize: false, cascade: false, write: true) + static let noCascadeNoWrite: ActionOption = ActionOption(synchronize: false, cascade: false, write: false) + static let cascade: ActionOption = ActionOption(synchronize: false, cascade: true, write: true) + static let syncedCascade: ActionOption = ActionOption(synchronize: true, cascade: true, write: true) } public class StoredCollection: SomeCollection { @@ -146,7 +147,7 @@ public class StoredCollection: SomeCollection { // MARK: - Loading /// Sets the collection as changed to trigger a write - fileprivate func requestWrite() { + public func requestWrite() { self._triggerWrite = true } @@ -203,8 +204,9 @@ public class StoredCollection: SomeCollection { func setItems(_ items: [T]) { self.items.removeAll() for item in items { - self.addItem(instance: item) + self._addItem(instance: item) } + self.requestWrite() } @MainActor @@ -217,7 +219,6 @@ public class StoredCollection: SomeCollection { self.addOrUpdate(contentOfs: items) } - self.requestWrite() } /// Updates the whole index with the items array @@ -254,10 +255,10 @@ public class StoredCollection: SomeCollection { fileprivate func _rawAddOrUpdate(instance: T) -> ActionResult { if let index = self.items.firstIndex(where: { $0.id == instance.id }) { - let updated = self.updateItem(instance, index: index, actionOption: .standard) + let updated = self._updateItem(instance, index: index, actionOption: .standard) return ActionResult(instance: instance, method: .update, pending: !updated) } else { - let added = self.addItem(instance: instance) + let added = self._addItem(instance: instance) return ActionResult(instance: instance, method: .insert, pending: !added) } } @@ -268,7 +269,7 @@ public class StoredCollection: SomeCollection { self.requestWrite() } self.items.removeAll() - self.addItem(instance: instance) + self._addItem(instance: instance) } /// Deletes the instance in the collection and sets the collection as changed to trigger a write @@ -313,12 +314,11 @@ public class StoredCollection: SomeCollection { } func add(instance: T, actionOption: ActionOption) { - self.addItem(instance: instance, actionOption: actionOption) - self.requestWrite() + self._addItem(instance: instance, actionOption: actionOption) } /// Adds an instance to the collection - @discardableResult fileprivate func addItem(instance: T, actionOption: ActionOption = .standard) -> Bool { + @discardableResult fileprivate func _addItem(instance: T, actionOption: ActionOption = .standard) -> Bool { if !self.hasLoaded { self.addPendingOperation(method: .add, instance: instance, actionOption: actionOption) @@ -339,12 +339,12 @@ public class StoredCollection: SomeCollection { } func update(_ instance: T, index: Int, actionOption: ActionOption) { - self.updateItem(instance, index: index, actionOption: actionOption) - self.requestWrite() + self._updateItem(instance, index: index, actionOption: actionOption) +// self.requestWrite() } /// Updates an instance to the collection by index - @discardableResult fileprivate func updateItem(_ instance: T, index: Int, actionOption: ActionOption) -> Bool { + @discardableResult fileprivate func _updateItem(_ instance: T, index: Int, actionOption: ActionOption) -> Bool { if !self.hasLoaded { self.addPendingOperation(method: .update, instance: instance, actionOption: actionOption) @@ -415,7 +415,9 @@ public class StoredCollection: SomeCollection { if let instance = self.findById(realId) { self.deleteItem(instance, actionOption: actionOption) } - self.requestWrite() + if actionOption.write { + self.requestWrite() + } } /// Returns the instance corresponding to the provided [id] diff --git a/LeStorage/SyncedCollection.swift b/LeStorage/SyncedCollection.swift index 12b7d4e..4fae2f3 100644 --- a/LeStorage/SyncedCollection.swift +++ b/LeStorage/SyncedCollection.swift @@ -338,13 +338,22 @@ public class SyncedCollection: SomeSyncedCollection, Collect func deleteUnusedGranted(instance: T) { guard instance.sharing != nil else { return } - self.deleteNoSyncNoCascade(id: instance.stringId) + self.deleteByStringId(instance.stringId) instance.deleteUnusedSharedDependencies(store: self.store) } /// Deletes the instance in the collection without synchronization - func deleteNoSyncNoCascade(id: String) { - self.collection.deleteByStringId(id, actionOption: .standard) +// func deleteNoSyncNoCascade(id: String) { +// self.collection.deleteByStringId(id, actionOption: .standard) +// } +// +// /// Deletes the instance in the collection without synchronization +// func deleteNoSyncNoCascadeNoWrite(id: String) { +// self.collection.deleteByStringId(id, actionOption: .noCascadeNoWrite) +// } + + func deleteByStringId(_ id: String, actionOption: ActionOption = .standard) { + self.collection.deleteByStringId(id, actionOption: actionOption) } // MARK: - Collection Delegate @@ -472,6 +481,10 @@ public class SyncedCollection: SomeSyncedCollection, Collect return self.collection.items } + public func requestWrite() { + self.collection.requestWrite() + } + } class OperationBatch {