diff --git a/LeStorage.xcodeproj/project.pbxproj b/LeStorage.xcodeproj/project.pbxproj index c69dcd5..79fe587 100644 --- a/LeStorage.xcodeproj/project.pbxproj +++ b/LeStorage.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ C425D4452B6D24E1002A7B48 /* LeStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = C425D4372B6D24E1002A7B48 /* LeStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 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 */; }; 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 */; }; @@ -49,6 +50,7 @@ C425D4432B6D24E1002A7B48 /* LeStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeStorageTests.swift; sourceTree = ""; }; C425D4572B6D2519002A7B48 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; C456EFE12BE52379007388E2 /* StoredSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredSingleton.swift; sourceTree = ""; }; + C45D35902C0A1DB5000F379F /* FailedAPICall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedAPICall.swift; sourceTree = ""; }; C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extensions.swift"; sourceTree = ""; }; C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredCollection.swift; sourceTree = ""; }; C4A47D502B6D2C4E00ADC637 /* Codable+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Codable+Extensions.swift"; sourceTree = ""; }; @@ -159,6 +161,7 @@ children = ( C4A47D9A2B7CFFC500ADC637 /* Settings.swift */, C4A47D992B7CFFC500ADC637 /* ApiCall.swift */, + C45D35902C0A1DB5000F379F /* FailedAPICall.swift */, ); path = Codables; sourceTree = ""; @@ -290,6 +293,7 @@ C4A47D942B7CF7C500ADC637 /* MicroStorage.swift in Sources */, C49EF0242BD6BDC50077B5AA /* FileManager+Extensions.swift in Sources */, C425D4582B6D2519002A7B48 /* Store.swift in Sources */, + C45D35912C0A1DB5000F379F /* FailedAPICall.swift in Sources */, C4A47D6B2B71244100ADC637 /* Collection+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/LeStorage/Codables/ApiCall.swift b/LeStorage/Codables/ApiCall.swift index c9879b6..aff67ca 100644 --- a/LeStorage/Codables/ApiCall.swift +++ b/LeStorage/Codables/ApiCall.swift @@ -8,7 +8,9 @@ import Foundation protocol SomeCall: Storable { + var id: String { get } var lastAttemptDate: Date { get } + var attemptsCount: Int { get } } class ApiCall: ModelObject, Storable, SomeCall { diff --git a/LeStorage/Codables/FailedAPICall.swift b/LeStorage/Codables/FailedAPICall.swift new file mode 100644 index 0000000..4dbe87b --- /dev/null +++ b/LeStorage/Codables/FailedAPICall.swift @@ -0,0 +1,39 @@ +// +// FailedAPICall.swift +// LeStorage +// +// Created by Laurent Morvillier on 31/05/2024. +// + +import Foundation + +class FailedAPICall: ModelObject, Storable { + + static func resourceName() -> String { return "failed_api_calls" } + static func tokenExemptedMethods() -> [HTTPMethod] { return HTTPMethod.allCases } + + var id: String = Store.randomId() + + /// The creation date of the call + var date: Date = Date() + + /// The id of the API call + var callId: String + + /// The type of the call + var type: String + + /// The JSON representation of the API call + var apiCall: String + + /// The server error + var error: String + + init(callId: String, type: String, apiCall: String, error: String) { + self.callId = callId + self.type = type + self.apiCall = apiCall + self.error = error + } + +} diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index 1e829c2..7f5c815 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -50,7 +50,6 @@ fileprivate enum ServiceConf: String { } } - } public class Services { @@ -87,12 +86,6 @@ public class Services { return try await _runRequest(request, apiCallId: apiCallId) } -// fileprivate func _runRequest(servicePath: String, method: HTTPMethod, payload: T, apiCallId: String? = nil) async throws -> U { -// var request = try self._baseRequest(servicePath: servicePath, method: method) -// request.httpBody = try jsonEncoder.encode(payload) -// return try await _runRequest(request, apiCallId: apiCallId) -// } - fileprivate func _runRequest(_ request: URLRequest, apiCallId: String? = nil) async throws -> T { Logger.log("Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) @@ -110,17 +103,20 @@ public class Services { } } default: - if let apiCallId, let type = (T.self as? any Storable.Type) { - try Store.main.rescheduleApiCall(id: apiCallId, type: type) - } Logger.log("Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") - var dataString = String(describing: String(data: task.0, encoding: .utf8)) + var errorString = String(describing: String(data: task.0, encoding: .utf8)) if let nfe: NonFieldError = try? JSONDecoder().decode(NonFieldError.self, from: task.0) { if let reason = nfe.non_field_errors.first { - dataString = reason + errorString = reason } } - throw ServiceError.responseError(response: dataString) + + if let apiCallId, let type = (T.self as? any Storable.Type) { + try Store.main.rescheduleApiCall(id: apiCallId, type: type) + Store.main.logFailedAPICall(apiCallId, collectionName: type.resourceName(), error: errorString) + } + + throw ServiceError.responseError(response: errorString) } } return try jsonDecoder.decode(T.self, from: task.0) diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index e2df548..7f4f629 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -68,8 +68,11 @@ public class Store { } } + fileprivate var _failedAPICallsCollection: StoredCollection? = nil + public init() { FileManager.default.createDirectoryInDocuments(directoryName: Store.storageDirectory) +// self._failedAPICallsCollection = registerCollection(synchronized: true) } /// Registers a collection @@ -106,15 +109,6 @@ public class Store { return self._settingsStorage.item.userId } - /// Returns the user's UUID -// public var currentUserUUID: UUID? { -// if let uuidString = self._settingsStorage.item.userId, -// let uuid = UUID(uuidString: uuidString) { -// return uuid -// } -// return nil -// } - /// Returns the username public func userName() -> String? { return self._settingsStorage.item.username @@ -271,4 +265,27 @@ public class Store { return self._collections[resourceName]?.contentOfApiCallFile() ?? "" } + public func logsFailedAPICalls() { + self._failedAPICallsCollection = self.registerCollection(synchronized: true) + } + + func logFailedAPICall(_ apiCallId: String, collectionName: String, error: String) { + + guard let failedAPICallsCollection = self._failedAPICallsCollection, let collection = self._collections[collectionName], let apiCall = try? collection.apiCallById(apiCallId) else { + return + } + + if !failedAPICallsCollection.contains(where: { $0.callId == apiCallId }) && apiCall.attemptsCount > 5 { + + do { + let string = try apiCall.jsonString() + let failedAPICall = FailedAPICall(callId: apiCall.id, type: collectionName, apiCall: string, error: error) + try failedAPICallsCollection.addOrUpdate(instance: failedAPICall) + } catch { + Logger.error(error) + } + } + + } + } diff --git a/LeStorage/StoredCollection.swift b/LeStorage/StoredCollection.swift index 65b5b07..b95759d 100644 --- a/LeStorage/StoredCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -28,6 +28,7 @@ protocol SomeCollection: Identifiable { func reset() func resetApiCalls() + func apiCallById(_ id: String) throws -> (any SomeCall)? } extension Notification.Name { @@ -573,6 +574,13 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti try apiCallsCollection.deleteById(id) } + func apiCallById(_ id: String) throws -> (any SomeCall)? { + guard let apiCallsCollection else { + throw StoreError.apiCallCollectionNotRegistered(type: T.resourceName()) + } + return apiCallsCollection.findById(id) + } + /// Returns if the API call collection is not empty func hasPendingAPICalls() -> Bool { guard let apiCallsCollection else { return false }