Fixes calls rescheduling

multistore
Laurent 2 years ago
parent 2a5a6e6c48
commit 93516188c5
  1. 12
      LeStorage/ApiCall.swift
  2. 46
      LeStorage/Services.swift
  3. 95
      LeStorage/Store.swift
  4. 136
      LeStorage/StoredCollection.swift
  5. 4
      LeStorage/Utils/Collection+Extension.swift

@ -8,7 +8,7 @@
import Foundation import Foundation
protocol SomeCall : Storable { protocol SomeCall : Storable {
func execute() throws // func execute() throws
var lastAttemptDate: Date { get } var lastAttemptDate: Date { get }
} }
@ -44,10 +44,10 @@ class ApiCall<T : Storable> : ModelObject, Storable, SomeCall {
} }
/// Executes the api call /// Executes the api call
func execute() throws { // func execute() throws {
Task { // Task {
try await Store.main.execute(apiCall: self) // try await Store.main.execute(apiCall: self)
} // }
} // }
} }

@ -21,10 +21,10 @@ enum ServiceError: Error {
class Services { class Services {
init(url: String) { init(url: String) {
self._baseURL = url self.baseURL = url
} }
fileprivate var _baseURL: String fileprivate(set) var baseURL: String
fileprivate var jsonDecoder: JSONDecoder = { fileprivate var jsonDecoder: JSONDecoder = {
let decoder = JSONDecoder() let decoder = JSONDecoder()
@ -50,7 +50,7 @@ class Services {
} }
default: default:
if let apiCallId, let type = (T.self as? any Storable.Type) { if let apiCallId, let type = (T.self as? any Storable.Type) {
try Store.main.startCallsRescheduling(apiCallId: apiCallId, type: type) try Store.main.rescheduleApiCall(id: apiCallId, type: type)
} }
} }
} }
@ -63,20 +63,8 @@ class Services {
return try self._baseRequest(servicePath: servicePath, method: .get) return try self._baseRequest(servicePath: servicePath, method: .get)
} }
// fileprivate func postRequest(servicePath: String) throws -> URLRequest {
// return try self._baseRequest(servicePath: servicePath, method: .post)
// }
//
// fileprivate func putRequest(servicePath: String) throws -> URLRequest {
// return try self._baseRequest(servicePath: servicePath, method: .put)
// }
//
// fileprivate func deleteRequest(servicePath: String) throws -> URLRequest {
// return try self._baseRequest(servicePath: servicePath, method: .delete)
// }
fileprivate func _baseRequest(servicePath: String, method: Method) throws -> URLRequest { fileprivate func _baseRequest(servicePath: String, method: Method) throws -> URLRequest {
let urlString = _baseURL + servicePath let urlString = baseURL + servicePath
guard let url = URL(string: urlString) else { guard let url = URL(string: urlString) else {
throw ServiceError.urlCreationError(url: urlString) throw ServiceError.urlCreationError(url: urlString)
} }
@ -93,33 +81,7 @@ class Services {
return try await self.runRequest(getRequest) return try await self.runRequest(getRequest)
} }
func insert<T : Storable>(_ instance: T) async throws -> T {
let apiCall = try self._createCall(method: Method.post, instance: instance)
return try await self.runApiCall(apiCall)
}
func update<T : Storable>(_ instance: T) async throws -> T {
let apiCall = try self._createCall(method: Method.put, instance: instance)
return try await self.runApiCall(apiCall)
}
func delete<T : Storable>(_ instance: T) async throws -> T {
let apiCall = try self._createCall(method: Method.delete, instance: instance)
return try await self.runApiCall(apiCall)
}
fileprivate func _createCall<T : Storable>(method: Method, instance: T) throws -> ApiCall<T> {
let jsonString = try instance.jsonString()
let url = self._baseURL + T.resourceName() + "/"
return ApiCall(url: url, method: method.rawValue, dataId: String(instance.id), body: jsonString)
}
func runApiCall<T : Storable>(_ apiCall: ApiCall<T>) async throws -> T { func runApiCall<T : Storable>(_ apiCall: ApiCall<T>) async throws -> T {
apiCall.lastAttemptDate = Date()
apiCall.attemptsCount += 1
try Store.main.registerApiCall(apiCall)
let request = try self._request(from: apiCall) let request = try self._request(from: apiCall)
return try await self.runRequest(request, apiCallId: apiCall.id) return try await self.runRequest(request, apiCallId: apiCall.id)
} }

@ -40,8 +40,8 @@ public class Store {
/// The dictionary of registered StoredCollections /// The dictionary of registered StoredCollections
fileprivate var _collections: [String : any SomeCollection] = [:] fileprivate var _collections: [String : any SomeCollection] = [:]
/// The dictionary of ApiCall StoredCollections corresponding to the synchronized registered collections // /// The dictionary of ApiCall StoredCollections corresponding to the synchronized registered collections
fileprivate var _apiCallsCollections: [String : any SomeCollection] = [:] // fileprivate var _apiCallsCollections: [String : any SomeCollection] = [:]
/// The list of migrations to apply /// The list of migrations to apply
fileprivate var _migrations: [SomeMigration] = [] fileprivate var _migrations: [SomeMigration] = []
@ -60,12 +60,12 @@ public class Store {
let collection = StoredCollection<T>(synchronized: synchronized, store: Store.main, loadCompletion: nil) let collection = StoredCollection<T>(synchronized: synchronized, store: Store.main, loadCompletion: nil)
self._collections[T.resourceName()] = collection self._collections[T.resourceName()] = collection
if synchronized { // register additional collection for api calls // if synchronized { // register additional collection for api calls
let apiCallCollection = StoredCollection<ApiCall<T>>(synchronized: false, store: Store.main, loadCompletion: { apiCallCollection in // let apiCallCollection = StoredCollection<ApiCall<T>>(synchronized: false, store: Store.main, loadCompletion: { apiCallCollection in
self._rescheduleCalls(collection: apiCallCollection) // self._rescheduleCalls(collection: apiCallCollection)
}) // })
self._apiCallsCollections[T.resourceName()] = apiCallCollection // self._apiCallsCollections[T.resourceName()] = apiCallCollection
} // }
return collection return collection
} }
@ -148,78 +148,19 @@ public class Store {
// MARK: - Api call rescheduling // MARK: - Api call rescheduling
/// Schedules all stored api calls from all collections
fileprivate func _rescheduleCalls<T : Storable>(collection: StoredCollection<ApiCall<T>>) {
for apiCall in collection {
self.startCallsRescheduling(apiCall: apiCall)
}
}
/// Returns an API call collection corresponding to a type T
func apiCallCollection<T : Storable>() throws -> StoredCollection<ApiCall<T>> {
if let apiCallCollection = self._apiCallsCollections[T.resourceName()] as? StoredCollection<ApiCall<T>> {
return apiCallCollection
}
throw StoreError.apiCallCollectionNotRegistered(type: T.resourceName())
}
/// Registers an api call into its collection
func registerApiCall<T : Storable>(_ apiCall: ApiCall<T>) throws {
let collection: StoredCollection<ApiCall<T>> = try self.apiCallCollection()
if let existingDataCall = collection.first(where: { $0.dataId == apiCall.dataId }) {
switch apiCall.method {
case Method.put.rawValue:
existingDataCall.body = apiCall.body
collection.addOrUpdate(instance: existingDataCall)
case Method.delete.rawValue:
try self.deleteApiCallById(existingDataCall.id, collectionName: T.resourceName())
default:
collection.addOrUpdate(instance: apiCall) // rewrite new attempt values
}
} else {
collection.addOrUpdate(instance: apiCall)
}
}
/// Deletes an ApiCall by [id] and [collectionName] /// Deletes an ApiCall by [id] and [collectionName]
func deleteApiCallById(_ id: String, collectionName: String) throws { func deleteApiCallById(_ id: String, collectionName: String) throws {
if let collection = self._collections[collectionName] {
if let collection = self._apiCallsCollections[collectionName] { try collection.deleteApiCallById(id)
try collection.deleteById(id) } else {
return
}
throw StoreError.collectionNotRegistered(type: collectionName) throw StoreError.collectionNotRegistered(type: collectionName)
} }
/// Schedule an ApiCall for its execution in the future
func startCallsRescheduling<T : Storable>(apiCall: ApiCall<T>) {
let delay = pow(2, 0 + apiCall.attemptsCount)
let seconds = NSDecimalNumber(decimal: delay).intValue
Logger.log("Rerun request in \(seconds) seconds...")
DispatchQueue(label: "queue.scheduling", qos: .utility)
.asyncAfter(deadline: .now() + .seconds(seconds)) {
Logger.log("Try to execute api call...")
Task {
do {
_ = try await self._executeApiCall(apiCall)
} catch {
Logger.error(error)
}
}
}
} }
/// Reschedule an ApiCall by id /// Reschedule an ApiCall by id
func startCallsRescheduling<T : Storable>(apiCallId: String, type: T.Type) throws { func rescheduleApiCall<T : Storable>(id: String, type: T.Type) throws {
let apiCallCollection: StoredCollection<ApiCall<T>> = try self.apiCallCollection() let collection: StoredCollection<T> = try self.collection()
if let apiCall = apiCallCollection.findById(apiCallId) { collection.rescheduleApiCallsIfNecessary()
self.startCallsRescheduling(apiCall: apiCall)
}
} }
/// Executes an ApiCall /// Executes an ApiCall
@ -231,8 +172,12 @@ public class Store {
} }
/// Executes an ApiCall /// Executes an ApiCall
func execute<T>(apiCall: ApiCall<T>) async throws { // func execute<T>(apiCall: ApiCall<T>) async throws {
_ = try await self._executeApiCall(apiCall) // _ = try await self._executeApiCall(apiCall)
// }
func execute<T>(apiCall: ApiCall<T>) async throws -> T {
return try await self._executeApiCall(apiCall)
} }
/// Retrieves all the items on the server /// Retrieves all the items on the server

@ -7,9 +7,14 @@
import Foundation import Foundation
enum StoredCollectionError : Error {
case unmanagedHTTPMethod(method: String)
}
protocol SomeCollection : Identifiable { protocol SomeCollection : Identifiable {
func allItems() -> [any Storable] func allItems() -> [any Storable]
func deleteById(_ id: String) throws func deleteById(_ id: String) throws
func deleteApiCallById(_ id: String) throws
} }
extension Notification.Name { extension Notification.Name {
@ -34,6 +39,8 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
/// Provides fast access for instances if the collection has been instanced with [indexed] = true /// Provides fast access for instances if the collection has been instanced with [indexed] = true
fileprivate var _index: [String : T]? = nil fileprivate var _index: [String : T]? = nil
fileprivate var apiCallsCollection: StoredCollection<ApiCall<T>>? = nil
/// Indicates whether the collection has changed, thus requiring a write operation /// Indicates whether the collection has changed, thus requiring a write operation
fileprivate var _hasChanged: Bool = false { fileprivate var _hasChanged: Bool = false {
didSet { didSet {
@ -58,6 +65,13 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
} }
self._store = store self._store = store
self.loadCompletion = loadCompletion self.loadCompletion = loadCompletion
if synchronized {
self.apiCallsCollection = StoredCollection<ApiCall<T>>(synchronized: false, store: store, loadCompletion: { apiCallCollection in
self._rescheduleApiCalls()
})
}
self._load() self._load()
} }
@ -131,8 +145,6 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
/// Adds it if its id is not found, and otherwise updates it /// Adds it if its id is not found, and otherwise updates it
public func addOrUpdate(instance: T) { public func addOrUpdate(instance: T) {
// DispatchQueue(label: "lestorage.queue.items").sync {
defer { defer {
self._hasChanged = true self._hasChanged = true
} }
@ -146,7 +158,6 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
self._index?[instance.stringId] = instance self._index?[instance.stringId] = instance
self._sendInsertionIfNecessary(instance) self._sendInsertionIfNecessary(instance)
} }
// }
} }
@ -232,16 +243,56 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
// MARK: - Synchronization // MARK: - Synchronization
fileprivate func _callForInstance(_ instance: T, method: Method) throws -> ApiCall<T>? {
guard let apiCallCollection = self.apiCallsCollection else {
throw StoreError.apiCallCollectionNotRegistered(type: T.resourceName())
}
if let existingCall = apiCallCollection.first(where: { $0.dataId == instance.id }) {
switch existingCall.method {
case Method.post.rawValue, Method.put.rawValue:
existingCall.body = try instance.jsonString()
return existingCall
case Method.delete.rawValue:
try self.deleteApiCallById(existingCall.id)
return nil
default:
throw StoredCollectionError.unmanagedHTTPMethod(method: existingCall.method)
}
} else {
return try self._createCall(instance, method: method)
}
}
fileprivate func _createCall(_ instance: T, method: Method) throws -> ApiCall<T> {
guard let baseURL = _store.service?.baseURL else {
throw StoreError.missingService
}
let jsonString = try instance.jsonString()
let url = baseURL + T.resourceName() + "/"
return ApiCall(url: url, method: method.rawValue, dataId: String(instance.id), body: jsonString)
}
fileprivate func _prepareCall(apiCall: ApiCall<T>) {
apiCall.lastAttemptDate = Date()
apiCall.attemptsCount += 1
self.apiCallsCollection?.addOrUpdate(instance: apiCall)
}
/// Sends an insert api call for the provided [instance] /// Sends an insert api call for the provided [instance]
fileprivate func _sendInsertionIfNecessary(_ instance: T) { fileprivate func _sendInsertionIfNecessary(_ instance: T) {
guard self.synchronized else { guard self.synchronized else {
return return
} }
Logger.log("Call service...")
Task { Task {
do { do {
let _ = try await self._store.service?.insert(instance) if let apiCall = try self._callForInstance(instance, method: Method.post) {
self._prepareCall(apiCall: apiCall)
_ = try await self._store.execute(apiCall: apiCall)
}
} catch { } catch {
self.rescheduleApiCallsIfNecessary()
Logger.error(error) Logger.error(error)
} }
} }
@ -256,9 +307,15 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
Task { Task {
do { do {
let _ = try await self._store.service?.update(instance) if let apiCall = try self._callForInstance(instance, method: Method.put) {
self._prepareCall(apiCall: apiCall)
_ = try await self._store.execute(apiCall: apiCall)
}
// let _ = try await self._store.service?.update(instance)
} catch { } catch {
Logger.error(error) Logger.error(error)
self.rescheduleApiCallsIfNecessary()
} }
} }
@ -272,12 +329,77 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
Task { Task {
do { do {
let _ = try await self._store.service?.delete(instance)
if let apiCall = try self._callForInstance(instance, method: Method.delete) {
self._prepareCall(apiCall: apiCall)
_ = try await self._store.execute(apiCall: apiCall)
}
// let _ = try await self._store.service?.delete(instance)
} catch { } catch {
Logger.error(error) Logger.error(error)
self.rescheduleApiCallsIfNecessary()
}
} }
} }
// 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
func rescheduleApiCallsIfNecessary() {
if !self._isRetryingCalls {
self._rescheduleApiCalls()
}
}
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 {
let _ = try await Store.main.execute(apiCall: apiCall)
} catch {
Logger.error(error)
}
}
if apiCallsCollection.isEmpty {
self._isRetryingCalls = false
} else {
self._rescheduleApiCalls()
}
}
}
func deleteApiCallById(_ id: String) throws {
guard let apiCallsCollection else {
throw StoreError.apiCallCollectionNotRegistered(type: T.resourceName())
}
try apiCallsCollection.deleteById(id)
} }
// MARK: - RandomAccessCollection // MARK: - RandomAccessCollection

@ -26,3 +26,7 @@ extension Array {
} }
} }
extension RandomAccessCollection {
var isNotEmpty: Bool { return !self.isEmpty }
}

Loading…
Cancel
Save