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.
320 lines
9.5 KiB
320 lines
9.5 KiB
//
|
|
// StoredCollection.swift
|
|
// LeStorage
|
|
//
|
|
// Created by Laurent Morvillier on 11/10/2024.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
|
|
|
|
/// Migrates if necessary and asynchronously decodes the json file
|
|
func load() async {
|
|
do {
|
|
if self.inMemory {
|
|
try await self.loadDataFromServerIfAllowed()
|
|
} else {
|
|
try self.loadFromFile()
|
|
}
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
/// Loads the collection using the server data only if the collection file doesn't exists
|
|
func loadCollectionsFromServerIfNoFile() async throws {
|
|
let fileURL: URL = try self.store.fileURL(type: T.self)
|
|
if !FileManager.default.fileExists(atPath: fileURL.path()) {
|
|
try await self.loadDataFromServerIfAllowed()
|
|
}
|
|
}
|
|
|
|
func loadDataFromServerIfAllowed() async throws {
|
|
try await self.loadDataFromServerIfAllowed(clear: false)
|
|
}
|
|
|
|
/// Retrieves the data from the server and loads it into the items array
|
|
public func loadDataFromServerIfAllowed(clear: Bool = false) async throws {
|
|
guard !(self is StoredSingleton<T>) else {
|
|
throw StoreError.cannotSyncCollection(name: self.resourceName)
|
|
}
|
|
do {
|
|
let items: [T] = try await self.store.getItems()
|
|
if items.count > 0 {
|
|
DispatchQueue.main.async {
|
|
if clear {
|
|
self.clear()
|
|
}
|
|
self.addOrUpdateNoSync(contentOfs: items)
|
|
}
|
|
}
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
self.setAsLoaded()
|
|
}
|
|
|
|
/// Updates a local item from a server instance. This method is typically used when the server makes update
|
|
/// to an object when it's inserted. The StoredCollection possibly needs to update its own copy with new values.
|
|
/// - serverInstance: the instance of the object on the server
|
|
func updateFromServerInstance(_ serverInstance: T) {
|
|
DispatchQueue.main.async {
|
|
if let localInstance = self.findById(serverInstance.id) {
|
|
localInstance.copy(from: serverInstance)
|
|
self.setChanged()
|
|
// let modified = localInstance.copyFromServerInstance(serverInstance)
|
|
// if modified {
|
|
// self.setChanged()
|
|
// }
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Basic operations
|
|
|
|
/// Adds or update an instance without synchronizing it
|
|
func addOrUpdateNoSync(_ instance: T) throws {
|
|
self.addOrUpdateItem(instance: instance)
|
|
}
|
|
|
|
/// Adds or update a sequence of elements without synchronizing it
|
|
func addOrUpdateNoSync(contentOfs sequence: any Sequence<T>) {
|
|
self.addSequence(sequence)
|
|
}
|
|
|
|
/// Deletes the instance in the collection without synchronization
|
|
func deleteNoSync(instance: T) throws {
|
|
defer {
|
|
self.setChanged()
|
|
}
|
|
self.deleteItem(instance)
|
|
}
|
|
|
|
/// Deletes the instance in the collection without synchronization
|
|
func deleteByStringIdNoSync(_ id: String) {
|
|
defer {
|
|
self.setChanged()
|
|
}
|
|
let realId = T.buildRealId(id: id)
|
|
if let instance = self.findById(realId) {
|
|
self.deleteItem(instance)
|
|
}
|
|
}
|
|
|
|
/// Adds or update an instance and writes
|
|
public func addOrUpdate(instance: T) {
|
|
defer {
|
|
self.setChanged()
|
|
}
|
|
|
|
instance.lastUpdate = Date()
|
|
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
|
|
self.updateItem(instance, index: index)
|
|
self._sendUpdate(instance)
|
|
} else {
|
|
self.addItem(instance: instance)
|
|
self._sendInsertion(instance)
|
|
}
|
|
}
|
|
|
|
/// Adds or update a sequence and writes
|
|
public func addOrUpdate(contentOfs sequence: any Sequence<T>) {
|
|
defer {
|
|
self.setChanged()
|
|
}
|
|
|
|
let date = Date()
|
|
let batch = OperationBatch<T>()
|
|
|
|
for instance in sequence {
|
|
instance.lastUpdate = date
|
|
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
|
|
self.updateItem(instance, index: index)
|
|
batch.addUpdate(instance)
|
|
// self._sendUpdateIfNecessary(instance)
|
|
} else { // insert
|
|
self.addItem(instance: instance)
|
|
batch.addInsert(instance)
|
|
// self._sendInsertionIfNecessary(instance)
|
|
}
|
|
}
|
|
|
|
self._sendOperationBatch(batch)
|
|
|
|
}
|
|
|
|
/// Proceeds to delete all instance of the collection, properly cleaning up dependencies and sending API calls
|
|
public func deleteAll() throws {
|
|
self.delete(contentOfs: self.items)
|
|
}
|
|
|
|
/// Deletes all items of the sequence by id and sets the collection as changed to trigger a write
|
|
public func delete(contentOfs sequence: any Sequence<T>) {
|
|
|
|
defer {
|
|
self.setChanged()
|
|
}
|
|
|
|
for instance in sequence {
|
|
self.deleteItem(instance)
|
|
StoreCenter.main.createDeleteLog(instance)
|
|
}
|
|
|
|
let batch = OperationBatch<T>()
|
|
batch.deletes = Array(sequence)
|
|
self._sendOperationBatch(batch)
|
|
}
|
|
|
|
/// Deletes an instance and writes
|
|
public func delete(instance: T) {
|
|
defer {
|
|
self.setChanged()
|
|
}
|
|
self._deleteNoWrite(instance: instance)
|
|
}
|
|
|
|
/// Deletes an instance without writing, logs the operation and sends an API call
|
|
fileprivate func _deleteNoWrite(instance: T) {
|
|
self.deleteItem(instance)
|
|
StoreCenter.main.createDeleteLog(instance)
|
|
|
|
self._sendDeletion(instance)
|
|
}
|
|
|
|
public func deleteDependencies(_ items: any Sequence<T>) {
|
|
self.delete(contentOfs: items)
|
|
}
|
|
|
|
// MARK: - Send requests
|
|
|
|
fileprivate func _sendInsertion(_ instance: T) {
|
|
self._sendOperationBatch(OperationBatch(insert: instance))
|
|
}
|
|
|
|
fileprivate func _sendUpdate(_ instance: T) {
|
|
self._sendOperationBatch(OperationBatch(update: instance))
|
|
}
|
|
|
|
fileprivate func _sendDeletion(_ instance: T) {
|
|
self._sendOperationBatch(OperationBatch(delete: instance))
|
|
}
|
|
|
|
fileprivate func _sendOperationBatch(_ batch: OperationBatch<T>) {
|
|
Task {
|
|
do {
|
|
let success = try await StoreCenter.main.sendOperationBatch(batch)
|
|
for item in success {
|
|
self.updateFromServerInstance(item)
|
|
}
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sends an insert api call for the provided
|
|
/// Calls copyFromServerInstance on the instance with the result of the HTTP call
|
|
/// - Parameters:
|
|
/// - instance: the object to POST
|
|
// fileprivate func _sendInsertionIfNecessary(_ instance: T) {
|
|
//
|
|
// Task {
|
|
// do {
|
|
// if let result = try await self.store.sendInsertion(instance) {
|
|
// self.updateFromServerInstance(result)
|
|
// }
|
|
// } catch {
|
|
// Logger.error(error)
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// /// Sends an update api call for the provided [instance]
|
|
// /// - Parameters:
|
|
// /// - instance: the object to PUT
|
|
// fileprivate func _sendUpdateIfNecessary(_ instance: T) {
|
|
// Task {
|
|
// do {
|
|
// try await self.store.sendUpdate(instance)
|
|
// } catch {
|
|
// Logger.error(error)
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// /// Sends an delete api call for the provided [instance]
|
|
// /// - Parameters:
|
|
// /// - instance: the object to DELETE
|
|
// fileprivate func _sendDeletionIfNecessary(_ instance: T) {
|
|
// Task {
|
|
// do {
|
|
// try await self.store.sendDeletion(instance)
|
|
// } catch {
|
|
// Logger.error(error)
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// MARK: - Synchronization
|
|
|
|
/// Adds or update an instance if it is newer than the local instance
|
|
func addOrUpdateIfNewer(_ instance: T, shared: Bool) {
|
|
defer {
|
|
self.setChanged()
|
|
}
|
|
|
|
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
|
|
let localInstance = self.items[index]
|
|
if instance.lastUpdate > localInstance.lastUpdate {
|
|
self.updateItem(instance, index: index)
|
|
}
|
|
} else { // insert
|
|
if shared {
|
|
instance.shared = true
|
|
}
|
|
self.addItem(instance: instance)
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Migrations
|
|
|
|
/// Sends a POST request for the instance, and changes the collection to perform a write
|
|
public func writeChangeAndInsertOnServer(instance: T) {
|
|
defer {
|
|
self.setChanged()
|
|
}
|
|
self._sendInsertion(instance)
|
|
}
|
|
|
|
}
|
|
|
|
class OperationBatch<T> {
|
|
var inserts: [T] = []
|
|
var updates: [T] = []
|
|
var deletes: [T] = []
|
|
|
|
init() {
|
|
|
|
}
|
|
init(insert: T) {
|
|
self.inserts = [insert]
|
|
}
|
|
init(update: T) {
|
|
self.updates = [update]
|
|
}
|
|
init(delete: T) {
|
|
self.deletes = [delete]
|
|
}
|
|
|
|
func addInsert(_ instance: T) {
|
|
self.inserts.append(instance)
|
|
}
|
|
func addUpdate(_ instance: T) {
|
|
self.updates.append(instance)
|
|
}
|
|
func addDelete(_ instance: T) {
|
|
self.deletes.append(instance)
|
|
}
|
|
}
|
|
|