diff --git a/LeStorage/ApiCallCollection.swift b/LeStorage/ApiCallCollection.swift index f9e7111..0f4958d 100644 --- a/LeStorage/ApiCallCollection.swift +++ b/LeStorage/ApiCallCollection.swift @@ -7,22 +7,31 @@ import Foundation +/// ApiCallCollection is an object communicating with a server to synchronize data managed locally +/// The Api calls are serialized and stored in a JSON file +/// Failing Api calls are stored forever and will be executed again later actor ApiCallCollection { /// The reference to the Store fileprivate var _store: Store + /// The list of api calls fileprivate(set) var items: [ApiCall] = [] - /// number of time an execution loop has been called + /// The number of time an execution loop has been called fileprivate var _attemptLoops: Int = 0 /// Indicates if the collection is currently retrying ApiCalls fileprivate var _isRetryingCalls: Bool = false + /// Indicates whether the collection content has changed + /// Initiates a write when true fileprivate var _hasChanged: Bool = false { didSet { - self._write() + if self._hasChanged { + self._write() + self._hasChanged = false + } } } @@ -31,10 +40,13 @@ actor ApiCallCollection { } /// Starts the JSON file decoding synchronously or asynchronously + /// Reschedule Api calls if not empty func loadFromFile() throws { try self._decodeJSONFile() + self.rescheduleApiCallsIfNecessary() } + /// Returns the file URL of the collection fileprivate func _urlForJSONFile() throws -> URL { return try ApiCall.urlForJSONFile() } @@ -48,13 +60,10 @@ actor ApiCallCollection { let decoded: [ApiCall] = try jsonString.decodeArray() ?? [] Logger.log("loaded \(T.fileName()) with \(decoded.count) items") self.items = decoded - - self.rescheduleApiCallsIfNecessary() - } - } + /// Writes the content of the data fileprivate func _write() { let fileName = ApiCall.fileName() DispatchQueue(label: "lestorage.queue.write", qos: .utility).asyncAndWait { @@ -69,6 +78,7 @@ actor ApiCallCollection { } } + /// Adds or update an API call instance func addOrUpdate(_ instance: ApiCall) { if let index = self.items.firstIndex(where: { $0.id == instance.id }) { self.items[index] = instance @@ -84,17 +94,20 @@ actor ApiCallCollection { self._hasChanged = true } - func deleteByDataId(_ id: String) { - if let apiCallIndex = self.items.firstIndex(where: { $0.dataId == id }) { + /// Deletes a call by a data id + func deleteByDataId(_ dataId: String) { + if let apiCallIndex = self.items.firstIndex(where: { $0.dataId == dataId }) { self.items.remove(at: apiCallIndex) self._hasChanged = true } } + /// Returns the Api call associated with the provided id func findById(_ id: String) -> ApiCall? { return self.items.first(where: { $0.id == id }) } + /// Removes all objects in memory and deletes the JSON file func reset() { self.items.removeAll() @@ -108,6 +121,7 @@ actor ApiCallCollection { } } + /// Reschedule the execution of API calls fileprivate func _rescheduleApiCalls() { guard self.items.isNotEmpty else { @@ -131,12 +145,13 @@ actor ApiCallCollection { do { try await self._executeApiCall(apiCall) -// let _ = try await Store.main.execute(apiCall: apiCall) } catch { Logger.error(error) } } - + + self._hasChanged = true + if self.items.isEmpty { self._isRetryingCalls = false } else { @@ -229,6 +244,7 @@ actor ApiCallCollection { } + /// Initiates the process of sending the data with the server fileprivate func _synchronize(_ instance: T, method: HTTPMethod) async throws { if let apiCall = try self._callForInstance(instance, method: method) { try self._prepareCall(apiCall: apiCall) @@ -236,6 +252,8 @@ actor ApiCallCollection { } } + /// Executes an API call + /// For POST requests, potentially copies additional data coming from the server during the insert fileprivate func _executeApiCall(_ apiCall: ApiCall) async throws { let result = try await self._store.execute(apiCall: apiCall) switch apiCall.method { @@ -249,6 +267,7 @@ actor ApiCallCollection { Logger.log("") } + /// Returns the content of the API call file as a String func contentOfApiCallFile() -> String? { guard let fileURL = try? self._urlForJSONFile() else { return nil } if FileManager.default.fileExists(atPath: fileURL.path()) { diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index 9d06665..62def65 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -116,7 +116,7 @@ public class Services { } if let apiCallId, let type = (T.self as? any Storable.Type) { - try Store.main.rescheduleApiCall(id: apiCallId, type: type) + try Store.main.rescheduleApiCalls(id: apiCallId, type: type) Store.main.logFailedAPICall(apiCallId, request: request, collectionName: type.resourceName(), error: errorString) } else { Store.main.logFailedAPICall(request: request, error: errorString) diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 9d1921b..ece0ab4 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -73,7 +73,6 @@ public class Store { public init() { FileManager.default.createDirectoryInDocuments(directoryName: Store.storageDirectory) -// self._failedAPICallsCollection = registerCollection(synchronized: true) } /// Registers a collection @@ -191,11 +190,6 @@ public class Store { throw StoreError.collectionNotRegistered(type: T.resourceName()) } - /// Deletes the dependencies of a collection -// public func deleteDependencies(items: any Sequence) throws { -// try self.collection().deleteDependencies(items) -// } - // MARK: - Api call rescheduling /// Deletes an ApiCall by [id] and [collectionName] @@ -208,7 +202,7 @@ public class Store { } /// Reschedule an ApiCall by id - func rescheduleApiCall(id: String, type: T.Type) throws { + func rescheduleApiCalls(id: String, type: T.Type) throws { guard self.collectionsCanSynchronize else { return } @@ -312,6 +306,7 @@ public class Store { } + /// Logs a failed Api call with its request and error message func logFailedAPICall(request: URLRequest, error: String) { guard let failedAPICallsCollection = self._failedAPICallsCollection, diff --git a/LeStorage/StoredCollection.swift b/LeStorage/StoredCollection.swift index f21f683..c14558e 100644 --- a/LeStorage/StoredCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -116,10 +116,6 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti } } - -// self.apiCallsCollection = StoredCollection>(synchronized: false, store: store, loadCompletion: { apiCallCollection in -// self._rescheduleApiCalls() -// }) } self._load() @@ -131,21 +127,21 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti // MARK: - Paths - fileprivate func _storageDirectoryPath() throws -> URL { - return try FileUtils.pathForDirectoryInDocuments(directory: Store.storageDirectory) - } - - fileprivate func _writeToStorageDirectory(content: String, fileName: String) throws { - var fileURL = try self._storageDirectoryPath() - fileURL.append(component: fileName) - try content.write(to: fileURL, atomically: false, encoding: .utf8) - } - - fileprivate func _urlForJSONFile() throws -> URL { - var storageDirectory = try self._storageDirectoryPath() - storageDirectory.append(component: T.fileName()) - return storageDirectory - } +// fileprivate func _storageDirectoryPath() throws -> URL { +// return try FileUtils.pathForDirectoryInDocuments(directory: Store.storageDirectory) +// } +// +// fileprivate func _writeToStorageDirectory(content: String, fileName: String) throws { +// var fileURL = try self._storageDirectoryPath() +// fileURL.append(component: fileName) +// try content.write(to: fileURL, atomically: false, encoding: .utf8) +// } +// +// fileprivate func _urlForJSONFile() throws -> URL { +// var storageDirectory = try self._storageDirectoryPath() +// storageDirectory.append(component: T.fileName()) +// return storageDirectory +// } // MARK: - Loading @@ -181,7 +177,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti /// Decodes the json file into the items array fileprivate func _decodeJSONFile() throws { - let fileURL = try self._urlForJSONFile() + let fileURL = try T.urlForJSONFile() if FileManager.default.fileExists(atPath: fileURL.path()) { let jsonString: String = try FileUtils.readFile(fileURL: fileURL) @@ -396,8 +392,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti Logger.log("Start write to \(T.fileName())...") do { let jsonString: String = try self.items.jsonString() - try self._writeToStorageDirectory(content: jsonString, fileName: T.fileName()) -// let _ = try FileUtils.writeToDocumentDirectory(content: jsonString, fileName: T.fileName()) + try T.writeToStorageDirectory(content: jsonString, fileName: T.fileName()) } catch { Logger.error(error) // TODO how to notify the main project } @@ -414,7 +409,7 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti self.items.removeAll() do { - let url: URL = try self._urlForJSONFile() + let url: URL = try T.urlForJSONFile() if FileManager.default.fileExists(atPath: url.path()) { try FileManager.default.removeItem(at: url) } @@ -466,66 +461,16 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti } } + /// Reschedule the api calls if possible func rescheduleApiCallsIfNecessary() { Task { await self.apiCallsCollection?.rescheduleApiCallsIfNecessary() } } -// -// /// number of time an execution loop has been called -// fileprivate var _attemptLoops: Int = 0 -// -// /// Indicates if the collection is currently retrying ApiCalls -// fileprivate var _isRetryingCalls: Bool = false -// -// /// Reschedule API calls -// fileprivate func _rescheduleApiCalls() { -// -// guard let apiCallsCollection, apiCallsCollection.isNotEmpty else { -// return -// } -// -// self._isRetryingCalls = true -// self._attemptLoops += 1 -// -// Task { -// -// let delay = pow(2, self._attemptLoops) -// let seconds = NSDecimalNumber(decimal: delay).intValue -// Logger.log("wait for \(seconds) sec") -// try await Task.sleep(until: .now + .seconds(seconds)) -// -// let apiCallsCopy = apiCallsCollection.items -// for apiCall in apiCallsCopy { -// apiCall.attemptsCount += 1 -// apiCall.lastAttemptDate = Date() -// -// do { -// try await self._executeApiCall(apiCall) -//// let _ = try await Store.main.execute(apiCall: apiCall) -// } catch { -// Logger.error(error) -// } -// } -// -// if apiCallsCollection.isEmpty { -// self._isRetryingCalls = false -// } else { -// self._rescheduleApiCalls() -// } -// -// } -// -// } - + /// Returns the content of the API call file as a String func contentOfApiCallFile() async -> String? { return await self.apiCallsCollection?.contentOfApiCallFile() -// guard let fileURL = try? self.apiCallsCollection?._urlForJSONFile() else { return nil } -// if FileManager.default.fileExists(atPath: fileURL.path()) { -// return try? FileUtils.readFile(fileURL: fileURL) -// } -// return nil } /// Returns if the API call collection is not empty