|
|
|
|
@ -20,6 +20,7 @@ protocol SomeCollection: Identifiable { |
|
|
|
|
func reset() |
|
|
|
|
func loadDataFromServerIfAllowed() throws |
|
|
|
|
var synchronized: Bool { get } |
|
|
|
|
func hasPendingAPICalls() -> Bool |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -49,7 +50,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
fileprivate var loadCompletion: ((StoredCollection<T>) -> ())? = nil |
|
|
|
|
|
|
|
|
|
/// Provides fast access for instances if the collection has been instanced with [indexed] = true |
|
|
|
|
fileprivate var _index: [String : T]? = nil |
|
|
|
|
fileprivate var _indexes: [String : T]? = nil |
|
|
|
|
|
|
|
|
|
fileprivate var apiCallsCollection: StoredCollection<ApiCall<T>>? = nil |
|
|
|
|
|
|
|
|
|
@ -58,9 +59,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
didSet { |
|
|
|
|
if self._hasChanged == true { |
|
|
|
|
|
|
|
|
|
if !self._inMemory { |
|
|
|
|
self._scheduleWrite() |
|
|
|
|
} |
|
|
|
|
self._scheduleWrite() |
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
NotificationCenter.default.post(name: NSNotification.Name.CollectionDidChange, object: self) |
|
|
|
|
} |
|
|
|
|
@ -76,7 +75,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
self.synchronized = synchronized |
|
|
|
|
self.asynchronousIO = asynchronousIO |
|
|
|
|
if indexed { |
|
|
|
|
self._index = [:] |
|
|
|
|
self._indexes = [:] |
|
|
|
|
} |
|
|
|
|
self._inMemory = inMemory |
|
|
|
|
self._sendsUpdate = sendsUpdate |
|
|
|
|
@ -129,40 +128,45 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
|
|
|
|
|
fileprivate func _loadFromFile() throws { |
|
|
|
|
let url: URL = try self._urlForJSONFile() |
|
|
|
|
if FileManager.default.fileExists(atPath: url.path()) { |
|
|
|
|
|
|
|
|
|
if self.asynchronousIO { |
|
|
|
|
Task(priority: .high) { |
|
|
|
|
// try await Store.main.performMigrationIfNecessary(self) |
|
|
|
|
try self._decodeJSONFile() |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
if self.asynchronousIO { |
|
|
|
|
Task(priority: .high) { |
|
|
|
|
try self._decodeJSONFile() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} else { |
|
|
|
|
try self._decodeJSONFile() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Decodes the json file into the items array |
|
|
|
|
fileprivate func _decodeJSONFile() throws { |
|
|
|
|
let fileURL = try self._urlForJSONFile() |
|
|
|
|
let jsonString: String = try FileUtils.readFile(fileURL: fileURL) |
|
|
|
|
if let decoded: [T] = try jsonString.decodeArray() { |
|
|
|
|
|
|
|
|
|
if FileManager.default.fileExists(atPath: fileURL.path()) { |
|
|
|
|
let jsonString: String = try FileUtils.readFile(fileURL: fileURL) |
|
|
|
|
let decoded: [T] = try jsonString.decodeArray() ?? [] |
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
Logger.log("loaded \(T.fileName()) with \(decoded.count) items") |
|
|
|
|
self.items = decoded |
|
|
|
|
self._updateIndexIfNecessary() |
|
|
|
|
self.loadCompletion?(self) |
|
|
|
|
|
|
|
|
|
NotificationCenter.default.post(name: NSNotification.Name.CollectionDidLoad, object: self) |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
Logger.log("collection \(T.fileName()) has no file yet") |
|
|
|
|
self.loadCompletion?(self) |
|
|
|
|
NotificationCenter.default.post(name: NSNotification.Name.CollectionDidLoad, object: self) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Updates the whole index with the items array |
|
|
|
|
fileprivate func _updateIndexIfNecessary() { |
|
|
|
|
if let _ = self._index { |
|
|
|
|
self._index = self.items.dictionary(handler: { $0.stringId }) |
|
|
|
|
if let _ = self._indexes { |
|
|
|
|
self._indexes = self.items.dictionary { $0.stringId } |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -198,10 +202,10 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
try self._sendUpdateIfNecessary(instance) |
|
|
|
|
} else { // insert |
|
|
|
|
self.items.append(instance) |
|
|
|
|
self._index?[instance.stringId] = instance |
|
|
|
|
try self._sendInsertionIfNecessary(instance) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self._indexes?[instance.stringId] = instance |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func setSingletonNoSync(instance: T) { |
|
|
|
|
@ -221,7 +225,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
|
|
|
|
|
try instance.deleteDependencies() |
|
|
|
|
self.items.removeAll { $0.id == instance.id } |
|
|
|
|
self._index?.removeValue(forKey: instance.stringId) |
|
|
|
|
self._indexes?.removeValue(forKey: instance.stringId) |
|
|
|
|
try self._sendDeletionIfNecessary(instance) |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
@ -236,7 +240,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
for instance in sequence { |
|
|
|
|
try instance.deleteDependencies() |
|
|
|
|
self.items.removeAll { $0.id == instance.id } |
|
|
|
|
self._index?.removeValue(forKey: instance.stringId) |
|
|
|
|
self._indexes?.removeValue(forKey: instance.stringId) |
|
|
|
|
try self._sendDeletionIfNecessary(instance) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -259,18 +263,18 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
} |
|
|
|
|
} else { // insert |
|
|
|
|
self.items.append(instance) |
|
|
|
|
self._index?[instance.stringId] = instance |
|
|
|
|
if shouldSync { |
|
|
|
|
try self._sendInsertionIfNecessary(instance) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
self._indexes?[instance.stringId] = instance |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns the instance corresponding to the provided [id] |
|
|
|
|
public func findById(_ id: String) -> T? { |
|
|
|
|
if let index = self._index, let instance = index[id] { |
|
|
|
|
if let index = self._indexes, let instance = index[id] { |
|
|
|
|
return instance |
|
|
|
|
} |
|
|
|
|
return self.items.first(where: { $0.id == id }) |
|
|
|
|
@ -304,6 +308,9 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
|
|
|
|
|
/// Schedules a write operation |
|
|
|
|
fileprivate func _scheduleWrite() { |
|
|
|
|
|
|
|
|
|
guard !self._inMemory else { return } |
|
|
|
|
|
|
|
|
|
if self.asynchronousIO { |
|
|
|
|
DispatchQueue(label: "lestorage.queue.write", qos: .utility).asyncAndWait { // sync to make sure we don't have writes performed at the same time |
|
|
|
|
self._write() |
|
|
|
|
@ -330,7 +337,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
self.items.removeAll() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func reset() { |
|
|
|
|
public func reset() { |
|
|
|
|
self.items.removeAll() |
|
|
|
|
|
|
|
|
|
do { |
|
|
|
|
@ -372,10 +379,15 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
fileprivate func _createCall(_ instance: T, method: HTTPMethod) throws -> ApiCall<T> { |
|
|
|
|
let baseURL = try _store.service().baseURL |
|
|
|
|
let jsonString = try instance.jsonString() |
|
|
|
|
var url = baseURL + T.resourceName() + "/" |
|
|
|
|
if method == .put || method == .delete { |
|
|
|
|
url += (instance.stringId + "/") |
|
|
|
|
|
|
|
|
|
let url: String |
|
|
|
|
switch method { |
|
|
|
|
case .get, .post: |
|
|
|
|
url = baseURL + T.path() |
|
|
|
|
case .put, .delete: |
|
|
|
|
url = baseURL + T.path(id: instance.stringId) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return ApiCall(url: url, method: method.rawValue, dataId: String(instance.id), body: jsonString) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -508,6 +520,11 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti |
|
|
|
|
try apiCallsCollection.deleteById(id) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func hasPendingAPICalls() -> Bool { |
|
|
|
|
guard let apiCallsCollection else { return false } |
|
|
|
|
return apiCallsCollection.isNotEmpty |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - RandomAccessCollection |
|
|
|
|
|
|
|
|
|
public var startIndex: Int { return self.items.startIndex } |
|
|
|
|
|