using Beam.Abstractions; using Beam.Models; namespace Beam.Downloaders { /// /// A download-managing class that retrieves binary data through , /// applies an , and supports failure detection /// plus exponential-back-off retries. Safe to instantiate per request. /// public class UnitDownloaderBinary( HttpClient client, AsyncTransformer transformer, AsyncDownloadFailurePredicate?[]? failurePredicates = null) : IUnitDownloader { public HttpClient Client { get; } = client; public virtual AsyncTransformer Transformer { get; } = transformer; public virtual AsyncDownloadFailurePredicate?[]? FailurePredicates { get; } = failurePredicates; public int LinksPerDownload { get; } = 1; /// Runs all configured failure predicates in parallel on the raw HTTP response. protected virtual async Task IsFailure(ByteDocument response) { if (FailurePredicates is null) return false; var failed = false; await Parallel.ForEachAsync(FailurePredicates, async (pred, ct) => { if (failed || pred is null) return; if (await pred(response)) failed = true; }); return failed; } /// One attempt without retries or back-off. protected virtual async Task<(bool Success, T? Result)> TryDownloadWithNoRetries(string link, CancellationToken ct) { try { using var response = await Client.GetAsync(link, HttpCompletionOption.ResponseHeadersRead, ct); if (!response.IsSuccessStatusCode) return (false, default); var bytes = await response.Content.ReadAsByteArrayAsync(ct); var doc = new ByteDocument(link, bytes); if (await IsFailure(doc)) return (false, default); return (true, await Transformer(doc)); } catch { return (false, default); } } public async Task<(bool, T?)> TryDownload( IOrdered[] link, CancellationToken ct, int maximumRetryCount = 7, IProgress? tryProgress = null) { if (link.Length == 0) return (false, default); T? result = default; var attempt = 0; while (attempt < maximumRetryCount) { ct.ThrowIfCancellationRequested(); (var success, result) = await TryDownloadWithNoRetries(link[0].Data, ct); if (success && result is not null) return (true, result); ++attempt; tryProgress?.Report(new RetryReport(attempt, link[0].Data)); await Task.Delay((int)Math.Pow(2, attempt) * 1000, ct); } return (false, result); } } }