diff --git a/LeStorage.xcodeproj/project.pbxproj b/LeStorage.xcodeproj/project.pbxproj index 1ad3e38..d77ab30 100644 --- a/LeStorage.xcodeproj/project.pbxproj +++ b/LeStorage.xcodeproj/project.pbxproj @@ -21,9 +21,10 @@ C48638B32D9BC6A8007E3E06 /* PendingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48638B22D9BC6A8007E3E06 /* PendingOperation.swift */; }; C488C8802CCBDC210082001F /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C488C87F2CCBDC210082001F /* NetworkMonitor.swift */; }; C49774DF2DC4B3D7005CD239 /* SyncData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49774DE2DC4B3D7005CD239 /* SyncData.swift */; }; + C49779FC2DDB5D89005CD239 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49779FB2DDB5D89005CD239 /* String+Extensions.swift */; }; C49B6E502C2089B6002BDE1B /* ApiCallCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */; }; C49EF0242BD6BDC50077B5AA /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */; }; - C4A47D4F2B6D280200ADC637 /* BaseCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D4E2B6D280200ADC637 /* BaseCollection.swift */; }; + C4A47D4F2B6D280200ADC637 /* StoredCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */; }; C4A47D512B6D2C4E00ADC637 /* Codable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D502B6D2C4E00ADC637 /* Codable+Extensions.swift */; }; C4A47D532B6D2C5F00ADC637 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D522B6D2C5F00ADC637 /* Logger.swift */; }; C4A47D552B6D2DBF00ADC637 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */; }; @@ -79,9 +80,10 @@ C48638B22D9BC6A8007E3E06 /* PendingOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingOperation.swift; sourceTree = ""; }; C488C87F2CCBDC210082001F /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; C49774DE2DC4B3D7005CD239 /* SyncData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncData.swift; sourceTree = ""; }; + C49779FB2DDB5D89005CD239 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCallCollection.swift; sourceTree = ""; }; C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extensions.swift"; sourceTree = ""; }; - C4A47D4E2B6D280200ADC637 /* BaseCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCollection.swift; sourceTree = ""; }; + C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredCollection.swift; sourceTree = ""; }; C4A47D502B6D2C4E00ADC637 /* Codable+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Codable+Extensions.swift"; sourceTree = ""; }; C4A47D522B6D2C5F00ADC637 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; @@ -169,7 +171,7 @@ C425D4572B6D2519002A7B48 /* Store.swift */, C4FC2E282C2B2EC30021F3BF /* StoreCenter.swift */, C4A47D642B6E92FE00ADC637 /* Storable.swift */, - C4A47D4E2B6D280200ADC637 /* BaseCollection.swift */, + C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */, C4D477A02CB9586A0077713D /* SyncedCollection.swift */, C456EFE12BE52379007388E2 /* StoredSingleton.swift */, C4D4779E2CB92FD80077713D /* SyncedStorable.swift */, @@ -198,6 +200,7 @@ C4A47D522B6D2C5F00ADC637 /* Logger.swift */, C4B96E1C2D8C53D700C2955F /* UIDevice+Extensions.swift */, C4FAE69B2CEB8E9500790446 /* URLManager.swift */, + C49779FB2DDB5D89005CD239 /* String+Extensions.swift */, ); path = Utils; sourceTree = ""; @@ -363,8 +366,9 @@ C4D477972CB66EEA0077713D /* Date+Extensions.swift in Sources */, C488C8802CCBDC210082001F /* NetworkMonitor.swift in Sources */, C4A47D6D2B71364600ADC637 /* ModelObject.swift in Sources */, + C49779FC2DDB5D89005CD239 /* String+Extensions.swift in Sources */, C400D7232CC2AF560092237C /* GetSyncData.swift in Sources */, - C4A47D4F2B6D280200ADC637 /* BaseCollection.swift in Sources */, + C4A47D4F2B6D280200ADC637 /* StoredCollection.swift in Sources */, C4A47D9C2B7CFFE000ADC637 /* Settings.swift in Sources */, C49774DF2DC4B3D7005CD239 /* SyncData.swift in Sources */, C4FC2E292C2B2EC30021F3BF /* StoreCenter.swift in Sources */, diff --git a/LeStorage/ApiCallCollection.swift b/LeStorage/ApiCallCollection.swift index d415cf3..ca51a31 100644 --- a/LeStorage/ApiCallCollection.swift +++ b/LeStorage/ApiCallCollection.swift @@ -442,6 +442,7 @@ actor ApiCallCollection: SomeCallCollection { /// Returns if the API call collection is not empty func hasPendingCalls() -> Bool { +// print("\(T.resourceName()) calls = \(self.items.count)") return self.items.isNotEmpty } diff --git a/LeStorage/Codables/PendingOperation.swift b/LeStorage/Codables/PendingOperation.swift index c982b60..d0113d1 100644 --- a/LeStorage/Codables/PendingOperation.swift +++ b/LeStorage/Codables/PendingOperation.swift @@ -8,7 +8,8 @@ import Foundation enum StorageMethod: String, Codable { - case addOrUpdate + case add + case update case delete case deleteUnusedShared } @@ -18,12 +19,12 @@ class PendingOperation: Codable, Equatable { var id: String = Store.randomId() var method: StorageMethod var data: T - var shouldBeSynchronized: Bool + var actionOption: ActionOption - init(method: StorageMethod, data: T, shouldBeSynchronized: Bool) { + init(method: StorageMethod, data: T, actionOption: ActionOption) { self.method = method self.data = data - self.shouldBeSynchronized = shouldBeSynchronized + self.actionOption = actionOption } static func == (lhs: PendingOperation, rhs: PendingOperation) -> Bool { diff --git a/LeStorage/ModelObject.swift b/LeStorage/ModelObject.swift index 6f6e859..05c8bf3 100644 --- a/LeStorage/ModelObject.swift +++ b/LeStorage/ModelObject.swift @@ -15,7 +15,7 @@ open class ModelObject: NSObject { public override init() { } - open func deleteDependencies(store: Store, shouldBeSynchronized: Bool) { + open func deleteDependencies(store: Store, actionOption: ActionOption) { } diff --git a/LeStorage/PendingOperationManager.swift b/LeStorage/PendingOperationManager.swift index 8a354c2..60015c0 100644 --- a/LeStorage/PendingOperationManager.swift +++ b/LeStorage/PendingOperationManager.swift @@ -32,10 +32,10 @@ class PendingOperationManager { } } - func addPendingOperation(method: StorageMethod, instance: T, shouldBeSynchronized: Bool) { + func addPendingOperation(method: StorageMethod, instance: T, actionOption: ActionOption) { Logger.log("addPendingOperation: \(method), \(instance)") - let operation = PendingOperation(method: method, data: instance, shouldBeSynchronized: shouldBeSynchronized) + let operation = PendingOperation(method: method, data: instance, actionOption: actionOption) self.items.append(operation) self._writeIfNecessary() diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index 7bdbf11..919913f 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(store: Store, shouldBeSynchronized: Bool) + func deleteDependencies(store: Store, actionOption: ActionOption) /// A method that deletes dependencies of shared resources, but only if they are themselves shared /// and not referenced by other objects in the store diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 8b7a8b0..52d55bb 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -18,6 +18,7 @@ public enum StoreError: Error, LocalizedError { case apiCallCollectionNotRegistered(type: String) case synchronizationInactive case storeNotRegistered(id: String) + case castIssue(type: String) public var errorDescription: String? { switch self { @@ -39,6 +40,8 @@ public enum StoreError: Error, LocalizedError { return "The synchronization is not active on this StoreCenter" case .storeNotRegistered(let id): return "The store with identifier \(id) is not registered" + case .castIssue(let type): + return "Can't cast to \(type)" } } @@ -51,6 +54,9 @@ final public class Store { /// The dictionary of registered collections fileprivate var _collections: [String : any SomeCollection] = [:] + /// The dictionary of all StoredCollection + fileprivate var _baseCollections: [String : any SomeCollection] = [:] + /// The store identifier, used to name the store directory, and to perform filtering requests to the server public fileprivate(set) var identifier: String? = nil @@ -90,12 +96,14 @@ final public class Store { /// - inMemory: Indicates if the collection should only live in memory, and not write into a file public func registerCollection(indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil) -> StoredCollection { - if let collection: StoredCollection = try? self.collection() as? StoredCollection { - return collection + if let _ = try? self.someCollection(type: T.self) { + fatalError("collection already registered") +// return collection } let collection = StoredCollection(store: self, indexed: indexed, inMemory: inMemory, limit: limit) self._collections[T.resourceName()] = collection + self._baseCollections[T.resourceName()] = collection return collection } @@ -112,6 +120,8 @@ final public class Store { let collection = SyncedCollection(store: self, indexed: indexed, inMemory: inMemory, limit: limit, synchronousLoading: synchronousLoading) self._collections[T.resourceName()] = collection + self._baseCollections[T.resourceName()] = collection.collection + self.storeCenter.loadApiCallCollection(type: T.self) return collection } @@ -119,6 +129,8 @@ final public class Store { func asyncLoadingSynchronizedCollection(inMemory: Bool = false) async -> SyncedCollection { let collection = await SyncedCollection(store: self, inMemory: inMemory) self._collections[T.resourceName()] = collection + self._baseCollections[T.resourceName()] = collection.collection + self.storeCenter.loadApiCallCollection(type: T.self) return collection } @@ -126,6 +138,8 @@ final public class Store { func asyncLoadingStoredCollection(inMemory: Bool = false) async -> StoredCollection { let collection = await StoredCollection(store: self, inMemory: inMemory) self._collections[T.resourceName()] = collection + self._baseCollections[T.resourceName()] = collection + return collection } @@ -153,7 +167,7 @@ final public class Store { /// - Parameters: /// - id: the id of the data public func findById(_ id: T.ID) -> T? { - guard let collection = self._collections[T.resourceName()] as? BaseCollection else { + guard let collection = self._baseCollections[T.resourceName()] as? StoredCollection else { Logger.w("Collection \(T.resourceName()) not registered") return nil } @@ -174,13 +188,21 @@ final public class Store { } /// Returns a collection by type - func collection() throws -> BaseCollection { - if let collection = self._collections[T.resourceName()] as? BaseCollection { + func someCollection(type: T.Type) throws -> any SomeCollection { + if let collection = self._collections[T.resourceName()] { return collection } throw StoreError.collectionNotRegistered(type: T.resourceName()) } + /// Returns a collection by type +// func collection() throws -> BaseCollection { +// if let collection = self._collections[T.resourceName()] as? BaseCollection { +// return collection +// } +// throw StoreError.collectionNotRegistered(type: T.resourceName()) +// } + func registerOrGetSyncedCollection(_ type: T.Type) -> SyncedCollection { do { return try self.syncedCollection() @@ -236,19 +258,19 @@ final public class Store { } /// Calls deleteById from the collection corresponding to the instance - func deleteNoSync(instance: T) { - do { - let collection: BaseCollection = try self.collection() - collection.delete(instance: instance) - } catch { - Logger.error(error) - } - } +// func deleteNoSync(instance: T) { +// do { +// let collection: BaseCollection = try self.collection() +// collection.delete(instance: instance) +// } catch { +// Logger.error(error) +// } +// } /// Calls deleteById from the collection corresponding to the instance - func deleteNoSync(type: T.Type, id: String) throws { + func deleteNoSyncNoCascade(type: T.Type, id: String) throws { let collection: SyncedCollection = try self.syncedCollection() - collection.deleteByStringIdNoSync(id) + collection.deleteNoSyncNoCascade(id: id) } /// Calls deleteById from the collection corresponding to the instance @@ -273,7 +295,7 @@ final public class Store { public func deleteUnusedSharedDependencies(type: T.Type, shouldBeSynchronized: Bool, _ handler: (T) throws -> Bool) { do { - let collection: BaseCollection = try self.collection() + let collection: SyncedCollection = try self.syncedCollection() let items = try collection.items.filter(handler) self.deleteUnusedSharedDependencies(items) } catch { @@ -300,36 +322,25 @@ final public class Store { } } - 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) { + public func deleteAllDependencies(type: T.Type, actionOption: ActionOption) { do { - let collection: BaseCollection = try self.collection() - let items = try collection.items.filter(handler) - try self._deleteDependencies(items, shouldBeSynchronized: shouldBeSynchronized) + let collection = try self.someCollection(type: type) + collection.deleteAllItemsAndDependencies(actionOption: actionOption) } catch { Logger.error(error) } } - fileprivate func _deleteDependencies(_ items: [T], shouldBeSynchronized: Bool) throws { + public func deleteDependencies(type: T.Type, actionOption: ActionOption, _ isIncluded: (any Storable) -> Bool) { + do { - let collection: BaseCollection = try self.collection() - for item in items { - item.deleteDependencies(store: self, shouldBeSynchronized: shouldBeSynchronized) - } - collection.deleteDependencies(items) + let collection: any SomeCollection = try self.someCollection(type: type) + collection.deleteDependencies(actionOption: actionOption, isIncluded) } catch { Logger.error(error) } + } // MARK: - Write diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index 1304092..659319a 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -765,7 +765,7 @@ public class StoreCenter { @MainActor func synchronizationDelete(id: String, type: T.Type, storeId: String?) { do { - try self._store(id: storeId).deleteNoSync(type: type, id: id) + try self._store(id: storeId).deleteNoSyncNoCascade(type: type, id: id) } catch { Logger.error(error) } @@ -780,7 +780,7 @@ public class StoreCenter { if self._instanceShared(id: id, type: type) { let count = self.mainStore.referenceCount(type: type, id: id) if count == 0 { - try self._store(id: storeId).deleteNoSync(type: type, id: id) + try self._store(id: storeId).deleteNoSyncNoCascade(type: type, id: id) } } } catch { @@ -994,29 +994,31 @@ public class StoreCenter { } /// Returns the collection hosting an instance - func collectionOfInstance(_ instance: T) -> BaseCollection? { + func collectionOfInstance(_ instance: T) -> (any SomeCollection)? { do { - let collection: BaseCollection = try self.mainStore.collection() - if collection.findById(instance.id) != nil { - return collection + if let storeId = instance.getStoreId() { + let store = try self.store(identifier: storeId) + return try store.someCollection(type: T.self) } else { - return self.collectionOfInstanceInSubStores(instance) + return try Store.main.someCollection(type: T.self) } } catch { - return self.collectionOfInstanceInSubStores(instance) + Logger.error(error) } + return nil + } /// Search inside the additional stores to find the collection hosting the instance - func collectionOfInstanceInSubStores(_ instance: T) -> BaseCollection? { - for store in self._stores.values { - let collection: BaseCollection? = try? store.collection() - if collection?.findById(instance.id) != nil { - return collection - } - } - return nil - } +// func collectionOfInstanceInSubStores(_ instance: T) -> BaseCollection? { +// for store in self._stores.values { +// let collection: BaseCollection? = try? store.collection() +// if collection?.findById(instance.id) != nil { +// return collection +// } +// } +// return nil +// } // MARK: - Data Access diff --git a/LeStorage/BaseCollection.swift b/LeStorage/StoredCollection.swift similarity index 66% rename from LeStorage/BaseCollection.swift rename to LeStorage/StoredCollection.swift index dd0de3d..553804f 100644 --- a/LeStorage/BaseCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -1,5 +1,5 @@ // -// BaseCollection.swift +// StoredCollection.swift // LeStorage // // Created by Laurent Morvillier on 02/02/2024. @@ -8,27 +8,58 @@ import Foundation import Combine -public protocol CollectionHolder { - associatedtype Item: Storable +public protocol SomeCollection: Identifiable { - var items: [Item] { get } - func reset() -} + associatedtype Item: Storable -public protocol SomeCollection: CollectionHolder, Identifiable { - - var resourceName: String { get } var hasLoaded: Bool { get } var inMemory: Bool { get } var type: any Storable.Type { get } + func reset() func referenceCount(type: S.Type, id: String) -> Int + var items: [Item] { get } + + func deleteAllItemsAndDependencies(actionOption: ActionOption) + func deleteDependencies(actionOption: ActionOption, _ isIncluded: (Item) -> Bool) + func findById(_ id: Item.ID) -> Item? + +} + +protocol CollectionDelegate { + associatedtype Item: Storable + func loadingForMemoryCollection() async + func itemMerged(_ pendingOperation: PendingOperation) } -public class BaseCollection: SomeCollection, CollectionHolder { +enum CollectionMethod { + case insert + case update + case delete +} + +public struct ActionResult { + var instance: T + var method: CollectionMethod + var pending: Bool +} + +public struct ActionOption: Codable { + var synchronize: Bool + var cascade: 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) + +} +public class StoredCollection: SomeCollection { + + public typealias Item = T + /// Doesn't write the collection in a file fileprivate(set) public var inMemory: Bool = false @@ -47,7 +78,7 @@ public class BaseCollection: SomeCollection, CollectionHolder { /// Indicates whether the collection has changed, thus requiring a write operation fileprivate var _triggerWrite: Bool = false { didSet { - if self._triggerWrite == true { + if self._triggerWrite == true && self.inMemory == false { self._scheduleWrite() DispatchQueue.main.async { @@ -65,6 +96,8 @@ public class BaseCollection: SomeCollection, CollectionHolder { /// Sets a max number of items inside the collection fileprivate(set) var limit: Int? = nil + fileprivate var _delegate: (any CollectionDelegate)? = nil + init(store: Store, inMemory: Bool = false) async { self.store = store if self.inMemory == false { @@ -72,13 +105,14 @@ public class BaseCollection: SomeCollection, CollectionHolder { } } - init(store: Store, indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false) { + init(store: Store, indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false, delegate: (any CollectionDelegate)? = nil) { if indexed { self._indexes = [:] } self.inMemory = inMemory self.store = store self.limit = limit + self._delegate = delegate if synchronousLoading { Task { @@ -109,7 +143,7 @@ public class BaseCollection: SomeCollection, CollectionHolder { // MARK: - Loading /// Sets the collection as changed to trigger a write - func triggerWrite() { + fileprivate func requestWrite() { self._triggerWrite = true } @@ -118,6 +152,7 @@ public class BaseCollection: SomeCollection, CollectionHolder { if !self.inMemory { await self.loadFromFile() } else { + await self._delegate?.loadingForMemoryCollection() await MainActor.run { self.setAsLoaded() } @@ -168,6 +203,19 @@ public class BaseCollection: SomeCollection, CollectionHolder { self.items = items self._updateIndexIfNecessary() } + + @MainActor + func loadAndWrite(_ items: [T], clear: Bool = false) { + if clear { + self.setItems(items) + self.setAsLoaded() + } else { + self.setAsLoaded() + self.addOrUpdate(contentOfs: items) + } + + self.requestWrite() + } /// Updates the whole index with the items array fileprivate func _updateIndexIfNecessary() { @@ -180,74 +228,69 @@ public class BaseCollection: SomeCollection, CollectionHolder { /// Adds or updates the provided instance inside the collection /// Adds it if its id is not found, and otherwise updates it - public func addOrUpdate(instance: T) { - self.addOrUpdateItem(instance: instance) + @discardableResult public func addOrUpdate(instance: T) -> ActionResult { + defer { + self.requestWrite() + } + return self._rawAddOrUpdate(instance: instance) } - /// Adds or update an instance inside the collection and writes - func addOrUpdateItem(instance: T) { + /// Adds or update a sequence of elements + public func addOrUpdate(contentOfs sequence: any Sequence, _ handler: ((ActionResult) -> ())? = nil) { defer { - self._triggerWrite = true + self.requestWrite() + } + + for instance in sequence { + let result = self._rawAddOrUpdate(instance: instance) + handler?(result) } + } + + fileprivate func _rawAddOrUpdate(instance: T) -> ActionResult { if let index = self.items.firstIndex(where: { $0.id == instance.id }) { - self.updateItem(instance, index: index) + let updated = self.updateItem(instance, index: index, actionOption: .standard) + return ActionResult(instance: instance, method: .update, pending: !updated) } else { - self.addItem(instance: instance) + let added = self.addItem(instance: instance) + return ActionResult(instance: instance, method: .insert, pending: !added) } - } /// A method the treat the collection as a single instance holder func setSingletonNoSync(instance: T) { defer { - self._triggerWrite = true + self.requestWrite() } self.items.removeAll() self.addItem(instance: instance) } /// Deletes the instance in the collection and sets the collection as changed to trigger a write - public func delete(instance: T) { + public func delete(instance: T, actionOption: ActionOption) { defer { self._triggerWrite = true } - self.deleteItem(instance) + self.deleteItem(instance, actionOption: actionOption) } /// Deletes all items of the sequence by id and sets the collection as changed to trigger a write - public func delete(contentOfs sequence: any RandomAccessCollection) { - - defer { - self._triggerWrite = true - } - - for instance in sequence { - self.deleteItem(instance) - } - } - - /// Adds or update a sequence of elements - public func addOrUpdate(contentOfs sequence: any Sequence) { - self.addSequence(sequence) - // self._addOrUpdate(contentOfs: sequence) + public func delete(contentOfs sequence: any RandomAccessCollection, _ handler: ((ActionResult) -> ())? = nil) { + self.delete(contentOfs: sequence, actionOption: .cascade, handler: handler) } + + func delete(contentOfs sequence: any RandomAccessCollection, actionOption: ActionOption, handler: ((ActionResult) -> ())? = nil) { - /// Adds a sequence of objects inside the collection and performs a write - func addSequence(_ sequence: any Sequence) { defer { self._triggerWrite = true } for instance in sequence { - if let index = self.items.firstIndex(where: { $0.id == instance.id }) { - self.updateItem(instance, index: index) - } else { // insert - self.addItem(instance: instance) - } + let deleted = self.deleteItem(instance, actionOption: actionOption) + handler?(ActionResult(instance: instance, method: .delete, pending: !deleted)) } - } /// This method sets the storeId for the given instance if the collection belongs to a store with an id @@ -261,11 +304,16 @@ public class BaseCollection: SomeCollection, CollectionHolder { } } + func add(instance: T, actionOption: ActionOption) { + self.addItem(instance: instance, actionOption: actionOption) + self.requestWrite() + } + /// Adds an instance to the collection - @discardableResult func addItem(instance: T, shouldBeSynchronized: Bool = false) -> Bool { + @discardableResult fileprivate func addItem(instance: T, actionOption: ActionOption = .standard) -> Bool { if !self.hasLoaded { - self.addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized) + self.addPendingOperation(method: .add, instance: instance, actionOption: actionOption) return false } @@ -277,11 +325,16 @@ public class BaseCollection: SomeCollection, CollectionHolder { return true } + func update(_ instance: T, index: Int, actionOption: ActionOption) { + self.updateItem(instance, index: index, actionOption: actionOption) + self.requestWrite() + } + /// Updates an instance to the collection by index - @discardableResult func updateItem(_ instance: T, index: Int, shouldBeSynchronized: Bool = false) -> Bool { + @discardableResult fileprivate func updateItem(_ instance: T, index: Int, actionOption: ActionOption) -> Bool { if !self.hasLoaded { - self.addPendingOperation(method: .addOrUpdate, instance: instance, shouldBeSynchronized: shouldBeSynchronized) + self.addPendingOperation(method: .update, instance: instance, actionOption: actionOption) return false } @@ -296,15 +349,15 @@ public class BaseCollection: SomeCollection, CollectionHolder { } /// Deletes an instance from the collection - @discardableResult func deleteItem(_ instance: T, shouldBeSynchronized: Bool = false) -> Bool { + @discardableResult fileprivate func deleteItem(_ instance: T, actionOption: ActionOption = .cascade) -> Bool { if !self.hasLoaded { - self.addPendingOperation(method: .delete, instance: instance, shouldBeSynchronized: shouldBeSynchronized) + self.addPendingOperation(method: .delete, instance: instance, actionOption: actionOption) return false } - 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) + if actionOption.cascade { // 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, actionOption: actionOption) } self.localDeleteOnly(instance: instance) @@ -312,10 +365,10 @@ public class BaseCollection: SomeCollection, CollectionHolder { } /// Deletes an instance from the collection - @discardableResult func deleteUnusedShared(_ instance: T, shouldBeSynchronized: Bool = false) -> Bool { + @discardableResult func deleteUnusedShared(_ instance: T, actionOption: ActionOption) -> Bool { if !self.hasLoaded { - self.addPendingOperation(method: .deleteUnusedShared, instance: instance, shouldBeSynchronized: shouldBeSynchronized) + self.addPendingOperation(method: .deleteUnusedShared, instance: instance, actionOption: actionOption) return false } @@ -339,6 +392,13 @@ public class BaseCollection: SomeCollection, CollectionHolder { } } + func deleteByStringId(_ id: String, actionOption: ActionOption = .cascade) { + let realId = T.buildRealId(id: id) + if let instance = self.findById(realId) { + self.deleteItem(instance, actionOption: actionOption) + } + } + /// Returns the instance corresponding to the provided [id] public func findById(_ id: T.ID) -> T? { if let index = self._indexes, let instance = index[id] { @@ -361,17 +421,33 @@ public class BaseCollection: SomeCollection, CollectionHolder { } } + public func deleteAllItemsAndDependencies(actionOption: ActionOption) { + self._delete(contentOfs: self.items, actionOption: actionOption) + } + + public func deleteDependencies(actionOption: ActionOption, _ isIncluded: (T) -> Bool) { + let items = self.items.filter(isIncluded) + self._delete(contentOfs: items, actionOption: actionOption) + } + + fileprivate func _delete(contentOfs sequence: any RandomAccessCollection, actionOption: ActionOption) { + for instance in sequence { + self.deleteItem(instance, actionOption: actionOption) + } + + } + // MARK: - Pending operations - func addPendingOperation(method: StorageMethod, instance: T, shouldBeSynchronized: Bool) { + func addPendingOperation(method: StorageMethod, instance: T, actionOption: ActionOption) { if self.pendingOperationManager == nil { self.pendingOperationManager = PendingOperationManager(store: self.store, inMemory: self.inMemory) } - self._addPendingOperationIfPossible(method: method, instance: instance, shouldBeSynchronized: false) + self._addPendingOperationIfPossible(method: method, instance: instance, actionOption: actionOption) } - fileprivate func _addPendingOperationIfPossible(method: StorageMethod, instance: T, shouldBeSynchronized: Bool) { - self.pendingOperationManager?.addPendingOperation(method: method, instance: instance, shouldBeSynchronized: shouldBeSynchronized) + fileprivate func _addPendingOperationIfPossible(method: StorageMethod, instance: T, actionOption: ActionOption) { + self.pendingOperationManager?.addPendingOperation(method: method, instance: instance, actionOption: actionOption) } fileprivate func _mergePendingOperations() { @@ -381,18 +457,16 @@ public class BaseCollection: SomeCollection, CollectionHolder { Logger.log(">>> Merge pending: \(manager.items.count)") for item in manager.items { let data = item.data - switch (item.method, item.shouldBeSynchronized) { - case (.addOrUpdate, true): + switch item.method { + case .add, .update: self.addOrUpdate(instance: data) - case (.addOrUpdate, false): - self.addOrUpdateItem(instance: data) - case (.delete, true): - self.delete(instance: data) - case (.delete, false): - self.deleteItem(data) - case (.deleteUnusedShared, _): - self.deleteUnusedShared(data) + case .delete: + self.deleteItem(data, actionOption: item.actionOption) + case .deleteUnusedShared: + self.deleteUnusedShared(data, actionOption: item.actionOption) } + + self._delegate?.itemMerged(item) } self.pendingOperationManager = nil @@ -402,10 +476,7 @@ public class BaseCollection: SomeCollection, CollectionHolder { /// Schedules a write operation fileprivate func _scheduleWrite() { - - guard !self.inMemory else { return } - - DispatchQueue(label: "lestorage.queue.write", qos: .utility).asyncAndWait { // sync to make sure we don't have writes performed at the same time + DispatchQueue(label: "lestorage.queue.write", qos: .utility).asyncAndWait { self._write() } } @@ -431,7 +502,6 @@ public class BaseCollection: SomeCollection, CollectionHolder { public func reset() { self.items.removeAll() self.store.removeFile(type: T.self) - triggerWrite() } public var type: any Storable.Type { return T.self } @@ -450,48 +520,33 @@ public class BaseCollection: SomeCollection, CollectionHolder { }.count } } - + + // MARK: - for Synced Collection + + @MainActor + func updateLocalInstance(_ serverInstance: T) { + if let localInstance = self.findById(serverInstance.id) { + localInstance.copy(from: serverInstance) + self.requestWrite() + } + } + } -public class StoredCollection: BaseCollection, RandomAccessCollection { +extension StoredCollection: RandomAccessCollection { - /// Returns a dummy StoredCollection instance public static func placeholder() -> StoredCollection { return StoredCollection(store: Store(storeCenter: StoreCenter.main)) } - - // MARK: - RandomAccessCollection - - public var startIndex: Int { return self.items.startIndex } - - public var endIndex: Int { return self.items.endIndex } - - public func index(after i: Int) -> Int { - return self.items.index(after: i) - } - - open subscript(index: Int) -> T { - get { - return self.items[index] - } - set(newValue) { - self.items[index] = newValue - self._triggerWrite = true - } - } - -} - -extension SyncedCollection: RandomAccessCollection { public var startIndex: Int { return self.items.startIndex } - + public var endIndex: Int { return self.items.endIndex } - + public func index(after i: Int) -> Int { return self.items.index(after: i) } - + public subscript(index: Int) -> T { get { return self.items[index] @@ -501,4 +556,5 @@ extension SyncedCollection: RandomAccessCollection { self._triggerWrite = true } } + } diff --git a/LeStorage/StoredSingleton.swift b/LeStorage/StoredSingleton.swift index 1ef5dc4..be5e4da 100644 --- a/LeStorage/StoredSingleton.swift +++ b/LeStorage/StoredSingleton.swift @@ -25,7 +25,7 @@ public class StoredSingleton: SyncedCollection { /// Sets the singleton to the collection without synchronizing it public func setItemNoSync(_ instance: T) { - self.setSingletonNoSync(instance: instance) + self.collection.setSingletonNoSync(instance: instance) } /// updates the existing singleton @@ -37,7 +37,7 @@ public class StoredSingleton: SyncedCollection { /// Returns the singleton public func item() -> T? { - return self.items.first + return self.collection.items.first } public func tryPutBeforeUpdating(_ instance: T) async throws { diff --git a/LeStorage/SyncedCollection.swift b/LeStorage/SyncedCollection.swift index 1e803d6..288d646 100644 --- a/LeStorage/SyncedCollection.swift +++ b/LeStorage/SyncedCollection.swift @@ -12,25 +12,48 @@ protocol SomeSyncedCollection: SomeCollection { func loadCollectionsFromServerIfNoFile() async throws } -public class SyncedCollection: BaseCollection, SomeSyncedCollection { +public class SyncedCollection: SomeSyncedCollection, CollectionDelegate { + + public typealias Item = T + + let store: Store + let collection: StoredCollection + + init(store: Store, indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false) { + + self.store = store + self.collection = StoredCollection(store: store, indexed: indexed, limit: limit, synchronousLoading: synchronousLoading) + + } + + init(store: Store, inMemory: Bool) async { + self.store = store + self.collection = await StoredCollection(store: store, inMemory: inMemory) + } + + var storeCenter: StoreCenter { return self.store.storeCenter } + + public var storeId: String? { + return self.store.identifier + } /// Returns a dummy SyncedCollection instance public static func placeholder() -> SyncedCollection { return SyncedCollection(store: Store(storeCenter: StoreCenter.main)) } - + /// Migrates if necessary and asynchronously decodes the json file - override func load() async { - do { - if self.inMemory { - try await self.loadDataFromServerIfAllowed() - } else { - await self.loadFromFile() - } - } catch { - Logger.error(error) - } - } +// override func load() async { +// do { +// if self.inMemory { +// try await self.loadDataFromServerIfAllowed() +// } else { +// await self.loadFromFile() +// } +// } catch { +// Logger.error(error) +// } +// } /// Loads the collection using the server data only if the collection file doesn't exists func loadCollectionsFromServerIfNoFile() async throws { @@ -64,90 +87,56 @@ public class SyncedCollection: BaseCollection, SomeSynced } Task { - await _updateLocalInstance(serverInstance) + await self.collection.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.triggerWrite() - } } @MainActor func loadItems(_ items: [T], clear: Bool = false) { - if clear { - self.setItems(items) - self.setAsLoaded() - } else { - self.setAsLoaded() - self.addOrUpdateNoSync(contentOfs: items) - } - - self.triggerWrite() + self.collection.loadAndWrite(items, clear: clear) } // MARK: - Basic operations with sync /// Adds or update an instance synchronously, dispatching network operations to background tasks - public override func addOrUpdate(instance: T) { - if let result = _addOrUpdateCore(instance: instance) { - if result.isNewItem { - Task { await self._sendInsertion(result.item) } - } else { - Task { await self._sendUpdate(result.item) } - } + public func addOrUpdate(instance: T) { + let result = _addOrUpdateCore(instance: instance) + if result.method == .insert { + Task { await self._sendInsertion(instance) } + } else { + Task { await self._sendUpdate(instance) } } } /// Private helper function that contains the shared logic - private func _addOrUpdateCore(instance: T) -> (item: T, isNewItem: Bool)? { + private func _addOrUpdateCore(instance: T) -> ActionResult { instance.lastUpdate = Date() - if let index = self.items.firstIndex(where: { $0.id == instance.id }) { - if self.updateItem(instance, index: index, shouldBeSynchronized: true) { - self.triggerWrite() - if instance.shared == true { - self._cleanUpSharedDependencies() - } - return (instance, false) - } - } else { - if self.addItem(instance: instance, shouldBeSynchronized: true) { - self.triggerWrite() - return (instance, true) + + let result = self.collection.addOrUpdate(instance: instance) + if result.method == .update { + if instance.shared == true { + self._cleanUpSharedDependencies() } } - return nil + + return result } fileprivate func _addOrUpdateCore(contentOfs sequence: any Sequence) -> OperationBatch { - - defer { - self.triggerWrite() - } let date = Date() let batch = OperationBatch() for instance in sequence { + instance.lastUpdate = date - if let index = self.items.firstIndex(where: { $0.id == instance.id }) { - if self.updateItem(instance, index: index, shouldBeSynchronized: true) { - batch.addUpdate(instance) - } - } else { // insert - if self.addItem(instance: instance, shouldBeSynchronized: true) { - batch.addInsert(instance) - } + let result = self.collection.addOrUpdate(instance: instance) + + if result.method == .insert { + batch.addInsert(instance) + } else { + batch.addUpdate(instance) } } @@ -158,48 +147,47 @@ public class SyncedCollection: BaseCollection, SomeSynced } /// Adds or update a sequence and writes - override public func addOrUpdate(contentOfs sequence: any Sequence) { + public func addOrUpdate(contentOfs sequence: any Sequence) { let batch = self._addOrUpdateCore(contentOfs: sequence) Task { await self._sendOperationBatch(batch) } } - - /// Deletes all items of the sequence by id and sets the collection as changed to trigger a write - fileprivate func _deleteCore(contentOfs sequence: any RandomAccessCollection) -> OperationBatch { - - defer { - self.triggerWrite() - } - - var deleted: [T] = [] + + /// Deletes an instance and writes + public func delete(instance: T) { - for instance in sequence { - if self.deleteItem(instance, shouldBeSynchronized: true) { - deleted.append(instance) - } - self.storeCenter.createDeleteLog(instance) - } + self.collection.delete(instance: instance, actionOption: .syncedCascade) + self.storeCenter.createDeleteLog(instance) + Task { await self._sendDeletion(instance) } - let batch = OperationBatch() - batch.deletes = deleted - return batch } /// Deletes all items of the sequence by id and sets the collection as changed to trigger a write - public override func delete(contentOfs sequence: any RandomAccessCollection) { - guard sequence.isNotEmpty else { return } - let batch = self._deleteCore(contentOfs: sequence) - Task { await self._sendOperationBatch(batch) } + public func delete(contentOfs sequence: any RandomAccessCollection) { + self.delete(contentOfs: sequence, actionOption: .syncedCascade) } - /// Deletes an instance and writes - override public func delete(instance: T) { - defer { - self.triggerWrite() + func delete(contentOfs sequence: any RandomAccessCollection, actionOption: ActionOption) { + guard sequence.isNotEmpty else { return } + let batch = self._deleteCore(contentOfs: sequence, actionOption: actionOption) + if actionOption.synchronize { + Task { await self._sendOperationBatch(batch) } } - self.deleteItem(instance, shouldBeSynchronized: true) - self.storeCenter.createDeleteLog(instance) + } + + /// Deletes all items of the sequence by id and sets the collection as changed to trigger a write + fileprivate func _deleteCore(contentOfs sequence: any RandomAccessCollection, actionOption: ActionOption) -> OperationBatch { - Task { await self._sendDeletion(instance) } + var deleted: [T] = [] + self.collection.delete(contentOfs: sequence, actionOption: actionOption) { result in + self.storeCenter.createDeleteLog(result.instance) + if !result.pending { + deleted.append(result.instance) + } + } + + let batch = OperationBatch() + batch.deletes = deleted + return batch } /// Deletes an instance without writing, logs the operation and sends an API call @@ -209,30 +197,33 @@ public class SyncedCollection: BaseCollection, SomeSynced //// await self._sendDeletion(instance) // } - public func deleteDependencies(_ items: any RandomAccessCollection, shouldBeSynchronized: Bool) { - guard items.isNotEmpty else { return } - if shouldBeSynchronized { - self.delete(contentOfs: items) - } else { - self.deleteNoSync(contentOfs: items) - } - } +// public func deleteDependencies(_ items: any RandomAccessCollection, actionOption: ActionOption) { +// guard items.isNotEmpty else { return } +// if actionOption.synchronize { +// self.delete(contentOfs: items) +// } else { +// self.deleteNoSync(contentOfs: items) +// } +// } - public override func deleteDependencies(_ items: any Sequence) { - super.deleteDependencies(items) - - let batch = OperationBatch() - batch.deletes = Array(items) - Task { await self._sendOperationBatch(batch) } - } +// public func deleteDependencies(_ items: any Sequence) { +// +// self.collection.deleteDependencies(items) +// +//// 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) - } +// public func deleteDependenciesAsync(_ items: any Sequence) async { +// super.deleteDependencies(items) +// +// let batch = OperationBatch() +// batch.deletes = Array(items) +// await self._sendOperationBatch(batch) +// } fileprivate func _cleanUpSharedDependencies() { for relationship in T.relationships() { @@ -261,22 +252,38 @@ public class SyncedCollection: BaseCollection, SomeSynced fileprivate func _deleteUnusedSharedInstances() { - let sharedItems = self.items.filter { $0.shared == true } + let sharedItems = self.collection.items.filter { $0.shared == true } for sharedItem in sharedItems { self.store.deleteUnusedSharedIfNecessary(sharedItem) } } + public func deleteAllItemsAndDependencies(actionOption: ActionOption) { + if actionOption.synchronize { + self.delete(contentOfs: self.items, actionOption: actionOption) + } else { + self.collection.deleteAllItemsAndDependencies(actionOption: actionOption) + } + } + + public func deleteDependencies(actionOption: ActionOption, _ isIncluded: (T) -> Bool) { + let items = self.items.filter(isIncluded) + if actionOption.synchronize { + self.delete(contentOfs: items, actionOption: actionOption) + } else { + self.collection.delete(contentOfs: items) + } + } + // 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)) - } + let result = _addOrUpdateCore(instance: instance) + if result.method == .insert { + try await self._executeBatchOnce(OperationBatch(insert: instance)) + } else { + try await self._executeBatchOnce(OperationBatch(update: instance)) } } @@ -288,16 +295,13 @@ public class SyncedCollection: BaseCollection, SomeSynced /// 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) + let batch = self._deleteCore(contentOfs: sequence, actionOption: .syncedCascade) try await self._executeBatchOnce(batch) } /// Deletes an instance and writes func deleteAsync(instance: T) async throws { - defer { - self.triggerWrite() - } - self.deleteItem(instance, shouldBeSynchronized: true) + self.collection.delete(instance: instance, actionOption: .syncedCascade) self.storeCenter.createDeleteLog(instance) try await self._executeBatchOnce(OperationBatch(delete: instance)) } @@ -306,51 +310,63 @@ public class SyncedCollection: BaseCollection, SomeSynced /// Adds or update an instance without synchronizing it func addOrUpdateNoSync(_ instance: T) { - self.addOrUpdateItem(instance: instance) + self.collection.addOrUpdate(instance: instance) +// self.addOrUpdateItem(instance: instance) } /// Adds or update a sequence of elements without synchronizing it func addOrUpdateNoSync(contentOfs sequence: any Sequence) { - self.addSequence(sequence) + self.collection.addOrUpdate(contentOfs: sequence) } /// Deletes the instance in the collection without synchronization - func deleteNoSync(contentOfs sequence: any Sequence) { - defer { - self.triggerWrite() - } - for item in sequence { - self.deleteItem(item, shouldBeSynchronized: false) - } - } +// func deleteNoSync(instance: T) { +// self.collection.delete(instance: instance) +// } /// Deletes the instance in the collection without synchronization - func deleteNoSync(instance: T) { - defer { - self.triggerWrite() - } - self.deleteItem(instance, shouldBeSynchronized: false) + func deleteNoSync(contentOfs sequence: any RandomAccessCollection) { + self.collection.delete(contentOfs: sequence) } 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.triggerWrite() + self.delete(instance: instance) + instance.deleteUnusedSharedDependencies(store: self.store) } /// Deletes the instance in the collection without synchronization - func deleteByStringIdNoSync(_ id: String) { - defer { - self.triggerWrite() + func deleteNoSyncNoCascade(id: String) { + self.collection.deleteByStringId(id, actionOption: .standard) + } + + // MARK: - Collection Delegate + + func loadingForMemoryCollection() async { + do { + try await self.loadDataFromServerIfAllowed() + } catch { + Logger.error(error) } - let realId = T.buildRealId(id: id) - if let instance = self.findById(realId) { - self.deleteItem(instance, shouldBeSynchronized: false) + } + + func itemMerged(_ pendingOperation: PendingOperation) { + + let batch = OperationBatch() + switch pendingOperation.method { + case .add: + batch.inserts.append(pendingOperation.data) + case .update: + batch.updates.append(pendingOperation.data) + case .delete: + batch.deletes.append(pendingOperation.data) + case .deleteUnusedShared: + break } + + Task { await self._sendOperationBatch(batch) } } // MARK: - Send requests @@ -397,14 +413,15 @@ public class SyncedCollection: BaseCollection, SomeSynced /// Adds or update an instance if it is newer than the local instance func addOrUpdateIfNewer(_ instance: T, shared: Bool) { - defer { - self.triggerWrite() - } + +// defer { +// self.triggerWrite() +// } - if let index = self.items.firstIndex(where: { $0.id == instance.id }) { - let localInstance = self.items[index] + if let index = self.collection.items.firstIndex(where: { $0.id == instance.id }) { + let localInstance = self.collection.items[index] if instance.lastUpdate > localInstance.lastUpdate { - self.updateItem(instance, index: index) + self.collection.update(instance, index: index, actionOption: .standard) } else { print("do not update \(T.resourceName()): \(instance.lastUpdate.timeIntervalSince1970) / local: \(localInstance.lastUpdate.timeIntervalSince1970)") } @@ -412,7 +429,7 @@ public class SyncedCollection: BaseCollection, SomeSynced if shared { instance.shared = true } - self.addItem(instance: instance, shouldBeSynchronized: false) + self.collection.add(instance: instance, actionOption: .standard) } } @@ -421,14 +438,37 @@ public class SyncedCollection: BaseCollection, SomeSynced /// Sends a POST request for the instance, and changes the collection to perform a write public func writeChangeAndInsertOnServer(instance: T) { + + self.collection.addOrUpdate(instance: instance) Task { await self._sendInsertion(instance) - await MainActor.run { - self.triggerWrite() - } } } + // MARK: - SomeCollection + + public var hasLoaded: Bool { return self.collection.hasLoaded} + + public var inMemory: Bool { return self.collection.inMemory } + + public var type: any Storable.Type { return T.self } + + public func referenceCount(type: S.Type, id: String) -> Int where S : Storable { + return self.collection.referenceCount(type: type, id: id) + } + + public func reset() { + self.collection.reset() + } + + public func findById(_ id: T.ID) -> T? { + return self.collection.findById(id) + } + + public var items: [T] { + return self.collection.items + } + } class OperationBatch { @@ -459,3 +499,25 @@ class OperationBatch { self.deletes.append(instance) } } + +extension SyncedCollection: RandomAccessCollection { + + public var startIndex: Int { return self.collection.items.startIndex } + + public var endIndex: Int { return self.collection.items.endIndex } + + public func index(after i: Int) -> Int { + return self.collection.items.index(after: i) + } + + public subscript(index: Int) -> T { + get { + return self.collection.items[index] + } + set(newValue) { + self.collection.update(newValue, index: index, actionOption: .standard) +// self.collection.items[index] = newValue +// self._triggerWrite = true + } + } +} diff --git a/LeStorage/SyncedStorable.swift b/LeStorage/SyncedStorable.swift index 66f30a2..2c94188 100644 --- a/LeStorage/SyncedStorable.swift +++ b/LeStorage/SyncedStorable.swift @@ -30,13 +30,7 @@ public protocol SideStorable { var storeId: String? { get set } } -public extension SyncedStorable { - - func copy() -> Self { - let copy = Self() - copy.copy(from: self) - return copy - } +extension Storable { func getStoreId() -> String? { if let alt = self as? SideStorable { @@ -46,3 +40,13 @@ public extension SyncedStorable { } } + +public extension SyncedStorable { + + func copy() -> Self { + let copy = Self() + copy.copy(from: self) + return copy + } + +} diff --git a/LeStorage/Utils/String+Extensions.swift b/LeStorage/Utils/String+Extensions.swift new file mode 100644 index 0000000..65587f4 --- /dev/null +++ b/LeStorage/Utils/String+Extensions.swift @@ -0,0 +1,17 @@ +// +// String+Extensions.swift +// LeStorage +// +// Created by Laurent Morvillier on 19/05/2025. +// + +import Foundation + +public extension String { + + static func random(length: Int = 10) -> String { + let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + return String((0.. init() { - let storeCenter = StoreCenter.main + let dir = "test_" + String.random() + let storeCenter: StoreCenter = StoreCenter(directoryName:dir) intObjects = storeCenter.mainStore.registerCollection() stringObjects = storeCenter.mainStore.registerCollection() } diff --git a/LeStorageTests/StoredCollectionTests.swift b/LeStorageTests/StoredCollectionTests.swift index 1ccf990..46cc9da 100644 --- a/LeStorageTests/StoredCollectionTests.swift +++ b/LeStorageTests/StoredCollectionTests.swift @@ -67,7 +67,7 @@ struct StoredCollectionTests { let item = MockStorable(id: "1", name: "Test") collection.addOrUpdate(instance: item) - collection.deleteById("1") + collection.deleteByStringId("1") let search = collection.findById("1") #expect(search == nil) }