From 239a47e17b660e4bdcf0ce75253be157a479482f Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 9 Feb 2024 18:09:25 +0100 Subject: [PATCH] Adds indexes and doc --- LeStorage/Storable.swift | 1 + LeStorage/Store.swift | 28 ++++++++++++++++++ LeStorage/StoredCollection.swift | 34 ++++++++++++++++++---- LeStorage/Utils/Collection+Extension.swift | 6 ++++ 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index e1352b6..a544943 100644 --- a/LeStorage/Storable.swift +++ b/LeStorage/Storable.swift @@ -22,4 +22,5 @@ extension Storable { return self.resourceName() + ".json" } + var stringId: String { return String(self.id) } } diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 639d5a2..f0c8d6d 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -17,12 +17,15 @@ enum StoreError: Error { public class Store { + /// The Store singleton public static let main = Store() + /// A method to provide ids corresponding to the django storage public static func randomId() -> String { return UUID().uuidString.lowercased() } + /// The URL of the django API public var synchronizationApiURL: String? { didSet { if let url = synchronizationApiURL { @@ -30,18 +33,27 @@ public class Store { } } } + + /// The services performing the API calls fileprivate var _services: Services? + /// The dictionary of registered StoredCollections fileprivate var _collections: [String : any SomeCollection] = [:] + + /// The dictionary of ApiCall StoredCollections corresponding to the synchronized registered collections fileprivate var _apiCallsCollections: [String : any SomeCollection] = [:] + /// The list of migrations to apply fileprivate var _migrations: [SomeMigration] = [] + /// The collection of performed migration on the store fileprivate lazy var _migrationCollection: StoredCollection = { StoredCollection(synchronized: false, store: Store.main, asynchronousIO: false) }() public init() { } + /// Registers a collection + /// [synchronize] denotes a collection which modification will be sent to the django server public func registerCollection(synchronized: Bool) -> StoredCollection { // register collection @@ -58,10 +70,12 @@ public class Store { return collection } + /// The service instance var service: Services? { return self._services } + /// Looks for an instance by id public func findById(_ id: String) -> T? { guard let collection = self._collections[T.resourceName()] as? StoredCollection else { Logger.w("Collection \(T.resourceName()) not registered") @@ -70,6 +84,7 @@ public class Store { return collection.findById(id) } + /// Filters a collection with a [isIncluded] predicate public func filter(isIncluded: (T) throws -> (Bool)) rethrows -> [T] { do { return try self.collection().filter(isIncluded) @@ -78,6 +93,7 @@ public class Store { } } + /// Returns a collection by type func collection() throws -> StoredCollection { if let collection = self._collections[T.resourceName()] as? StoredCollection { return collection @@ -85,16 +101,19 @@ public class Store { throw StoreError.collectionNotRegistered(type: T.resourceName()) } + /// Deletes the dependencies of a collection public func deleteDependencies(items: any Sequence) throws { try self.collection().deleteDependencies(items) } // MARK: - Migration + /// [beta] Adds a migration to perform public func addMigration(_ migration: SomeMigration) { self._migrations.append(migration) } + /// [beta] Performs the migration if necessary func performMigrationIfNecessary(_ collection: StoredCollection) async throws { // Check for migrations @@ -129,12 +148,14 @@ public class Store { // MARK: - Api call rescheduling + /// Schedules all stored api calls from all collections fileprivate func _rescheduleCalls(collection: StoredCollection>) { for apiCall in collection { self.startCallsRescheduling(apiCall: apiCall) } } + /// Returns an API call collection corresponding to a type T func apiCallCollection() throws -> StoredCollection> { if let apiCallCollection = self._apiCallsCollections[T.resourceName()] as? StoredCollection> { return apiCallCollection @@ -142,6 +163,7 @@ public class Store { throw StoreError.apiCallCollectionNotRegistered(type: T.resourceName()) } + /// Registers an api call into its collection func registerApiCall(_ apiCall: ApiCall) throws { let collection: StoredCollection> = try self.apiCallCollection() @@ -161,6 +183,7 @@ public class Store { } + /// Deletes an ApiCall by [id] and [collectionName] func deleteApiCallById(_ id: String, collectionName: String) throws { if let collection = self._apiCallsCollections[collectionName] { @@ -170,6 +193,7 @@ public class Store { throw StoreError.collectionNotRegistered(type: collectionName) } + /// Schedule an ApiCall for its execution in the future func startCallsRescheduling(apiCall: ApiCall) { let delay = pow(2, 0 + apiCall.attemptsCount) let seconds = NSDecimalNumber(decimal: delay).intValue @@ -189,6 +213,7 @@ public class Store { } + /// Reschedule an ApiCall by id func startCallsRescheduling(apiCallId: String, type: T.Type) throws { let apiCallCollection: StoredCollection> = try self.apiCallCollection() if let apiCall = apiCallCollection.findById(apiCallId) { @@ -196,6 +221,7 @@ public class Store { } } + /// Executes an ApiCall fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws -> T { guard let service else { throw StoreError.missingService @@ -203,10 +229,12 @@ public class Store { return try await service.runApiCall(apiCall) } + /// Executes an ApiCall func execute(apiCall: ApiCall) async throws { _ = try await self._executeApiCall(apiCall) } + /// Retrieves all the items on the server func getItems() async throws -> [T] { guard let service else { throw StoreError.missingService diff --git a/LeStorage/StoredCollection.swift b/LeStorage/StoredCollection.swift index 30aca4f..5db082e 100644 --- a/LeStorage/StoredCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -34,7 +34,10 @@ public class StoredCollection : RandomAccessCollection, SomeCollec /// Notifies the closure when the loading is done fileprivate var loadCompletion: ((StoredCollection) -> ())? = nil - + + /// Provides fast access for instances if the collection has been instanced with [indexed] = true + fileprivate var _index: [String : T]? = nil + /// Indicates whether the collection has changed, thus requiring a write operation fileprivate var _hasChanged: Bool = false { didSet { @@ -48,11 +51,15 @@ public class StoredCollection : RandomAccessCollection, SomeCollec } } + /// Denotes a collection that loads and writes asynchronousIO fileprivate var asynchronousIO: Bool = true - init(synchronized: Bool, store: Store, asynchronousIO: Bool = true, loadCompletion: ((StoredCollection) -> ())? = nil) { + init(synchronized: Bool, store: Store, indexed: Bool = false, asynchronousIO: Bool = true, loadCompletion: ((StoredCollection) -> ())? = nil) { self.synchronized = synchronized self.asynchronousIO = asynchronousIO + if indexed { + self._index = [:] + } self._store = store self.loadCompletion = loadCompletion self._load() @@ -85,12 +92,14 @@ public class StoredCollection : RandomAccessCollection, SomeCollec } + /// Decodes the json file into the items array fileprivate func _decodeJSONFile() throws { let jsonString = try FileUtils.readDocumentFile(fileName: T.fileName()) if let decoded: [T] = try jsonString.decodeArray() { - DispatchQueue.main.sync { + DispatchQueue.main.async { Logger.log("loaded \(T.fileName()) with \(decoded.count) items") self.items = decoded + self._updateIndexIfNecessary() self.loadCompletion?(self) NotificationCenter.default.post(name: NSNotification.Name.CollectionDidLoad, object: self) @@ -98,6 +107,14 @@ public class StoredCollection : RandomAccessCollection, SomeCollec } } + /// Updates the whole index with the items array + fileprivate func _updateIndexIfNecessary() { + if let index = self._index { + self._index = self.items.dictionary(handler: { $0.stringId }) + } + } + + /// Retrieves the data from the server and loads it into the items array public func loadDataFromServer() throws { guard self.synchronized else { throw StoreError.unSynchronizedCollection @@ -120,11 +137,13 @@ public class StoredCollection : RandomAccessCollection, SomeCollec self._hasChanged = true } + // update if let index = self.items.firstIndex(where: { $0.id == instance.id }) { self.items[index] = instance self._sendUpdateIfNecessary(instance) - } else { + } else { // insert self.items.append(instance) + self._index?[instance.stringId] = instance self._sendInsertionIfNecessary(instance) } @@ -138,10 +157,12 @@ public class StoredCollection : RandomAccessCollection, SomeCollec try instance.deleteDependencies() self.items.removeAll { $0.id == instance.id } + self._index?.removeValue(forKey: instance.stringId) self._sendDeletionIfNecessary(instance) } - public func batchInsert(_ sequence: any Sequence) { + /// Inserts the whole sequence into the items array, no updates + public func append(contentOfs sequence: any Sequence) { defer { self._hasChanged = true } @@ -153,6 +174,9 @@ public class StoredCollection : RandomAccessCollection, SomeCollec /// Returns the instance corresponding to the provided [id] public func findById(_ id: String) -> T? { + if let index = self._index, let instance = index[id] { + return instance + } return self.items.first(where: { $0.id == id }) } diff --git a/LeStorage/Utils/Collection+Extension.swift b/LeStorage/Utils/Collection+Extension.swift index 74e883d..357b104 100644 --- a/LeStorage/Utils/Collection+Extension.swift +++ b/LeStorage/Utils/Collection+Extension.swift @@ -19,4 +19,10 @@ extension Array { } } + func dictionary(handler: (Element) -> (T)) -> [T : Element] { + return self.reduce(into: [T : Element]()) { r, e in + r[handler(e)] = e + } + } + }