Fixes calls rescheduling

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

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

@ -21,10 +21,10 @@ enum ServiceError: Error {
class Services {
init(url: String) {
self._baseURL = url
self.baseURL = url
}
fileprivate var _baseURL: String
fileprivate(set) var baseURL: String
fileprivate var jsonDecoder: JSONDecoder = {
let decoder = JSONDecoder()
@ -50,7 +50,7 @@ class Services {
}
default:
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)
}
// 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 {
let urlString = _baseURL + servicePath
let urlString = baseURL + servicePath
guard let url = URL(string: urlString) else {
throw ServiceError.urlCreationError(url: urlString)
}
@ -93,33 +81,7 @@ class Services {
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 {
apiCall.lastAttemptDate = Date()
apiCall.attemptsCount += 1
try Store.main.registerApiCall(apiCall)
let request = try self._request(from: apiCall)
return try await self.runRequest(request, apiCallId: apiCall.id)
}

@ -40,8 +40,8 @@ public class Store {
/// The dictionary of registered StoredCollections
fileprivate var _collections: [String : any SomeCollection] = [:]
/// The dictionary of ApiCall StoredCollections corresponding to the synchronized registered collections
fileprivate var _apiCallsCollections: [String : any SomeCollection] = [:]
// /// The dictionary of ApiCall StoredCollections corresponding to the synchronized registered collections
// fileprivate var _apiCallsCollections: [String : any SomeCollection] = [:]
/// The list of migrations to apply
fileprivate var _migrations: [SomeMigration] = []
@ -60,12 +60,12 @@ public class Store {
let collection = StoredCollection<T>(synchronized: synchronized, store: Store.main, loadCompletion: nil)
self._collections[T.resourceName()] = collection
if synchronized { // register additional collection for api calls
let apiCallCollection = StoredCollection<ApiCall<T>>(synchronized: false, store: Store.main, loadCompletion: { apiCallCollection in
self._rescheduleCalls(collection: apiCallCollection)
})
self._apiCallsCollections[T.resourceName()] = apiCallCollection
}
// if synchronized { // register additional collection for api calls
// let apiCallCollection = StoredCollection<ApiCall<T>>(synchronized: false, store: Store.main, loadCompletion: { apiCallCollection in
// self._rescheduleCalls(collection: apiCallCollection)
// })
// self._apiCallsCollections[T.resourceName()] = apiCallCollection
// }
return collection
}
@ -148,78 +148,19 @@ public class Store {
// 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]
func deleteApiCallById(_ id: String, collectionName: String) throws {
if let collection = self._apiCallsCollections[collectionName] {
try collection.deleteById(id)
return
}
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)
}
}
if let collection = self._collections[collectionName] {
try collection.deleteApiCallById(id)
} else {
throw StoreError.collectionNotRegistered(type: collectionName)
}
}
/// Reschedule an ApiCall by id
func startCallsRescheduling<T : Storable>(apiCallId: String, type: T.Type) throws {
let apiCallCollection: StoredCollection<ApiCall<T>> = try self.apiCallCollection()
if let apiCall = apiCallCollection.findById(apiCallId) {
self.startCallsRescheduling(apiCall: apiCall)
}
func rescheduleApiCall<T : Storable>(id: String, type: T.Type) throws {
let collection: StoredCollection<T> = try self.collection()
collection.rescheduleApiCallsIfNecessary()
}
/// Executes an ApiCall
@ -231,8 +172,12 @@ public class Store {
}
/// Executes an ApiCall
func execute<T>(apiCall: ApiCall<T>) async throws {
_ = try await self._executeApiCall(apiCall)
// func execute<T>(apiCall: ApiCall<T>) async throws {
// _ = 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

@ -7,9 +7,14 @@
import Foundation
enum StoredCollectionError : Error {
case unmanagedHTTPMethod(method: String)
}
protocol SomeCollection : Identifiable {
func allItems() -> [any Storable]
func deleteById(_ id: String) throws
func deleteApiCallById(_ id: String) throws
}
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
fileprivate var _index: [String : T]? = nil
fileprivate var apiCallsCollection: StoredCollection<ApiCall<T>>? = nil
/// Indicates whether the collection has changed, thus requiring a write operation
fileprivate var _hasChanged: Bool = false {
didSet {
@ -58,9 +65,16 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
}
self._store = store
self.loadCompletion = loadCompletion
if synchronized {
self.apiCallsCollection = StoredCollection<ApiCall<T>>(synchronized: false, store: store, loadCompletion: { apiCallCollection in
self._rescheduleApiCalls()
})
}
self._load()
}
// MARK: - Loading
/// Migrates if necessary and asynchronously decodes the json file
@ -77,17 +91,17 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
} else {
try self._decodeJSONFile()
}
}
// else {
// try? self.loadDataFromServer()
// }
// else {
// try? self.loadDataFromServer()
// }
} catch {
Logger.log(error)
}
}
/// Decodes the json file into the items array
fileprivate func _decodeJSONFile() throws {
let jsonString = try FileUtils.readDocumentFile(fileName: T.fileName())
@ -131,36 +145,33 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
/// Adds it if its id is not found, and otherwise updates it
public func addOrUpdate(instance: T) {
// DispatchQueue(label: "lestorage.queue.items").sync {
defer {
self._hasChanged = true
}
defer {
self._hasChanged = true
}
// update
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
self.items[index] = instance
self._sendUpdateIfNecessary(instance)
} else { // insert
self.items.append(instance)
self._index?[instance.stringId] = instance
self._sendInsertionIfNecessary(instance)
}
// }
// update
if let index = self.items.firstIndex(where: { $0.id == instance.id }) {
self.items[index] = instance
self._sendUpdateIfNecessary(instance)
} else { // insert
self.items.append(instance)
self._index?[instance.stringId] = instance
self._sendInsertionIfNecessary(instance)
}
}
/// Deletes the instance in the collection by id
public func delete(instance: T) throws {
defer {
self._hasChanged = true
}
try instance.deleteDependencies()
self.items.removeAll { $0.id == instance.id }
self._index?.removeValue(forKey: instance.stringId)
self._sendDeletionIfNecessary(instance)
defer {
self._hasChanged = true
}
try instance.deleteDependencies()
self.items.removeAll { $0.id == instance.id }
self._index?.removeValue(forKey: instance.stringId)
self._sendDeletionIfNecessary(instance)
}
@ -232,16 +243,56 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
// 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]
fileprivate func _sendInsertionIfNecessary(_ instance: T) {
guard self.synchronized else {
return
}
Logger.log("Call service...")
Task {
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 {
self.rescheduleApiCallsIfNecessary()
Logger.error(error)
}
}
@ -256,9 +307,15 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
Task {
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 {
Logger.error(error)
self.rescheduleApiCallsIfNecessary()
}
}
@ -272,14 +329,79 @@ public class StoredCollection<T : Storable> : RandomAccessCollection, SomeCollec
Task {
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 {
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
public var startIndex: Int { return self.items.startIndex }

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

Loading…
Cancel
Save