|
|
|
@ -25,7 +25,7 @@ public protocol SomeCollection<Item>: Identifiable { |
|
|
|
func deleteDependencies(actionOption: ActionOption, _ isIncluded: (Item) -> Bool) |
|
|
|
func deleteDependencies(actionOption: ActionOption, _ isIncluded: (Item) -> Bool) |
|
|
|
|
|
|
|
|
|
|
|
func findById(_ id: Item.ID) -> Item? |
|
|
|
func findById(_ id: Item.ID) -> Item? |
|
|
|
func requestWrite() |
|
|
|
func requestWriteIfNecessary() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
protocol CollectionDelegate<Item> { |
|
|
|
protocol CollectionDelegate<Item> { |
|
|
|
@ -81,15 +81,16 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
/// Indicates whether the collection has changed, thus requiring a write operation |
|
|
|
/// Indicates whether the collection has changed, thus requiring a write operation |
|
|
|
fileprivate var _triggerWrite: Bool = false { |
|
|
|
fileprivate var _triggerWrite: Bool = false { |
|
|
|
didSet { |
|
|
|
didSet { |
|
|
|
if self._triggerWrite == true && self.inMemory == false { |
|
|
|
if self._triggerWrite == true { |
|
|
|
|
|
|
|
|
|
|
|
self._scheduleWrite() |
|
|
|
self._scheduleWrite() |
|
|
|
DispatchQueue.main.async { |
|
|
|
|
|
|
|
NotificationCenter.default.post( |
|
|
|
|
|
|
|
name: NSNotification.Name.CollectionDidChange, object: self) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
self._triggerWrite = false |
|
|
|
self._triggerWrite = false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
|
|
|
NotificationCenter.default.post( |
|
|
|
|
|
|
|
name: NSNotification.Name.CollectionDidChange, object: self) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -99,8 +100,6 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
/// Sets a max number of items inside the collection |
|
|
|
/// Sets a max number of items inside the collection |
|
|
|
fileprivate(set) var limit: Int? = nil |
|
|
|
fileprivate(set) var limit: Int? = nil |
|
|
|
|
|
|
|
|
|
|
|
fileprivate var _delegate: (any CollectionDelegate<T>)? = nil |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
init(store: Store, inMemory: Bool = false) async { |
|
|
|
init(store: Store, inMemory: Bool = false) async { |
|
|
|
self.store = store |
|
|
|
self.store = store |
|
|
|
if self.inMemory == false { |
|
|
|
if self.inMemory == false { |
|
|
|
@ -108,24 +107,19 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
init(store: Store, indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, synchronousLoading: Bool = false, noLoad: Bool = false, delegate: (any CollectionDelegate<T>)? = nil) { |
|
|
|
init(store: Store, indexed: Bool = false, inMemory: Bool = false, limit: Int? = nil, noLoad: Bool = false) { |
|
|
|
if indexed { |
|
|
|
if indexed { |
|
|
|
self._indexes = [:] |
|
|
|
self._indexes = [:] |
|
|
|
} |
|
|
|
} |
|
|
|
self.inMemory = inMemory |
|
|
|
self.inMemory = inMemory |
|
|
|
self.store = store |
|
|
|
self.store = store |
|
|
|
self.limit = limit |
|
|
|
self.limit = limit |
|
|
|
self._delegate = delegate |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if noLoad { |
|
|
|
if noLoad { |
|
|
|
self.hasLoaded = true |
|
|
|
self.hasLoaded = true |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
Task { |
|
|
|
Task { |
|
|
|
if synchronousLoading { |
|
|
|
await self.load() |
|
|
|
await self.loadFromFile() |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
await self.load() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -149,8 +143,10 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
// MARK: - Loading |
|
|
|
// MARK: - Loading |
|
|
|
|
|
|
|
|
|
|
|
/// Sets the collection as changed to trigger a write |
|
|
|
/// Sets the collection as changed to trigger a write |
|
|
|
public func requestWrite() { |
|
|
|
public func requestWriteIfNecessary() { |
|
|
|
self._triggerWrite = true |
|
|
|
if self.inMemory == false { |
|
|
|
|
|
|
|
self._triggerWrite = true |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// Migrates if necessary and asynchronously decodes the json file |
|
|
|
/// Migrates if necessary and asynchronously decodes the json file |
|
|
|
@ -158,19 +154,31 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
if !self.inMemory { |
|
|
|
if !self.inMemory { |
|
|
|
await self.loadFromFile() |
|
|
|
await self.loadFromFile() |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
await self._delegate?.loadingForMemoryCollection() |
|
|
|
|
|
|
|
await MainActor.run { |
|
|
|
await MainActor.run { |
|
|
|
self.setAsLoaded() |
|
|
|
self.setAsLoaded() |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// Starts the JSON file decoding synchronously or asynchronously |
|
|
|
/// Starts the JSON file decoding asynchronously |
|
|
|
func loadFromFile() async { |
|
|
|
func loadFromFile() async { |
|
|
|
do { |
|
|
|
do { |
|
|
|
try await self._decodeJSONFile() |
|
|
|
try await self._decodeJSONFile() |
|
|
|
} catch { |
|
|
|
} catch { |
|
|
|
Logger.error(error) |
|
|
|
Logger.error(error) |
|
|
|
|
|
|
|
await MainActor.run { |
|
|
|
|
|
|
|
self.setAsLoaded() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
do { |
|
|
|
|
|
|
|
let fileURL = try self.store.fileURL(type: T.self) |
|
|
|
|
|
|
|
let jsonString: String = try FileUtils.readFile(fileURL: fileURL) |
|
|
|
|
|
|
|
if !jsonString.isEmpty { |
|
|
|
|
|
|
|
StoreCenter.main.log(message: "Could not decode: \(jsonString)") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -204,11 +212,10 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
|
|
|
|
|
|
|
|
/// Sets a collection of items and indexes them |
|
|
|
/// Sets a collection of items and indexes them |
|
|
|
func setItems(_ items: [T]) { |
|
|
|
func setItems(_ items: [T]) { |
|
|
|
self.items.removeAll() |
|
|
|
self.clear() |
|
|
|
for item in items { |
|
|
|
for item in items { |
|
|
|
self._addItem(instance: item) |
|
|
|
self._addItem(instance: item) |
|
|
|
} |
|
|
|
} |
|
|
|
self.requestWrite() |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@MainActor |
|
|
|
@MainActor |
|
|
|
@ -220,7 +227,7 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
self.setAsLoaded() |
|
|
|
self.setAsLoaded() |
|
|
|
self.addOrUpdate(contentOfs: items) |
|
|
|
self.addOrUpdate(contentOfs: items) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
self.requestWriteIfNecessary() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// Updates the whole index with the items array |
|
|
|
/// Updates the whole index with the items array |
|
|
|
@ -236,7 +243,7 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
/// Adds it if its id is not found, and otherwise updates it |
|
|
|
/// Adds it if its id is not found, and otherwise updates it |
|
|
|
@discardableResult public func addOrUpdate(instance: T) -> ActionResult<T> { |
|
|
|
@discardableResult public func addOrUpdate(instance: T) -> ActionResult<T> { |
|
|
|
defer { |
|
|
|
defer { |
|
|
|
self.requestWrite() |
|
|
|
self.requestWriteIfNecessary() |
|
|
|
} |
|
|
|
} |
|
|
|
return self._rawAddOrUpdate(instance: instance) |
|
|
|
return self._rawAddOrUpdate(instance: instance) |
|
|
|
} |
|
|
|
} |
|
|
|
@ -245,7 +252,7 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
public func addOrUpdate(contentOfs sequence: any Sequence<T>, _ handler: ((ActionResult<T>) -> ())? = nil) { |
|
|
|
public func addOrUpdate(contentOfs sequence: any Sequence<T>, _ handler: ((ActionResult<T>) -> ())? = nil) { |
|
|
|
|
|
|
|
|
|
|
|
defer { |
|
|
|
defer { |
|
|
|
self.requestWrite() |
|
|
|
self.requestWriteIfNecessary() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for instance in sequence { |
|
|
|
for instance in sequence { |
|
|
|
@ -268,9 +275,9 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
/// A method the treat the collection as a single instance holder |
|
|
|
/// A method the treat the collection as a single instance holder |
|
|
|
func setSingletonNoSync(instance: T) { |
|
|
|
func setSingletonNoSync(instance: T) { |
|
|
|
defer { |
|
|
|
defer { |
|
|
|
self.requestWrite() |
|
|
|
self.requestWriteIfNecessary() |
|
|
|
} |
|
|
|
} |
|
|
|
self.items.removeAll() |
|
|
|
self.clear() |
|
|
|
self._addItem(instance: instance) |
|
|
|
self._addItem(instance: instance) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -326,7 +333,8 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
self.addPendingOperation(method: .add, instance: instance, actionOption: actionOption) |
|
|
|
self.addPendingOperation(method: .add, instance: instance, actionOption: actionOption) |
|
|
|
return false |
|
|
|
return false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
self.invalidateCache() |
|
|
|
|
|
|
|
|
|
|
|
self._affectStoreIdIfNecessary(instance: instance) |
|
|
|
self._affectStoreIdIfNecessary(instance: instance) |
|
|
|
self.items.append(instance) |
|
|
|
self.items.append(instance) |
|
|
|
instance.store = self.store |
|
|
|
instance.store = self.store |
|
|
|
@ -352,6 +360,7 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
self.addPendingOperation(method: .update, instance: instance, actionOption: actionOption) |
|
|
|
self.addPendingOperation(method: .update, instance: instance, actionOption: actionOption) |
|
|
|
return false |
|
|
|
return false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
self.invalidateCache() |
|
|
|
|
|
|
|
|
|
|
|
let item = self.items[index] |
|
|
|
let item = self.items[index] |
|
|
|
if item !== instance { |
|
|
|
if item !== instance { |
|
|
|
@ -401,6 +410,7 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func localDeleteOnly(instance: T) { |
|
|
|
func localDeleteOnly(instance: T) { |
|
|
|
|
|
|
|
self.invalidateCache() |
|
|
|
self.items.removeAll { $0.id == instance.id } |
|
|
|
self.items.removeAll { $0.id == instance.id } |
|
|
|
self._indexes?.removeValue(forKey: instance.id) |
|
|
|
self._indexes?.removeValue(forKey: instance.id) |
|
|
|
} |
|
|
|
} |
|
|
|
@ -418,7 +428,7 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
self.deleteItem(instance, actionOption: actionOption) |
|
|
|
self.deleteItem(instance, actionOption: actionOption) |
|
|
|
} |
|
|
|
} |
|
|
|
if actionOption.write { |
|
|
|
if actionOption.write { |
|
|
|
self.requestWrite() |
|
|
|
self.requestWriteIfNecessary() |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -430,12 +440,12 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
return self.items.first(where: { $0.id == id }) |
|
|
|
return self.items.first(where: { $0.id == id }) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// Proceeds to "hard" delete the items without synchronizing them |
|
|
|
/// Deletes a list of items |
|
|
|
/// Also removes related API calls |
|
|
|
|
|
|
|
public func deleteDependencies(_ items: any Sequence<T>) { |
|
|
|
public func deleteDependencies(_ items: any Sequence<T>) { |
|
|
|
defer { |
|
|
|
defer { |
|
|
|
self.requestWrite() |
|
|
|
self.requestWriteIfNecessary() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
self.invalidateCache() |
|
|
|
let itemsArray = Array(items) // fix error if items is self.items |
|
|
|
let itemsArray = Array(items) // fix error if items is self.items |
|
|
|
for item in itemsArray { |
|
|
|
for item in itemsArray { |
|
|
|
if let index = self.items.firstIndex(where: { $0.id == item.id }) { |
|
|
|
if let index = self.items.firstIndex(where: { $0.id == item.id }) { |
|
|
|
@ -489,7 +499,6 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
self.deleteUnusedShared(data, actionOption: item.actionOption) |
|
|
|
self.deleteUnusedShared(data, actionOption: item.actionOption) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
self._delegate?.itemMerged(item) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
manager.reset() |
|
|
|
manager.reset() |
|
|
|
|
|
|
|
|
|
|
|
@ -528,12 +537,13 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
|
|
|
|
|
|
|
|
/// Simply clears the items of the collection |
|
|
|
/// Simply clears the items of the collection |
|
|
|
public func clear() { |
|
|
|
public func clear() { |
|
|
|
|
|
|
|
self.invalidateCache() |
|
|
|
self.items.removeAll() |
|
|
|
self.items.removeAll() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// Removes the items of the collection and deletes the corresponding file |
|
|
|
/// Removes the items of the collection and deletes the corresponding file |
|
|
|
public func reset() { |
|
|
|
public func reset() { |
|
|
|
self.items.removeAll() |
|
|
|
self.clear() |
|
|
|
self.store.removeFile(type: T.self) |
|
|
|
self.store.removeFile(type: T.self) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -562,10 +572,35 @@ public class StoredCollection<T: Storable>: SomeCollection { |
|
|
|
func updateLocalInstance(_ serverInstance: T) { |
|
|
|
func updateLocalInstance(_ serverInstance: T) { |
|
|
|
if let localInstance = self.findById(serverInstance.id) { |
|
|
|
if let localInstance = self.findById(serverInstance.id) { |
|
|
|
localInstance.copy(from: serverInstance) |
|
|
|
localInstance.copy(from: serverInstance) |
|
|
|
self.requestWrite() |
|
|
|
self.requestWriteIfNecessary() |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Cached queries |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fileprivate var _cacheVersion = 0 |
|
|
|
|
|
|
|
fileprivate var _queryCache: [AnyHashable: (version: Int, result: Any)] = [:] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Generic query method with caching |
|
|
|
|
|
|
|
public func cached<Result>( |
|
|
|
|
|
|
|
key: AnyHashable, |
|
|
|
|
|
|
|
compute: ([T]) -> Result |
|
|
|
|
|
|
|
) -> Result { |
|
|
|
|
|
|
|
if let cached = self._queryCache[key], |
|
|
|
|
|
|
|
cached.version == self._cacheVersion, |
|
|
|
|
|
|
|
let result = cached.result as? Result { |
|
|
|
|
|
|
|
return result |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let result = compute(items) |
|
|
|
|
|
|
|
self._queryCache[key] = (self._cacheVersion, result) |
|
|
|
|
|
|
|
return result |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private func invalidateCache() { |
|
|
|
|
|
|
|
self._cacheVersion += 1 |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
extension StoredCollection: RandomAccessCollection { |
|
|
|
extension StoredCollection: RandomAccessCollection { |
|
|
|
|