diff --git a/LeStorage/Codables/ApiCall.swift b/LeStorage/Codables/ApiCall.swift index 28bc65e..07eda98 100644 --- a/LeStorage/Codables/ApiCall.swift +++ b/LeStorage/Codables/ApiCall.swift @@ -30,6 +30,7 @@ public class ApiCall: ModelObject, Storable, SomeCall { public static func resourceName() -> String { return "apicalls_" + T.resourceName() } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + public static func storeParent() -> Bool { return false } public var id: String = Store.randomId() @@ -112,7 +113,8 @@ class OldApiCall: ModelObject, Storable, SomeCall { static func resourceName() -> String { return "apicalls_" + T.resourceName() } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } - + static func storeParent() -> Bool { return false } + var id: String = Store.randomId() /// The transactionId to group calls together diff --git a/LeStorage/Codables/DataAccess.swift b/LeStorage/Codables/DataAccess.swift index 8b2e5f5..0b8ffb9 100644 --- a/LeStorage/Codables/DataAccess.swift +++ b/LeStorage/Codables/DataAccess.swift @@ -13,6 +13,7 @@ class DataAccess: SyncedModelObject, SyncedStorable { static func resourceName() -> String { return "data-access" } static func relationships() -> [Relationship] { return [] } static var copyServerResponse: Bool = false + static func storeParent() -> Bool { return false } override required init() { super.init() diff --git a/LeStorage/Codables/DataLog.swift b/LeStorage/Codables/DataLog.swift index 4dfe94b..c7da4db 100644 --- a/LeStorage/Codables/DataLog.swift +++ b/LeStorage/Codables/DataLog.swift @@ -12,7 +12,8 @@ class DataLog: ModelObject, Storable { static func resourceName() -> String { return "data-logs" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func relationships() -> [Relationship] { return [] } - + static func storeParent() -> Bool { return false } + var id: String = Store.randomId() /// The id of the underlying data diff --git a/LeStorage/Codables/FailedAPICall.swift b/LeStorage/Codables/FailedAPICall.swift index 855db57..b592984 100644 --- a/LeStorage/Codables/FailedAPICall.swift +++ b/LeStorage/Codables/FailedAPICall.swift @@ -13,6 +13,7 @@ class FailedAPICall: SyncedModelObject, SyncedStorable { static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func relationships() -> [Relationship] { return [] } static var copyServerResponse: Bool = false + static func storeParent() -> Bool { return false } override required init() { self.callId = "" diff --git a/LeStorage/Codables/GetSyncData.swift b/LeStorage/Codables/GetSyncData.swift index ae9252b..e19dedf 100644 --- a/LeStorage/Codables/GetSyncData.swift +++ b/LeStorage/Codables/GetSyncData.swift @@ -9,6 +9,10 @@ import Foundation class GetSyncData: SyncedModelObject, SyncedStorable, URLParameterConvertible { + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + static var copyServerResponse: Bool = false + static func storeParent() -> Bool { return false } + var date: String = "" enum CodingKeys: String, CodingKey { @@ -25,9 +29,6 @@ class GetSyncData: SyncedModelObject, SyncedStorable, URLParameterConvertible { try super.init(from: decoder) } - static func tokenExemptedMethods() -> [HTTPMethod] { return [] } - static var copyServerResponse: Bool = false - static func resourceName() -> String { return "sync-data" } diff --git a/LeStorage/Codables/Log.swift b/LeStorage/Codables/Log.swift index 5d13b2b..994478d 100644 --- a/LeStorage/Codables/Log.swift +++ b/LeStorage/Codables/Log.swift @@ -13,6 +13,7 @@ class Log: SyncedModelObject, SyncedStorable { static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func relationships() -> [Relationship] { return [] } static var copyServerResponse: Bool = false + static func storeParent() -> Bool { return false } override required init() { super.init() diff --git a/LeStorage/Codables/SyncData.swift b/LeStorage/Codables/SyncData.swift index 1dab2c4..bb71822 100644 --- a/LeStorage/Codables/SyncData.swift +++ b/LeStorage/Codables/SyncData.swift @@ -25,6 +25,7 @@ class SyncData { var updates: [SyncedStorableArray] = [] var deletions: [ObjectIdentifierArray] = [] + var shared: [SyncedStorableArray] = [] var grants: [SyncedStorableArray] = [] var revocations: [ObjectIdentifierArray] = [] var revocationParents: [[ObjectIdentifierArray]] = [] @@ -47,6 +48,9 @@ class SyncData { if let deletions = json["deletions"] as? [String: Any] { self.deletions = try storeCenter.decodeObjectIdentifierDictionary(deletions) } + if let shared = json["shared"] as? [String: Any] { + self.shared = try storeCenter.decodeDictionary(shared) + } if let grants = json["grants"] as? [String: Any] { self.grants = try storeCenter.decodeDictionary(grants) } diff --git a/LeStorage/ModelObject.swift b/LeStorage/ModelObject.swift index 05c8bf3..4410d54 100644 --- a/LeStorage/ModelObject.swift +++ b/LeStorage/ModelObject.swift @@ -56,7 +56,7 @@ open class SyncedModelObject: BaseModelObject { public var relatedUser: String? = nil public var lastUpdate: Date = Date() - public var shared: Bool? + public var sharing: SharingStatus? public override init() { super.init() @@ -73,7 +73,7 @@ open class SyncedModelObject: BaseModelObject { let container = try decoder.container(keyedBy: CodingKeys.self) self.relatedUser = try container.decodeIfPresent(String.self, forKey: .relatedUser) self.lastUpdate = try container.decodeIfPresent(Date.self, forKey: .lastUpdate) ?? Date() - self.shared = try container.decodeIfPresent(Bool.self, forKey: .shared) + self.sharing = try container.decodeIfPresent(SharingStatus.self, forKey: .shared) try super.init(from: decoder) } @@ -83,8 +83,8 @@ open class SyncedModelObject: BaseModelObject { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(relatedUser, forKey: .relatedUser) try container.encode(lastUpdate, forKey: .lastUpdate) - if self.shared == true { - try container.encodeIfPresent(shared, forKey: .shared) + if self.sharing != nil { + try container.encodeIfPresent(sharing, forKey: .shared) } try super.encode(to: encoder) diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index 919913f..acde2d1 100644 --- a/LeStorage/Storable.swift +++ b/LeStorage/Storable.swift @@ -35,6 +35,8 @@ public protocol Storable: Codable, Identifiable, NSObjectProtocol { /// This method returns RelationShips objects of the type static func relationships() -> [Relationship] + static func storeParent() -> Bool + } extension Storable { diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 31f902b..1a5a3b8 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -112,13 +112,13 @@ final public class Store { /// - Parameters: /// - indexed: Creates an index to quickly access the data /// - inMemory: Indicates if the collection should only live in memory, and not write into a file - public func registerSynchronizedCollection(indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false) -> SyncedCollection { + public func registerSynchronizedCollection(indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false, noLoad: Bool = false) -> SyncedCollection { if let collection: SyncedCollection = try? self.syncedCollection() { return collection } - let collection = SyncedCollection(store: self, indexed: indexed, inMemory: inMemory, limit: limit, synchronousLoading: synchronousLoading) + let collection = SyncedCollection(store: self, indexed: indexed, inMemory: inMemory, limit: limit, synchronousLoading: synchronousLoading, noLoad: noLoad) self._collections[T.resourceName()] = collection self._baseCollections[T.resourceName()] = collection.collection @@ -207,7 +207,7 @@ final public class Store { do { return try self.syncedCollection() } catch { - return self.registerSynchronizedCollection(indexed: true, inMemory: false) + return self.registerSynchronizedCollection(indexed: true, inMemory: false, noLoad: true) } } @@ -253,7 +253,7 @@ final public class Store { /// Calls addOrUpdateIfNewer from the collection corresponding to the instance @MainActor - func addOrUpdateIfNewer(_ instance: T, shared: Bool) { + func addOrUpdateIfNewer(_ instance: T, shared: SharingStatus?) { let collection: SyncedCollection = self.registerOrGetSyncedCollection(T.self) collection.addOrUpdateIfNewer(instance, shared: shared) } @@ -294,7 +294,7 @@ final public class Store { } } - public func deleteUnusedSharedDependencies(type: T.Type, shouldBeSynchronized: Bool, _ handler: (T) throws -> Bool) { + public func deleteUnusedSharedDependencies(type: T.Type, _ handler: (T) throws -> Bool) { do { let collection: SyncedCollection = try self.syncedCollection() let items = try collection.items.filter(handler) @@ -305,12 +305,21 @@ final public class Store { } + public func deleteUnusedSharedDependencies(type: T.Type) { + do { + let collection: SyncedCollection = try self.syncedCollection() + self.deleteUnusedSharedDependencies(collection.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(_ items: [T]) { do { for item in items { - guard item.shared == true else { continue } + guard item.sharing != nil 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) @@ -333,17 +342,40 @@ final public class Store { } - public func deleteDependencies(type: T.Type, actionOption: ActionOption, _ isIncluded: (any Storable) -> Bool) { - + public func deleteDependencies(type: T.Type, actionOption: ActionOption, _ isIncluded: (T) -> Bool) where T: SyncedStorable { do { - let collection: any SomeCollection = try self.someCollection(type: type) - collection.deleteDependencies(actionOption: actionOption, isIncluded) + let collection = try self.someCollection(type: type) + if let syncCollection = collection as? SyncedCollection { + syncCollection.deleteDependencies(actionOption: actionOption, isIncluded) + } } catch { Logger.error(error) } - } + public func deleteDependencies(type: T.Type, actionOption: ActionOption, _ isIncluded: (T) -> Bool) where T: Storable { + do { + let collection = try self.someCollection(type: type) + if let syncCollection = collection as? StoredCollection { + syncCollection.deleteDependencies(actionOption: actionOption, isIncluded) + } + } catch { + Logger.error(error) + } + } + +// public func deleteDependencies(type: T.Type, actionOption: ActionOption, _ isIncluded: (T) -> Bool) { +// +// do { +// let collection: any SomeCollection = try self.someCollection(type: type) +// if let syncCollection = collection as? SyncedCollection { +// collection.deleteDependencies(actionOption: actionOption, isIncluded) +// } catch { +// Logger.error(error) +// } +// +// } + // MARK: - Write /// Returns the directory URL of the store diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index 3f6aa83..cab4378 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -182,6 +182,8 @@ public class StoreCenter { } } } + + // MARK: - Store management /// Registers a store into the list of stores /// - Parameters: @@ -200,7 +202,7 @@ public class StoreCenter { /// - Parameters: /// - identifier: The store identifer /// - parameter: The parameter name used to filter data on the server - public func requestStore(identifier: String) -> Store { + func requestStore(identifier: String) -> Store { if let store = self._stores[identifier] { return store } else { @@ -210,6 +212,29 @@ public class StoreCenter { } } + /// Returns the store corresponding to the provided id, and creates one if necessary, otherwise returns the main store + fileprivate func _requestStore(id: String?) -> Store { + if let storeId = id { + if let store = self._stores[storeId] { + return store + } else { + let store = Store(storeCenter: self, identifier: storeId) + self._registerStore(store: store) + return store + } + } else { + return self.mainStore + } + } + + fileprivate func _store(id: String?) -> Store? { + if let storeId = id, let store = self._stores[storeId] { + return store + } else { + return self.mainStore + } + } + public func store(identifier: String) throws -> Store { if let store = self._stores[identifier] { return store @@ -217,6 +242,15 @@ public class StoreCenter { throw StoreError.storeNotRegistered(id: identifier) } + /// Deletes the directory using its identifier + /// - Parameters: + /// - identifier: The name of the directory + public func destroyStore(identifier: String) { + let directory = "\(self.directoryName)/\(identifier)" + FileManager.default.deleteDirectoryInDocuments(directoryName: directory) + self._stores.removeValue(forKey: identifier) + } + // MARK: - Settings /// Sets the user info given a user @@ -535,7 +569,7 @@ public class StoreCenter { } func itemsRetrieved(_ results: [T], storeId: String?, clear: Bool) async { - await self._store(id: storeId).loadCollectionItems(results, clear: clear) + await self._requestStore(id: storeId).loadCollectionItems(results, clear: clear) } /// Returns the names of all collections @@ -631,7 +665,7 @@ public class StoreCenter { } let array = try self.decodeDictionary(json) - await self._syncAddOrUpdate(array, shared: true) + await self._syncAddOrUpdate(array, shared: .shared) } catch { Logger.error(error) } @@ -642,11 +676,12 @@ public class StoreCenter { await self._syncAddOrUpdate(syncData.updates) await self._syncDelete(syncData.deletions) - await self._syncAddOrUpdate(syncData.grants, shared: true) + await self._syncAddOrUpdate(syncData.shared, shared: .shared) + await self._syncAddOrUpdate(syncData.grants, shared: .granted) await self.syncRevoke(syncData.revocations, parents: syncData.revocationParents) // self._syncAddOrUpdate(syncData.relationshipSets) // await self._syncDelete(syncData.relationshipRemovals) - await self._syncAddOrUpdate(syncData.sharedRelationshipSets, shared: true) + await self._syncAddOrUpdate(syncData.sharedRelationshipSets, shared: .granted) await self._syncRevoke(syncData.sharedRelationshipRemovals) Logger.log("sync content: updates = \(syncData.updates.count) / deletions = \(syncData.deletions.count), grants = \(syncData.grants.count)") @@ -668,7 +703,7 @@ public class StoreCenter { /// - updateArrays: the server updates /// - shared: indicates if the content should be flagged as shared @MainActor - func _syncAddOrUpdate(_ updateArrays: [SyncedStorableArray], shared: Bool = false) async { + func _syncAddOrUpdate(_ updateArrays: [SyncedStorableArray], shared: SharingStatus? = nil) async { for updateArray in updateArrays { for item in updateArray.items { @@ -727,21 +762,6 @@ public class StoreCenter { } - /// Returns the store corresponding to the provided id, and creates one if necessary - fileprivate func _store(id: String?) -> Store { - if let storeId = id { - if let store = self._stores[storeId] { - return store - } else { - let store = Store(storeCenter: self, identifier: storeId) - self._registerStore(store: store) - return store - } - } else { - return self.mainStore - } - } - /// Returns whether a data has already been deleted by, to avoid inserting it again fileprivate func _hasAlreadyBeenDeleted(_ instance: T) -> Bool { return self._deleteLogs.contains(where: { @@ -750,10 +770,10 @@ public class StoreCenter { } /// Adds or updates an instance into the store - func synchronizationAddOrUpdate(_ instance: T, storeId: String?, shared: Bool) async { + func synchronizationAddOrUpdate(_ instance: T, storeId: String?, shared: SharingStatus?) async { let hasAlreadyBeenDeleted: Bool = self._hasAlreadyBeenDeleted(instance) if !hasAlreadyBeenDeleted { - await self._store(id: storeId).addOrUpdateIfNewer(instance, shared: shared) + await self._requestStore(id: storeId).addOrUpdateIfNewer(instance, shared: shared) } } @@ -761,7 +781,7 @@ public class StoreCenter { @MainActor func synchronizationDelete(id: String, type: T.Type, storeId: String?) { do { - try self._store(id: storeId).deleteNoSyncNoCascade(type: type, id: id) + try self._store(id: storeId)?.deleteNoSyncNoCascade(type: type, id: id) } catch { Logger.error(error) } @@ -776,7 +796,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).deleteNoSyncNoCascade(type: type, id: id) + try self._store(id: storeId)?.deleteNoSyncNoCascade(type: type, id: id) } } } catch { @@ -788,7 +808,7 @@ public class StoreCenter { fileprivate func _instanceShared(id: String, type: T.Type) -> Bool { let realId: T.ID = T.buildRealId(id: id) let instance: T? = self.mainStore.findById(realId) - return instance?.shared == true + return instance?.sharing != nil } /// Deletes a data log by data id @@ -967,15 +987,6 @@ public class StoreCenter { return !self._blackListedUserName.contains(where: { $0 == userName }) } - /// Deletes the directory using its identifier - /// - Parameters: - /// - identifier: The name of the directory - public func destroyStore(identifier: String) { - let directory = "\(self.directoryName)/\(identifier)" - FileManager.default.deleteDirectoryInDocuments(directoryName: directory) - self._stores.removeValue(forKey: identifier) - } - // MARK: - Instant update /// Updates a local object with a server instance diff --git a/LeStorage/StoredCollection.swift b/LeStorage/StoredCollection.swift index 70c694e..12b1080 100644 --- a/LeStorage/StoredCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -105,7 +105,7 @@ public class StoredCollection: SomeCollection { } } - init(store: Store, indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false, delegate: (any CollectionDelegate)? = nil) { + init(store: Store, indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false, noLoad: Bool = false, delegate: (any CollectionDelegate)? = nil) { if indexed { self._indexes = [:] } @@ -114,15 +114,18 @@ public class StoredCollection: SomeCollection { self.limit = limit self._delegate = delegate - if synchronousLoading { - Task { - await self.loadFromFile() - } + if noLoad { + self.hasLoaded = true } else { - Task(priority: .high) { - await self.load() + Task { + if synchronousLoading { + await self.loadFromFile() + } else { + await self.load() + } } } + } init(store: Store) { @@ -322,6 +325,11 @@ public class StoredCollection: SomeCollection { instance.store = self.store self._indexes?[instance.id] = instance self._applyLimitIfPresent() + + if T.storeParent() { + _ = self.storeCenter.requestStore(identifier: instance.stringId) // make directory + } + return true } @@ -361,6 +369,11 @@ public class StoredCollection: SomeCollection { } self.localDeleteOnly(instance: instance) + + if T.storeParent() { + self.storeCenter.destroyStore(identifier: instance.stringId) + } + return true } diff --git a/LeStorage/SyncedCollection.swift b/LeStorage/SyncedCollection.swift index 288d646..6c5a756 100644 --- a/LeStorage/SyncedCollection.swift +++ b/LeStorage/SyncedCollection.swift @@ -19,10 +19,10 @@ public class SyncedCollection: SomeSyncedCollection, Collect let store: Store let collection: StoredCollection - 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, noLoad: Bool = false) { self.store = store - self.collection = StoredCollection(store: store, indexed: indexed, limit: limit, synchronousLoading: synchronousLoading) + self.collection = StoredCollection(store: store, indexed: indexed, limit: limit, synchronousLoading: synchronousLoading, noLoad: noLoad) } @@ -115,7 +115,7 @@ public class SyncedCollection: SomeSyncedCollection, Collect let result = self.collection.addOrUpdate(instance: instance) if result.method == .update { - if instance.shared == true { + if instance.sharing != nil { self._cleanUpSharedDependencies() } } @@ -252,7 +252,7 @@ public class SyncedCollection: SomeSyncedCollection, Collect fileprivate func _deleteUnusedSharedInstances() { - let sharedItems = self.collection.items.filter { $0.shared == true } + let sharedItems = self.collection.items.filter { $0.sharing != nil } for sharedItem in sharedItems { self.store.deleteUnusedSharedIfNecessary(sharedItem) } @@ -331,7 +331,7 @@ public class SyncedCollection: SomeSyncedCollection, Collect func deleteUnusedShared(instance: T) { - guard instance.shared == true else { return } + guard instance.sharing != nil else { return } self.delete(instance: instance) instance.deleteUnusedSharedDependencies(store: self.store) @@ -412,7 +412,7 @@ public class SyncedCollection: SomeSyncedCollection, Collect // MARK: - Synchronization /// Adds or update an instance if it is newer than the local instance - func addOrUpdateIfNewer(_ instance: T, shared: Bool) { + func addOrUpdateIfNewer(_ instance: T, shared: SharingStatus?) { // defer { // self.triggerWrite() @@ -426,9 +426,7 @@ public class SyncedCollection: SomeSyncedCollection, Collect print("do not update \(T.resourceName()): \(instance.lastUpdate.timeIntervalSince1970) / local: \(localInstance.lastUpdate.timeIntervalSince1970)") } } else { // insert - if shared { - instance.shared = true - } + instance.sharing = shared self.collection.add(instance: instance, actionOption: .standard) } diff --git a/LeStorage/SyncedStorable.swift b/LeStorage/SyncedStorable.swift index 2c94188..4f503ff 100644 --- a/LeStorage/SyncedStorable.swift +++ b/LeStorage/SyncedStorable.swift @@ -7,10 +7,15 @@ import Foundation +public enum SharingStatus: Int, Codable { + case shared = 1 + case granted +} + public protocol SyncedStorable: Storable { var lastUpdate: Date { get set } - var shared: Bool? { get set } + var sharing: SharingStatus? { get set } init()