Refactor asynchronicity for API calls collections

multistore
Laurent 1 year ago
parent a8133b697c
commit 2191733d97
  1. 4
      LeStorage.xcodeproj/project.pbxproj
  2. 260
      LeStorage/ApiCallCollection.swift
  3. 4
      LeStorage/Services.swift
  4. 16
      LeStorage/Storable.swift
  5. 27
      LeStorage/Store.swift
  6. 297
      LeStorage/StoredCollection.swift

@ -14,6 +14,7 @@
C425D4582B6D2519002A7B48 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4572B6D2519002A7B48 /* Store.swift */; };
C456EFE22BE52379007388E2 /* StoredSingleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C456EFE12BE52379007388E2 /* StoredSingleton.swift */; };
C45D35912C0A1DB5000F379F /* FailedAPICall.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D35902C0A1DB5000F379F /* FailedAPICall.swift */; };
C49B6E502C2089B6002BDE1B /* ApiCallCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */; };
C49EF0242BD6BDC50077B5AA /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */; };
C4A47D4F2B6D280200ADC637 /* StoredCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */; };
C4A47D512B6D2C4E00ADC637 /* Codable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D502B6D2C4E00ADC637 /* Codable+Extensions.swift */; };
@ -51,6 +52,7 @@
C425D4572B6D2519002A7B48 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
C456EFE12BE52379007388E2 /* StoredSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredSingleton.swift; sourceTree = "<group>"; };
C45D35902C0A1DB5000F379F /* FailedAPICall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedAPICall.swift; sourceTree = "<group>"; };
C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiCallCollection.swift; sourceTree = "<group>"; };
C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extensions.swift"; sourceTree = "<group>"; };
C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredCollection.swift; sourceTree = "<group>"; };
C4A47D502B6D2C4E00ADC637 /* Codable+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Codable+Extensions.swift"; sourceTree = "<group>"; };
@ -113,6 +115,7 @@
C425D4372B6D24E1002A7B48 /* LeStorage.h */,
C425D4382B6D24E1002A7B48 /* LeStorage.docc */,
C4A47D9D2B7CFFF500ADC637 /* Codables */,
C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */,
C4A47D6C2B71364600ADC637 /* ModelObject.swift */,
C4A47D602B6D3C1300ADC637 /* Services.swift */,
C425D4572B6D2519002A7B48 /* Store.swift */,
@ -294,6 +297,7 @@
C49EF0242BD6BDC50077B5AA /* FileManager+Extensions.swift in Sources */,
C425D4582B6D2519002A7B48 /* Store.swift in Sources */,
C45D35912C0A1DB5000F379F /* FailedAPICall.swift in Sources */,
C49B6E502C2089B6002BDE1B /* ApiCallCollection.swift in Sources */,
C4A47D6B2B71244100ADC637 /* Collection+Extension.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

@ -0,0 +1,260 @@
//
// SafeCollection.swift
// LeStorage
//
// Created by Laurent Morvillier on 17/06/2024.
//
import Foundation
actor ApiCallCollection<T: Storable> {
/// The reference to the Store
fileprivate var _store: Store
fileprivate(set) var items: [ApiCall<T>] = []
/// 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
fileprivate var _hasChanged: Bool = false {
didSet {
self._write()
}
}
init(store: Store) {
self._store = store
}
/// Starts the JSON file decoding synchronously or asynchronously
func loadFromFile() throws {
try self._decodeJSONFile()
}
fileprivate func _urlForJSONFile() throws -> URL {
return try ApiCall<T>.urlForJSONFile()
}
/// Decodes the json file into the items array
fileprivate func _decodeJSONFile() throws {
let fileURL = try self._urlForJSONFile()
if FileManager.default.fileExists(atPath: fileURL.path()) {
let jsonString: String = try FileUtils.readFile(fileURL: fileURL)
let decoded: [ApiCall<T>] = try jsonString.decodeArray() ?? []
Logger.log("loaded \(T.fileName()) with \(decoded.count) items")
self.items = decoded
self.rescheduleApiCallsIfNecessary()
}
}
fileprivate func _write() {
let fileName = ApiCall<T>.fileName()
DispatchQueue(label: "lestorage.queue.write", qos: .utility).asyncAndWait {
Logger.log("Start write to \(fileName)...")
do {
let jsonString: String = try self.items.jsonString()
try T.writeToStorageDirectory(content: jsonString, fileName: fileName)
} catch {
Logger.error(error)
}
Logger.log("End write")
}
}
func addOrUpdate(_ instance: ApiCall<T>) {
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
self.items[index] = instance
} else {
self.items.append(instance)
}
self._hasChanged = true
}
/// Deletes an API call by [id]
func deleteById(_ id: String) {
self.items.removeAll(where: { $0.id == id })
self._hasChanged = true
}
func deleteByDataId(_ id: String) {
if let apiCallIndex = self.items.firstIndex(where: { $0.dataId == id }) {
self.items.remove(at: apiCallIndex)
self._hasChanged = true
}
}
func findById(_ id: String) -> ApiCall<T>? {
return self.items.first(where: { $0.id == id })
}
func reset() {
self.items.removeAll()
do {
let url: URL = try self._urlForJSONFile()
if FileManager.default.fileExists(atPath: url.path()) {
try FileManager.default.removeItem(at: url)
}
} catch {
Logger.error(error)
}
}
fileprivate func _rescheduleApiCalls() {
guard self.items.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 = self.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 self.items.isEmpty {
self._isRetryingCalls = false
} else {
self._rescheduleApiCalls()
}
}
}
// MARK: - Synchronization
/// Returns an APICall instance for the Storable [instance] and an HTTP [method]
/// The method updates existing calls or creates a new one
fileprivate func _callForInstance(_ instance: T, method: HTTPMethod) throws -> ApiCall<T>? {
if let existingCall = self.items.first(where: { $0.dataId == instance.id }) {
switch method {
case .delete:
self.deleteById(existingCall.id) // delete the existing call as we don't need it
if existingCall.method == HTTPMethod.post {
return nil // if the post has not been done, we can just stop here
} else {
return try self._createCall(instance, method: method) // otherwise it's a put and we want to send the delete
}
default: // here we should only trying to PUT, so we update the existing POST/PUT with the instance new values
existingCall.body = try instance.jsonString()
return existingCall
}
} else {
return try self._createCall(instance, method: method)
}
}
/// Creates an API call for the Storable [instance] and an HTTP [method]
fileprivate func _createCall(_ instance: T, method: HTTPMethod) throws -> ApiCall<T> {
let jsonString = try instance.jsonString()
return ApiCall(method: method, dataId: String(instance.id), body: jsonString)
}
/// Prepares a call for execution by updating its properties and adding it to its collection for storage
fileprivate func _prepareCall(apiCall: ApiCall<T>) throws {
apiCall.lastAttemptDate = Date()
apiCall.attemptsCount += 1
self.addOrUpdate(apiCall)
}
/// Reschedule API calls if necessary
func rescheduleApiCallsIfNecessary() {
if !self._isRetryingCalls {
self._rescheduleApiCalls()
}
}
/// Sends an insert api call for the provided [instance]
func sendInsertion(_ instance: T) {
Task {
do {
try await self._synchronize(instance, method: HTTPMethod.post)
} catch {
self.rescheduleApiCallsIfNecessary()
Logger.error(error)
}
}
}
/// Sends an update api call for the provided [instance]
func sendUpdate(_ instance: T) {
Task {
do {
try await self._synchronize(instance, method: HTTPMethod.put)
} catch {
self.rescheduleApiCallsIfNecessary()
Logger.error(error)
}
}
}
/// Sends an delete api call for the provided [instance]
func sendDeletion(_ instance: T) {
Task {
do {
try await self._synchronize(instance, method: HTTPMethod.delete)
} catch {
self.rescheduleApiCallsIfNecessary()
Logger.error(error)
}
}
}
fileprivate func _synchronize(_ instance: T, method: HTTPMethod) async throws {
if let apiCall = try self._callForInstance(instance, method: method) {
try self._prepareCall(apiCall: apiCall)
try await self._executeApiCall(apiCall)
}
}
fileprivate func _executeApiCall(_ apiCall: ApiCall<T>) async throws {
let result = try await self._store.execute(apiCall: apiCall)
switch apiCall.method {
case .post:
if let instance = self.findById(result.stringId) {
self._hasChanged = instance.copyFromServerInstance(result)
}
default:
break
}
Logger.log("")
}
func contentOfApiCallFile() -> String? {
guard let fileURL = try? self._urlForJSONFile() else { return nil }
if FileManager.default.fileExists(atPath: fileURL.path()) {
return try? FileUtils.readFile(fileURL: fileURL)
}
return nil
}
}

@ -102,9 +102,7 @@ public class Services {
case 200..<300:
if let apiCallId,
let collectionName = (T.self as? any Storable.Type)?.resourceName() {
try await MainActor.run {
try Store.main.deleteApiCallById(apiCallId, collectionName: collectionName)
}
try await Store.main.deleteApiCallById(apiCallId, collectionName: collectionName)
}
default:
/*

@ -47,4 +47,20 @@ extension Storable {
return path
}
static func storageDirectoryPath() throws -> URL {
return try FileUtils.pathForDirectoryInDocuments(directory: Store.storageDirectory)
}
static 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)
}
static func urlForJSONFile() throws -> URL {
var storageDirectory = try self.storageDirectoryPath()
storageDirectory.append(component: self.fileName())
return storageDirectory
}
}

@ -199,9 +199,9 @@ public class Store {
// MARK: - Api call rescheduling
/// Deletes an ApiCall by [id] and [collectionName]
func deleteApiCallById(_ id: String, collectionName: String) throws {
func deleteApiCallById(_ id: String, collectionName: String) async throws {
if let collection = self._collections[collectionName] {
try collection.deleteApiCallById(id)
try await collection.deleteApiCallById(id)
} else {
throw StoreError.collectionNotRegistered(type: collectionName)
}
@ -257,8 +257,13 @@ public class Store {
}
/// Returns whether any collection has pending API calls
public func hasPendingAPICalls() -> Bool {
return self._collections.values.contains(where: { $0.hasPendingAPICalls() })
public func hasPendingAPICalls() async -> Bool {
for collection in self._collections.values {
if await collection.hasPendingAPICalls() {
return true
}
}
return false
}
/// Returns the names of all collections
@ -267,8 +272,8 @@ public class Store {
}
/// Returns the content of the api call file
public func apiCallsFile(resourceName: String) -> String {
return self._collections[resourceName]?.contentOfApiCallFile() ?? ""
public func apiCallsFileContent(resourceName: String) async -> String {
return await self._collections[resourceName]?.contentOfApiCallFile() ?? ""
}
/// This method triggers the framework to save and send failed api calls
@ -282,11 +287,14 @@ public class Store {
guard let failedAPICallsCollection = self._failedAPICallsCollection,
let collection = self._collections[collectionName],
collectionName != FailedAPICall.resourceName(),
let apiCall = try? collection.apiCallById(apiCallId) else {
collectionName != FailedAPICall.resourceName()
else {
return
}
Task {
if let apiCall = await collection.apiCallById(apiCallId) {
if !failedAPICallsCollection.contains(where: { $0.callId == apiCallId }) && apiCall.attemptsCount > 6 {
do {
@ -298,6 +306,9 @@ public class Store {
Logger.error(error)
}
}
}
}
}

@ -13,22 +13,34 @@ enum StoredCollectionError: Error {
case missingInstance
}
protocol CollectionHolder {
associatedtype Item
var items: [Item] { get }
}
extension CollectionHolder {
}
protocol SomeCollection: Identifiable {
var resourceName: String { get }
var synchronized: Bool { get }
func allItems() -> [any Storable]
func deleteById(_ id: String) throws
func deleteApiCallById(_ id: String) throws
func loadDataFromServerIfAllowed() async throws
func hasPendingAPICalls() -> Bool
func contentOfApiCallFile() -> String?
func reset()
func resetApiCalls()
func apiCallById(_ id: String) throws -> (any SomeCall)?
func deleteApiCallById(_ id: String) async throws
func apiCallById(_ id: String) async -> (any SomeCall)?
func hasPendingAPICalls() async -> Bool
func contentOfApiCallFile() async -> String?
}
extension Notification.Name {
@ -36,7 +48,7 @@ extension Notification.Name {
public static let CollectionDidChange: Notification.Name = Notification.Name.init("notification.collectionDidChange")
}
public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollection {
public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollection, CollectionHolder {
/// If true, will synchronize the data with the provided server located at the Store's synchronizationApiURL
let synchronized: Bool
@ -60,7 +72,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
fileprivate var _indexes: [String : T]? = nil
/// Collection of API calls used to store HTTP calls
fileprivate var apiCallsCollection: StoredCollection<ApiCall<T>>? = nil
fileprivate var apiCallsCollection: ApiCallCollection<T>? = nil
/// Indicates whether the collection has changed, thus requiring a write operation
fileprivate var _hasChanged: Bool = false {
@ -94,9 +106,20 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
self.loadCompletion = loadCompletion
if synchronized {
self.apiCallsCollection = StoredCollection<ApiCall<T>>(synchronized: false, store: store, loadCompletion: { apiCallCollection in
self._rescheduleApiCalls()
})
let apiCallCollection = ApiCallCollection<T>(store: store)
self.apiCallsCollection = apiCallCollection
Task {
do {
try await apiCallCollection.loadFromFile()
} catch {
Logger.error(error)
}
}
// self.apiCallsCollection = StoredCollection<ApiCall<T>>(synchronized: false, store: store, loadCompletion: { apiCallCollection in
// self._rescheduleApiCalls()
// })
}
self._load()
@ -216,20 +239,20 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
// update
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
self.items[index] = instance
try self._sendUpdateIfNecessary(instance)
self._sendUpdateIfNecessary(instance)
} else { // insert
self.items.append(instance)
try self._sendInsertionIfNecessary(instance)
self._sendInsertionIfNecessary(instance)
}
self._indexes?[instance.stringId] = instance
}
public func writeChangeAndInsertOnServer(instance: T) throws {
public func writeChangeAndInsertOnServer(instance: T) {
defer {
self._hasChanged = true
}
try self._sendInsertionIfNecessary(instance)
self._sendInsertionIfNecessary(instance)
}
/// A method the treat the collection as a single instance holder
@ -251,8 +274,8 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
try instance.deleteDependencies()
self.items.removeAll { $0.id == instance.id }
self._indexes?.removeValue(forKey: instance.stringId)
try self._sendDeletionIfNecessary(instance)
self._sendDeletionIfNecessary(instance)
}
/// Deletes all items of the sequence by id
@ -266,7 +289,7 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
try instance.deleteDependencies()
self.items.removeAll { $0.id == instance.id }
self._indexes?.removeValue(forKey: instance.stringId)
try self._sendDeletionIfNecessary(instance)
self._sendDeletionIfNecessary(instance)
}
}
@ -285,12 +308,12 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
self.items[index] = instance
if shouldSync {
try self._sendUpdateIfNecessary(instance)
self._sendUpdateIfNecessary(instance)
}
} else { // insert
self.items.append(instance)
if shouldSync {
try self._sendInsertionIfNecessary(instance)
self._sendInsertionIfNecessary(instance)
}
}
self._indexes?[instance.stringId] = instance
@ -322,9 +345,9 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
for item in items {
self.items.removeAll(where: { $0.id == item.id })
/// remove related API call if existing
if let apiCallIndex = self.apiCallsCollection?.firstIndex(where: { $0.dataId == item.id }) {
self.apiCallsCollection?.items.remove(at: apiCallIndex)
Task {
/// Remove related API call if existing
await self.apiCallsCollection?.deleteByDataId(item.stringId)
}
}
@ -335,6 +358,16 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
try self.delete(contentOfs: self.items)
}
// MARK: - Some Collection
func deleteApiCallById(_ id: String) async throws {
await self.apiCallsCollection?.deleteById(id)
}
func apiCallById(_ id: String) async -> (any SomeCall)? {
return await self.apiCallsCollection?.findById(id)
}
// MARK: - SomeCall
/// Returns the collection items as [any Storable]
@ -395,202 +428,110 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
/// Removes the collection related API calls collection
public func resetApiCalls() {
if let apiCallsCollection = self.apiCallsCollection {
apiCallsCollection.reset()
}
}
// MARK: - Synchronization
/// Returns an APICall instance for the Storable [instance] and an HTTP [method]
/// The method updates existing calls or creates a new one
fileprivate func _callForInstance(_ instance: T, method: HTTPMethod) throws -> ApiCall<T>? {
guard let apiCallCollection = self.apiCallsCollection else {
throw StoredCollectionError.missingApiCallCollection
}
if let existingCall = apiCallCollection.first(where: { $0.dataId == instance.id }) {
switch method {
case .delete:
try self.deleteApiCallById(existingCall.id) // delete the existing call as we don't need it
if existingCall.method == HTTPMethod.post {
return nil // if the post has not been done, we can just stop here
} else {
return try self._createCall(instance, method: method) // otherwise it's a put and we want to send the delete
}
default: // here we should only trying to PUT, so we update the existing POST/PUT with the instance new values
existingCall.body = try instance.jsonString()
return existingCall
}
} else {
return try self._createCall(instance, method: method)
Task {
await apiCallsCollection.reset()
}
}
/// Creates an API call for the Storable [instance] and an HTTP [method]
fileprivate func _createCall(_ instance: T, method: HTTPMethod) throws -> ApiCall<T> {
let jsonString = try instance.jsonString()
return ApiCall(method: method, dataId: String(instance.id), body: jsonString)
}
/// Prepares a call for execution by updating its properties and adding it to its collection for storage
fileprivate func _prepareCall(apiCall: ApiCall<T>) throws {
apiCall.lastAttemptDate = Date()
apiCall.attemptsCount += 1
try self.apiCallsCollection?.addOrUpdate(instance: apiCall)
}
// MARK: - Reschedule calls
/// Sends an insert api call for the provided [instance]
fileprivate func _sendInsertionIfNecessary(_ instance: T) throws {
fileprivate func _sendInsertionIfNecessary(_ instance: T) {
guard self.synchronized, Store.main.collectionsCanSynchronize else {
return
}
Task {
do {
try await self._synchronize(instance, method: HTTPMethod.post)
} catch {
self.rescheduleApiCallsIfNecessary()
Logger.error(error)
}
await self.apiCallsCollection?.sendInsertion(instance)
}
}
/// Sends an update api call for the provided [instance]
fileprivate func _sendUpdateIfNecessary(_ instance: T) throws {
fileprivate func _sendUpdateIfNecessary(_ instance: T) {
guard self.synchronized, self._sendsUpdate, Store.main.collectionsCanSynchronize else {
return
}
Task {
do {
try await self._synchronize(instance, method: HTTPMethod.put)
} catch {
self.rescheduleApiCallsIfNecessary()
Logger.error(error)
await self.apiCallsCollection?.sendUpdate(instance)
}
}
}
/// Sends an delete api call for the provided [instance]
fileprivate func _sendDeletionIfNecessary(_ instance: T) throws {
fileprivate func _sendDeletionIfNecessary(_ instance: T) {
guard self.synchronized, Store.main.collectionsCanSynchronize else {
return
}
Task {
do {
try await self._synchronize(instance, method: HTTPMethod.delete)
} catch {
self.rescheduleApiCallsIfNecessary()
Logger.error(error)
}
}
}
fileprivate func _synchronize(_ instance: T, method: HTTPMethod) async throws {
if let apiCall = try self._callForInstance(instance, method: method) {
try self._prepareCall(apiCall: apiCall)
try await self._executeApiCall(apiCall)
await self.apiCallsCollection?.sendDeletion(instance)
}
}
fileprivate func _executeApiCall(_ apiCall: ApiCall<T>) async throws {
let result = try await self._store.execute(apiCall: apiCall)
switch apiCall.method {
case .post:
// DispatchQueue.main.async {
if let instance = self.findById(result.stringId) {
self._hasChanged = instance.copyFromServerInstance(result)
}
// }
default:
break
}
Logger.log("")
}
// MARK: - Reschedule calls
/// 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 if necessary
func rescheduleApiCallsIfNecessary() {
if !self._isRetryingCalls {
self._rescheduleApiCalls()
}
}
/// 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()
}
await self.apiCallsCollection?.rescheduleApiCallsIfNecessary()
}
}
/// Deletes an API call by [id]
func deleteApiCallById(_ id: String) throws {
guard let apiCallsCollection else {
throw StoreError.apiCallCollectionNotRegistered(type: T.resourceName())
}
try apiCallsCollection.deleteById(id)
}
//
// /// 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()
// }
//
// }
//
// }
func apiCallById(_ id: String) throws -> (any SomeCall)? {
guard let apiCallsCollection else {
throw StoreError.apiCallCollectionNotRegistered(type: T.resourceName())
}
return apiCallsCollection.findById(id)
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
func hasPendingAPICalls() -> Bool {
func hasPendingAPICalls() async -> Bool {
guard let apiCallsCollection else { return false }
return apiCallsCollection.isNotEmpty
}
func contentOfApiCallFile() -> String? {
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
return await apiCallsCollection.items.isNotEmpty
}
// MARK: - RandomAccessCollection

Loading…
Cancel
Save