Adds indexes and doc

multistore
Laurent 2 years ago
parent 11fb813734
commit 239a47e17b
  1. 1
      LeStorage/Storable.swift
  2. 28
      LeStorage/Store.swift
  3. 32
      LeStorage/StoredCollection.swift
  4. 6
      LeStorage/Utils/Collection+Extension.swift

@ -22,4 +22,5 @@ extension Storable {
return self.resourceName() + ".json" return self.resourceName() + ".json"
} }
var stringId: String { return String(self.id) }
} }

@ -17,12 +17,15 @@ enum StoreError: Error {
public class Store { public class Store {
/// The Store singleton
public static let main = Store() public static let main = Store()
/// A method to provide ids corresponding to the django storage
public static func randomId() -> String { public static func randomId() -> String {
return UUID().uuidString.lowercased() return UUID().uuidString.lowercased()
} }
/// The URL of the django API
public var synchronizationApiURL: String? { public var synchronizationApiURL: String? {
didSet { didSet {
if let url = synchronizationApiURL { if let url = synchronizationApiURL {
@ -30,18 +33,27 @@ public class Store {
} }
} }
} }
/// The services performing the API calls
fileprivate var _services: Services? fileprivate var _services: Services?
/// The dictionary of registered StoredCollections
fileprivate var _collections: [String : any SomeCollection] = [:] fileprivate var _collections: [String : any SomeCollection] = [:]
/// The dictionary of ApiCall StoredCollections corresponding to the synchronized registered collections
fileprivate var _apiCallsCollections: [String : any SomeCollection] = [:] fileprivate var _apiCallsCollections: [String : any SomeCollection] = [:]
/// The list of migrations to apply
fileprivate var _migrations: [SomeMigration] = [] fileprivate var _migrations: [SomeMigration] = []
/// The collection of performed migration on the store
fileprivate lazy var _migrationCollection: StoredCollection<MigrationHistory> = { StoredCollection(synchronized: false, store: Store.main, asynchronousIO: false) fileprivate lazy var _migrationCollection: StoredCollection<MigrationHistory> = { StoredCollection(synchronized: false, store: Store.main, asynchronousIO: false)
}() }()
public init() { } public init() { }
/// Registers a collection
/// [synchronize] denotes a collection which modification will be sent to the django server
public func registerCollection<T : Storable>(synchronized: Bool) -> StoredCollection<T> { public func registerCollection<T : Storable>(synchronized: Bool) -> StoredCollection<T> {
// register collection // register collection
@ -58,10 +70,12 @@ public class Store {
return collection return collection
} }
/// The service instance
var service: Services? { var service: Services? {
return self._services return self._services
} }
/// Looks for an instance by id
public 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 { guard let collection = self._collections[T.resourceName()] as? StoredCollection<T> else {
Logger.w("Collection \(T.resourceName()) not registered") Logger.w("Collection \(T.resourceName()) not registered")
@ -70,6 +84,7 @@ public class Store {
return collection.findById(id) return collection.findById(id)
} }
/// Filters a collection with a [isIncluded] predicate
public func filter<T : Storable>(isIncluded: (T) throws -> (Bool)) rethrows -> [T] { public func filter<T : Storable>(isIncluded: (T) throws -> (Bool)) rethrows -> [T] {
do { do {
return try self.collection().filter(isIncluded) return try self.collection().filter(isIncluded)
@ -78,6 +93,7 @@ public class Store {
} }
} }
/// Returns a collection by type
func collection<T : Storable>() throws -> StoredCollection<T> { func collection<T : Storable>() throws -> StoredCollection<T> {
if let collection = self._collections[T.resourceName()] as? StoredCollection<T> { if let collection = self._collections[T.resourceName()] as? StoredCollection<T> {
return collection return collection
@ -85,16 +101,19 @@ public class Store {
throw StoreError.collectionNotRegistered(type: T.resourceName()) throw StoreError.collectionNotRegistered(type: T.resourceName())
} }
/// Deletes the dependencies of a collection
public func deleteDependencies<T : Storable>(items: any Sequence<T>) throws { public func deleteDependencies<T : Storable>(items: any Sequence<T>) throws {
try self.collection().deleteDependencies(items) try self.collection().deleteDependencies(items)
} }
// MARK: - Migration // MARK: - Migration
/// [beta] Adds a migration to perform
public func addMigration(_ migration: SomeMigration) { public func addMigration(_ migration: SomeMigration) {
self._migrations.append(migration) self._migrations.append(migration)
} }
/// [beta] Performs the migration if necessary
func performMigrationIfNecessary<T : Storable>(_ collection: StoredCollection<T>) async throws { func performMigrationIfNecessary<T : Storable>(_ collection: StoredCollection<T>) async throws {
// Check for migrations // Check for migrations
@ -129,12 +148,14 @@ public class Store {
// MARK: - Api call rescheduling // MARK: - Api call rescheduling
/// Schedules all stored api calls from all collections
fileprivate func _rescheduleCalls<T : Storable>(collection: StoredCollection<ApiCall<T>>) { fileprivate func _rescheduleCalls<T : Storable>(collection: StoredCollection<ApiCall<T>>) {
for apiCall in collection { for apiCall in collection {
self.startCallsRescheduling(apiCall: apiCall) self.startCallsRescheduling(apiCall: apiCall)
} }
} }
/// Returns an API call collection corresponding to a type T
func apiCallCollection<T : Storable>() throws -> StoredCollection<ApiCall<T>> { func apiCallCollection<T : Storable>() throws -> StoredCollection<ApiCall<T>> {
if let apiCallCollection = self._apiCallsCollections[T.resourceName()] as? StoredCollection<ApiCall<T>> { if let apiCallCollection = self._apiCallsCollections[T.resourceName()] as? StoredCollection<ApiCall<T>> {
return apiCallCollection return apiCallCollection
@ -142,6 +163,7 @@ public class Store {
throw StoreError.apiCallCollectionNotRegistered(type: T.resourceName()) throw StoreError.apiCallCollectionNotRegistered(type: T.resourceName())
} }
/// Registers an api call into its collection
func registerApiCall<T : Storable>(_ apiCall: ApiCall<T>) throws { func registerApiCall<T : Storable>(_ apiCall: ApiCall<T>) throws {
let collection: StoredCollection<ApiCall<T>> = try self.apiCallCollection() let collection: StoredCollection<ApiCall<T>> = try self.apiCallCollection()
@ -161,6 +183,7 @@ public class Store {
} }
/// Deletes an ApiCall by [id] and [collectionName]
func deleteApiCallById(_ id: String, collectionName: String) throws { func deleteApiCallById(_ id: String, collectionName: String) throws {
if let collection = self._apiCallsCollections[collectionName] { if let collection = self._apiCallsCollections[collectionName] {
@ -170,6 +193,7 @@ public class Store {
throw StoreError.collectionNotRegistered(type: collectionName) throw StoreError.collectionNotRegistered(type: collectionName)
} }
/// Schedule an ApiCall for its execution in the future
func startCallsRescheduling<T : Storable>(apiCall: ApiCall<T>) { func startCallsRescheduling<T : Storable>(apiCall: ApiCall<T>) {
let delay = pow(2, 0 + apiCall.attemptsCount) let delay = pow(2, 0 + apiCall.attemptsCount)
let seconds = NSDecimalNumber(decimal: delay).intValue let seconds = NSDecimalNumber(decimal: delay).intValue
@ -189,6 +213,7 @@ public class Store {
} }
/// Reschedule an ApiCall by id
func startCallsRescheduling<T : Storable>(apiCallId: String, type: T.Type) throws { func startCallsRescheduling<T : Storable>(apiCallId: String, type: T.Type) throws {
let apiCallCollection: StoredCollection<ApiCall<T>> = try self.apiCallCollection() let apiCallCollection: StoredCollection<ApiCall<T>> = try self.apiCallCollection()
if let apiCall = apiCallCollection.findById(apiCallId) { if let apiCall = apiCallCollection.findById(apiCallId) {
@ -196,6 +221,7 @@ public class Store {
} }
} }
/// Executes an ApiCall
fileprivate func _executeApiCall<T : Storable>(_ apiCall: ApiCall<T>) async throws -> T { fileprivate func _executeApiCall<T : Storable>(_ apiCall: ApiCall<T>) async throws -> T {
guard let service else { guard let service else {
throw StoreError.missingService throw StoreError.missingService
@ -203,10 +229,12 @@ public class Store {
return try await service.runApiCall(apiCall) return try await service.runApiCall(apiCall)
} }
/// Executes an ApiCall
func execute<T>(apiCall: ApiCall<T>) async throws { func execute<T>(apiCall: ApiCall<T>) async throws {
_ = try await self._executeApiCall(apiCall) _ = try await self._executeApiCall(apiCall)
} }
/// Retrieves all the items on the server
func getItems<T : Storable>() async throws -> [T] { func getItems<T : Storable>() async throws -> [T] {
guard let service else { guard let service else {
throw StoreError.missingService throw StoreError.missingService

@ -35,6 +35,9 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
/// Notifies the closure when the loading is done /// Notifies the closure when the loading is done
fileprivate var loadCompletion: ((StoredCollection<T>) -> ())? = nil fileprivate var loadCompletion: ((StoredCollection<T>) -> ())? = 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 /// Indicates whether the collection has changed, thus requiring a write operation
fileprivate var _hasChanged: Bool = false { fileprivate var _hasChanged: Bool = false {
didSet { didSet {
@ -48,11 +51,15 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
} }
} }
/// Denotes a collection that loads and writes asynchronousIO
fileprivate var asynchronousIO: Bool = true fileprivate var asynchronousIO: Bool = true
init(synchronized: Bool, store: Store, asynchronousIO: Bool = true, loadCompletion: ((StoredCollection<T>) -> ())? = nil) { init(synchronized: Bool, store: Store, indexed: Bool = false, asynchronousIO: Bool = true, loadCompletion: ((StoredCollection<T>) -> ())? = nil) {
self.synchronized = synchronized self.synchronized = synchronized
self.asynchronousIO = asynchronousIO self.asynchronousIO = asynchronousIO
if indexed {
self._index = [:]
}
self._store = store self._store = store
self.loadCompletion = loadCompletion self.loadCompletion = loadCompletion
self._load() self._load()
@ -85,12 +92,14 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
} }
/// Decodes the json file into the items array
fileprivate func _decodeJSONFile() throws { fileprivate func _decodeJSONFile() throws {
let jsonString = try FileUtils.readDocumentFile(fileName: T.fileName()) let jsonString = try FileUtils.readDocumentFile(fileName: T.fileName())
if let decoded: [T] = try jsonString.decodeArray() { if let decoded: [T] = try jsonString.decodeArray() {
DispatchQueue.main.sync { DispatchQueue.main.async {
Logger.log("loaded \(T.fileName()) with \(decoded.count) items") Logger.log("loaded \(T.fileName()) with \(decoded.count) items")
self.items = decoded self.items = decoded
self._updateIndexIfNecessary()
self.loadCompletion?(self) self.loadCompletion?(self)
NotificationCenter.default.post(name: NSNotification.Name.CollectionDidLoad, object: self) NotificationCenter.default.post(name: NSNotification.Name.CollectionDidLoad, object: self)
@ -98,6 +107,14 @@ public class StoredCollection<T : Storable> : 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 { public func loadDataFromServer() throws {
guard self.synchronized else { guard self.synchronized else {
throw StoreError.unSynchronizedCollection throw StoreError.unSynchronizedCollection
@ -120,11 +137,13 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
self._hasChanged = true self._hasChanged = true
} }
// update
if let index = self.items.firstIndex(where: { $0.id == instance.id }) { if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
self.items[index] = instance self.items[index] = instance
self._sendUpdateIfNecessary(instance) self._sendUpdateIfNecessary(instance)
} else { } else { // insert
self.items.append(instance) self.items.append(instance)
self._index?[instance.stringId] = instance
self._sendInsertionIfNecessary(instance) self._sendInsertionIfNecessary(instance)
} }
@ -138,10 +157,12 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
try instance.deleteDependencies() try instance.deleteDependencies()
self.items.removeAll { $0.id == instance.id } self.items.removeAll { $0.id == instance.id }
self._index?.removeValue(forKey: instance.stringId)
self._sendDeletionIfNecessary(instance) self._sendDeletionIfNecessary(instance)
} }
public func batchInsert(_ sequence: any Sequence<T>) { /// Inserts the whole sequence into the items array, no updates
public func append(contentOfs sequence: any Sequence<T>) {
defer { defer {
self._hasChanged = true self._hasChanged = true
} }
@ -153,6 +174,9 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
/// Returns the instance corresponding to the provided [id] /// Returns the instance corresponding to the provided [id]
public func findById(_ id: String) -> T? { 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 }) return self.items.first(where: { $0.id == id })
} }

@ -19,4 +19,10 @@ extension Array {
} }
} }
func dictionary<T : Hashable>(handler: (Element) -> (T)) -> [T : Element] {
return self.reduce(into: [T : Element]()) { r, e in
r[handler(e)] = e
}
}
} }

Loading…
Cancel
Save