diff --git a/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift index c7eb635bf..0b8025591 100644 --- a/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift @@ -82,28 +82,23 @@ final class FeedbinAccountDelegate: AccountDelegate { } func refreshAll(for account: Account) async throws { - + refreshProgress.addToNumberOfTasksAndRemaining(5) + do { + try await refreshAccount(account) + } catch { + refreshProgress.clear() + let wrappedError = AccountError.wrappedError(error: error, account: account) + throw wrappedError + } + try await withCheckedThrowingContinuation { continuation in - refreshAccount(account) { result in + self.refreshArticlesAndStatuses(account) { result in switch result { case .success(): - - self.refreshArticlesAndStatuses(account) { result in - switch result { - case .success(): - continuation.resume() - case .failure(let error): - DispatchQueue.main.async { - self.refreshProgress.clear() - let wrappedError = AccountError.wrappedError(error: error, account: account) - continuation.resume(throwing: wrappedError) - } - } - } - + continuation.resume() case .failure(let error): DispatchQueue.main.async { self.refreshProgress.clear() @@ -253,7 +248,6 @@ final class FeedbinAccountDelegate: AccountDelegate { os_log(.info, log: self.log, "Retrieving unread entries failed: %@.", error.localizedDescription) group.leave() } - } group.enter() @@ -268,7 +262,6 @@ final class FeedbinAccountDelegate: AccountDelegate { os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription) group.leave() } - } group.notify(queue: DispatchQueue.main) { @@ -279,67 +272,43 @@ final class FeedbinAccountDelegate: AccountDelegate { completion(.success(())) } } - } func importOPML(for account: Account, opmlFile: URL) async throws { - try await withCheckedThrowingContinuation { continuation in - self.importOPML(for: account, opmlFile: opmlFile) { result in - switch result { - case .success: - continuation.resume() - case .failure(let error): - continuation.resume(throwing: error) - } - } - } - } - - private func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) { - - var fileData: Data? - - do { - fileData = try Data(contentsOf: opmlFile) - } catch { - completion(.failure(error)) + let opmlData = try Data(contentsOf: opmlFile) + if opmlData.isEmpty { return } - - guard let opmlData = fileData else { - completion(.success(())) - return - } - + os_log(.debug, log: log, "Begin importing OPML...") isOPMLImportInProgress = true refreshProgress.addToNumberOfTasksAndRemaining(1) - - caller.importOPML(opmlData: opmlData) { result in - switch result { - case .success(let importResult): - if importResult.complete { - os_log(.debug, log: self.log, "Import OPML done.") - self.refreshProgress.completeTask() - self.isOPMLImportInProgress = false - DispatchQueue.main.async { - completion(.success(())) - } - } else { - self.checkImportResult(opmlImportResultID: importResult.importResultID, completion: completion) - } - case .failure(let error): - os_log(.debug, log: self.log, "Import OPML failed.") - self.refreshProgress.completeTask() - self.isOPMLImportInProgress = false - DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) - completion(.failure(wrappedError)) - } + + do { + let importResult = try await caller.importOPML(opmlData: opmlData) + + if importResult.complete { + os_log(.debug, log: self.log, "Import OPML done.") + + refreshProgress.completeTask() + isOPMLImportInProgress = false + } else { + try await checkImportResult(opmlImportResultID: importResult.importResultID) + + refreshProgress.completeTask() + isOPMLImportInProgress = false } + + } catch { + os_log(.debug, log: self.log, "Import OPML failed.") + + refreshProgress.completeTask() + isOPMLImportInProgress = false + + let wrappedError = AccountError.wrappedError(error: error, account: account) + throw wrappedError } - } func createFolder(for account: Account, name: String) async throws -> Folder { @@ -787,29 +756,9 @@ final class FeedbinAccountDelegate: AccountDelegate { static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, secretsProvider: SecretsProvider) async throws -> Credentials? { - try await withCheckedThrowingContinuation { continuation in - - self.validateCredentials(transport: transport, credentials: credentials, endpoint: endpoint, secretsProvider: secretsProvider) { result in - switch result { - case .success(let credentials): - continuation.resume(returning: credentials) - case .failure(let error): - continuation.resume(throwing: error) - } - } - } - } - - private static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, secretsProvider: SecretsProvider, completion: @escaping (Result) -> Void) { - let caller = FeedbinAPICaller(transport: transport) caller.credentials = credentials - caller.validateCredentials() { result in - DispatchQueue.main.async { - completion(result) - } - } - + return try await caller.validateCredentials() } // MARK: Suspend and Resume (for iOS) @@ -841,89 +790,67 @@ final class FeedbinAccountDelegate: AccountDelegate { private extension FeedbinAccountDelegate { + func checkImportResult(opmlImportResultID: Int) async throws { + + try await withCheckedThrowingContinuation { continuation in + self.checkImportResult(opmlImportResultID: opmlImportResultID) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + func checkImportResult(opmlImportResultID: Int, completion: @escaping (Result) -> Void) { DispatchQueue.main.async { Timer.scheduledTimer(withTimeInterval: 15, repeats: true) { timer in - os_log(.debug, log: self.log, "Checking status of OPML import...") - - self.caller.retrieveOPMLImportResult(importID: opmlImportResultID) { result in - switch result { - case .success(let importResult): - if let result = importResult, result.complete { + Task { @MainActor in + + os_log(.debug, log: self.log, "Checking status of OPML import...") + + do { + let importResult = try await self.caller.retrieveOPMLImportResult(importID: opmlImportResultID) + + if let importResult, importResult.complete { os_log(.debug, log: self.log, "Checking status of OPML import successfully completed.") timer.invalidate() - self.refreshProgress.completeTask() - self.isOPMLImportInProgress = false - DispatchQueue.main.async { - completion(.success(())) - } + completion(.success(())) } - case .failure(let error): + + } catch { os_log(.debug, log: self.log, "Import OPML check failed.") timer.invalidate() - self.refreshProgress.completeTask() - self.isOPMLImportInProgress = false - DispatchQueue.main.async { - completion(.failure(error)) - } - } - } - - } - - } - - } - - func refreshAccount(_ account: Account, completion: @escaping (Result) -> Void) { - - caller.retrieveTags { result in - switch result { - case .success(let tags): - - self.refreshProgress.completeTask() - self.caller.retrieveSubscriptions { result in - switch result { - case .success(let subscriptions): - - self.refreshProgress.completeTask() - self.forceExpireFolderFeedRelationship(account, tags) - self.caller.retrieveTaggings { result in - - MainActor.assumeIsolated { - switch result { - case .success(let taggings): - - BatchUpdate.shared.perform { - self.syncFolders(account, tags) - self.syncFeeds(account, subscriptions) - self.syncFeedFolderRelationship(account, taggings) - } - - self.refreshProgress.completeTask() - completion(.success(())) - - case .failure(let error): - completion(.failure(error)) - } - } - } - - case .failure(let error): completion(.failure(error)) } - } - - case .failure(let error): - completion(.failure(error)) } - } - + } + + func refreshAccount(_ account: Account) async throws { + + let tags = try await caller.retrieveTags() + refreshProgress.completeTask() + + let subscriptions = try await caller.retrieveSubscriptions() + refreshProgress.completeTask() + forceExpireFolderFeedRelationship(account, tags) + + let taggings = try await caller.retrieveTaggings() + + BatchUpdate.shared.perform { + self.syncFolders(account, tags) + self.syncFeeds(account, subscriptions) + self.syncFeedFolderRelationship(account, taggings) + } + + refreshProgress.completeTask() } func refreshArticlesAndStatuses(_ account: Account, completion: @escaping (Result) -> Void) { diff --git a/Account/Sources/Account/FeedbinAPICaller.swift b/Account/Sources/Account/FeedbinAPICaller.swift index 5b36672a1..09b48de15 100644 --- a/Account/Sources/Account/FeedbinAPICaller.swift +++ b/Account/Sources/Account/FeedbinAPICaller.swift @@ -55,119 +55,61 @@ final class FeedbinAPICaller: NSObject { suspended = false } - func validateCredentials(completion: @escaping (Result) -> Void) { - + func validateCredentials() async throws -> Credentials? { + let callURL = feedbinBaseURL.appendingPathComponent("authentication.json") let request = URLRequest(url: callURL, credentials: credentials) - - transport.send(request: request) { result in - - if self.suspended { - completion(.failure(TransportError.suspended)) - return - } - - switch result { - case .success: - completion(.success(self.credentials)) - case .failure(let error): - switch error { - case TransportError.httpError(let status): - if status == 401 { - completion(.success(nil)) - } else { - completion(.failure(error)) - } - default: - completion(.failure(error)) - } + + do { + try await transport.send(request: request) + return credentials + } catch { + if case TransportError.httpError(let status) = error, status == 401 { + return nil } + throw error } - } - - func importOPML(opmlData: Data, completion: @escaping (Result) -> Void) { - + + func importOPML(opmlData: Data) async throws -> FeedbinImportResult { + let callURL = feedbinBaseURL.appendingPathComponent("imports.json") var request = URLRequest(url: callURL, credentials: credentials) request.addValue("text/xml; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) - transport.send(request: request, method: HTTPMethod.post, payload: opmlData) { result in - - if self.suspended { - completion(.failure(TransportError.suspended)) - return - } - - switch result { - case .success(let (_, data)): - - guard let resultData = data else { - completion(.failure(TransportError.noData)) - break - } - - do { - let result = try JSONDecoder().decode(FeedbinImportResult.self, from: resultData) - completion(.success(result)) - } catch { - completion(.failure(error)) - } - - case .failure(let error): - completion(.failure(error)) - } - + let (_, data) = try await transport.send(request: request, method: HTTPMethod.post, payload: opmlData) + guard let data else { + throw TransportError.noData } - + + let parsingTask = Task.detached { () throws -> FeedbinImportResult in + try JSONDecoder().decode(FeedbinImportResult.self, from: data) + } + + let importResult = try await parsingTask.value + return importResult } - - func retrieveOPMLImportResult(importID: Int, completion: @escaping (Result) -> Void) { - + + func retrieveOPMLImportResult(importID: Int) async throws -> FeedbinImportResult? { + let callURL = feedbinBaseURL.appendingPathComponent("imports/\(importID).json") let request = URLRequest(url: callURL, credentials: credentials) - - transport.send(request: request, resultType: FeedbinImportResult.self) { result in - - if self.suspended { - completion(.failure(TransportError.suspended)) - return - } - - switch result { - case .success(let (_, importResult)): - completion(.success(importResult)) - case .failure(let error): - completion(.failure(error)) - } - - } - + + let (_, importResult) = try await transport.send(request: request, resultType: FeedbinImportResult.self) + return importResult } - - func retrieveTags(completion: @escaping (Result<[FeedbinTag]?, Error>) -> Void) { - + + func retrieveTags() async throws -> [FeedbinTag]? { + let callURL = feedbinBaseURL.appendingPathComponent("tags.json") let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.tags] let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) - transport.send(request: request, resultType: [FeedbinTag].self) { result in - - if self.suspended { - completion(.failure(TransportError.suspended)) - return - } - - switch result { - case .success(let (response, tags)): - self.storeConditionalGet(key: ConditionalGetKeys.tags, headers: response.allHeaderFields) - completion(.success(tags)) - case .failure(let error): - completion(.failure(error)) - } - - } - + let (response, tags) = try await transport.send(request: request, resultType: [FeedbinTag].self) + + storeConditionalGet(key: ConditionalGetKeys.tags, headers: response.allHeaderFields) + + return tags } func renameTag(oldName: String, newName: String, completion: @escaping (Result) -> Void) { @@ -190,31 +132,19 @@ final class FeedbinAPICaller: NSObject { } } - func retrieveSubscriptions(completion: @escaping (Result<[FeedbinSubscription]?, Error>) -> Void) { - + func retrieveSubscriptions() async throws -> [FeedbinSubscription]? { + var callComponents = URLComponents(url: feedbinBaseURL.appendingPathComponent("subscriptions.json"), resolvingAgainstBaseURL: false)! callComponents.queryItems = [URLQueryItem(name: "mode", value: "extended")] let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.subscriptions] let request = URLRequest(url: callComponents.url!, credentials: credentials, conditionalGet: conditionalGet) - - transport.send(request: request, resultType: [FeedbinSubscription].self) { result in - - if self.suspended { - completion(.failure(TransportError.suspended)) - return - } - - switch result { - case .success(let (response, subscriptions)): - self.storeConditionalGet(key: ConditionalGetKeys.subscriptions, headers: response.allHeaderFields) - completion(.success(subscriptions)) - case .failure(let error): - completion(.failure(error)) - } - - } - + + let (response, subscriptions) = try await transport.send(request: request, resultType: [FeedbinSubscription].self) + + storeConditionalGet(key: ConditionalGetKeys.subscriptions, headers: response.allHeaderFields) + + return subscriptions } func createSubscription(url: String, completion: @escaping (Result) -> Void) { @@ -334,30 +264,19 @@ final class FeedbinAPICaller: NSObject { } } - func retrieveTaggings(completion: @escaping (Result<[FeedbinTagging]?, Error>) -> Void) { - + func retrieveTaggings() async throws -> [FeedbinTagging]? { + let callURL = feedbinBaseURL.appendingPathComponent("taggings.json") let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.taggings] let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) - - transport.send(request: request, resultType: [FeedbinTagging].self) { result in - if self.suspended { - completion(.failure(TransportError.suspended)) - return - } - switch result { - case .success(let (response, taggings)): - self.storeConditionalGet(key: ConditionalGetKeys.taggings, headers: response.allHeaderFields) - completion(.success(taggings)) - case .failure(let error): - completion(.failure(error)) - } - - } - + let (response, taggings) = try await transport.send(request: request, resultType: [FeedbinTagging].self) + + storeConditionalGet(key: ConditionalGetKeys.taggings, headers: response.allHeaderFields) + + return taggings } - + func createTagging(feedID: Int, name: String, completion: @escaping (Result) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("taggings.json")