diff --git a/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift index eb08e774b..c425e2d0a 100644 --- a/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/LocalAccountDelegate.swift @@ -275,25 +275,26 @@ private extension LocalAccountDelegate { // with an Untitled name if we don't delay it being added to the sidebar. BatchUpdate.shared.start() refreshProgress.addToNumberOfTasksAndRemaining(1) - FeedFinder.find(url: url) { result in - - switch result { - case .success(let feedSpecifiers): + + Task { @MainActor in + + do { + let feedSpecifiers = try await FeedFinder.find(url: url) guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), - let url = URL(string: bestFeedSpecifier.urlString) else { - self.refreshProgress.completeTask() - BatchUpdate.shared.end() - completion(.failure(AccountError.createErrorNotFound)) - return + let url = URL(string: bestFeedSpecifier.urlString) else { + self.refreshProgress.completeTask() + BatchUpdate.shared.end() + completion(.failure(AccountError.createErrorNotFound)) + return } - + if account.hasFeed(withURL: bestFeedSpecifier.urlString) { self.refreshProgress.completeTask() BatchUpdate.shared.end() completion(.failure(AccountError.createErrorAlreadySubscribed)) return } - + InitialFeedDownloader.download(url) { parsedFeed in self.refreshProgress.completeTask() @@ -310,10 +311,9 @@ private extension LocalAccountDelegate { BatchUpdate.shared.end() completion(.failure(AccountError.createErrorNotFound)) } - + } - - case .failure: + } catch { BatchUpdate.shared.end() self.refreshProgress.completeTask() completion(.failure(AccountError.createErrorNotFound)) diff --git a/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift index cc9d73f41..dcf6c6992 100644 --- a/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift @@ -389,19 +389,20 @@ public enum ReaderAPIAccountDelegateError: LocalizedError { } func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { - guard let url = URL(string: url) else { - completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) - return - } - - refreshProgress.addToNumberOfTasksAndRemaining(2) - - FeedFinder.find(url: url) { result in - self.refreshProgress.completeTask() - switch result { - case .success(let feedSpecifiers): - let feedSpecifiers = feedSpecifiers.filter { !$0.urlString.contains("json") } + Task { @MainActor in + + guard let url = URL(string: url) else { + completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) + return + } + + refreshProgress.addToNumberOfTasksAndRemaining(2) + + do { + var feedSpecifiers = try await FeedFinder.find(url: url) + self.refreshProgress.completeTask() + feedSpecifiers = feedSpecifiers.filter { !$0.urlString.contains("json") } guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers) else { self.refreshProgress.clear() completion(.failure(AccountError.createErrorNotFound)) @@ -409,32 +410,24 @@ public enum ReaderAPIAccountDelegateError: LocalizedError { } self.caller.createSubscription(url: bestFeedSpecifier.urlString, name: name) { result in - self.refreshProgress.completeTask() - switch result { - case .success(let subResult): - switch subResult { - case .created(let subscription): - self.createFeed(account: account, subscription: subscription, name: name, container: container, completion: completion) - case .notFound: - DispatchQueue.main.async { + Task { @MainActor in + self.refreshProgress.completeTask() + switch result { + case .success(let subResult): + switch subResult { + case .created(let subscription): + self.createFeed(account: account, subscription: subscription, name: name, container: container, completion: completion) + case .notFound: completion(.failure(AccountError.createErrorNotFound)) } - } - case .failure(let error): - DispatchQueue.main.async { + case .failure(let error): let wrappedError = WrappedAccountError(accountID: account.accountID, accountNameForDisplay: account.nameForDisplay, underlyingError: error) completion(.failure(wrappedError)) } } - } - case .failure: - self.refreshProgress.clear() - completion(.failure(AccountError.createErrorNotFound)) } - } - } func renameFeed(for account: Account, feed: Feed, name: String) async throws { diff --git a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift index 8ac652c11..20ec14bb3 100644 --- a/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift +++ b/Account/Sources/Account/CloudKit/CloudKitAccountDelegate.swift @@ -629,11 +629,12 @@ private extension CloudKitAccountDelegate { } refreshProgress.addToNumberOfTasksAndRemaining(5) - FeedFinder.find(url: url) { result in - - self.refreshProgress.completeTask() - switch result { - case .success(let feedSpecifiers): + + Task { @MainActor in + do { + let feedSpecifiers = try await FeedFinder.find(url: url) + self.refreshProgress.completeTask() + guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), let url = URL(string: bestFeedSpecifier.urlString) else { self.refreshProgress.completeTasks(3) if validateFeed { @@ -644,13 +645,13 @@ private extension CloudKitAccountDelegate { } return } - + if account.hasFeed(withURL: bestFeedSpecifier.urlString) { self.refreshProgress.completeTasks(4) completion(.failure(AccountError.createErrorAlreadySubscribed)) return } - + let feed = account.createFeed(with: nil, url: url.absoluteString, feedID: url.absoluteString, homePageURL: nil) feed.editedName = editedName container.addFeed(feed) @@ -662,12 +663,12 @@ private extension CloudKitAccountDelegate { account.update(feed, with: parsedFeed) { result in switch result { case .success: - + self.accountZone.createFeed(url: bestFeedSpecifier.urlString, - name: parsedFeed.title, - editedName: editedName, - homePageURL: parsedFeed.homePageURL, - container: container) { result in + name: parsedFeed.title, + editedName: editedName, + homePageURL: parsedFeed.homePageURL, + container: container) { result in self.refreshProgress.completeTask() switch result { @@ -680,7 +681,7 @@ private extension CloudKitAccountDelegate { self.refreshProgress.completeTasks(2) completion(.failure(error)) } - + } case .failure(let error): @@ -688,18 +689,17 @@ private extension CloudKitAccountDelegate { self.refreshProgress.completeTasks(3) completion(.failure(error)) } - + } } else { self.refreshProgress.completeTasks(3) container.removeFeed(feed) completion(.failure(AccountError.createErrorNotFound)) } - + } - - case .failure: - self.refreshProgress.completeTasks(3) + } catch { + self.refreshProgress.completeTasks(4) if validateFeed { self.refreshProgress.completeTask() completion(.failure(AccountError.createErrorNotFound)) diff --git a/FeedFinder/Sources/FeedFinder/FeedFinder.swift b/FeedFinder/Sources/FeedFinder/FeedFinder.swift index 1a1dc88e4..8865eb4b1 100644 --- a/FeedFinder/Sources/FeedFinder/FeedFinder.swift +++ b/FeedFinder/Sources/FeedFinder/FeedFinder.swift @@ -14,9 +14,27 @@ import AccountError public final class FeedFinder { - public static func find(url: URL, completion: @escaping (Result, Error>) -> Void) { + @MainActor public static func find(url: URL) async throws -> Set { + + try await withCheckedThrowingContinuation { continuation in + + find(url: url) { result in + switch result { + case .success(let feedSpecifiers): + continuation.resume(returning: feedSpecifiers) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } +} + +private extension FeedFinder { + + @MainActor static func find(url: URL, completion: @escaping (Result, Error>) -> Void) { downloadAddingToCache(url) { (data, response, error) in - + if response?.forcedStatusCode == 404 { if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), urlComponents.host == "micro.blog" { urlComponents.path = "\(urlComponents.path).json" @@ -29,39 +47,36 @@ public final class FeedFinder { } return } - + if let error = error { completion(.failure(error)) return } - + guard let data = data, let response = response else { completion(.failure(AccountError.createErrorNotFound)) return } - + if !response.statusIsOK || data.isEmpty { completion(.failure(AccountError.createErrorNotFound)) return } - + if FeedFinder.isFeed(data, url.absoluteString) { let feedSpecifier = FeedSpecifier(title: nil, urlString: url.absoluteString, source: .UserEntered, orderFound: 1) completion(.success(Set([feedSpecifier]))) return } - + if !FeedFinder.isHTML(data) { completion(.failure(AccountError.createErrorNotFound)) return } - + FeedFinder.findFeedsInHTMLPage(htmlData: data, urlString: url.absoluteString, completion: completion) } } -} - -private extension FeedFinder { static func addFeedSpecifier(_ feedSpecifier: FeedSpecifier, feedSpecifiers: inout [String: FeedSpecifier]) { // If there’s an existing feed specifier, merge the two so that we have the best data. If one has a title and one doesn’t, use that non-nil title. Use the better source.