Adds deleteDependencies function to cascade delete

multistore
Laurent 2 years ago
parent cc0c8db826
commit b8579f1fd3
  1. 8
      LeStorage.xcodeproj/project.pbxproj
  2. 16
      LeStorage/ApiCall.swift
  3. 20
      LeStorage/ModelObject.swift
  4. 3
      LeStorage/Storable.swift
  5. 33
      LeStorage/Store.swift
  6. 40
      LeStorage/StoredCollection.swift
  7. 18
      LeStorage/Utils/Collection+Extension.swift

@ -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 = "<group>"; };
C4A47D642B6E92FE00ADC637 /* Storable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storable.swift; sourceTree = "<group>"; };
C4A47D662B6FF83A00ADC637 /* ApiCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCall.swift; sourceTree = "<group>"; };
C4A47D6A2B71244100ADC637 /* Collection+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Extension.swift"; sourceTree = "<group>"; };
C4A47D6C2B71364600ADC637 /* ModelObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelObject.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
@ -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;
};

@ -9,14 +9,13 @@ import Foundation
protocol SomeCall : Storable {
func execute() throws
var lastAttemptDate: Date { get }
}
struct ApiCall<T : Storable> : Storable, SomeCall {
class ApiCall<T : Storable> : 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<T : Storable> : 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)
}
}

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

@ -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<T : Storable>(_ id: String) -> T? {
return Store.main.findById(id)
}
}

@ -58,7 +58,7 @@ public class Store {
return self._services
}
func findById<T : Storable>(_ id: String) -> T? {
public func findById<T : Storable>(_ id: String) -> T? {
guard let collection = self._collections[T.resourceName()] as? StoredCollection<T> else {
Logger.w("Collection \(T.resourceName()) not registered")
return nil
@ -66,6 +66,25 @@ public class Store {
return collection.findById(id)
}
public func filter<T : Storable>(isIncluded: (T) throws -> (Bool)) rethrows -> [T] {
do {
return try self.collection().filter(isIncluded)
} catch {
return []
}
}
func collection<T : Storable>() throws -> StoredCollection<T> {
if let collection = self._collections[T.resourceName()] as? StoredCollection<T> {
return collection
}
throw StoreError.collectionNotRegistered(type: T.resourceName())
}
public func deleteDependencies<T : Storable>(items: any Sequence<T>) throws {
try self.collection().deleteDependencies(items)
}
// MARK: - Api call rescheduling
func apiCallCollection<T : Storable>() throws -> StoredCollection<ApiCall<T>> {
@ -82,12 +101,12 @@ public class Store {
func deleteApiCallById<T : Storable> (_ id: String, type: T.Type) throws {
let collection: StoredCollection<ApiCall<T>> = 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<T : Storable> (_ apiCall: ApiCall<T>) throws {
let collection: StoredCollection<ApiCall<T>> = 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<T : Storable>(_ apiCall: ApiCall<T>) async throws -> T {

@ -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<T : Storable> : 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<T : Storable> : 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<T : Storable> : 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<T>) {
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<T : Storable> : 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<T : Storable> : 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<T : Storable> : 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<T : Storable> : 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<T : Storable> : RandomAccessCollection, SomeCollec
}
/// Sends an delete api call for the provided [instance]
fileprivate func _sendDeletionIfNecessary(_ instance: T) {
guard self.synchronized else {
return

@ -0,0 +1,18 @@
//
// Collection+Extension.swift
// LeStorage
//
// Created by Laurent Morvillier on 05/02/2024.
//
import Foundation
extension Array {
func sorted<V : Comparable>(keyPath: KeyPath<Element, V>, ascending: Bool = true) -> [Element] {
return self.sorted { e1, e2 in
return e1[keyPath: keyPath] > e2[keyPath: keyPath]
}
}
}
Loading…
Cancel
Save