improves delete dependencies system

sync3
Laurent 6 months ago
parent 775bed665b
commit 1f78cc7be4
  1. 30
      LeStorage/BaseCollection.swift
  2. 2
      LeStorage/ModelObject.swift
  3. 2
      LeStorage/Storable.swift
  4. 56
      LeStorage/Store.swift
  5. 19
      LeStorage/StoreCenter.swift
  6. 197
      LeStorage/SyncedCollection.swift

@ -272,7 +272,7 @@ public class BaseCollection<T: Storable>: 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<T: Storable>: 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<T: Storable>: 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<T: Storable>: 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<T: Storable>: 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<T>(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<T: Storable>: 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
}

@ -15,7 +15,7 @@ open class ModelObject: NSObject {
public override init() { }
open func deleteDependencies(shouldBeSynchronized: Bool) {
open func deleteDependencies(store: Store, shouldBeSynchronized: Bool) {
}

@ -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

@ -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<T: Storable>(isIncluded: (T) throws -> (Bool)) rethrows -> [T] {
// do {
// return try self.collection().filter(isIncluded)
// } catch {
// return []
// }
// }
/// Returns a collection by type
func syncedCollection<T: SyncedStorable>() throws -> SyncedCollection<T> {
if let collection = self._collections[T.resourceName()] as? SyncedCollection<T> {
@ -265,6 +255,38 @@ final public class Store {
return count
}
public func deleteAllDependencies<T: Storable>(type: T.Type, shouldBeSynchronized: Bool) {
do {
let collection: BaseCollection<T> = try self.collection()
try self._deleteDependencies(Array(collection.items), shouldBeSynchronized: shouldBeSynchronized)
} catch {
Logger.error(error)
}
}
public func deleteDependencies<T: Storable>(type: T.Type, shouldBeSynchronized: Bool, _ handler: (T) throws -> Bool) {
do {
let collection: BaseCollection<T> = try self.collection()
let items = try collection.items.filter(handler)
try self._deleteDependencies(items, shouldBeSynchronized: shouldBeSynchronized)
} catch {
Logger.error(error)
}
}
fileprivate func _deleteDependencies<T: Storable>(_ items: [T], shouldBeSynchronized: Bool) throws {
do {
let collection: BaseCollection<T> = 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

@ -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 {
@ -210,6 +210,13 @@ public class StoreCenter {
}
}
public func store(identifier: String) throws -> Store {
if let store = self._stores[identifier] {
return store
}
throw StoreError.storeNotRegistered(id: identifier)
}
// MARK: - Settings
/// Sets the user info given a user
@ -797,13 +804,9 @@ public class StoreCenter {
/// Creates a delete log for an instance
func createDeleteLog<T: Storable>(_ instance: T) {
self._addDataLog(instance, method: .delete)
}
/// Adds a datalog for an instance with the associated method
fileprivate func _addDataLog<T: Storable>(_ 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)
}

@ -40,10 +40,6 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, 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<T : SyncedStorable>: BaseCollection<T>, 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<T : SyncedStorable>: BaseCollection<T>, 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<T : SyncedStorable>: BaseCollection<T>, 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<T>) -> OperationBatch<T> {
defer {
@ -190,11 +156,6 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
Task { await self._sendOperationBatch(batch) }
}
func addOrUpdateAsync(contentOfs sequence: any Sequence<T>) 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<T : SyncedStorable>: BaseCollection<T>, 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<T>) 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<T>, shouldBeSynchronized: Bool) {
guard items.isNotEmpty else { return }
@ -269,6 +216,57 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
}
}
public override func deleteDependencies(_ items: any Sequence<T>) {
super.deleteDependencies(items)
let batch = OperationBatch<T>()
batch.deletes = Array(items)
Task { await self._sendOperationBatch(batch) }
}
public func deleteDependenciesAsync(_ items: any Sequence<T>) async {
super.deleteDependencies(items)
let batch = OperationBatch<T>()
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<T>) 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<T>) 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<T : SyncedStorable>: BaseCollection<T>, 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

Loading…
Cancel
Save