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/SyncedCollection.swift

463 lines
15 KiB

//
// SyncedCollection.swift
// LeStorage
//
// Created by Laurent Morvillier on 11/10/2024.
//
import Foundation
protocol SomeSyncedCollection: SomeCollection {
func loadDataFromServerIfAllowed(clear: Bool) async throws
func loadCollectionsFromServerIfNoFile() async throws
}
public class SyncedCollection<T : SyncedStorable>: BaseCollection<T>, SomeSyncedCollection {
/// Returns a dummy SyncedCollection instance
public static func placeholder() -> SyncedCollection<T> {
return SyncedCollection<T>(store: Store(storeCenter: StoreCenter.main))
}
/// Migrates if necessary and asynchronously decodes the json file
override func load() async {
do {
if self.inMemory {
try await self.loadDataFromServerIfAllowed()
} else {
await 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 {
try await self.storeCenter.sendGetRequest(T.self, storeId: self.storeId, clear: clear)
} catch {
Logger.error(error)
}
}
/// 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 SyncedCollection possibly needs to update its own copy with new values.
/// - serverInstance: the instance of the object on the server
func updateFromServerInstance(_ serverInstance: T) {
guard T.copyServerResponse else {
return
}
DispatchQueue.main.async {
if let localInstance = self.findById(serverInstance.id) {
localInstance.copy(from: serverInstance)
self.setChanged()
}
}
}
@MainActor
func loadItems(_ items: [T], clear: Bool = false) {
if clear {
self.setItems(items)
} else {
self.addOrUpdateNoSync(contentOfs: items, checkLoaded: false)
}
self.setAsLoaded()
self.setChanged()
}
// MARK: - Basic operations with sync
/// Adds or update an instance asynchronously and waits for network operations
func addOrUpdateAsync(instance: T) async {
if let result = _addOrUpdateCore(instance: instance) {
if result.isNewItem {
await self._executeBatchOnce(OperationBatch(insert: result.item))
} else {
await self._executeBatchOnce(OperationBatch(update: result.item))
}
}
}
/// Adds or update an instance synchronously, dispatching network operations to background tasks
public override func addOrUpdate(instance: T) {
if let result = _addOrUpdateCore(instance: instance) {
if result.isNewItem {
Task { await self._sendInsertion(result.item) }
} else {
Task { await self._sendUpdate(result.item) }
}
}
}
/// Private helper function that contains the shared logic
private func _addOrUpdateCore(instance: T) -> (item: T, isNewItem: Bool)? {
instance.lastUpdate = Date()
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
if self.updateItem(instance, index: index, shouldBeSynchronized: true) {
self.setChanged()
return (instance, false)
}
} else {
if self.addItem(instance: instance, shouldBeSynchronized: true) {
self.setChanged()
return (instance, true)
}
}
return nil
}
// func addOrUpdateAsync(instance: T) async {
// instance.lastUpdate = Date()
// if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
// if self.updateItem(instance, index: index, shouldBeSynchronized: true) {
// await self._sendUpdate(instance)
// self.setChanged()
// }
// } else {
// if self.addItem(instance: instance, shouldBeSynchronized: true) {
// await self._sendInsertion(instance)
// self.setChanged()
// }
// }
// }
//
// /// Adds or update an instance and writes
// public override func addOrUpdate(instance: T) {
// instance.lastUpdate = Date()
// if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
// if self.updateItem(instance, index: index, shouldBeSynchronized: true) {
// Task { await self._sendUpdate(instance) }
// self.setChanged()
// }
// } else {
// if self.addItem(instance: instance, shouldBeSynchronized: true) {
// Task { await self._sendInsertion(instance) }
// self.setChanged()
// }
// }
// }
fileprivate func _addOrUpdateCore(contentOfs sequence: any Sequence<T>) -> OperationBatch<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 }) {
if self.updateItem(instance, index: index, shouldBeSynchronized: true) {
batch.addUpdate(instance)
}
} else { // insert
if self.addItem(instance: instance, shouldBeSynchronized: true) {
batch.addInsert(instance)
}
}
}
return batch
}
/// Adds or update a sequence and writes
override public func addOrUpdate(contentOfs sequence: any Sequence<T>) {
let batch = self._addOrUpdateCore(contentOfs: sequence)
Task { await self._sendOperationBatch(batch) }
}
func addOrUpdateAsync(contentOfs sequence: any Sequence<T>) async {
let batch = self._addOrUpdateCore(contentOfs: sequence)
await self._executeBatchOnce(batch)
}
/// Proceeds to delete all instance of the collection, properly cleaning up dependencies and sending API calls
override 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
fileprivate func _deleteCore(contentOfs sequence: any RandomAccessCollection<T>) -> OperationBatch<T> {
defer {
self.setChanged()
}
var deleted: [T] = []
for instance in sequence {
if self.deleteItem(instance, shouldBeSynchronized: true) {
deleted.append(instance)
}
self.storeCenter.createDeleteLog(instance)
}
let batch = OperationBatch<T>()
batch.deletes = deleted
return batch
}
/// Deletes all items of the sequence by id and sets the collection as changed to trigger a write
public override func delete(contentOfs sequence: any RandomAccessCollection<T>) {
guard sequence.isNotEmpty else { return }
let batch = self._deleteCore(contentOfs: sequence)
Task { await self._sendOperationBatch(batch) }
}
/// Deletes all items of the sequence by id and sets the collection as changed to trigger a write
public func deleteAsync(contentOfs sequence: any RandomAccessCollection<T>) async {
guard sequence.isNotEmpty else { return }
let batch = self._deleteCore(contentOfs: sequence)
await self._executeBatchOnce(batch)
}
/// Deletes an instance and writes
func deleteAsync(instance: T) async {
defer {
self.setChanged()
}
self._deleteNoWrite(instance: instance)
Task { await self._executeBatchOnce(OperationBatch(delete: instance)) }
}
/// Deletes an instance and writes
override public func delete(instance: T) {
defer {
self.setChanged()
}
self._deleteNoWrite(instance: instance)
Task { await self._sendDeletion(instance) }
}
/// Deletes an instance without writing, logs the operation and sends an API call
fileprivate func _deleteNoWrite(instance: T) {
self.deleteItem(instance, shouldBeSynchronized: true)
self.storeCenter.createDeleteLog(instance)
// await self._sendDeletion(instance)
}
public func deleteDependencies(_ items: any RandomAccessCollection<T>, shouldBeSynchronized: Bool) {
guard items.isNotEmpty else { return }
if shouldBeSynchronized {
self.delete(contentOfs: items)
} else {
self.deleteNoSync(contentOfs: items)
}
}
// MARK: - Basic operations without sync
/// Adds or update an instance without synchronizing it
func addOrUpdateNoSync(_ instance: T) {
self.addOrUpdateItem(instance: instance)
}
/// Adds or update a sequence of elements without synchronizing it
func addOrUpdateNoSync(contentOfs sequence: any Sequence<T>, checkLoaded: Bool = false) {
self.addSequence(sequence, checkLoaded: false)
}
/// Deletes the instance in the collection without synchronization
func deleteNoSync(contentOfs sequence: any Sequence<T>) {
defer {
self.setChanged()
}
for item in sequence {
self.deleteItem(item, shouldBeSynchronized: false)
}
}
/// Deletes the instance in the collection without synchronization
func deleteNoSync(instance: T) {
defer {
self.setChanged()
}
self.deleteItem(instance, shouldBeSynchronized: false)
}
/// 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, shouldBeSynchronized: false)
}
}
// MARK: - Send requests
fileprivate func _sendInsertion(_ instance: T) async {
await self._sendOperationBatch(OperationBatch(insert: instance))
}
fileprivate func _sendUpdate(_ instance: T) async {
await self._sendOperationBatch(OperationBatch(update: instance))
}
fileprivate func _sendDeletion(_ instance: T) async {
await self._sendOperationBatch(OperationBatch(delete: instance))
}
fileprivate func _sendOperationBatch(_ batch: OperationBatch<T>) async {
do {
try await self.storeCenter.sendOperationBatch(batch)
} catch {
Logger.error(error)
}
}
fileprivate func _executeBatchOnce(_ batch: OperationBatch<T>) async {
do {
try await self.storeCenter.singleBatchExecution(batch)
} catch {
Logger.error(error)
}
}
// MARK: Single calls
public func addsIfPostSucceeds(_ instance: T) async throws {
if let result = try await self.storeCenter.service().post(instance) {
self.addOrUpdateNoSync(result)
}
}
public func updateIfPutSucceeds(_ instance: T) async throws {
if let result = try await self.storeCenter.service().put(instance) {
self.addOrUpdateNoSync(result)
}
}
/// 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 {
print("do not update \(T.resourceName()): \(instance.lastUpdate.timeIntervalSince1970) / local: \(localInstance.lastUpdate.timeIntervalSince1970)")
}
} else { // insert
if shared {
instance.shared = true
}
self.addItem(instance: instance, shouldBeSynchronized: false)
}
}
// MARK: - Others
/// Sends a POST request for the instance, and changes the collection to perform a write
public func writeChangeAndInsertOnServer(instance: T) {
Task {
await self._sendInsertion(instance)
await MainActor.run {
self.setChanged()
}
}
}
}
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)
}
}