|
1 | 1 | // https://github.com/dotnet/runtime/blob/v7.0.3/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs |
2 | 2 |
|
3 | 3 | using Fusillade; |
| 4 | +using Punchclock; |
4 | 5 | using Splat; |
| 6 | +using System.Reactive.Subjects; |
| 7 | +using System.Reactive.Threading.Tasks; |
| 8 | + |
5 | 9 | #if ANDROID |
6 | 10 | using HttpHandlerType = Xamarin.Android.Net.AndroidMessageHandler; |
7 | 11 | #elif IOS || MACCATALYST |
@@ -30,6 +34,10 @@ public FusilladeHttpClientFactory(HttpMessageHandler handler) |
30 | 34 | { |
31 | 35 | this.handler = handler; |
32 | 36 | Locator.CurrentMutable.RegisterConstant(handler, typeof(HttpMessageHandler)); |
| 37 | + OperationQueue operationQueue = new(12); |
| 38 | + NetCache.Speculative = new RateLimitedHttpMessageHandler2(handler, Priority.Speculative, 0, 5242880L, opQueue: operationQueue); |
| 39 | + NetCache.UserInitiated = new RateLimitedHttpMessageHandler2(handler, Priority.UserInitiated, opQueue: operationQueue); |
| 40 | + NetCache.Background = new RateLimitedHttpMessageHandler2(handler, Priority.Background, opQueue: operationQueue); |
33 | 41 | } |
34 | 42 |
|
35 | 43 | public static HttpMessageHandler CreateHandler() |
@@ -179,3 +187,229 @@ public void Dispose() |
179 | 187 | GC.SuppressFinalize(this); |
180 | 188 | } |
181 | 189 | } |
| 190 | + |
| 191 | +sealed class InflightRequest(Action onFullyCancelled) |
| 192 | +{ |
| 193 | + int _refCount = 1; |
| 194 | + |
| 195 | + public AsyncSubject<HttpResponseMessage> Response { get; protected set; } |
| 196 | + = new AsyncSubject<HttpResponseMessage>(); |
| 197 | + |
| 198 | + public void AddRef() => Interlocked.Increment(ref _refCount); |
| 199 | + |
| 200 | + public void Cancel() |
| 201 | + { |
| 202 | + if (Interlocked.Decrement(ref _refCount) <= 0) |
| 203 | + { |
| 204 | + onFullyCancelled(); |
| 205 | + } |
| 206 | + } |
| 207 | +} |
| 208 | + |
| 209 | +/// <summary> |
| 210 | +/// A http handler which will limit the rate at which we can read. |
| 211 | +/// </summary> |
| 212 | +/// <remarks> |
| 213 | +/// Initializes a new instance of the <see cref="RateLimitedHttpMessageHandler2"/> class. |
| 214 | +/// </remarks> |
| 215 | +/// <param name="handler">The handler we are wrapping.</param> |
| 216 | +/// <param name="basePriority">The base priority of the request.</param> |
| 217 | +/// <param name="priority">The priority of the request.</param> |
| 218 | +/// <param name="maxBytesToRead">The maximum number of bytes we can read.</param> |
| 219 | +/// <param name="opQueue">The operation queue on which to run the operation.</param> |
| 220 | +/// <param name="cacheResultFunc">A method that is called if we need to get cached results.</param> |
| 221 | +sealed class RateLimitedHttpMessageHandler2(HttpMessageHandler handler, Priority basePriority, int priority = 0, long? maxBytesToRead = null, OperationQueue? opQueue = null, Func<HttpRequestMessage, HttpResponseMessage, string, CancellationToken, Task>? cacheResultFunc = null) : LimitingHttpMessageHandler(handler) |
| 222 | +{ |
| 223 | + readonly int _priority = (int)basePriority + priority; |
| 224 | + readonly Dictionary<string, InflightRequest> _inflightResponses = new(); |
| 225 | + long? _maxBytesToRead = maxBytesToRead; |
| 226 | + |
| 227 | + /// <summary> |
| 228 | + /// Generates a unique key for a <see cref="HttpRequestMessage"/>. |
| 229 | + /// This assists with the caching. |
| 230 | + /// </summary> |
| 231 | + /// <param name="originalRequestUri"></param> |
| 232 | + /// <param name="request">The request to generate a unique key for.</param> |
| 233 | + /// <returns>The unique key.</returns> |
| 234 | + public static string UniqueKeyForRequest( |
| 235 | + string originalRequestUri, |
| 236 | + HttpRequestMessage request) |
| 237 | + { |
| 238 | + // https://github.com/reactiveui/Fusillade/blob/2.4.67/src/Fusillade/RateLimitedHttpMessageHandler.cs#L54-L89 |
| 239 | + |
| 240 | + using var s = new MemoryStream(); |
| 241 | + s.Write(Encoding.UTF8.GetBytes(originalRequestUri)); |
| 242 | + s.Write("\r\n"u8); |
| 243 | + s.Write(Encoding.UTF8.GetBytes(request.Method.Method)); |
| 244 | + s.Write("\r\n"u8); |
| 245 | + static void Write(Stream s, IEnumerable<object> items) |
| 246 | + { |
| 247 | + foreach (var item in items) |
| 248 | + { |
| 249 | + var str = item.ToString(); |
| 250 | + if (!string.IsNullOrEmpty(str)) |
| 251 | + s.Write(Encoding.UTF8.GetBytes(str)); |
| 252 | + s.Write("|"u8); |
| 253 | + } |
| 254 | + } |
| 255 | + Write(s, request.Headers.Accept); |
| 256 | + s.Write("\r\n"u8); |
| 257 | + Write(s, request.Headers.AcceptEncoding); |
| 258 | + s.Write("\r\n"u8); |
| 259 | + var referrer = request.Headers.Referrer; |
| 260 | + if (referrer == default) |
| 261 | + s.Write("http://example"u8); |
| 262 | + else |
| 263 | + s.Write(Encoding.UTF8.GetBytes(referrer.ToString())); |
| 264 | + s.Write("\r\n"u8); |
| 265 | + Write(s, request.Headers.UserAgent); |
| 266 | + s.Write("\r\n"u8); |
| 267 | + if (request.Headers.Authorization != null) |
| 268 | + { |
| 269 | + var parameter = request.Headers.Authorization.Parameter; |
| 270 | + if (!string.IsNullOrEmpty(parameter)) |
| 271 | + s.Write(Encoding.UTF8.GetBytes(parameter)); |
| 272 | + s.Write(Encoding.UTF8.GetBytes(request.Headers.Authorization.Scheme)); |
| 273 | + s.Write("\r\n"u8); |
| 274 | + } |
| 275 | + s.Position = 0; |
| 276 | + var bytes = SHA384.HashData(s); |
| 277 | + var str = bytes.ToHexString(); |
| 278 | + return str; |
| 279 | + } |
| 280 | + |
| 281 | + /// <summary> |
| 282 | + /// Generates a unique key for a <see cref="HttpRequestMessage"/>. |
| 283 | + /// This assists with the caching. |
| 284 | + /// </summary> |
| 285 | + /// <param name="request">The request to generate a unique key for.</param> |
| 286 | + /// <returns>The unique key.</returns> |
| 287 | + public static string UniqueKeyForRequest(HttpRequestMessage request) |
| 288 | + { |
| 289 | + var requestUriString = ImageHttpClientService.ImageHttpRequestMessage.GetOriginalRequestUri(request); |
| 290 | + return UniqueKeyForRequest(requestUriString, request); |
| 291 | + } |
| 292 | + |
| 293 | + /// <inheritdoc /> |
| 294 | + public override void ResetLimit(long? maxBytesToRead = null) => _maxBytesToRead = maxBytesToRead; |
| 295 | + |
| 296 | + /// <inheritdoc /> |
| 297 | + protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) |
| 298 | + { |
| 299 | + if (request is null) |
| 300 | + { |
| 301 | + throw new ArgumentNullException(nameof(request)); |
| 302 | + } |
| 303 | + |
| 304 | + var method = request.Method; |
| 305 | + if (method != HttpMethod.Get && method != HttpMethod.Head && method != HttpMethod.Options) |
| 306 | + { |
| 307 | + return base.SendAsync(request, cancellationToken); |
| 308 | + } |
| 309 | + |
| 310 | + var cacheResult = cacheResultFunc; |
| 311 | + if (cacheResult == null && NetCache.RequestCache != null) |
| 312 | + { |
| 313 | + cacheResult = NetCache.RequestCache.Save; |
| 314 | + } |
| 315 | + |
| 316 | + if (_maxBytesToRead < 0) |
| 317 | + { |
| 318 | + var tcs = new TaskCompletionSource<HttpResponseMessage>(); |
| 319 | +#if NETSTANDARD2_0 |
| 320 | + tcs.SetCanceled(); |
| 321 | +#else |
| 322 | + tcs.SetCanceled(cancellationToken); |
| 323 | +#endif |
| 324 | + return tcs.Task; |
| 325 | + } |
| 326 | + |
| 327 | + var key = UniqueKeyForRequest(request); |
| 328 | + var realToken = new CancellationTokenSource(); |
| 329 | + var ret = new InflightRequest(() => |
| 330 | + { |
| 331 | + lock (_inflightResponses) |
| 332 | + { |
| 333 | + _inflightResponses.Remove(key); |
| 334 | + } |
| 335 | + |
| 336 | + realToken.Cancel(); |
| 337 | + }); |
| 338 | + |
| 339 | + lock (_inflightResponses) |
| 340 | + { |
| 341 | + if (_inflightResponses.TryGetValue(key, out var value)) |
| 342 | + { |
| 343 | + var val = value; |
| 344 | + val.AddRef(); |
| 345 | + cancellationToken.Register(val.Cancel); |
| 346 | + |
| 347 | + return val.Response.ToTask(cancellationToken); |
| 348 | + } |
| 349 | + |
| 350 | + _inflightResponses[key] = ret; |
| 351 | + } |
| 352 | + |
| 353 | + cancellationToken.Register(ret.Cancel); |
| 354 | + |
| 355 | + var queue = new OperationQueue(); |
| 356 | + |
| 357 | + queue.Enqueue( |
| 358 | + _priority, |
| 359 | + null!, |
| 360 | + async () => |
| 361 | + { |
| 362 | + try |
| 363 | + { |
| 364 | + var resp = await base.SendAsync(request, realToken.Token).ConfigureAwait(false); |
| 365 | + |
| 366 | + if (_maxBytesToRead != null && resp.Content?.Headers.ContentLength != null) |
| 367 | + { |
| 368 | + _maxBytesToRead -= resp.Content.Headers.ContentLength; |
| 369 | + } |
| 370 | + |
| 371 | + if (cacheResult != null && resp.Content != null) |
| 372 | + { |
| 373 | + var ms = new MemoryStream(); |
| 374 | +#if NET5_0_OR_GREATER |
| 375 | + var stream = await resp.Content.ReadAsStreamAsync(realToken.Token).ConfigureAwait(false); |
| 376 | +#else |
| 377 | + var stream = await resp.Content.ReadAsStreamAsync().ConfigureAwait(false); |
| 378 | +#endif |
| 379 | + await stream.CopyToAsync(ms, 32 * 1024, realToken.Token).ConfigureAwait(false); |
| 380 | + |
| 381 | + realToken.Token.ThrowIfCancellationRequested(); |
| 382 | + |
| 383 | + var newResp = new HttpResponseMessage(); |
| 384 | + foreach (var kvp in resp.Headers) |
| 385 | + { |
| 386 | + newResp.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value); |
| 387 | + } |
| 388 | + |
| 389 | + var newContent = new ByteArrayContent(ms.ToArray()); |
| 390 | + foreach (var kvp in resp.Content.Headers) |
| 391 | + { |
| 392 | + newContent.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value); |
| 393 | + } |
| 394 | + |
| 395 | + newResp.Content = newContent; |
| 396 | + |
| 397 | + resp = newResp; |
| 398 | + await cacheResult(request, resp, key, realToken.Token).ConfigureAwait(false); |
| 399 | + } |
| 400 | + |
| 401 | + return resp; |
| 402 | + } |
| 403 | + finally |
| 404 | + { |
| 405 | + lock (_inflightResponses) |
| 406 | + { |
| 407 | + _inflightResponses.Remove(key); |
| 408 | + } |
| 409 | + } |
| 410 | + }, |
| 411 | + realToken.Token).ToObservable().Subscribe(ret.Response); |
| 412 | + |
| 413 | + return ret.Response.ToTask(cancellationToken); |
| 414 | + } |
| 415 | +} |
0 commit comments