You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
LeStorage/LeStorage/Store.swift

247 lines
8.2 KiB

//
// Store.swift
// LeStorage
//
// Created by Laurent Morvillier on 02/02/2024.
//
import Foundation
import UIKit
enum StoreError: Error {
case missingService
case unexpectedCollectionType(name: String)
case apiCallCollectionNotRegistered(type: String)
case collectionNotRegistered(type: String)
case unSynchronizedCollection
}
public class Store {
/// The Store singleton
public static let main = Store()
// public fileprivate(set) var currentUser: User? = nil
/// 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 {
self._services = Services(url: url)
}
}
}
/// The services performing the API calls
fileprivate var _services: Services?
/// The service instance
public var service: Services? {
return self._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<MigrationHistory> = { StoredCollection(synchronized: false, store: Store.main, asynchronousIO: false)
}()
fileprivate var settingsStorage: MicroStorage<Settings> = MicroStorage()
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, indexed: Bool = false) -> StoredCollection<T> {
// register collection
let collection = StoredCollection<T>(synchronized: synchronized, store: Store.main, indexed: indexed, loadCompletion: nil)
self._collections[T.resourceName()] = collection
// if synchronized { // register additional collection for api calls
// let apiCallCollection = StoredCollection<ApiCall<T>>(synchronized: false, store: Store.main, loadCompletion: { apiCallCollection in
// self._rescheduleCalls(collection: apiCallCollection)
// })
// self._apiCallsCollections[T.resourceName()] = apiCallCollection
// }
return collection
}
// MARK: - Settings
func setUserUUID(uuidString: String) {
self.settingsStorage.update { settings in
settings.userUUIDString = uuidString
}
}
public func currentUserUUID() throws -> UUID {
if let uuidString = self.settingsStorage.item.userUUIDString, let uuid = UUID(uuidString: uuidString) {
return uuid
} else {
let uuid = UIDevice.current.identifierForVendor ?? UUID()
self.settingsStorage.update { settings in
settings.userUUIDString = uuid.uuidString
}
return uuid
}
}
func userName() -> String? {
return self.settingsStorage.item.username
}
func setUserName(_ username: String) {
self.settingsStorage.item.username = username
}
public func disconnect() {
try? self.service?.disconnect()
self.settingsStorage.item.userUUIDString = nil
}
public func hasToken() -> Bool {
guard let service else { return false }
do {
_ = try service.keychainStore.getToken()
return true
} catch {
return false
}
}
// MARK: - Convenience
/// Looks for an instance by id
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
}
return collection.findById(id)
}
/// Filters a collection with a [isIncluded] predicate
public func filter<T: Storable>(isIncluded: (T) throws -> (Bool)) rethrows -> [T] {
do {
return try self.collection().filter(isIncluded)
} catch {
return []
}
}
/// Returns a collection by type
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())
}
/// Deletes the dependencies of a collection
public func deleteDependencies<T: Storable>(items: any Sequence<T>) 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<T: Storable>(_ collection: StoredCollection<T>) async throws {
// Check for migrations
let migrations = self._migrations.filter { $0.resourceName == T.resourceName() }
if migrations.isEmpty { return }
// Check for applied migrations
var version: Int = -1
var performedMigration: MigrationHistory? = self._migrationCollection.first(where: { $0.resourceName == T.resourceName() })
if let performedMigration {
version = Int(performedMigration.version)
}
// Apply necessary migrations
let applicableMigrations = migrations.filter { $0.version > version }
.sorted(keyPath: \.version)
for migration in applicableMigrations {
Logger.log("Start migration for \(migration.resourceName), version: \(migration.version)")
try migration.migrate(synchronized: collection.synchronized)
if let performedMigration {
performedMigration.version = migration.version
} else {
performedMigration = MigrationHistory(version: migration.version, resourceName: migration.resourceName)
}
try self._migrationCollection.addOrUpdate(instance: performedMigration!)
}
}
// MARK: - Api call rescheduling
/// Deletes an ApiCall by [id] and [collectionName]
func deleteApiCallById(_ id: String, collectionName: String) throws {
if let collection = self._collections[collectionName] {
try collection.deleteApiCallById(id)
} else {
throw StoreError.collectionNotRegistered(type: collectionName)
}
}
/// Reschedule an ApiCall by id
func rescheduleApiCall<T: Storable>(id: String, type: T.Type) throws {
let collection: StoredCollection<T> = try self.collection()
collection.rescheduleApiCallsIfNecessary()
}
/// Executes an ApiCall
fileprivate func _executeApiCall<T: Storable>(_ apiCall: ApiCall<T>) async throws -> T {
guard let service else {
throw StoreError.missingService
}
return try await service.runApiCall(apiCall)
}
/// Executes an ApiCall
// func execute<T>(apiCall: ApiCall<T>) async throws {
// _ = try await self._executeApiCall(apiCall)
// }
func execute<T>(apiCall: ApiCall<T>) async throws -> T {
return try await self._executeApiCall(apiCall)
}
/// Retrieves all the items on the server
func getItems<T: Storable>() async throws -> [T] {
guard let service else {
throw StoreError.missingService
}
return try await service.get()
}
public func reset() {
for collection in self._collections.values {
collection.reset()
}
}
}