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

311 lines
11 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 collectionNotRegistered(type: String)
case cannotSyncCollection(name: String)
case apiCallCollectionNotRegistered(type: String)
public var errorDescription: String? {
switch self {
case .missingService:
return "Services instance is nil"
case .missingUserId:
return "The user id is missing"
case .collectionNotRegistered(let type):
return "The collection \(type) is not registered"
case .cannotSyncCollection(let name):
return "Tries to load the collection \(name) from the server while it's not authorized"
case .apiCallCollectionNotRegistered(let type):
return "The api call collection has not been registered for \(type)"
}
}
}
final public class Store {
/// The Store singleton
public static let main = Store()
/// The dictionary of registered StoredCollections
fileprivate var _collections: [String : any SomeCollection] = [:]
/// The name of the directory to store the json files
static let storageDirectory = "storage"
/// 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() {
self._createDirectory(directory: Store.storageDirectory)
}
public required init(identifier: String) {
self.identifier = identifier
let directory = "\(Store.storageDirectory)/\(identifier)"
self._createDirectory(directory: directory)
}
/// 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() {
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) -> StoredCollection<T> {
if let collection: StoredCollection<T> = try? self.collection() {
return collection
}
let collection = StoredCollection<T>(store: self, indexed: indexed, inMemory: inMemory, limit: limit)
self._collections[T.resourceName()] = collection
StoreCenter.main.loadApiCallCollection(type: T.self)
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, sendsUpdate: Bool = true) -> StoredSingleton<T> {
let storedObject = StoredSingleton<T>(store: self, inMemory: inMemory)
self._collections[T.resourceName()] = storedObject
if synchronized {
StoreCenter.main.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? StoredCollection<T> else {
Logger.w("Collection \(T.resourceName()) not registered")
return nil
}
return collection.findById(id)
}
/// Filters a collection by predicate
/// - Parameters:
/// - isIncluded: a predicate to returns if a data should be filtered in
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())
}
func registerOrGetSyncedCollection<T: SyncedStorable>(_ type: T.Type) -> StoredCollection<T> {
do {
return try self.collection()
} catch {
return self.registerSynchronizedCollection(indexed: true, inMemory: false)
}
}
/// Loads all collection with the data from the server
public func loadCollectionsFromServer() {
for collection in self._syncedCollections() {
Task {
try? await collection.loadDataFromServerIfAllowed()
}
}
}
/// 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()
}
}
/// Returns the names of all collections
public func collectionNames() -> [(String, any Storable.Type)] {
return self._collections.values.map { ($0.resourceName, $0.type) }
}
// MARK: - Synchronization
/// Calls addOrUpdateIfNewer from the collection corresponding to the instance
func addOrUpdateIfNewer<T: SyncedStorable>(_ instance: T, shared: Bool) {
let collection: StoredCollection<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: StoredCollection<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: StoredCollection<T> = try self.collection()
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
}
// MARK: - Write
/// Returns the directory URL of the store
fileprivate func _directoryPath() throws -> URL {
var url = try FileUtils.pathForDirectoryInDocuments(directory: Store.storageDirectory)
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 {
let fileURL = try self._directoryPath()
return fileURL.appending(component: T.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 StoreCenter.main.getItems(identifier: identifier)
} else {
return try await StoreCenter.main.getItems()
}
}
/// Requests an insertion to the StoreCenter
/// - Parameters:
/// - instance: an object to insert
// func sendInsertion<T: SyncedStorable>(_ instance: T) async throws -> T? {
// return try await StoreCenter.main.sendInsertion(instance)
// }
//
// /// Requests an update to the StoreCenter
// /// - Parameters:
// /// - instance: an object to update
// @discardableResult func sendUpdate<T: SyncedStorable>(_ instance: T) async throws -> T? {
// return try await StoreCenter.main.sendUpdate(instance)
// }
//
// /// Requests a deletion to the StoreCenter
// /// - Parameters:
// /// - instance: an object to delete
// func sendDeletion<T: SyncedStorable>(_ instance: T) async throws {
// return try await StoreCenter.main.sendDeletion(instance)
// }
/// 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 }
}
}