|
|
|
|
@ -1,5 +1,5 @@ |
|
|
|
|
// |
|
|
|
|
// StoredCollection.swift |
|
|
|
|
// BaseCollection.swift |
|
|
|
|
// LeStorage |
|
|
|
|
// |
|
|
|
|
// Created by Laurent Morvillier on 02/02/2024. |
|
|
|
|
@ -7,22 +7,23 @@ |
|
|
|
|
|
|
|
|
|
import Foundation |
|
|
|
|
|
|
|
|
|
protocol CollectionHolder { |
|
|
|
|
associatedtype Item |
|
|
|
|
public protocol CollectionHolder { |
|
|
|
|
associatedtype Item: Storable |
|
|
|
|
|
|
|
|
|
var items: [Item] { get } |
|
|
|
|
func reset() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
protocol SomeCollection: CollectionHolder, Identifiable { |
|
|
|
|
public protocol SomeCollection: CollectionHolder, Identifiable { |
|
|
|
|
|
|
|
|
|
var resourceName: String { get } |
|
|
|
|
var hasLoaded: Bool { get } |
|
|
|
|
var inMemory: Bool { get } |
|
|
|
|
var type: any Storable.Type { get } |
|
|
|
|
|
|
|
|
|
func allItems() -> [any Storable] |
|
|
|
|
func referenceCount<S: Storable>(type: S.Type, id: String) -> Int |
|
|
|
|
|
|
|
|
|
func findById(_ id: Item.ID) -> Item? |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
protocol SomeSyncedCollection: SomeCollection { |
|
|
|
|
@ -30,11 +31,10 @@ protocol SomeSyncedCollection: SomeCollection { |
|
|
|
|
func loadCollectionsFromServerIfNoFile() async throws |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollection, CollectionHolder |
|
|
|
|
{ |
|
|
|
|
public class BaseCollection<T: Storable>: SomeCollection, CollectionHolder { |
|
|
|
|
|
|
|
|
|
/// Doesn't write the collection in a file |
|
|
|
|
fileprivate(set) var inMemory: Bool = false |
|
|
|
|
fileprivate(set) public var inMemory: Bool = false |
|
|
|
|
|
|
|
|
|
/// The list of stored items |
|
|
|
|
@Published public fileprivate(set) var items: [T] = [] |
|
|
|
|
@ -45,6 +45,9 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
/// Provides fast access for instances if the collection has been instanced with [indexed] = true |
|
|
|
|
fileprivate var _indexes: [T.ID: T]? = nil |
|
|
|
|
|
|
|
|
|
/// A PendingOperationManager instance that manages operations while the collection is not loaded |
|
|
|
|
fileprivate var _pendingOperationManager: PendingOperationManager<T>? = nil |
|
|
|
|
|
|
|
|
|
/// Indicates whether the collection has changed, thus requiring a write operation |
|
|
|
|
fileprivate var _hasChanged: Bool = false { |
|
|
|
|
didSet { |
|
|
|
|
@ -63,6 +66,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
/// Indicates if the collection has loaded locally, with or without a file |
|
|
|
|
fileprivate(set) public var hasLoaded: Bool = false |
|
|
|
|
|
|
|
|
|
/// Sets a max number of items inside the collection |
|
|
|
|
fileprivate(set) var limit: Int? = nil |
|
|
|
|
|
|
|
|
|
init(store: Store, indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil) { |
|
|
|
|
@ -73,21 +77,18 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
self.store = store |
|
|
|
|
self.limit = limit |
|
|
|
|
|
|
|
|
|
self.load() |
|
|
|
|
Task(priority: .high) { |
|
|
|
|
await self.load() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate init() { |
|
|
|
|
// self.synchronized = false |
|
|
|
|
init() { |
|
|
|
|
self.store = Store.main |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns a dummy StoredCollection instance |
|
|
|
|
public static func placeholder() -> StoredCollection<T> { |
|
|
|
|
return StoredCollection<T>() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns the name of the managed resource |
|
|
|
|
var resourceName: String { |
|
|
|
|
public var resourceName: String { |
|
|
|
|
return T.resourceName() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -103,25 +104,27 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Migrates if necessary and asynchronously decodes the json file |
|
|
|
|
func load() { |
|
|
|
|
|
|
|
|
|
do { |
|
|
|
|
if !self.inMemory { |
|
|
|
|
try self.loadFromFile() |
|
|
|
|
func load() async { |
|
|
|
|
if !self.inMemory { |
|
|
|
|
await self.loadFromFile() |
|
|
|
|
} else { |
|
|
|
|
await MainActor.run { |
|
|
|
|
self.setAsLoaded() |
|
|
|
|
} |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Starts the JSON file decoding synchronously or asynchronously |
|
|
|
|
func loadFromFile() throws { |
|
|
|
|
try self._decodeJSONFile() |
|
|
|
|
func loadFromFile() async { |
|
|
|
|
do { |
|
|
|
|
try await self._decodeJSONFile() |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Decodes the json file into the items array |
|
|
|
|
fileprivate func _decodeJSONFile() throws { |
|
|
|
|
fileprivate func _decodeJSONFile() async throws { |
|
|
|
|
|
|
|
|
|
let fileURL = try self.store.fileURL(type: T.self) |
|
|
|
|
|
|
|
|
|
@ -130,17 +133,37 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
let decoded: [T] = try jsonString.decodeArray() ?? [] |
|
|
|
|
self._setItems(decoded) |
|
|
|
|
} |
|
|
|
|
self.setAsLoaded() |
|
|
|
|
await MainActor.run { |
|
|
|
|
self.setAsLoaded() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Sets the collection as loaded |
|
|
|
|
/// Send a CollectionDidLoad event |
|
|
|
|
@MainActor |
|
|
|
|
func setAsLoaded() { |
|
|
|
|
self.hasLoaded = true |
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
NotificationCenter.default.post( |
|
|
|
|
name: NSNotification.Name.CollectionDidLoad, object: self) |
|
|
|
|
self._mergePendingOperations() |
|
|
|
|
|
|
|
|
|
NotificationCenter.default.post( |
|
|
|
|
name: NSNotification.Name.CollectionDidLoad, object: self) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _mergePendingOperations() { |
|
|
|
|
|
|
|
|
|
guard let manager = self._pendingOperationManager, manager.items.isNotEmpty else { return } |
|
|
|
|
|
|
|
|
|
Logger.log(">>> Merge pending: \(manager.items.count)") |
|
|
|
|
for operation in manager.items { |
|
|
|
|
switch operation.method { |
|
|
|
|
case .addOrUpdate: |
|
|
|
|
self.addOrUpdate(instance: operation.data) |
|
|
|
|
case .delete: |
|
|
|
|
self.delete(instance: operation.data) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
self._pendingOperationManager = nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Sets a collection of items and indexes them |
|
|
|
|
@ -201,7 +224,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Deletes an item by its id |
|
|
|
|
func deleteById(_ id: T.ID) { |
|
|
|
|
public func deleteById(_ id: T.ID) { |
|
|
|
|
if let instance = self.findById(id) { |
|
|
|
|
self.delete(instance: instance) |
|
|
|
|
} |
|
|
|
|
@ -261,16 +284,28 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Adds an instance to the collection |
|
|
|
|
func addItem(instance: T) { |
|
|
|
|
@discardableResult func addItem(instance: T) -> Bool { |
|
|
|
|
|
|
|
|
|
if !self.hasLoaded { |
|
|
|
|
self._addPendingOperation(method: .addOrUpdate, instance: instance) |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self._affectStoreIdIfNecessary(instance: instance) |
|
|
|
|
self.items.append(instance) |
|
|
|
|
instance.store = self.store |
|
|
|
|
self._indexes?[instance.id] = instance |
|
|
|
|
self._applyLimitIfPresent() |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Updates an instance to the collection by index |
|
|
|
|
func updateItem(_ instance: T, index: Int) { |
|
|
|
|
@discardableResult func updateItem(_ instance: T, index: Int) -> Bool { |
|
|
|
|
|
|
|
|
|
if !self.hasLoaded { |
|
|
|
|
self._addPendingOperation(method: .addOrUpdate, instance: instance) |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let item = self.items[index] |
|
|
|
|
if item !== instance { |
|
|
|
|
@ -279,15 +314,30 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
|
|
|
|
|
instance.store = self.store |
|
|
|
|
self._indexes?[instance.id] = instance |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Deletes an instance from the collection |
|
|
|
|
func deleteItem(_ instance: T) { |
|
|
|
|
@discardableResult func deleteItem(_ instance: T) -> Bool { |
|
|
|
|
|
|
|
|
|
if !self.hasLoaded { |
|
|
|
|
self._addPendingOperation(method: .addOrUpdate, instance: instance) |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
instance.deleteDependencies() |
|
|
|
|
self.items.removeAll { $0.id == instance.id } |
|
|
|
|
self._indexes?.removeValue(forKey: instance.id) |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _addPendingOperation(method: StorageMethod, instance: T) { |
|
|
|
|
if self._pendingOperationManager == nil { |
|
|
|
|
self._pendingOperationManager = PendingOperationManager<T>(store: self.store, inMemory: self.inMemory) |
|
|
|
|
} |
|
|
|
|
self._pendingOperationManager?.addPendingOperation(method: method, instance: instance) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// If the collection has more instance that its limit, remove the surplus |
|
|
|
|
fileprivate func _applyLimitIfPresent() { |
|
|
|
|
if let limit { |
|
|
|
|
@ -323,13 +373,6 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
self.delete(contentOfs: self.items) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - SomeCall |
|
|
|
|
|
|
|
|
|
/// Returns the collection items as [any Storable] |
|
|
|
|
func allItems() -> [any Storable] { |
|
|
|
|
return self.items |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - File access |
|
|
|
|
|
|
|
|
|
/// Schedules a write operation |
|
|
|
|
@ -355,7 +398,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Simply clears the items of the collection |
|
|
|
|
func clear() { |
|
|
|
|
public func clear() { |
|
|
|
|
self.items.removeAll() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -363,14 +406,16 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
public func reset() { |
|
|
|
|
self.items.removeAll() |
|
|
|
|
self.store.removeFile(type: T.self) |
|
|
|
|
setChanged() |
|
|
|
|
self.hasLoaded = false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var type: any Storable.Type { return T.self } |
|
|
|
|
public var type: any Storable.Type { return T.self } |
|
|
|
|
|
|
|
|
|
// MARK: - Reference count |
|
|
|
|
|
|
|
|
|
/// Counts the references to an object - given its type and id - inside the collection |
|
|
|
|
func referenceCount<S: Storable>(type: S.Type, id: String) -> Int { |
|
|
|
|
public func referenceCount<S: Storable>(type: S.Type, id: String) -> Int { |
|
|
|
|
let relationships = T.relationships().filter { $0.type == type } |
|
|
|
|
guard relationships.count > 0 else { return 0 } |
|
|
|
|
|
|
|
|
|
@ -382,6 +427,15 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public class StoredCollection<T: Storable>: BaseCollection<T>, RandomAccessCollection { |
|
|
|
|
|
|
|
|
|
/// Returns a dummy StoredCollection instance |
|
|
|
|
public static func placeholder() -> StoredCollection<T> { |
|
|
|
|
return StoredCollection<T>() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - RandomAccessCollection |
|
|
|
|
|
|
|
|
|
public var startIndex: Int { return self.items.startIndex } |
|
|
|
|
@ -403,3 +457,24 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
extension SyncedCollection: RandomAccessCollection { |
|
|
|
|
|
|
|
|
|
public var startIndex: Int { return self.items.startIndex } |
|
|
|
|
|
|
|
|
|
public var endIndex: Int { return self.items.endIndex } |
|
|
|
|
|
|
|
|
|
public func index(after i: Int) -> Int { |
|
|
|
|
return self.items.index(after: i) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public subscript(index: Int) -> T { |
|
|
|
|
get { |
|
|
|
|
return self.items[index] |
|
|
|
|
} |
|
|
|
|
set(newValue) { |
|
|
|
|
self.items[index] = newValue |
|
|
|
|
self._hasChanged = true |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |