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

264 lines
8.1 KiB

//
// Store.swift
// LeStorage
//
// Created by Laurent Morvillier on 02/02/2024.
//
import Foundation
import UIKit
public enum ResetOption {
case all
case synchronizedOnly
}
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()
/// 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?
public func service() throws -> Services {
if let service = self._services {
return service
} else {
throw StoreError.missingService
}
}
/// The dictionary of registered StoredCollections
fileprivate var _collections: [String : any SomeCollection] = [:]
/// A store for the Settings object
fileprivate var _settingsStorage: MicroStorage<Settings> = MicroStorage(fileName: "settings.json")
/// The name of the directory to store the json files
static let storageDirectory = "storage"
/// Indicates to Stored Collection if they can synchronize
public var collectionsCanSynchronize: Bool = true {
didSet {
Logger.log(">>> collectionsCanSynchronize = \(self.collectionsCanSynchronize)")
}
}
public init() {
FileManager.default.createDirectoryInDocuments(directoryName: Store.storageDirectory)
}
/// 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, inMemory: Bool = false, sendsUpdate: Bool = true) -> StoredCollection<T> {
// register collection
let collection = StoredCollection<T>(synchronized: synchronized, store: Store.main, indexed: indexed, inMemory: inMemory, sendsUpdate: sendsUpdate, loadCompletion: nil)
self._collections[T.resourceName()] = collection
return collection
}
/// Registers a StoredSingleton instance
public func registerObject<T : Storable>(synchronized: Bool, inMemory: Bool = false, sendsUpdate: Bool = true) -> StoredSingleton<T> {
// register collection
let storedObject = StoredSingleton<T>(synchronized: synchronized, store: Store.main, inMemory: inMemory, sendsUpdate: sendsUpdate, loadCompletion: nil)
self._collections[T.resourceName()] = storedObject
return storedObject
}
// MARK: - Settings
/// Stores the user UUID
func setUserUUID(uuidString: String) {
self._settingsStorage.update { settings in
settings.userId = uuidString
}
}
/// Returns the user's UUID
public var currentUserUUID: UUID? {
if let uuidString = self._settingsStorage.item.userId,
let uuid = UUID(uuidString: uuidString) {
return uuid
}
return nil
}
/// Returns a UUID, using the user's if possible
public func mandatoryUserUUID() -> UUID {
if let uuid = self.currentUserUUID {
return uuid
} else {
let uuid = UIDevice.current.identifierForVendor ?? UUID()
self._settingsStorage.update { settings in
settings.userId = uuid.uuidString
}
return uuid
}
}
/// Returns the username
func userName() -> String? {
return self._settingsStorage.item.username
}
/// Sets the username
func setUserName(_ username: String) {
self._settingsStorage.update { settings in
settings.username = username
}
}
/// Disconnect the user from the storage and resets collection
public func disconnect(resetOption: ResetOption? = nil) {
try? self.service().disconnect()
self._settingsStorage.update { settings in
settings.username = nil
settings.userId = nil
}
switch resetOption {
case .all:
for collection in self._collections.values {
collection.reset()
}
case .synchronizedOnly:
for collection in self._collections.values {
if collection.synchronized {
collection.reset()
}
}
default:
break
}
}
/// Returns whether the system has a user token
public func hasToken() -> Bool {
do {
_ = try self.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: - 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 {
guard self.collectionsCanSynchronize else {
return
}
let collection: StoredCollection<T> = try self.collection()
collection.rescheduleApiCallsIfNecessary()
}
/// Executes an ApiCall
fileprivate func _executeApiCall<T: Storable>(_ apiCall: ApiCall<T>) async throws -> T {
return try await self.service().runApiCall(apiCall)
}
/// Executes an API call
func execute<T>(apiCall: ApiCall<T>) async throws -> T {
return try await self._executeApiCall(apiCall)
}
// MARK: -
/// Retrieves all the items on the server
func getItems<T: Storable>() async throws -> [T] {
return try await self.service().get()
}
/// Resets all registered collection
public func reset() {
for collection in self._collections.values {
collection.reset()
}
}
/// Loads all collection with the data from the server
public func loadCollectionFromServer() {
for collection in self._collections.values {
Task {
try? await collection.loadDataFromServerIfAllowed()
}
}
}
/// Returns whether any collection has pending API calls
public func hasPendingAPICalls() -> Bool {
return self._collections.values.contains(where: { $0.hasPendingAPICalls() })
}
}