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

387 lines
14 KiB

//
// Store.swift
// LeStorage
//
// Created by Laurent Morvillier on 02/02/2024.
//
import Foundation
import UIKit
public enum StoreError: Error, LocalizedError {
case missingService
case missingUserId
case missingUsername
case missingToken
case missingKeychainStore
case collectionNotRegistered(type: String)
case apiCallCollectionNotRegistered(type: String)
case synchronizationInactive
case storeNotRegistered(id: String)
public var localizedDescription: String {
switch self {
case .missingService:
return "L'instance des services est nulle"
case .missingUsername:
return "Le nom d'utilisateur est manquant"
case .missingUserId:
return "L'identifiant utilisateur est manquant"
case .missingToken:
return "Aucun token n'est stocké"
case .missingKeychainStore:
return "Aucun magasin de trousseau n'est disponible"
case .collectionNotRegistered(let type):
return "La collection \(type) n'est pas enregistrée"
case .apiCallCollectionNotRegistered(let type):
return "La collection d'appels API n'a pas été enregistrée pour \(type)"
case .synchronizationInactive:
return "La synchronisation n'est pas active sur ce StoreCenter"
case .storeNotRegistered(let id):
return "Le magasin avec l'identifiant \(id) n'est pas enregistré"
}
}
public var errorDescription: String? {
switch self {
case .missingService:
return "Services instance is nil"
case .missingUsername:
return "The username is missing"
case .missingUserId:
return "The user id is missing"
case .missingToken:
return "There is no stored token"
case .missingKeychainStore:
return "There is no keychain store"
case .collectionNotRegistered(let type):
return "The collection \(type) is not registered"
case .apiCallCollectionNotRegistered(let type):
return "The api call collection has not been registered for \(type)"
case .synchronizationInactive:
return "The synchronization is not active on this StoreCenter"
case .storeNotRegistered(let id):
return "The store with identifier \(id) is not registered"
}
}
}
final public class Store {
fileprivate(set) var storeCenter: StoreCenter
/// The dictionary of registered collections
fileprivate var _collections: [String : any SomeCollection] = [:]
/// The store identifier, used to name the store directory, and to perform filtering requests to the server
public fileprivate(set) var identifier: String? = nil
public init(storeCenter: StoreCenter) {
self.storeCenter = storeCenter
}
public required init(storeCenter: StoreCenter, identifier: String) {
self.storeCenter = storeCenter
self.identifier = identifier
let directory = "\(storeCenter.directoryName)/\(identifier)"
self._createDirectory(directory: directory)
}
public static var main: Store { return StoreCenter.main.mainStore }
public func alternateStore(identifier: String) throws -> Store {
return try self.storeCenter.store(identifier: identifier)
}
/// Creates the store directory
/// - Parameters:
/// - directory: the name of the directory
fileprivate func _createDirectory(directory: String) {
FileManager.default.createDirectoryInDocuments(directoryName: directory)
}
/// A method to provide ids corresponding to the django storage
public static func randomId() -> String {
return UUID().uuidString.lowercased()
}
/// Registers a collection
/// - 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 registerCollection<T : Storable>(indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil) -> StoredCollection<T> {
if let collection: StoredCollection<T> = try? self.collection() as? StoredCollection<T> {
return collection
}
let collection = StoredCollection<T>(store: self, indexed: indexed, inMemory: inMemory, limit: limit)
self._collections[T.resourceName()] = collection
return collection
}
/// Registers a synchronized collection
/// - 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<T : SyncedStorable>(indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false) -> SyncedCollection<T> {
if let collection: SyncedCollection<T> = try? self.syncedCollection() {
return collection
}
let collection = SyncedCollection<T>(store: self, indexed: indexed, inMemory: inMemory, limit: limit, synchronousLoading: synchronousLoading)
self._collections[T.resourceName()] = collection
self.storeCenter.loadApiCallCollection(type: T.self)
return collection
}
func asyncLoadingSynchronizedCollection<T : SyncedStorable>(inMemory: Bool = false) async -> SyncedCollection<T> {
let collection = await SyncedCollection<T>(store: self, inMemory: inMemory)
self._collections[T.resourceName()] = collection
self.storeCenter.loadApiCallCollection(type: T.self)
return collection
}
func asyncLoadingStoredCollection<T : Storable>(inMemory: Bool = false) async -> StoredCollection<T> {
let collection = await StoredCollection<T>(store: self, inMemory: inMemory)
self._collections[T.resourceName()] = collection
return collection
}
/// Registers a singleton object
/// - Parameters:
/// - synchronized: indicates if the data is synchronized with the server
/// - inMemory: Indicates if the collection should only live in memory, and not write into a file
/// - sendsUpdate: Indicates if updates of items should be sent to the server
public func registerObject<T : Storable>(synchronized: Bool, inMemory: Bool = false, shouldLoadDataFromServer: Bool = true) -> StoredSingleton<T> {
let storedObject = StoredSingleton<T>(store: self, inMemory: inMemory, shouldLoadDataFromServer: shouldLoadDataFromServer)
self._collections[T.resourceName()] = storedObject
self._collections[T.resourceName()] = storedObject
if synchronized {
self.storeCenter.loadApiCallCollection(type: T.self)
}
return storedObject
}
// MARK: - Convenience
/// Looks for an instance by id
/// - Parameters:
/// - id: the id of the data
public func findById<T: Storable>(_ id: T.ID) -> T? {
guard let collection = self._collections[T.resourceName()] as? BaseCollection<T> else {
Logger.w("Collection \(T.resourceName()) not registered")
return nil
}
return collection.findById(id)
}
/// Returns a collection by type
func syncedCollection<T: SyncedStorable>() throws -> SyncedCollection<T> {
if let collection = self._collections[T.resourceName()] as? SyncedCollection<T> {
return collection
}
throw StoreError.collectionNotRegistered(type: T.resourceName())
}
/// Returns a collection by type
func collection<T: Storable>() throws -> BaseCollection<T> {
if let collection = self._collections[T.resourceName()] as? BaseCollection<T> {
return collection
}
throw StoreError.collectionNotRegistered(type: T.resourceName())
}
func registerOrGetSyncedCollection<T: SyncedStorable>(_ type: T.Type) -> SyncedCollection<T> {
do {
return try self.syncedCollection()
} catch {
return self.registerSynchronizedCollection(indexed: true, inMemory: false)
}
}
/// Loads all collection with the data from the server
public func loadCollectionsFromServer(clear: Bool) {
for collection in self._syncedCollections() {
Task {
do {
try await collection.loadDataFromServerIfAllowed(clear: clear)
} catch {
Logger.error(error)
}
}
}
}
/// Loads all synchronized collection with server data if they don't already have a local file
public func loadCollectionsFromServerIfNoFile() {
for collection in self._syncedCollections() {
Task {
do {
try await collection.loadCollectionsFromServerIfNoFile()
} catch {
Logger.error(error)
}
}
}
}
/// Returns the list of synchronized collection inside the store
fileprivate func _syncedCollections() -> [any SomeSyncedCollection] {
return self._collections.values.compactMap { $0 as? any SomeSyncedCollection }
}
/// Resets all registered collection
public func reset() {
for collection in self._collections.values {
collection.reset()
}
}
// MARK: - Synchronization
/// Calls addOrUpdateIfNewer from the collection corresponding to the instance
func addOrUpdateIfNewer<T: SyncedStorable>(_ instance: T, shared: Bool) {
let collection: SyncedCollection<T> = self.registerOrGetSyncedCollection(T.self)
collection.addOrUpdateIfNewer(instance, shared: shared)
}
/// Calls deleteById from the collection corresponding to the instance
func deleteNoSync<T: Storable>(instance: T) {
do {
let collection: BaseCollection<T> = try self.collection()
collection.delete(instance: instance)
} catch {
Logger.error(error)
}
}
/// Calls deleteById from the collection corresponding to the instance
func deleteNoSync<T: SyncedStorable>(type: T.Type, id: String) throws {
let collection: SyncedCollection<T> = try self.syncedCollection()
collection.deleteByStringIdNoSync(id)
}
/// Calls deleteById from the collection corresponding to the instance
func referenceCount<T: SyncedStorable>(type: T.Type, id: String) -> Int {
var count: Int = 0
for collection in self._collections.values {
count += collection.referenceCount(type: type, id: id)
}
return count
}
public func deleteAllDependencies<T: Storable>(type: T.Type, shouldBeSynchronized: Bool) {
do {
let collection: BaseCollection<T> = try self.collection()
try self._deleteDependencies(Array(collection.items), shouldBeSynchronized: shouldBeSynchronized)
} catch {
Logger.error(error)
}
}
public func deleteDependencies<T: Storable>(type: T.Type, shouldBeSynchronized: Bool, _ handler: (T) throws -> Bool) {
do {
let collection: BaseCollection<T> = try self.collection()
let items = try collection.items.filter(handler)
try self._deleteDependencies(items, shouldBeSynchronized: shouldBeSynchronized)
} catch {
Logger.error(error)
}
}
fileprivate func _deleteDependencies<T: Storable>(_ items: [T], shouldBeSynchronized: Bool) throws {
do {
let collection: BaseCollection<T> = try self.collection()
for item in items {
item.deleteDependencies(store: self, shouldBeSynchronized: shouldBeSynchronized)
}
collection.deleteDependencies(items)
} catch {
Logger.error(error)
}
}
// MARK: - Write
/// Returns the directory URL of the store
fileprivate func _directoryPath() throws -> URL {
var url = try FileUtils.pathForDirectoryInDocuments(directory: storeCenter.directoryName)
if let identifier {
url.append(component: identifier)
}
return url
}
/// Writes some content into a file inside the Store directory
/// - Parameters:
/// - content: the content to write
/// - fileName: the name of the file
func write(content: String, fileName: String) throws {
var fileURL = try self._directoryPath()
fileURL.append(component: fileName)
try content.write(to: fileURL, atomically: false, encoding: .utf8)
}
/// Returns the URL matching a Storable type
/// - Parameters:
/// - type: a Storable type
func fileURL<T: Storable>(type: T.Type) throws -> URL {
return try self.fileURL(fileName: T.fileName())
}
/// Returns the URL matching a Storable type
/// - Parameters:
/// - type: a Storable type
func fileURL(fileName: String) throws -> URL {
let fileURL = try self._directoryPath()
return fileURL.appending(component: fileName)
}
/// Removes a file matching a Storable type
/// - Parameters:
/// - type: a Storable type
func removeFile<T: Storable>(type: T.Type) {
do {
let url: URL = try self.fileURL(type: type)
if FileManager.default.fileExists(atPath: url.path()) {
try FileManager.default.removeItem(at: url)
}
} catch {
Logger.error(error)
}
}
/// Retrieves all the items on the server
public func getItems<T: SyncedStorable>() async throws -> [T] {
if let identifier = self.identifier {
return try await self.storeCenter.getItems(identifier: identifier)
} else {
return try await self.storeCenter.getItems()
}
}
func loadCollectionItems<T: SyncedStorable>(_ items: [T], clear: Bool) async {
do {
let collection: SyncedCollection<T> = try self.syncedCollection()
await collection.loadItems(items, clear: clear)
} catch {
Logger.error(error)
}
}
/// Returns whether all collections have loaded locally
public func fileCollectionsAllLoaded() -> Bool {
let fileCollections = self._collections.values.filter { $0.inMemory == false }
return fileCollections.allSatisfy { $0.hasLoaded }
}
}