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.
387 lines
14 KiB
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 }
|
|
}
|
|
|
|
}
|
|
|