diff --git a/LeStorage/ApiCallCollection.swift b/LeStorage/ApiCallCollection.swift index e7f4f8a..b52d8f2 100644 --- a/LeStorage/ApiCallCollection.swift +++ b/LeStorage/ApiCallCollection.swift @@ -143,6 +143,7 @@ actor ApiCallCollection: SomeCallCollection { self._isExecutingCalls = false self._schedulingTask?.cancel() self.items.removeAll() + self._hasChanged = true do { let url: URL = try self._urlForJSONFile() @@ -184,7 +185,7 @@ actor ApiCallCollection: SomeCallCollection { fileprivate func _waitAndExecuteApiCalls() async { // Logger.log("\(T.resourceName()) > RESCHED") - guard !self._isExecutingCalls, StoreCenter.main.collectionsCanSynchronize else { return } + guard !self._isExecutingCalls, StoreCenter.main.forceNoSynchronization == false else { return } guard self.items.isNotEmpty else { return } self._isExecutingCalls = true @@ -226,7 +227,7 @@ actor ApiCallCollection: SomeCallCollection { let _: Empty = try await StoreCenter.main.executeGet(apiCall: apiCall) } else { let results: [T] = try await StoreCenter.main.executeGet(apiCall: apiCall) - await StoreCenter.main.itemsRetrieved(results, storeId: apiCall.storeId) + await StoreCenter.main.itemsRetrieved(results, storeId: apiCall.storeId, clear: apiCall.option != .additive) } } @@ -284,21 +285,22 @@ actor ApiCallCollection: SomeCallCollection { } /// we want to avoid sending the same GET twice - fileprivate func _createGetCallIfNonExistent(_ parameters: [String : String]?) -> ApiCall? { + fileprivate func _createGetCallIfNonExistent(_ parameters: [String : String]?, clear: Bool) -> ApiCall? { if let _ = self.items.first(where: { $0.method == .get && $0.urlParameters == parameters }) { return nil } - let call = self._createCall(.get, instance: nil) + let option: CallOption? = !clear ? .additive : nil + let call = self._createCall(.get, instance: nil, option: option) call.urlParameters = parameters return call } /// Creates an API call for the Storable [instance] and an HTTP [method] - fileprivate func _createCall(_ method: HTTPMethod, instance: T?, transactionId: String? = nil) -> ApiCall { + fileprivate func _createCall(_ method: HTTPMethod, instance: T?, transactionId: String? = nil, option: CallOption? = nil) -> ApiCall { if let instance { - return ApiCall(method: method, data: instance, transactionId: transactionId) + return ApiCall(method: method, data: instance, transactionId: transactionId, option: option) } else { - return ApiCall(method: .get, data: nil) + return ApiCall(method: .get, data: nil, option: option) } } @@ -316,18 +318,18 @@ actor ApiCallCollection: SomeCallCollection { } /// Sends a GET request with an optional [storeId] - func sendGetRequest(storeId: String?) async throws { + func sendGetRequest(storeId: String?, clear: Bool) async throws { var parameters: [String : String]? = nil if let storeId { parameters = [Services.storeIdURLParameter : storeId] } - try await self._sendGetRequest(parameters: parameters) + try await self._sendGetRequest(parameters: parameters, clear: clear) } /// Sends an insert api call for the provided [instance] - fileprivate func _sendGetRequest(parameters: [String : String]?) async throws { + fileprivate func _sendGetRequest(parameters: [String : String]?, clear: Bool = true) async throws { - if let getCall = self._createGetCallIfNonExistent(parameters) { + if let getCall = self._createGetCallIfNonExistent(parameters, clear: clear) { do { try await self._prepareAndSendGetCall(getCall) } catch { diff --git a/LeStorage/BaseCollection.swift b/LeStorage/BaseCollection.swift index ce51dd3..5e550f9 100644 --- a/LeStorage/BaseCollection.swift +++ b/LeStorage/BaseCollection.swift @@ -26,11 +26,6 @@ public protocol SomeCollection: CollectionHolder, Identifiable { func findById(_ id: Item.ID) -> Item? } -protocol SomeSyncedCollection: SomeCollection { - func loadDataFromServerIfAllowed() async throws - func loadCollectionsFromServerIfNoFile() async throws -} - public class BaseCollection: SomeCollection, CollectionHolder { /// Doesn't write the collection in a file @@ -131,7 +126,7 @@ public class BaseCollection: SomeCollection, CollectionHolder { if FileManager.default.fileExists(atPath: fileURL.path()) { let jsonString: String = try FileUtils.readFile(fileURL: fileURL) let decoded: [T] = try jsonString.decodeArray() ?? [] - self._setItems(decoded) + self.setItems(decoded) } await MainActor.run { self.setAsLoaded() @@ -167,7 +162,7 @@ public class BaseCollection: SomeCollection, CollectionHolder { } /// Sets a collection of items and indexes them - fileprivate func _setItems(_ items: [T]) { + func setItems(_ items: [T]) { for item in items { item.store = self.store } @@ -182,15 +177,6 @@ public class BaseCollection: SomeCollection, CollectionHolder { } } - func clearAndLoadItems(_ items: [T]) async { - await MainActor.run { - self.clear() - self._setItems(items) - self.setAsLoaded() - self.setChanged() - } - } - // MARK: - Basic operations /// Adds or updates the provided instance inside the collection @@ -407,7 +393,6 @@ public class BaseCollection: SomeCollection, CollectionHolder { self.items.removeAll() self.store.removeFile(type: T.self) setChanged() - self.hasLoaded = false } public var type: any Storable.Type { return T.self } diff --git a/LeStorage/Codables/ApiCall.swift b/LeStorage/Codables/ApiCall.swift index 5b20fbf..3aa3696 100644 --- a/LeStorage/Codables/ApiCall.swift +++ b/LeStorage/Codables/ApiCall.swift @@ -17,6 +17,10 @@ public protocol SomeCall: Identifiable, Storable { var dataContent: String? { get } } +public enum CallOption: String, Codable { + case additive // keeps the content of the current collection +} + public class ApiCall: ModelObject, Storable, SomeCall { public static func resourceName() -> String { return "apicalls_" + T.resourceName() } @@ -24,7 +28,7 @@ public class ApiCall: ModelObject, Storable, SomeCall { public var id: String = Store.randomId() - /// The transactionId to group calls together + /// The transactionId serves to group calls together var transactionId: String = Store.randomId() /// Creation date of the call @@ -44,13 +48,17 @@ public class ApiCall: ModelObject, Storable, SomeCall { /// The parameters to add in the URL to obtain : "?p1=v1&p2=v2" var urlParameters: [String : String]? = nil + + /// The option for the call + var option: CallOption? = nil - init(method: HTTPMethod, data: T?, transactionId: String? = nil) { + init(method: HTTPMethod, data: T?, transactionId: String? = nil, option: CallOption? = nil) { self.method = method self.data = data if let transactionId { self.transactionId = transactionId } + self.option = option } public func copy(from other: any Storable) { diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 56064e8..6415e08 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -175,10 +175,14 @@ final public class Store { } /// Loads all collection with the data from the server - public func loadCollectionsFromServer() { + public func loadCollectionsFromServer(clear: Bool) { for collection in self._syncedCollections() { Task { - try? await collection.loadDataFromServerIfAllowed() + do { + try await collection.loadDataFromServerIfAllowed(clear: clear) + } catch { + Logger.error(error) + } } } } @@ -300,10 +304,10 @@ final public class Store { } } - func loadCollectionItems(_ items: [T]) async { + func loadCollectionItems(_ items: [T], clear: Bool) async { do { - let collection: BaseCollection = try self.collection() - await collection.clearAndLoadItems(items) + let collection: SyncedCollection = try self.syncedCollection() + await collection.loadItems(items, clear: clear) } catch { Logger.error(error) } diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index be59d97..39f0ea6 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -19,9 +19,6 @@ public class StoreCenter { /// A KeychainStore object used to store the user's token var keychainStore: KeychainStore? = nil - /// Indicates to Stored Collection if they can synchronize - public var collectionsCanSynchronize: Bool = true - /// Force the absence of synchronization public var forceNoSynchronization: Bool = false @@ -287,7 +284,7 @@ public class StoreCenter { do { try await apiCallCollection.loadFromFile() let count = await apiCallCollection.items.count - Logger.log("collection \(T.resourceName()) loaded with \(count)") +// Logger.log("collection \(T.resourceName()) loaded with \(count)") await apiCallCollection.rescheduleApiCallsIfNecessary() } catch { Logger.error(error) @@ -378,10 +375,10 @@ public class StoreCenter { /// Retry API calls immediately fileprivate func _resumeApiCalls() { - guard self.collectionsCanSynchronize else { + guard self.forceNoSynchronization == false else { return } - Logger.log("_resumeApiCalls") +// Logger.log("_resumeApiCalls") Task { for collection in self._apiCallCollections.values { await collection.resumeApiCalls() @@ -391,7 +388,7 @@ public class StoreCenter { /// Reschedule an ApiCall by id func rescheduleApiCalls(type: T.Type) async throws { - guard self.collectionsCanSynchronize else { + guard self.forceNoSynchronization == false else { return } let collection: ApiCallCollection = try self.apiCallCollection() @@ -419,7 +416,8 @@ public class StoreCenter { /// Returns whether the collection can synchronize fileprivate func _canSynchronise() -> Bool { - return !self.forceNoSynchronization && self.collectionsCanSynchronize + return !self.forceNoSynchronization + && self.isAuthenticated && self.userIsAllowed() } @@ -465,8 +463,8 @@ public class StoreCenter { return try await self.service().get(identifier: identifier) } - func itemsRetrieved(_ results: [T], storeId: String?) async { - await self._store(id: storeId).loadCollectionItems(results) + func itemsRetrieved(_ results: [T], storeId: String?, clear: Bool) async { + await self._store(id: storeId).loadCollectionItems(results, clear: clear) } /// Returns the names of all collections @@ -488,8 +486,8 @@ public class StoreCenter { } /// Loads all the data from the server for the users - public func initialSynchronization() { - Store.main.loadCollectionsFromServer() + public func initialSynchronization(clear: Bool) { + Store.main.loadCollectionsFromServer(clear: clear) // request data that has been shared with the user Task { @@ -522,11 +520,13 @@ public class StoreCenter { } - func sendGetRequest(_ type: T.Type, storeId: String?) async throws { - if self.canPerformGet(T.self) { - let apiCallCollection: ApiCallCollection = try self.apiCallCollection() - try await apiCallCollection.sendGetRequest(storeId: storeId) + func sendGetRequest(_ type: T.Type, storeId: String?, clear: Bool) async throws { + guard self._canSynchronise(), self.canPerformGet(T.self) else { + return } + + let apiCallCollection: ApiCallCollection = try self.apiCallCollection() + try await apiCallCollection.sendGetRequest(storeId: storeId, clear: clear) } func canPerformGet(_ type: T.Type) -> Bool { @@ -540,7 +540,8 @@ public class StoreCenter { let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { - Logger.w("data unrecognized") + let string = String(data: data, encoding: .utf8) ?? "--" + Logger.w("data unrecognized: \(string)") return } try await self._parseSyncUpdates(json, shared: true) diff --git a/LeStorage/SyncedCollection.swift b/LeStorage/SyncedCollection.swift index b0cf7d7..5c11eb6 100644 --- a/LeStorage/SyncedCollection.swift +++ b/LeStorage/SyncedCollection.swift @@ -7,6 +7,11 @@ import Foundation +protocol SomeSyncedCollection: SomeCollection { + func loadDataFromServerIfAllowed(clear: Bool) async throws + func loadCollectionsFromServerIfNoFile() async throws +} + public class SyncedCollection: BaseCollection, SomeSyncedCollection { /// Returns a dummy SyncedCollection instance @@ -35,9 +40,9 @@ public class SyncedCollection: BaseCollection, SomeSynced } } - func loadDataFromServerIfAllowed() async throws { - try await self.loadDataFromServerIfAllowed(clear: false) - } +// 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 { @@ -45,7 +50,7 @@ public class SyncedCollection: BaseCollection, SomeSynced throw StoreError.cannotSyncCollection(name: self.resourceName) } do { - try await StoreCenter.main.sendGetRequest(T.self, storeId: self.storeId) + try await StoreCenter.main.sendGetRequest(T.self, storeId: self.storeId, clear: clear) } catch { Logger.error(error) } @@ -68,6 +73,18 @@ public class SyncedCollection: BaseCollection, SomeSynced } } + @MainActor + func loadItems(_ items: [T], clear: Bool = false) { + if clear { + self.setItems(items) + } else { + self.addOrUpdateNoSync(contentOfs: items) + } + + self.setAsLoaded() + self.setChanged() + } + // MARK: - Basic operations with sync /// Adds or update an instance and writes @@ -285,7 +302,7 @@ public class SyncedCollection: BaseCollection, SomeSynced } - // MARK: - Migrations + // MARK: - Others /// Sends a POST request for the instance, and changes the collection to perform a write public func writeChangeAndInsertOnServer(instance: T) {