Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ nonisolated public final class HTMLMetadataDownloader: Sendable {
/// while its lookup runs, or after it found nothing, until `cache` is emptied.
private let urlsLookedUp = Mutex(Set<String>())

/// URLs whose last fetch failed and that aren’t due for a retry yet.
private let unavailableURLs = Mutex(Set<String>())

init() {
NotificationCenter.default.addObserver(self, selector: #selector(handleAppDidGoToBackground(_:)), name: .appDidGoToBackground, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleLowMemory(_:)), name: .lowMemory, object: nil)
Expand Down Expand Up @@ -59,6 +62,11 @@ nonisolated public final class HTMLMetadataDownloader: Sendable {
return nil
}

/// True when the last fetch failed and no retry is pending, so there is no point waiting for
/// metadata. Callers that can proceed without it — a `/favicon.ico` guess, say — should.
public func metadataIsUnavailable(for url: String) -> Bool {
unavailableURLs.withLock { $0.contains(url) }
}
}

// MARK: - Private
Expand Down Expand Up @@ -88,6 +96,7 @@ nonisolated private extension HTMLMetadataDownloader {
}

if await HTMLMetadataDatabase.shared.recentlyFailed(for: url) {
markUnavailable(url)
return
}

Expand Down Expand Up @@ -117,6 +126,20 @@ nonisolated private extension HTMLMetadataDownloader {

func cacheRecord(_ record: HTMLMetadataRecord) {
cache.withLock { $0[record.url] = record }
unavailableURLs.withLock { _ = $0.remove(record.url) }
}

/// Marked before any stale record is returned, so a usable record still wins.
func markUnavailable(_ url: String) {
guard unavailableURLs.withLock({ $0.insert(url).inserted }) else {
return
}
// Observers re-query on this Notification, and metadataIsUnavailable then tells them
// not to keep waiting. There is no record to send.
let userInfo: [String: Any] = [HTMLMetadataUserInfoKey.url: url]
NotificationCenter.default.postOnMainThread(
name: .htmlMetadataAvailable, object: self, userInfo: userInfo
)
}

func downloadMetadataIfNeeded(_ url: String) {
Expand Down Expand Up @@ -180,6 +203,7 @@ nonisolated private extension HTMLMetadataDownloader {
await HTMLMetadataDatabase.shared.noteFailure(url: url, statusCode: statusCode)
}

markUnavailable(url)
// Download failed — try returning stale cached data.
await returnStaleCacheIfAvailable(url)

Expand All @@ -190,6 +214,7 @@ nonisolated private extension HTMLMetadataDownloader {
} catch {
// Pre-response failure (DNS, TLS, network).
await HTMLMetadataDatabase.shared.noteTransientFailure(url: url)
markUnavailable(url)
await returnStaleCacheIfAvailable(url)

activityLog.didFail(.htmlMetadataDownloader, kind: kind, error: error)
Expand Down
24 changes: 19 additions & 5 deletions Modules/Images/Sources/Images/FaviconDownloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,24 +226,38 @@ private extension FaviconDownloader {
}
let faviconURLs = htmlMetadata.usableFaviconURLs() ?? [String]()

guard let scheme = url.scheme, let host = url.host else {
guard let defaultFaviconURL = Self.defaultFaviconURL(for: url) else {
return faviconURLs.isEmpty ? nil : faviconURLs
}

let defaultFaviconURL = "\(scheme)://\(host)/favicon.ico".lowercased(with: FaviconDownloader.localeForLowercasing)
return faviconURLs + [defaultFaviconURL]
}

/// `/favicon.ico` needs no HTML, so it’s still worth a try when the home page can’t be read.
func faviconURLsWithoutMetadata(homePageURL: String) -> [String]? {
guard HTMLMetadataDownloader.shared.metadataIsUnavailable(for: homePageURL), let url = URL(string: homePageURL), let defaultFaviconURL = Self.defaultFaviconURL(for: url) else {
return nil
}
return [defaultFaviconURL]
}

static func defaultFaviconURL(for url: URL) -> String? {
guard let scheme = url.scheme, let host = url.host else {
return nil
}
return "\(scheme)://\(host)/favicon.ico".lowercased(with: localeForLowercasing)
}

func homePageURLsSearching(with faviconURL: String) -> [String] {
searches.filter { $0.value.faviconURL == faviconURL }.map { $0.key }
}

/// Without the home page’s metadata in memory, the search starts when `.htmlMetadataAvailable` arrives.
/// Without the home page’s metadata in memory, the search starts when `.htmlMetadataAvailable` arrives,
/// or tries only `/favicon.ico` if the metadata isn’t coming.
func searchForFavicon(homePageURL: String) {
guard searches[homePageURL] == nil else {
return
}
guard let faviconURLs = findFaviconURLs(with: homePageURL) else {
guard let faviconURLs = findFaviconURLs(with: homePageURL) ?? faviconURLsWithoutMetadata(homePageURL: homePageURL) else {
homePagesAwaitingMetadataForSearch.insert(homePageURL)
return
}
Expand Down
Loading