From 018eaa33084da99c4b470fa8d857aed9c0e02cc2 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 14 Sep 2026 16:18:30 +0200 Subject: [PATCH] =?UTF-8?q?Ein=20verpasster=20Bildabruf=20war=20endg=C3=BC?= =?UTF-8?q?ltig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auf einem großen Fenster blieb die zweite Reihe fast vollständig grau, während die erste geladen hatte. Es lag weder am Server noch an fehlenden Bildern: sechzig sichtbare Kacheln teilen sich zwölf Verbindungen, und die hinteren stehen so lange in der Schlange, dass sie in den Zeitablauf laufen. Oben fragt niemand ein zweites Mal — task(id:) läuft einmal je Adresse. Jetzt wird bis zu dreimal gefragt, mit wachsendem Abstand. Aber nur, wo es sich lohnt: ein 404 heißt, dass es das Bild nicht gibt, und dreimal zu fragen macht es nicht vorhanden. Wiederholt werden Zeitabläufe, abgerissene Verbindungen, 5xx und 429. Co-Authored-By: Claude Opus 5 --- .../Components/ArtworkLoader.swift | 55 +++++++++++++++++-- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/Packages/FlixyKit/Sources/FlixyDesign/Components/ArtworkLoader.swift b/Packages/FlixyKit/Sources/FlixyDesign/Components/ArtworkLoader.swift index 4eb85fd..ae6c0ea 100644 --- a/Packages/FlixyKit/Sources/FlixyDesign/Components/ArtworkLoader.swift +++ b/Packages/FlixyKit/Sources/FlixyDesign/Components/ArtworkLoader.swift @@ -70,12 +70,7 @@ public actor ArtworkLoader { if let running = inFlight[url] { return await running.value } let task = Task { [session] in - guard let (data, response) = try? await session.data(from: url), - let http = response as? HTTPURLResponse, - (200..<300).contains(http.statusCode), - let image = PlatformImage(data: data) - else { return nil } - return image + await Self.fetch(url, using: session) } inFlight[url] = task @@ -85,6 +80,54 @@ public actor ArtworkLoader { return image } + /// Holt ein Bild — und gibt nicht beim ersten Nein auf. + /// + /// Der Grund ist ein Fehlerbild, das lange rätselhaft aussah: auf einem + /// großen Fenster blieb die **zweite** Reihe fast vollständig grau, + /// während die erste geladen hatte. Es lag nicht am Server und nicht an + /// fehlenden Bildern, sondern an der Gleichzeitigkeit — sechzig sichtbare + /// Kacheln teilen sich zwölf Verbindungen, die hinteren stehen so lange + /// in der Schlange, dass sie in den Zeitablauf laufen. + /// + /// Oben fragt niemand ein zweites Mal: `task(id:)` läuft einmal je + /// Adresse. Ein einziger verpasster Versuch war damit endgültig. + /// + /// **Nur wiederholt, was sich zu wiederholen lohnt.** Ein 404 heißt, dass + /// es das Bild nicht gibt — dreimal zu fragen macht es nicht vorhanden + /// und kostet den Server drei Antworten. Ein Zeitablauf oder ein + /// abgerissener Strang dagegen sagt nur, dass es *gerade* nicht ging. + private static func fetch( + _ url: URL, + using session: URLSession, + attempts: Int = 3 + ) async -> PlatformImage? { + for attempt in 1...attempts { + if Task.isCancelled { return nil } + + do { + let (data, response) = try await session.data(from: url) + guard let http = response as? HTTPURLResponse else { return nil } + + if (200..<300).contains(http.statusCode) { + return PlatformImage(data: data) + } + // Der Server hat geantwortet und nein gesagt. Bei 5xx und 429 + // lohnt ein zweiter Versuch, sonst nicht. + guard http.statusCode >= 500 || http.statusCode == 429 else { return nil } + } catch is CancellationError { + return nil + } catch { + // Zeitablauf, abgerissene Verbindung: der Fall, für den das + // alles hier gebaut ist. + } + + guard attempt < attempts else { return nil } + // Wachsender Abstand: ist die Leitung gerade verstopft, hilft es + // nicht, sofort noch einmal zu drängeln. + try? await Task.sleep(for: .milliseconds(300 * attempt * attempt)) + } + return nil + } } /// Ein Bildspeicher, der sich von überall lesen lässt.