Manage data sharing cleanups

sync3
Laurent 6 months ago
parent d70c649fc2
commit 307180a88b
  1. 26
      LeStorage/BaseCollection.swift
  2. 8
      LeStorage/Codables/ApiCall.swift
  3. 4
      LeStorage/Codables/DataAccess.swift
  4. 5
      LeStorage/Codables/DataLog.swift
  5. 3
      LeStorage/Codables/FailedAPICall.swift
  6. 4
      LeStorage/Codables/GetSyncData.swift
  7. 3
      LeStorage/Codables/Log.swift
  8. 1
      LeStorage/Codables/PendingOperation.swift
  9. 5
      LeStorage/ModelObject.swift
  10. 7
      LeStorage/Relationship.swift
  11. 5
      LeStorage/Storable.swift
  12. 47
      LeStorage/Store.swift
  13. 49
      LeStorage/SyncedCollection.swift

@ -306,7 +306,7 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
@discardableResult func deleteItem(_ instance: T, shouldBeSynchronized: Bool = false) -> Bool { @discardableResult func deleteItem(_ instance: T, shouldBeSynchronized: Bool = false) -> Bool {
if !self.hasLoaded { if !self.hasLoaded {
self.addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized) self.addPendingOperation(method: .delete, instance: instance, shouldBeSynchronized: shouldBeSynchronized)
return false return false
} }
@ -314,9 +314,29 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
instance.deleteDependencies(store: self.store, shouldBeSynchronized: shouldBeSynchronized) instance.deleteDependencies(store: self.store, shouldBeSynchronized: shouldBeSynchronized)
} }
self.localDeleteOnly(instance: instance)
return true
}
/// Deletes an instance from the collection
@discardableResult func deleteUnusedShared(_ instance: T, shouldBeSynchronized: Bool = false) -> Bool {
if !self.hasLoaded {
self.addPendingOperation(method: .deleteUnusedShared, instance: instance, shouldBeSynchronized: shouldBeSynchronized)
return false
}
// For shared objects, we need to check for dependencies that are also shared
// but not used elsewhere before deleting them
instance.deleteUnusedSharedDependencies(store: self.store)
self.localDeleteOnly(instance: instance)
return true
}
func localDeleteOnly(instance: T) {
self.items.removeAll { $0.id == instance.id } self.items.removeAll { $0.id == instance.id }
self._indexes?.removeValue(forKey: instance.id) self._indexes?.removeValue(forKey: instance.id)
return true
} }
/// If the collection has more instance that its limit, remove the surplus /// If the collection has more instance that its limit, remove the surplus
@ -382,6 +402,8 @@ public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder {
self.delete(instance: data) self.delete(instance: data)
case (.delete, false): case (.delete, false):
self.deleteItem(data) self.deleteItem(data)
case (.deleteUnusedShared, _):
self.deleteUnusedShared(data)
} }
} }

@ -69,6 +69,9 @@ public class ApiCall<T: Storable>: ModelObject, Storable, SomeCall {
public func copy(from other: any Storable) { public func copy(from other: any Storable) {
fatalError("should not happen") fatalError("should not happen")
} }
public func copyForUpdate(from other: any Storable) {
fatalError("should not happen")
}
func formattedURLParameters() -> String? { func formattedURLParameters() -> String? {
return self.urlParameters?.toQueryString() return self.urlParameters?.toQueryString()
@ -157,7 +160,10 @@ class OldApiCall<T: Storable>: ModelObject, Storable, SomeCall {
func copy(from other: any Storable) { func copy(from other: any Storable) {
fatalError("should not happen") fatalError("should not happen")
} }
public func copyForUpdate(from other: any Storable) {
fatalError("should not happen")
}
func formattedURLParameters() -> String? { func formattedURLParameters() -> String? {
return self.urlParameters?.toQueryString() return self.urlParameters?.toQueryString()
} }

@ -70,4 +70,8 @@ class DataAccess: SyncedModelObject, SyncedStorable {
self.grantedAt = dataAccess.grantedAt self.grantedAt = dataAccess.grantedAt
} }
public func copyForUpdate(from other: any Storable) {
self.copy(from: other)
}
} }

@ -33,5 +33,8 @@ class DataLog: ModelObject, Storable {
func copy(from other: any Storable) { func copy(from other: any Storable) {
fatalError("should not happen") fatalError("should not happen")
} }
public func copyForUpdate(from other: any Storable) {
fatalError("should not happen")
}
} }

@ -102,5 +102,8 @@ class FailedAPICall: SyncedModelObject, SyncedStorable {
self.error = fac.error self.error = fac.error
self.authentication = fac.authentication self.authentication = fac.authentication
} }
public func copyForUpdate(from other: any Storable) {
self.copy(from: other)
}
} }

@ -36,7 +36,9 @@ class GetSyncData: SyncedModelObject, SyncedStorable, URLParameterConvertible {
guard let getSyncData = other as? GetSyncData else { return } guard let getSyncData = other as? GetSyncData else { return }
self.date = getSyncData.date self.date = getSyncData.date
} }
public func copyForUpdate(from other: any Storable) {
fatalError("should not happen")
}
func queryParameters(storeCenter: StoreCenter) -> [String : String] { func queryParameters(storeCenter: StoreCenter) -> [String : String] {
return ["last_update" : self._formattedLastUpdate, return ["last_update" : self._formattedLastUpdate,
"device_id" : storeCenter.deviceId()] "device_id" : storeCenter.deviceId()]

@ -59,5 +59,8 @@ class Log: SyncedModelObject, SyncedStorable {
self.date = log.date self.date = log.date
self.message = log.message self.message = log.message
} }
public func copyForUpdate(from other: any Storable) {
fatalError("should not happen")
}
} }

@ -10,6 +10,7 @@ import Foundation
enum StorageMethod: String, Codable { enum StorageMethod: String, Codable {
case addOrUpdate case addOrUpdate
case delete case delete
case deleteUnusedShared
} }
class PendingOperation<T : Storable>: Codable, Equatable { class PendingOperation<T : Storable>: Codable, Equatable {

@ -18,6 +18,11 @@ open class ModelObject: NSObject {
open func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { open func deleteDependencies(store: Store, shouldBeSynchronized: Bool) {
} }
open func deleteUnusedSharedDependencies(store: Store) {
// Default implementation does nothing
// Subclasses should override this to handle their specific dependencies
}
static var relationshipNames: [String] = [] static var relationshipNames: [String] = []

@ -7,9 +7,10 @@
public struct Relationship { public struct Relationship {
public init(type: any Storable.Type, keyPath: AnyKeyPath) { public init(type: any Storable.Type, keyPath: AnyKeyPath, mainStoreLookup: Bool) {
self.type = type self.type = type
self.keyPath = keyPath self.keyPath = keyPath
self.mainStoreLookup = mainStoreLookup
} }
/// The type of the relationship /// The type of the relationship
@ -17,4 +18,8 @@ public struct Relationship {
/// the keyPath to access the relationship /// the keyPath to access the relationship
var keyPath: AnyKeyPath var keyPath: AnyKeyPath
/// Indicates whether the linked object is on the main Store
var mainStoreLookup: Bool
} }

@ -23,6 +23,11 @@ public protocol Storable: Codable, Identifiable, NSObjectProtocol {
/// so when we do that on the server, we also need to do it locally /// so when we do that on the server, we also need to do it locally
func deleteDependencies(store: Store, shouldBeSynchronized: Bool) func deleteDependencies(store: Store, shouldBeSynchronized: Bool)
/// A method that deletes dependencies of shared resources, but only if they are themselves shared
/// and not referenced by other objects in the store
/// This is used when cleaning up shared objects that are no longer in use
func deleteUnusedSharedDependencies(store: Store)
/// Copies the content of another item into the instance /// 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 /// This behavior has been made to get live updates when looking at properties in SwiftUI screens
func copy(from other: any Storable) func copy(from other: any Storable)

@ -46,7 +46,7 @@ public enum StoreError: Error, LocalizedError {
final public class Store { final public class Store {
fileprivate(set) var storeCenter: StoreCenter public fileprivate(set) var storeCenter: StoreCenter
/// The dictionary of registered collections /// The dictionary of registered collections
fileprivate var _collections: [String : any SomeCollection] = [:] fileprivate var _collections: [String : any SomeCollection] = [:]
@ -168,6 +168,11 @@ final public class Store {
throw StoreError.collectionNotRegistered(type: T.resourceName()) throw StoreError.collectionNotRegistered(type: T.resourceName())
} }
/// Returns a collection by type
func syncedCollection<T: SyncedStorable>(type: T.Type) throws -> SyncedCollection<T> {
return try self.syncedCollection()
}
/// Returns a collection by type /// Returns a collection by type
func collection<T: Storable>() throws -> BaseCollection<T> { func collection<T: Storable>() throws -> BaseCollection<T> {
if let collection = self._collections[T.resourceName()] as? BaseCollection<T> { if let collection = self._collections[T.resourceName()] as? BaseCollection<T> {
@ -255,6 +260,46 @@ final public class Store {
return count return count
} }
public func deleteUnusedSharedIfNecessary<T: SyncedStorable>(_ instance: T) {
if self.referenceCount(type: T.self, id: instance.stringId) == 0 {
do {
let collection: SyncedCollection<T> = try self.syncedCollection()
collection.deleteUnusedShared(instance: instance)
} catch {
Logger.error(error)
}
}
}
public func deleteUnusedSharedDependencies<T: SyncedStorable>(type: T.Type, shouldBeSynchronized: Bool, _ handler: (T) throws -> Bool) {
do {
let collection: BaseCollection<T> = try self.collection()
let items = try collection.items.filter(handler)
self.deleteUnusedSharedDependencies(items)
} catch {
Logger.error(error)
}
}
/// Deletes dependencies of shared objects that are not used elsewhere in the system
/// Similar to _deleteDependencies but only for unused shared objects
public func deleteUnusedSharedDependencies<T: SyncedStorable>(_ items: [T]) {
do {
for item in items {
guard item.shared == true else { continue }
if self.referenceCount(type: T.self, id: item.stringId) == 0 {
// Only delete if the shared item has no references
item.deleteUnusedSharedDependencies(store: self)
let collection: SyncedCollection<T> = try self.syncedCollection()
collection.deleteUnusedShared(instance: item)
}
}
} catch {
Logger.error(error)
}
}
public func deleteAllDependencies<T: Storable>(type: T.Type, shouldBeSynchronized: Bool) { public func deleteAllDependencies<T: Storable>(type: T.Type, shouldBeSynchronized: Bool) {
do { do {
let collection: BaseCollection<T> = try self.collection() let collection: BaseCollection<T> = try self.collection()

@ -114,6 +114,9 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
if let index = self.items.firstIndex(where: { $0.id == instance.id }) { if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
if self.updateItem(instance, index: index, shouldBeSynchronized: true) { if self.updateItem(instance, index: index, shouldBeSynchronized: true) {
self.setChanged() self.setChanged()
if instance.shared == true {
self._cleanUpSharedDependencies()
}
return (instance, false) return (instance, false)
} }
} else { } else {
@ -146,6 +149,9 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
} }
} }
} }
self._cleanUpSharedDependencies()
return batch return batch
} }
@ -232,6 +238,39 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
await self._sendOperationBatch(batch) await self._sendOperationBatch(batch)
} }
fileprivate func _cleanUpSharedDependencies() {
for relationship in T.relationships() {
if let syncedType = relationship.type as? (any SyncedStorable.Type) {
do {
try self._deleteUnusedSharedInstances(relationship: relationship, type: syncedType)
} catch {
Logger.error(error)
}
}
}
}
fileprivate func _deleteUnusedSharedInstances<S: SyncedStorable>(relationship: Relationship, type: S.Type) throws {
let store: Store
if relationship.mainStoreLookup {
store = self.store.storeCenter.mainStore
} else {
store = self.store
}
let collection: SyncedCollection<S> = try store.syncedCollection()
collection._deleteUnusedSharedInstances()
}
fileprivate func _deleteUnusedSharedInstances() {
let sharedItems = self.items.filter { $0.shared == true }
for sharedItem in sharedItems {
self.store.deleteUnusedSharedIfNecessary(sharedItem)
}
}
// MARK: - Asynchronous operations // MARK: - Asynchronous operations
/// Adds or update an instance asynchronously and waits for network operations /// Adds or update an instance asynchronously and waits for network operations
@ -297,6 +336,16 @@ public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSynced
self.deleteItem(instance, shouldBeSynchronized: false) self.deleteItem(instance, shouldBeSynchronized: false)
} }
func deleteUnusedShared(instance: T) {
guard instance.shared == true else { return }
// Delete the instance and its non-used shared dependencies
self.deleteUnusedShared(instance, shouldBeSynchronized: false)
self.setChanged()
}
/// Deletes the instance in the collection without synchronization /// Deletes the instance in the collection without synchronization
func deleteByStringIdNoSync(_ id: String) { func deleteByStringIdNoSync(_ id: String) {
defer { defer {

Loading…
Cancel
Save