|
|
|
@ -7,22 +7,31 @@ |
|
|
|
|
|
|
|
|
|
|
|
import Foundation |
|
|
|
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<T: Storable> { |
|
|
|
actor ApiCallCollection<T: Storable> { |
|
|
|
|
|
|
|
|
|
|
|
/// The reference to the Store |
|
|
|
/// The reference to the Store |
|
|
|
fileprivate var _store: Store |
|
|
|
fileprivate var _store: Store |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// The list of api calls |
|
|
|
fileprivate(set) var items: [ApiCall<T>] = [] |
|
|
|
fileprivate(set) var items: [ApiCall<T>] = [] |
|
|
|
|
|
|
|
|
|
|
|
/// number of time an execution loop has been called |
|
|
|
/// The number of time an execution loop has been called |
|
|
|
fileprivate var _attemptLoops: Int = 0 |
|
|
|
fileprivate var _attemptLoops: Int = 0 |
|
|
|
|
|
|
|
|
|
|
|
/// Indicates if the collection is currently retrying ApiCalls |
|
|
|
/// Indicates if the collection is currently retrying ApiCalls |
|
|
|
fileprivate var _isRetryingCalls: Bool = false |
|
|
|
fileprivate var _isRetryingCalls: Bool = false |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Indicates whether the collection content has changed |
|
|
|
|
|
|
|
/// Initiates a write when true |
|
|
|
fileprivate var _hasChanged: Bool = false { |
|
|
|
fileprivate var _hasChanged: Bool = false { |
|
|
|
didSet { |
|
|
|
didSet { |
|
|
|
|
|
|
|
if self._hasChanged { |
|
|
|
self._write() |
|
|
|
self._write() |
|
|
|
|
|
|
|
self._hasChanged = false |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -31,10 +40,13 @@ actor ApiCallCollection<T: Storable> { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// Starts the JSON file decoding synchronously or asynchronously |
|
|
|
/// Starts the JSON file decoding synchronously or asynchronously |
|
|
|
|
|
|
|
/// Reschedule Api calls if not empty |
|
|
|
func loadFromFile() throws { |
|
|
|
func loadFromFile() throws { |
|
|
|
try self._decodeJSONFile() |
|
|
|
try self._decodeJSONFile() |
|
|
|
|
|
|
|
self.rescheduleApiCallsIfNecessary() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns the file URL of the collection |
|
|
|
fileprivate func _urlForJSONFile() throws -> URL { |
|
|
|
fileprivate func _urlForJSONFile() throws -> URL { |
|
|
|
return try ApiCall<T>.urlForJSONFile() |
|
|
|
return try ApiCall<T>.urlForJSONFile() |
|
|
|
} |
|
|
|
} |
|
|
|
@ -48,13 +60,10 @@ actor ApiCallCollection<T: Storable> { |
|
|
|
let decoded: [ApiCall<T>] = try jsonString.decodeArray() ?? [] |
|
|
|
let decoded: [ApiCall<T>] = try jsonString.decodeArray() ?? [] |
|
|
|
Logger.log("loaded \(T.fileName()) with \(decoded.count) items") |
|
|
|
Logger.log("loaded \(T.fileName()) with \(decoded.count) items") |
|
|
|
self.items = decoded |
|
|
|
self.items = decoded |
|
|
|
|
|
|
|
|
|
|
|
self.rescheduleApiCallsIfNecessary() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Writes the content of the data |
|
|
|
fileprivate func _write() { |
|
|
|
fileprivate func _write() { |
|
|
|
let fileName = ApiCall<T>.fileName() |
|
|
|
let fileName = ApiCall<T>.fileName() |
|
|
|
DispatchQueue(label: "lestorage.queue.write", qos: .utility).asyncAndWait { |
|
|
|
DispatchQueue(label: "lestorage.queue.write", qos: .utility).asyncAndWait { |
|
|
|
@ -69,6 +78,7 @@ actor ApiCallCollection<T: Storable> { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Adds or update an API call instance |
|
|
|
func addOrUpdate(_ instance: ApiCall<T>) { |
|
|
|
func addOrUpdate(_ instance: ApiCall<T>) { |
|
|
|
if let index = self.items.firstIndex(where: { $0.id == instance.id }) { |
|
|
|
if let index = self.items.firstIndex(where: { $0.id == instance.id }) { |
|
|
|
self.items[index] = instance |
|
|
|
self.items[index] = instance |
|
|
|
@ -84,17 +94,20 @@ actor ApiCallCollection<T: Storable> { |
|
|
|
self._hasChanged = true |
|
|
|
self._hasChanged = true |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func deleteByDataId(_ id: String) { |
|
|
|
/// Deletes a call by a data id |
|
|
|
if let apiCallIndex = self.items.firstIndex(where: { $0.dataId == id }) { |
|
|
|
func deleteByDataId(_ dataId: String) { |
|
|
|
|
|
|
|
if let apiCallIndex = self.items.firstIndex(where: { $0.dataId == dataId }) { |
|
|
|
self.items.remove(at: apiCallIndex) |
|
|
|
self.items.remove(at: apiCallIndex) |
|
|
|
self._hasChanged = true |
|
|
|
self._hasChanged = true |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns the Api call associated with the provided id |
|
|
|
func findById(_ id: String) -> ApiCall<T>? { |
|
|
|
func findById(_ id: String) -> ApiCall<T>? { |
|
|
|
return self.items.first(where: { $0.id == id }) |
|
|
|
return self.items.first(where: { $0.id == id }) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Removes all objects in memory and deletes the JSON file |
|
|
|
func reset() { |
|
|
|
func reset() { |
|
|
|
self.items.removeAll() |
|
|
|
self.items.removeAll() |
|
|
|
|
|
|
|
|
|
|
|
@ -108,6 +121,7 @@ actor ApiCallCollection<T: Storable> { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Reschedule the execution of API calls |
|
|
|
fileprivate func _rescheduleApiCalls() { |
|
|
|
fileprivate func _rescheduleApiCalls() { |
|
|
|
|
|
|
|
|
|
|
|
guard self.items.isNotEmpty else { |
|
|
|
guard self.items.isNotEmpty else { |
|
|
|
@ -131,12 +145,13 @@ actor ApiCallCollection<T: Storable> { |
|
|
|
|
|
|
|
|
|
|
|
do { |
|
|
|
do { |
|
|
|
try await self._executeApiCall(apiCall) |
|
|
|
try await self._executeApiCall(apiCall) |
|
|
|
// let _ = try await Store.main.execute(apiCall: apiCall) |
|
|
|
|
|
|
|
} catch { |
|
|
|
} catch { |
|
|
|
Logger.error(error) |
|
|
|
Logger.error(error) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self._hasChanged = true |
|
|
|
|
|
|
|
|
|
|
|
if self.items.isEmpty { |
|
|
|
if self.items.isEmpty { |
|
|
|
self._isRetryingCalls = false |
|
|
|
self._isRetryingCalls = false |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
@ -229,6 +244,7 @@ actor ApiCallCollection<T: Storable> { |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Initiates the process of sending the data with the server |
|
|
|
fileprivate func _synchronize(_ instance: T, method: HTTPMethod) async throws { |
|
|
|
fileprivate func _synchronize(_ instance: T, method: HTTPMethod) async throws { |
|
|
|
if let apiCall = try self._callForInstance(instance, method: method) { |
|
|
|
if let apiCall = try self._callForInstance(instance, method: method) { |
|
|
|
try self._prepareCall(apiCall: apiCall) |
|
|
|
try self._prepareCall(apiCall: apiCall) |
|
|
|
@ -236,6 +252,8 @@ actor ApiCallCollection<T: Storable> { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Executes an API call |
|
|
|
|
|
|
|
/// For POST requests, potentially copies additional data coming from the server during the insert |
|
|
|
fileprivate func _executeApiCall(_ apiCall: ApiCall<T>) async throws { |
|
|
|
fileprivate func _executeApiCall(_ apiCall: ApiCall<T>) async throws { |
|
|
|
let result = try await self._store.execute(apiCall: apiCall) |
|
|
|
let result = try await self._store.execute(apiCall: apiCall) |
|
|
|
switch apiCall.method { |
|
|
|
switch apiCall.method { |
|
|
|
@ -249,6 +267,7 @@ actor ApiCallCollection<T: Storable> { |
|
|
|
Logger.log("") |
|
|
|
Logger.log("") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns the content of the API call file as a String |
|
|
|
func contentOfApiCallFile() -> String? { |
|
|
|
func contentOfApiCallFile() -> String? { |
|
|
|
guard let fileURL = try? self._urlForJSONFile() else { return nil } |
|
|
|
guard let fileURL = try? self._urlForJSONFile() else { return nil } |
|
|
|
if FileManager.default.fileExists(atPath: fileURL.path()) { |
|
|
|
if FileManager.default.fileExists(atPath: fileURL.path()) { |
|
|
|
|