From b8579f1fd36ba7f02b6c292976445f1ed52544b2 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 5 Feb 2024 17:04:13 +0100 Subject: [PATCH] Adds deleteDependencies function to cascade delete --- LeStorage.xcodeproj/project.pbxproj | 8 +++++ LeStorage/ApiCall.swift | 16 ++++++--- LeStorage/ModelObject.swift | 20 +++++++++++ LeStorage/Storable.swift | 3 +- LeStorage/Store.swift | 33 ++++++++++++++---- LeStorage/StoredCollection.swift | 40 ++++++++++++++++++---- LeStorage/Utils/Collection+Extension.swift | 18 ++++++++++ 7 files changed, 120 insertions(+), 18 deletions(-) create mode 100644 LeStorage/ModelObject.swift create mode 100644 LeStorage/Utils/Collection+Extension.swift diff --git a/LeStorage.xcodeproj/project.pbxproj b/LeStorage.xcodeproj/project.pbxproj index 6cd59d0..f88a387 100644 --- a/LeStorage.xcodeproj/project.pbxproj +++ b/LeStorage.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ C4A47D612B6D3C1300ADC637 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D602B6D3C1300ADC637 /* Services.swift */; }; C4A47D652B6E92FE00ADC637 /* Storable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D642B6E92FE00ADC637 /* Storable.swift */; }; C4A47D672B6FF83A00ADC637 /* ApiCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D662B6FF83A00ADC637 /* ApiCall.swift */; }; + C4A47D6B2B71244100ADC637 /* Collection+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D6A2B71244100ADC637 /* Collection+Extension.swift */; }; + C4A47D6D2B71364600ADC637 /* ModelObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D6C2B71364600ADC637 /* ModelObject.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -45,6 +47,8 @@ C4A47D602B6D3C1300ADC637 /* Services.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = ""; }; C4A47D642B6E92FE00ADC637 /* Storable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storable.swift; sourceTree = ""; }; C4A47D662B6FF83A00ADC637 /* ApiCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCall.swift; sourceTree = ""; }; + C4A47D6A2B71244100ADC637 /* Collection+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Extension.swift"; sourceTree = ""; }; + C4A47D6C2B71364600ADC637 /* ModelObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelObject.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -91,6 +95,7 @@ C425D4382B6D24E1002A7B48 /* LeStorage.docc */, C4A47D602B6D3C1300ADC637 /* Services.swift */, C4A47D662B6FF83A00ADC637 /* ApiCall.swift */, + C4A47D6C2B71364600ADC637 /* ModelObject.swift */, C425D4572B6D2519002A7B48 /* Store.swift */, C4A47D642B6E92FE00ADC637 /* Storable.swift */, C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */, @@ -113,6 +118,7 @@ C4A47D502B6D2C4E00ADC637 /* Codable+Extensions.swift */, C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */, C4A47D522B6D2C5F00ADC637 /* Logger.swift */, + C4A47D6A2B71244100ADC637 /* Collection+Extension.swift */, ); path = Utils; sourceTree = ""; @@ -232,9 +238,11 @@ C4A47D612B6D3C1300ADC637 /* Services.swift in Sources */, C4A47D552B6D2DBF00ADC637 /* FileUtils.swift in Sources */, C4A47D652B6E92FE00ADC637 /* Storable.swift in Sources */, + C4A47D6D2B71364600ADC637 /* ModelObject.swift in Sources */, C4A47D4F2B6D280200ADC637 /* StoredCollection.swift in Sources */, C4A47D672B6FF83A00ADC637 /* ApiCall.swift in Sources */, C425D4582B6D2519002A7B48 /* Store.swift in Sources */, + C4A47D6B2B71244100ADC637 /* Collection+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/LeStorage/ApiCall.swift b/LeStorage/ApiCall.swift index 3e1a8b0..98a95b8 100644 --- a/LeStorage/ApiCall.swift +++ b/LeStorage/ApiCall.swift @@ -9,14 +9,13 @@ import Foundation protocol SomeCall : Storable { func execute() throws + var lastAttemptDate: Date { get } } -struct ApiCall : Storable, SomeCall { +class ApiCall : ModelObject, Storable, SomeCall { static func resourceName() -> String { return "apicalls" } - var id: String = UUID().uuidString - /// The http URL of the call var url: String @@ -27,14 +26,21 @@ struct ApiCall : Storable, SomeCall { var body: Data /// The number of times the call has been executed - var attemptsCount = 1 + var attemptsCount: Int = 1 /// The date of the last execution - var lastAttemptDate = Date() + var lastAttemptDate: Date = Date() + + init(url: String, method: String, body: Data) { + self.url = url + self.method = method + self.body = body + } /// Executes the api call func execute() throws { Task { + self.lastAttemptDate = Date() try await Store.main.execute(apiCall: self) } } diff --git a/LeStorage/ModelObject.swift b/LeStorage/ModelObject.swift new file mode 100644 index 0000000..0ecde27 --- /dev/null +++ b/LeStorage/ModelObject.swift @@ -0,0 +1,20 @@ +// +// ModelObject.swift +// LeStorage +// +// Created by Laurent Morvillier on 05/02/2024. +// + +import Foundation + +open class ModelObject { + + public var id: String = Store.randomId() + + public init() { } + + open func deleteDependencies() throws { + + } + +} diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index 6293402..e5d439d 100644 --- a/LeStorage/Storable.swift +++ b/LeStorage/Storable.swift @@ -9,6 +9,7 @@ import Foundation public protocol Storable : Codable, Identifiable where ID : StringProtocol { static func resourceName() -> String + func deleteDependencies() throws } extension Storable { @@ -16,5 +17,5 @@ extension Storable { public func findById(_ id: String) -> T? { return Store.main.findById(id) } - + } diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 968be2b..b418e42 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -58,7 +58,7 @@ public class Store { return self._services } - func findById(_ id: String) -> T? { + public func findById(_ id: String) -> T? { guard let collection = self._collections[T.resourceName()] as? StoredCollection else { Logger.w("Collection \(T.resourceName()) not registered") return nil @@ -66,6 +66,25 @@ public class Store { return collection.findById(id) } + public func filter(isIncluded: (T) throws -> (Bool)) rethrows -> [T] { + do { + return try self.collection().filter(isIncluded) + } catch { + return [] + } + } + + func collection() throws -> StoredCollection { + if let collection = self._collections[T.resourceName()] as? StoredCollection { + return collection + } + throw StoreError.collectionNotRegistered(type: T.resourceName()) + } + + public func deleteDependencies(items: any Sequence) throws { + try self.collection().deleteDependencies(items) + } + // MARK: - Api call rescheduling func apiCallCollection() throws -> StoredCollection> { @@ -82,12 +101,12 @@ public class Store { func deleteApiCallById (_ id: String, type: T.Type) throws { let collection: StoredCollection> = try self.apiCallCollection() - collection.deleteById(id) + try collection.deleteById(id) } func deleteApiCallById(_ id: String, collectionName: String) throws { if let collection = self._apiCallsCollections[collectionName] { - collection.deleteById(id) + try collection.deleteById(id) return } throw StoreError.collectionNotRegistered(type: collectionName) @@ -95,7 +114,7 @@ public class Store { func deleteApiCall (_ apiCall: ApiCall) throws { let collection: StoredCollection> = try self.apiCallCollection() - collection.delete(instance: apiCall) + try collection.delete(instance: apiCall) } func startCallsRescheduling() { @@ -117,7 +136,10 @@ public class Store { do { for collection in self._apiCallsCollections.values { if let apiCalls = collection.allItems() as? [any SomeCall] { - for apiCall in apiCalls { + + let sortedCalls = apiCalls.sorted(keyPath: \.lastAttemptDate, ascending: true) + + for apiCall in sortedCalls { try apiCall.execute() } } else { @@ -128,7 +150,6 @@ public class Store { Logger.error(error) } } - } fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws -> T { diff --git a/LeStorage/StoredCollection.swift b/LeStorage/StoredCollection.swift index 47a4479..d6b3a5f 100644 --- a/LeStorage/StoredCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -9,17 +9,21 @@ import Foundation protocol SomeCollection : Identifiable { func allItems() -> [any Storable] - func deleteById(_ id: String) + func deleteById(_ id: String) throws } public class StoredCollection : RandomAccessCollection, SomeCollection, ObservableObject { + /// If true, will synchronize the data with the provided server located at the Store's synchronizationApiURL let synchronized: Bool + /// The list of stored items @Published public fileprivate(set) var items: [T] = [] + /// The reference to the Store fileprivate var _store: Store + /// Indicates whether the collection has changed, thus requiring a write operation fileprivate var _hasChanged: Bool = false { didSet { if self._hasChanged == true { @@ -35,10 +39,13 @@ public class StoredCollection : RandomAccessCollection, SomeCollec self._load() } + /// Returns the default filename for the collection fileprivate var _fileName: String { return T.resourceName() + ".json" } + /// 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) { defer { self._hasChanged = true @@ -54,37 +61,54 @@ public class StoredCollection : RandomAccessCollection, SomeCollec } - public func delete(instance: T) { - defer { + /// Deletes the instance in the collection by id + public func delete(instance: T) throws { + defer { self._hasChanged = true } + try instance.deleteDependencies() self.items.removeAll { $0.id == instance.id } self._sendDeletionIfNecessary(instance) } + /// Returns the instance corresponding to the provided [id] public func findById(_ id: String) -> T? { return self.items.first(where: { $0.id == id }) } - public func deleteById(_ id: String) { + /// Deletes the instance corresponding to the provided [id] + public func deleteById(_ id: String) throws { if let instance = self.findById(id) { - self.delete(instance: instance) + try self.delete(instance: instance) + } + } + + /// Proceeds to "hard" delete the items without synchronizing them + public func deleteDependencies(_ items: any Sequence) { + defer { + self._hasChanged = true + } + for item in items { + self.items.removeAll(where: { $0.id == item.id }) } } // MARK: - SomeCall + /// Returns the collection items as [any Storable] func allItems() -> [any Storable] { return self.items } // MARK: - File access + /// Schedules a write operation fileprivate func _scheduleWrite() { self._write() } + /// Writes all the items as a json array inside a file fileprivate func _write() { DispatchQueue(label: "lestorage.queue.write", qos: .background).async { do { @@ -96,8 +120,8 @@ public class StoredCollection : RandomAccessCollection, SomeCollec } } + /// Launches a load operation if the file exists fileprivate func _load() { - do { let url = try FileUtils.directoryURLForFileName(self._fileName) if FileManager.default.fileExists(atPath: url.path()) { @@ -109,6 +133,7 @@ public class StoredCollection : RandomAccessCollection, SomeCollec } + /// Loads asynchronously into memory the objects contained inside the collection file fileprivate func _loadAsync() { DispatchQueue(label: "lestorage.queue.read", qos: .background).async { @@ -130,6 +155,7 @@ public class StoredCollection : RandomAccessCollection, SomeCollec // MARK: - Synchronization + /// Sends an insert api call for the provided [instance] fileprivate func _sendInsertionIfNecessary(_ instance: T) { Logger.log("_sendInsertionIfNecessary...") guard self.synchronized else { @@ -146,6 +172,7 @@ public class StoredCollection : RandomAccessCollection, SomeCollec } + /// Sends an update api call for the provided [instance] fileprivate func _sendUpdateIfNecessary(_ instance: T) { guard self.synchronized else { return @@ -161,6 +188,7 @@ public class StoredCollection : RandomAccessCollection, SomeCollec } + /// Sends an delete api call for the provided [instance] fileprivate func _sendDeletionIfNecessary(_ instance: T) { guard self.synchronized else { return diff --git a/LeStorage/Utils/Collection+Extension.swift b/LeStorage/Utils/Collection+Extension.swift new file mode 100644 index 0000000..ba65dc7 --- /dev/null +++ b/LeStorage/Utils/Collection+Extension.swift @@ -0,0 +1,18 @@ +// +// Collection+Extension.swift +// LeStorage +// +// Created by Laurent Morvillier on 05/02/2024. +// + +import Foundation + +extension Array { + + func sorted(keyPath: KeyPath, ascending: Bool = true) -> [Element] { + return self.sorted { e1, e2 in + return e1[keyPath: keyPath] > e2[keyPath: keyPath] + } + } + +}