├── Images └── main.png ├── .github ├── renovate.json └── workflows │ ├── release.yml │ ├── ci-build.yml │ └── lock.yml ├── src ├── Directory.Build.targets ├── Fusillade.Tests │ ├── Http │ │ ├── fixtures │ │ │ └── ResponseWithETag │ │ ├── BaseHttpSchedulerSharedTests.cs │ │ ├── TestHttpMessageHandler.cs │ │ ├── HttpSchedulerCachingTests.cs │ │ └── HttpSchedulerSharedTests.cs │ ├── API │ │ ├── ApiApprovalTests.cs │ │ ├── ApiExtensions.cs │ │ ├── ApiApprovalTests.FusilladeTests.DotNet10_0.verified.txt │ │ ├── ApiApprovalTests.FusilladeTests.DotNet8_0.verified.txt │ │ └── ApiApprovalTests.FusilladeTests.DotNet9_0.verified.txt │ ├── Fusillade.Tests.csproj │ ├── NetCacheTests.cs │ └── IntegrationTestHelper.cs ├── Fusillade │ ├── Fusillade.csproj │ ├── ConcatenateMixin.cs │ ├── InflightRequest.cs │ ├── Priority.cs │ ├── Builder │ │ └── FusilladeSplatBuilderExtensions.cs │ ├── LimitingHttpMessageHandler.cs │ ├── OfflineHttpMessageHandler.cs │ ├── IRequestCache.cs │ ├── NetCache.cs │ └── RateLimitedHttpMessageHandler.cs ├── Directory.Packages.props ├── stylecop.json ├── Directory.Build.props └── Fusillade.sln ├── version.json ├── .gitattributes ├── LICENSE ├── CODE_OF_CONDUCT.md ├── .gitignore ├── CONTRIBUTING.md ├── README.md └── .editorconfig /Images/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactiveui/Fusillade/HEAD/Images/main.png -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["local>reactiveui/.github:renovate"] 4 | } -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(AssemblyName) ($(TargetFramework)) 4 | 5 | 6 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.0", 3 | "publicReleaseRefSpec": [ 4 | "^refs/heads/main", // we release out of master 5 | "^refs/heads/rel/\\d+\\.\\d+\\.\\d+" // we also release branches starting with rel/N.N.N 6 | ], 7 | "nugetPackageVersion":{ 8 | "semVer": 2 9 | }, 10 | "cloudBuild": { 11 | "setVersionVariables": true, 12 | "buildNumber": { 13 | "enabled": false 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Fusillade.Tests/Http/fixtures/ResponseWithETag: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Access-Control-Allow-Origin: * 3 | Content-Type: text/plain; charset=UTF-8 4 | Date: Tue, 24 Dec 2013 03:16:30 GMT 5 | Etag: "12345" 6 | Server: gunicorn/0.17.4 7 | Content-Length: 118 8 | Connection: keep-alive 9 | 10 | { 11 | "ETag": "12345", 12 | "Content-Type": "text/plain; charset=UTF-8", 13 | "Content-Length": "118", 14 | "Server": "httpbin" 15 | } 16 | -------------------------------------------------------------------------------- /src/Fusillade/Fusillade.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0;net8.0;net9.0;net10.0 4 | fusillade 5 | preview 6 | enable 7 | CS8625;CS8604;CS8600;CS8614;CS8603;CS8618;CS8619 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | release: 9 | uses: reactiveui/actions-common/.github/workflows/workflow-common-release.yml@main 10 | with: 11 | configuration: Release 12 | productNamespacePrefix: "Fusillade" 13 | installWorkflows: false 14 | secrets: 15 | SIGN_CLIENT_USER_ID: ${{ secrets.SIGN_CLIENT_USER_ID }} 16 | SIGN_CLIENT_SECRET: ${{ secrets.SIGN_CLIENT_SECRET }} 17 | SIGN_CLIENT_CONFIG: ${{ secrets.SIGN_CLIENT_CONFIG }} 18 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 19 | -------------------------------------------------------------------------------- /.github/workflows/ci-build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # ✅ run on main 7 | pull_request: # (optional) run on PRs targeting any branch 8 | 9 | # Needed so the reusable workflow can optionally delete the temp per-OS artifacts it creates. 10 | permissions: 11 | contents: read 12 | actions: write 13 | 14 | jobs: 15 | build: 16 | uses: reactiveui/actions-common/.github/workflows/workflow-common-setup-and-build.yml@main 17 | with: 18 | configuration: Release 19 | productNamespacePrefix: "Fusilade" 20 | secrets: 21 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 22 | -------------------------------------------------------------------------------- /src/Fusillade/ConcatenateMixin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 ReactiveUI and Contributors. All rights reserved. 2 | // Licensed to ReactiveUI and Contributors under one or more agreements. 3 | // ReactiveUI and Contributors licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for full license information. 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Reactive.Linq; 10 | using System.Text; 11 | 12 | namespace Fusillade; 13 | 14 | internal static class ConcatenateMixin 15 | { 16 | public static string ConcatenateAll(this IEnumerable enumerables, Func selector, char separator = '|') => 17 | enumerables.Aggregate(new StringBuilder(), (acc, x) => 18 | { 19 | acc.Append(selector(x)).Append(separator); 20 | return acc; 21 | }).ToString(); 22 | } 23 | -------------------------------------------------------------------------------- /src/Fusillade.Tests/Http/BaseHttpSchedulerSharedTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. 2 | // Licensed to the .NET Foundation under one or more agreements. 3 | // The .NET Foundation licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for full license information. 5 | 6 | using System.Net.Http; 7 | using Punchclock; 8 | 9 | namespace Fusillade.Tests 10 | { 11 | /// 12 | /// Checks to make sure the base http scheduler works. 13 | /// 14 | public class BaseHttpSchedulerSharedTests : HttpSchedulerSharedTests 15 | { 16 | /// 17 | protected override LimitingHttpMessageHandler CreateFixture(HttpMessageHandler? innerHandler) => 18 | new RateLimitedHttpMessageHandler(innerHandler, Priority.UserInitiated, opQueue: new OperationQueue(4)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock Threads' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | concurrency: 13 | group: lock 14 | 15 | jobs: 16 | action: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: dessant/lock-threads@v5 20 | with: 21 | github-token: ${{ github.token }} 22 | issue-inactive-days: '14' 23 | pr-inactive-days: '14' 24 | issue-comment: > 25 | This issue has been automatically locked since there 26 | has not been any recent activity after it was closed. 27 | Please open a new issue for related bugs. 28 | pr-comment: > 29 | This pull request has been automatically locked since there 30 | has not been any recent activity after it was closed. 31 | Please open a new issue for related bugs. -------------------------------------------------------------------------------- /src/Fusillade/InflightRequest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 ReactiveUI and Contributors. All rights reserved. 2 | // Licensed to ReactiveUI and Contributors under one or more agreements. 3 | // ReactiveUI and Contributors licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for full license information. 5 | 6 | using System; 7 | using System.Net.Http; 8 | using System.Reactive.Subjects; 9 | using System.Threading; 10 | 11 | namespace Fusillade; 12 | 13 | internal class InflightRequest(Action onFullyCancelled) 14 | { 15 | private int _refCount = 1; 16 | 17 | public AsyncSubject Response { get; protected set; } = new AsyncSubject(); 18 | 19 | public void AddRef() => Interlocked.Increment(ref _refCount); 20 | 21 | public void Cancel() 22 | { 23 | if (Interlocked.Decrement(ref _refCount) <= 0) 24 | { 25 | onFullyCancelled(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Catch all for anything we forgot. Add rules if you get CRLF to LF warnings. 2 | * text=auto 3 | 4 | # Text files that should be normalized to LF in odb. 5 | *.cs text eol=lf diff=csharp 6 | *.xaml text 7 | *.config text 8 | *.c text 9 | *.h text 10 | *.cpp text 11 | *.hpp text 12 | 13 | *.sln text 14 | *.csproj text 15 | *.vcxproj text 16 | 17 | *.md text 18 | *.tt text 19 | *.sh text 20 | *.ps1 text 21 | *.cmd text 22 | *.bat text 23 | *.markdown text 24 | *.msbuild text 25 | 26 | 27 | # Binary files that should not be normalized or diffed 28 | *.png binary 29 | *.jpg binary 30 | *.gif binary 31 | *.ico binary 32 | *.rc binary 33 | 34 | *.pfx binary 35 | *.snk binary 36 | *.dll binary 37 | *.exe binary 38 | *.lib binary 39 | *.exp binary 40 | *.pdb binary 41 | *.sdf binary 42 | *.7z binary 43 | 44 | # Generated file should just use CRLF, it's fiiine 45 | SolutionInfo.cs text eol=crlf diff=csharp 46 | -------------------------------------------------------------------------------- /src/Fusillade.Tests/API/ApiApprovalTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. 2 | // Licensed to the .NET Foundation under one or more agreements. 3 | // The .NET Foundation licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for full license information. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Threading.Tasks; 8 | using NUnit.Framework; 9 | 10 | namespace Fusillade.APITests 11 | { 12 | /// 13 | /// Tests for handling API approval. 14 | /// 15 | [ExcludeFromCodeCoverage] 16 | public class ApiApprovalTests 17 | { 18 | /// 19 | /// Tests to make sure the akavache project is approved. 20 | /// 21 | /// A representing the asynchronous unit test. 22 | [Test] 23 | public Task FusilladeTests() => typeof(OfflineHttpMessageHandler).Assembly.CheckApproval(new[] { "Fusillade" }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Fusillade/Priority.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 ReactiveUI and Contributors. All rights reserved. 2 | // Licensed to ReactiveUI and Contributors under one or more agreements. 3 | // ReactiveUI and Contributors licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for full license information. 5 | 6 | namespace Fusillade; 7 | 8 | /// 9 | /// This enumeration defines the default base priorities associated with the 10 | /// different NetCache instances. 11 | /// 12 | public enum Priority 13 | { 14 | /// 15 | /// This is a explicit task. 16 | /// 17 | Explicit = 0, 18 | 19 | /// 20 | /// A speculative priority where we aren't sure. 21 | /// 22 | Speculative = 10, 23 | 24 | /// 25 | /// This is background based task. 26 | /// 27 | Background = 20, 28 | 29 | /// 30 | /// This is a instance which is initiated by the user. 31 | /// 32 | UserInitiated = 100, 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Anaïs Betts 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /src/Fusillade/Builder/FusilladeSplatBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 ReactiveUI and Contributors. All rights reserved. 2 | // Licensed to ReactiveUI and Contributors under one or more agreements. 3 | // ReactiveUI and Contributors licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for full license information. 5 | 6 | using System; 7 | using Fusillade; 8 | 9 | namespace Splat.Builder; 10 | 11 | /// 12 | /// Splat module for configuring Fusillade. 13 | /// 14 | public static class FusilladeSplatBuilderExtensions 15 | { 16 | /// 17 | /// Creates the fusillade net cache. 18 | /// 19 | /// The builder. 20 | /// The App Instance for Chaining. 21 | public static IAppInstance CreateFusilladeNetCache(this IAppInstance builder) 22 | { 23 | if (builder is null) 24 | { 25 | throw new ArgumentNullException(nameof(builder)); 26 | } 27 | 28 | NetCache.CreateDefaultInstances(builder.Current); 29 | return builder; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Fusillade.Tests/Fusillade.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0;net10.0 4 | $(NoWarn);1591;CA1707;SA1633 5 | false 6 | $(NoWarn);CA2000;CA1031;CA1307;CA1305 7 | preview 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Fusillade.Tests/NetCacheTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. 2 | // Licensed to the .NET Foundation under one or more agreements. 3 | // The .NET Foundation licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for full license information. 5 | 6 | using NUnit.Framework; // switched from xUnit 7 | 8 | namespace Fusillade.Tests 9 | { 10 | /// 11 | /// Checks to make sure that the NetCache operates correctly. 12 | /// 13 | [TestFixture] 14 | [NonParallelizable] // replaces CollectionDefinition DisableParallelization 15 | public class NetCacheTests 16 | { 17 | /// 18 | /// Verifies that we are registering the default handlers correctly. 19 | /// 20 | [Test] 21 | public void DefaultValuesShouldBeRegistered() 22 | { 23 | using (Assert.EnterMultipleScope()) 24 | { 25 | Assert.That(NetCache.Speculative, Is.Not.Null); 26 | Assert.That(NetCache.UserInitiated, Is.Not.Null); 27 | Assert.That(NetCache.Background, Is.Not.Null); 28 | Assert.That(NetCache.Offline, Is.Not.Null); 29 | Assert.That(NetCache.OperationQueue, Is.Not.Null); 30 | Assert.That(NetCache.RequestCache, Is.Null); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Fusillade.Tests/Http/TestHttpMessageHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. 2 | // Licensed to the .NET Foundation under one or more agreements. 3 | // The .NET Foundation licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for full license information. 5 | 6 | using System; 7 | using System.Net.Http; 8 | using System.Reactive.Linq; 9 | using System.Reactive.Threading.Tasks; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace Fusillade.Tests 14 | { 15 | /// 16 | /// Tests the main http scheduler. 17 | /// 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// Creates a http response. 22 | public class TestHttpMessageHandler(Func> createResult) : HttpMessageHandler 23 | { 24 | /// 25 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 26 | { 27 | if (cancellationToken.IsCancellationRequested) 28 | { 29 | return Observable.Throw(new OperationCanceledException()).ToTask(); 30 | } 31 | 32 | return createResult(request).ToTask(cancellationToken); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "indentation": { 5 | "useTabs": false, 6 | "indentationSize": 4 7 | }, 8 | "documentationRules": { 9 | "documentExposedElements": true, 10 | "documentInternalElements": false, 11 | "documentPrivateElements": false, 12 | "documentInterfaces": true, 13 | "documentPrivateFields": false, 14 | "documentationCulture": "en-US", 15 | "companyName": "ReactiveUI and Contributors", 16 | "copyrightText": "Copyright (c) 2025 {companyName}. All rights reserved.\nLicensed to ReactiveUI and Contributors under one or more agreements.\nReactiveUI and Contributors licenses this file to you under the {licenseName} license.\nSee the {licenseFile} file in the project root for full license information.", 17 | "variables": { 18 | "licenseName": "MIT", 19 | "licenseFile": "LICENSE" 20 | }, 21 | "xmlHeader": false 22 | }, 23 | "layoutRules": { 24 | "newlineAtEndOfFile": "allow", 25 | "allowConsecutiveUsings": true 26 | }, 27 | "maintainabilityRules": { 28 | "topLevelTypes": [ 29 | "class", 30 | "interface", 31 | "struct", 32 | "enum", 33 | "delegate" 34 | ] 35 | }, 36 | "orderingRules": { 37 | "usingDirectivesPlacement": "outsideNamespace", 38 | "systemUsingDirectivesFirst": true 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Fusillade/LimitingHttpMessageHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 ReactiveUI and Contributors. All rights reserved. 2 | // Licensed to ReactiveUI and Contributors under one or more agreements. 3 | // ReactiveUI and Contributors licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for full license information. 5 | 6 | using System.Net.Http; 7 | 8 | namespace Fusillade; 9 | 10 | /// 11 | /// Limiting HTTP schedulers only allow a certain number of bytes to be 12 | /// read before cancelling all future requests. This is designed for 13 | /// reading data that may or may not be used by the user later, in order 14 | /// to improve response times should the user later request the data. 15 | /// 16 | public abstract class LimitingHttpMessageHandler : DelegatingHandler 17 | { 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// A inner handler we will call to get the data. 22 | protected LimitingHttpMessageHandler(HttpMessageHandler? innerHandler) 23 | : base(innerHandler!) 24 | { 25 | } 26 | 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | protected LimitingHttpMessageHandler() 31 | { 32 | } 33 | 34 | /// 35 | /// Resets the total limit of bytes to read. This is usually called 36 | /// when the app resumes from suspend, to indicate that we should 37 | /// fetch another set of data. 38 | /// 39 | /// The maximum number of bytes to read. 40 | public abstract void ResetLimit(long? maxBytesToRead = null); 41 | } 42 | -------------------------------------------------------------------------------- /src/Fusillade/OfflineHttpMessageHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 ReactiveUI and Contributors. All rights reserved. 2 | // Licensed to ReactiveUI and Contributors under one or more agreements. 3 | // ReactiveUI and Contributors licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for full license information. 5 | 6 | using System; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace Fusillade; 13 | 14 | /// 15 | /// A http handler that will make a response even if the HttpClient is offline. 16 | /// 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// A function that will retrieve a body. 21 | public class OfflineHttpMessageHandler(Func>? retrieveBodyFunc) : HttpMessageHandler 22 | { 23 | /// 24 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 25 | { 26 | var retrieveBody = retrieveBodyFunc; 27 | if (retrieveBody == null && NetCache.RequestCache != null) 28 | { 29 | retrieveBody = NetCache.RequestCache.Fetch; 30 | } 31 | 32 | if (retrieveBody == null) 33 | { 34 | throw new Exception("Configure NetCache.RequestCache before calling this!"); 35 | } 36 | 37 | var body = await retrieveBody(request, RateLimitedHttpMessageHandler.UniqueKeyForRequest(request), cancellationToken).ConfigureAwait(false); 38 | if (body == null) 39 | { 40 | return new HttpResponseMessage(HttpStatusCode.ServiceUnavailable); 41 | } 42 | 43 | var byteContent = new ByteArrayContent(body); 44 | return new HttpResponseMessage(HttpStatusCode.OK) { Content = byteContent }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Fusillade/IRequestCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 ReactiveUI and Contributors. All rights reserved. 2 | // Licensed to ReactiveUI and Contributors under one or more agreements. 3 | // ReactiveUI and Contributors licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for full license information. 5 | 6 | using System.Net.Http; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace Fusillade; 11 | 12 | /// 13 | /// This Interface is a simple cache for HTTP requests - it is intentionally 14 | /// *not* designed to conform to HTTP caching rules since you most likely want 15 | /// to override those rules in a client app anyways. 16 | /// 17 | public interface IRequestCache 18 | { 19 | /// 20 | /// Implement this method by saving the Body of the response. The 21 | /// response is already downloaded as a ByteArrayContent so you don't 22 | /// have to worry about consuming the stream. 23 | /// 24 | /// The originating request. 25 | /// The response whose body you should save. 26 | /// A unique key used to identify the request details. 27 | /// Cancellation token. 28 | /// Completion. 29 | Task Save(HttpRequestMessage request, HttpResponseMessage response, string key, CancellationToken ct); 30 | 31 | /// 32 | /// Implement this by loading the Body of the given request / key. 33 | /// 34 | /// The originating request. 35 | /// A unique key used to identify the request details, 36 | /// that was given in Save(). 37 | /// Cancellation token. 38 | /// The Body of the given request, or null if the search 39 | /// completed successfully but the response was not found. 40 | Task Fetch(HttpRequestMessage request, string key, CancellationToken ct); 41 | } 42 | -------------------------------------------------------------------------------- /src/Fusillade.Tests/API/ApiExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. 2 | // Licensed to the .NET Foundation under one or more agreements. 3 | // The .NET Foundation licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for full license information. 5 | 6 | using System; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Reflection; 9 | using System.Runtime.CompilerServices; 10 | using System.Threading.Tasks; 11 | using NUnit.Framework; // add for NUnit attributes if needed later 12 | using PublicApiGenerator; 13 | using VerifyNUnit; // changed from VerifyXunit 14 | 15 | namespace Fusillade.APITests; 16 | 17 | /// 18 | /// A helper for doing API approvals. 19 | /// 20 | public static class ApiExtensions 21 | { 22 | /// 23 | /// Checks to make sure the API is approved. 24 | /// 25 | /// The assembly that is being checked. 26 | /// The namespaces. 27 | /// The caller file path. 28 | /// 29 | /// A Task. 30 | /// 31 | public static async Task CheckApproval(this Assembly assembly, string[] namespaces, [CallerFilePath] string filePath = "") 32 | { 33 | var generatorOptions = new ApiGeneratorOptions { AllowNamespacePrefixes = namespaces }; 34 | var apiText = assembly.GeneratePublicApi(generatorOptions); 35 | var result = await Verifier.Verify(apiText, null, filePath) 36 | .UniqueForRuntimeAndVersion() 37 | .ScrubEmptyLines() 38 | .ScrubLines(l => 39 | l.StartsWith("[assembly: AssemblyVersion(", StringComparison.InvariantCulture) || 40 | l.StartsWith("[assembly: AssemblyFileVersion(", StringComparison.InvariantCulture) || 41 | l.StartsWith("[assembly: AssemblyInformationalVersion(", StringComparison.InvariantCulture) || 42 | l.StartsWith("[assembly: System.Reflection.AssemblyMetadata(", StringComparison.InvariantCulture)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (c) ReactiveUI and Contributors 5 | ReactiveUI and Contributors 6 | https://github.com/reactiveui/fusillade/ 7 | https://github.com/reactiveui/fusillade/releases 8 | https://github.com/reactiveui/fusillade 9 | 10 | $(NoWarn);VSX1000 11 | AnyCPU 12 | $(MSBuildProjectName.Contains('Tests')) 13 | git 14 | true 15 | $(MSBuildThisFileDirectory)analyzers.ruleset 16 | Embedded 17 | main.png 18 | README.md 19 | MIT 20 | true 21 | true 22 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 23 | True 24 | latest 25 | $(WarningsAsErrors);nullable 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at anais@anaisbetts.org. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ 51 | -------------------------------------------------------------------------------- /src/Fusillade.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 18 4 | VisualStudioVersion = 18.0.11205.157 d18.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fusillade", "Fusillade\Fusillade.csproj", "{26493C47-6A4A-4F2A-9F92-046AA8CD95CC}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fusillade.Tests", "Fusillade.Tests\Fusillade.Tests.csproj", "{BA0745E4-4566-4655-B83C-B4398F67DC39}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6B92A58B-DD1B-4AFF-A3CA-D51CBEB7B43D}" 11 | ProjectSection(SolutionItems) = preProject 12 | ..\.gitattributes = ..\.gitattributes 13 | ..\.gitignore = ..\.gitignore 14 | analyzers.ruleset = analyzers.ruleset 15 | ..\.github\workflows\ci-build.yml = ..\.github\workflows\ci-build.yml 16 | ..\CODE_OF_CONDUCT.md = ..\CODE_OF_CONDUCT.md 17 | ..\CONTRIBUTING.md = ..\CONTRIBUTING.md 18 | Directory.build.props = Directory.build.props 19 | Directory.build.targets = Directory.build.targets 20 | Directory.Packages.props = Directory.Packages.props 21 | ..\LICENSE = ..\LICENSE 22 | ..\README.md = ..\README.md 23 | ..\.github\workflows\release.yml = ..\.github\workflows\release.yml 24 | stylecop.json = stylecop.json 25 | ..\version.json = ..\version.json 26 | EndProjectSection 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {26493C47-6A4A-4F2A-9F92-046AA8CD95CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {26493C47-6A4A-4F2A-9F92-046AA8CD95CC}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {26493C47-6A4A-4F2A-9F92-046AA8CD95CC}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {26493C47-6A4A-4F2A-9F92-046AA8CD95CC}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {BA0745E4-4566-4655-B83C-B4398F67DC39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {BA0745E4-4566-4655-B83C-B4398F67DC39}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {BA0745E4-4566-4655-B83C-B4398F67DC39}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {BA0745E4-4566-4655-B83C-B4398F67DC39}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {96AB3D31-3E93-4E65-90D6-571A60C269E0} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /src/Fusillade.Tests/API/ApiApprovalTests.FusilladeTests.DotNet10_0.verified.txt: -------------------------------------------------------------------------------- 1 | [assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v10.0", FrameworkDisplayName=".NET 10.0")] 2 | namespace Fusillade 3 | { 4 | public interface IRequestCache 5 | { 6 | System.Threading.Tasks.Task Fetch(System.Net.Http.HttpRequestMessage request, string key, System.Threading.CancellationToken ct); 7 | System.Threading.Tasks.Task Save(System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpResponseMessage response, string key, System.Threading.CancellationToken ct); 8 | } 9 | public abstract class LimitingHttpMessageHandler : System.Net.Http.DelegatingHandler 10 | { 11 | protected LimitingHttpMessageHandler() { } 12 | protected LimitingHttpMessageHandler(System.Net.Http.HttpMessageHandler? innerHandler) { } 13 | public abstract void ResetLimit(long? maxBytesToRead = default); 14 | } 15 | public static class NetCache 16 | { 17 | public static System.Net.Http.HttpMessageHandler Background { get; set; } 18 | public static System.Net.Http.HttpMessageHandler Offline { get; set; } 19 | public static Punchclock.OperationQueue OperationQueue { get; set; } 20 | public static Fusillade.IRequestCache? RequestCache { get; set; } 21 | public static Fusillade.LimitingHttpMessageHandler Speculative { get; set; } 22 | public static System.Net.Http.HttpMessageHandler UserInitiated { get; set; } 23 | } 24 | public class OfflineHttpMessageHandler : System.Net.Http.HttpMessageHandler 25 | { 26 | public OfflineHttpMessageHandler(System.Func>? retrieveBodyFunc) { } 27 | protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } 28 | } 29 | public enum Priority 30 | { 31 | Explicit = 0, 32 | Speculative = 10, 33 | Background = 20, 34 | UserInitiated = 100, 35 | } 36 | public class RateLimitedHttpMessageHandler : Fusillade.LimitingHttpMessageHandler 37 | { 38 | public RateLimitedHttpMessageHandler(System.Net.Http.HttpMessageHandler? handler, Fusillade.Priority basePriority, int priority = 0, long? maxBytesToRead = default, Punchclock.OperationQueue? opQueue = null, System.Func? cacheResultFunc = null) { } 39 | public override void ResetLimit(long? maxBytesToRead = default) { } 40 | protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } 41 | public static string UniqueKeyForRequest(System.Net.Http.HttpRequestMessage request) { } 42 | } 43 | } 44 | namespace Splat.Builder 45 | { 46 | public static class FusilladeSplatBuilderExtensions 47 | { 48 | public static Splat.Builder.IAppInstance CreateFusilladeNetCache(this Splat.Builder.IAppInstance builder) { } 49 | } 50 | } -------------------------------------------------------------------------------- /src/Fusillade.Tests/API/ApiApprovalTests.FusilladeTests.DotNet8_0.verified.txt: -------------------------------------------------------------------------------- 1 | [assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v8.0", FrameworkDisplayName=".NET 8.0")] 2 | namespace Fusillade 3 | { 4 | public interface IRequestCache 5 | { 6 | System.Threading.Tasks.Task Fetch(System.Net.Http.HttpRequestMessage request, string key, System.Threading.CancellationToken ct); 7 | System.Threading.Tasks.Task Save(System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpResponseMessage response, string key, System.Threading.CancellationToken ct); 8 | } 9 | public abstract class LimitingHttpMessageHandler : System.Net.Http.DelegatingHandler 10 | { 11 | protected LimitingHttpMessageHandler() { } 12 | protected LimitingHttpMessageHandler(System.Net.Http.HttpMessageHandler? innerHandler) { } 13 | public abstract void ResetLimit(long? maxBytesToRead = default); 14 | } 15 | public static class NetCache 16 | { 17 | public static System.Net.Http.HttpMessageHandler Background { get; set; } 18 | public static System.Net.Http.HttpMessageHandler Offline { get; set; } 19 | public static Punchclock.OperationQueue OperationQueue { get; set; } 20 | public static Fusillade.IRequestCache? RequestCache { get; set; } 21 | public static Fusillade.LimitingHttpMessageHandler Speculative { get; set; } 22 | public static System.Net.Http.HttpMessageHandler UserInitiated { get; set; } 23 | } 24 | public class OfflineHttpMessageHandler : System.Net.Http.HttpMessageHandler 25 | { 26 | public OfflineHttpMessageHandler(System.Func>? retrieveBodyFunc) { } 27 | protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } 28 | } 29 | public enum Priority 30 | { 31 | Explicit = 0, 32 | Speculative = 10, 33 | Background = 20, 34 | UserInitiated = 100, 35 | } 36 | public class RateLimitedHttpMessageHandler : Fusillade.LimitingHttpMessageHandler 37 | { 38 | public RateLimitedHttpMessageHandler(System.Net.Http.HttpMessageHandler? handler, Fusillade.Priority basePriority, int priority = 0, long? maxBytesToRead = default, Punchclock.OperationQueue? opQueue = null, System.Func? cacheResultFunc = null) { } 39 | public override void ResetLimit(long? maxBytesToRead = default) { } 40 | protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } 41 | public static string UniqueKeyForRequest(System.Net.Http.HttpRequestMessage request) { } 42 | } 43 | } 44 | namespace Splat.Builder 45 | { 46 | public static class FusilladeSplatBuilderExtensions 47 | { 48 | public static Splat.Builder.IAppInstance CreateFusilladeNetCache(this Splat.Builder.IAppInstance builder) { } 49 | } 50 | } -------------------------------------------------------------------------------- /src/Fusillade.Tests/API/ApiApprovalTests.FusilladeTests.DotNet9_0.verified.txt: -------------------------------------------------------------------------------- 1 | [assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v9.0", FrameworkDisplayName=".NET 9.0")] 2 | namespace Fusillade 3 | { 4 | public interface IRequestCache 5 | { 6 | System.Threading.Tasks.Task Fetch(System.Net.Http.HttpRequestMessage request, string key, System.Threading.CancellationToken ct); 7 | System.Threading.Tasks.Task Save(System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpResponseMessage response, string key, System.Threading.CancellationToken ct); 8 | } 9 | public abstract class LimitingHttpMessageHandler : System.Net.Http.DelegatingHandler 10 | { 11 | protected LimitingHttpMessageHandler() { } 12 | protected LimitingHttpMessageHandler(System.Net.Http.HttpMessageHandler? innerHandler) { } 13 | public abstract void ResetLimit(long? maxBytesToRead = default); 14 | } 15 | public static class NetCache 16 | { 17 | public static System.Net.Http.HttpMessageHandler Background { get; set; } 18 | public static System.Net.Http.HttpMessageHandler Offline { get; set; } 19 | public static Punchclock.OperationQueue OperationQueue { get; set; } 20 | public static Fusillade.IRequestCache? RequestCache { get; set; } 21 | public static Fusillade.LimitingHttpMessageHandler Speculative { get; set; } 22 | public static System.Net.Http.HttpMessageHandler UserInitiated { get; set; } 23 | } 24 | public class OfflineHttpMessageHandler : System.Net.Http.HttpMessageHandler 25 | { 26 | public OfflineHttpMessageHandler(System.Func>? retrieveBodyFunc) { } 27 | protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } 28 | } 29 | public enum Priority 30 | { 31 | Explicit = 0, 32 | Speculative = 10, 33 | Background = 20, 34 | UserInitiated = 100, 35 | } 36 | public class RateLimitedHttpMessageHandler : Fusillade.LimitingHttpMessageHandler 37 | { 38 | public RateLimitedHttpMessageHandler(System.Net.Http.HttpMessageHandler? handler, Fusillade.Priority basePriority, int priority = 0, long? maxBytesToRead = default, Punchclock.OperationQueue? opQueue = null, System.Func? cacheResultFunc = null) { } 39 | public override void ResetLimit(long? maxBytesToRead = default) { } 40 | protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } 41 | public static string UniqueKeyForRequest(System.Net.Http.HttpRequestMessage request) { } 42 | } 43 | } 44 | namespace Splat.Builder 45 | { 46 | public static class FusilladeSplatBuilderExtensions 47 | { 48 | public static Splat.Builder.IAppInstance CreateFusilladeNetCache(this Splat.Builder.IAppInstance builder) { } 49 | } 50 | } -------------------------------------------------------------------------------- /src/Fusillade.Tests/IntegrationTestHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. 2 | // Licensed to the .NET Foundation under one or more agreements. 3 | // The .NET Foundation licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for full license information. 5 | 6 | using System; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Net; 10 | using System.Net.Http; 11 | using System.Reactive.Linq; 12 | using System.Text; 13 | 14 | namespace Fusillade.Tests 15 | { 16 | /// 17 | /// A helper for performing integration tests. 18 | /// 19 | public static class IntegrationTestHelper 20 | { 21 | /// 22 | /// Combines together paths together and then gets a full path. 23 | /// 24 | /// The paths to combine. 25 | /// The string path. 26 | public static string GetPath(params string[] paths) 27 | { 28 | var ret = GetIntegrationTestRootDirectory(); 29 | return new FileInfo(paths.Aggregate(ret, Path.Combine)).FullName; 30 | } 31 | 32 | /// 33 | /// Gets the root directory for the integration test. 34 | /// 35 | /// The path. 36 | public static string GetIntegrationTestRootDirectory() 37 | { 38 | // XXX: This is an evil hack, but it's okay for a unit test 39 | // We can't use Assembly.Location because unit test runners love 40 | // to move stuff to temp directories 41 | return Directory.GetParent(Directory.GetCurrentDirectory())!.Parent!.FullName; 42 | } 43 | 44 | /// 45 | /// Creates a response from a sample file with the data. 46 | /// 47 | /// The path to the file. 48 | /// The generated response. 49 | public static HttpResponseMessage GetResponse(params string[] paths) 50 | { 51 | var bytes = File.ReadAllBytes(GetPath(paths)); 52 | 53 | // Find the body 54 | var bodyIndex = -1; 55 | for (bodyIndex = 0; bodyIndex < bytes.Length - 3; bodyIndex++) 56 | { 57 | if (bytes[bodyIndex] != 0x0D || bytes[bodyIndex + 1] != 0x0A || 58 | bytes[bodyIndex + 2] != 0x0D || bytes[bodyIndex + 3] != 0x0A) 59 | { 60 | continue; 61 | } 62 | 63 | goto foundIt; 64 | } 65 | 66 | throw new Exception("Couldn't find response body"); 67 | 68 | foundIt: 69 | 70 | var headerText = Encoding.UTF8.GetString(bytes, 0, bodyIndex); 71 | var lines = headerText.Split('\n'); 72 | var statusCode = (HttpStatusCode)int.Parse(lines[0].Split(' ')[1]); 73 | var ret = new HttpResponseMessage(statusCode); 74 | 75 | ret.Content = new ByteArrayContent(bytes, bodyIndex + 2, bytes.Length - bodyIndex - 2); 76 | 77 | foreach (var line in lines.Skip(1)) 78 | { 79 | var separatorIndex = line.IndexOf(':'); 80 | var key = line.Substring(0, separatorIndex); 81 | var val = line.Substring(separatorIndex + 2).TrimEnd(); 82 | 83 | if (string.IsNullOrWhiteSpace(line)) 84 | { 85 | continue; 86 | } 87 | 88 | ret.Headers.TryAddWithoutValidation(key, val); 89 | ret.Content.Headers.TryAddWithoutValidation(key, val); 90 | } 91 | 92 | return ret; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | *.lock.json 45 | artifacts/ 46 | *.nuget.props 47 | *.nuget.targets 48 | 49 | *_i.c 50 | *_p.c 51 | *_i.h 52 | *.ilk 53 | *.meta 54 | *.obj 55 | *.pch 56 | *.pdb 57 | *.pgc 58 | *.pgd 59 | *.rsp 60 | *.sbr 61 | *.tlb 62 | *.tli 63 | *.tlh 64 | *.tmp 65 | *.tmp_proj 66 | *.log 67 | *.vspscc 68 | *.vssscc 69 | .builds 70 | *.pidb 71 | *.svclog 72 | *.scc 73 | 74 | # Chutzpah Test files 75 | _Chutzpah* 76 | 77 | # Visual C++ cache files 78 | ipch/ 79 | *.aps 80 | *.ncb 81 | *.opendb 82 | *.opensdf 83 | *.sdf 84 | *.cachefile 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # NuGet Packages 149 | *.nupkg 150 | # The packages folder can be ignored because of Package Restore 151 | **/packages/* 152 | # except build/, which is used as an MSBuild target. 153 | !**/packages/build/ 154 | # Uncomment if necessary however generally it will be regenerated when needed 155 | #!**/packages/repositories.config 156 | 157 | # Windows Azure Build Output 158 | csx/ 159 | *.build.csdef 160 | 161 | # Windows Azure Emulator 162 | ecf/ 163 | rcf/ 164 | 165 | # Windows Store app package directory 166 | AppPackages/ 167 | BundleArtifacts/ 168 | 169 | # Visual Studio cache files 170 | # files ending in .cache can be ignored 171 | *.[Cc]ache 172 | # but keep track of directories ending in .cache 173 | !*.[Cc]ache/ 174 | 175 | # Others 176 | ClientBin/ 177 | ~$* 178 | *~ 179 | *.dbmdl 180 | *.dbproj.schemaview 181 | *.pfx 182 | *.publishsettings 183 | node_modules/ 184 | orleans.codegen.cs 185 | 186 | # RIA/Silverlight projects 187 | Generated_Code/ 188 | 189 | # Backup & report files from converting an old project file 190 | # to a newer Visual Studio version. Backup files are not needed, 191 | # because we have git ;-) 192 | _UpgradeReport_Files/ 193 | Backup*/ 194 | UpgradeLog*.XML 195 | UpgradeLog*.htm 196 | 197 | # SQL Server files 198 | *.mdf 199 | *.ldf 200 | 201 | # Business Intelligence projects 202 | *.rdl.data 203 | *.bim.layout 204 | *.bim_*.settings 205 | 206 | # Microsoft Fakes 207 | FakesAssemblies/ 208 | 209 | # GhostDoc plugin setting file 210 | *.GhostDoc.xml 211 | 212 | # Node.js Tools for Visual Studio 213 | .ntvs_analysis.dat 214 | 215 | # Visual Studio 6 build log 216 | *.plg 217 | 218 | # Visual Studio 6 workspace options file 219 | *.opt 220 | 221 | # Visual Studio LightSwitch build output 222 | **/*.HTMLClient/GeneratedArtifacts 223 | **/*.DesktopClient/GeneratedArtifacts 224 | **/*.DesktopClient/ModelManifest.xml 225 | **/*.Server/GeneratedArtifacts 226 | **/*.Server/ModelManifest.xml 227 | _Pvt_Extensions 228 | 229 | # Paket dependency manager 230 | .paket/paket.exe 231 | 232 | # FAKE - F# Make 233 | .fake/ 234 | 235 | # Tools 236 | tools/ 237 | 238 | # ReactiveUI 239 | artifacts/ 240 | src/CommonAssemblyInfo.cs 241 | src/ReactiveUI.Events/Events_*.cs 242 | src/Fusillade.Tests/API/ApiApprovalTests.*.received.txt 243 | -------------------------------------------------------------------------------- /src/Fusillade/NetCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 ReactiveUI and Contributors. All rights reserved. 2 | // Licensed to ReactiveUI and Contributors under one or more agreements. 3 | // ReactiveUI and Contributors licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for full license information. 5 | 6 | using System; 7 | using System.Net.Http; 8 | using Punchclock; 9 | using Splat; 10 | 11 | namespace Fusillade; 12 | 13 | /// 14 | /// Handles caching for our Http requests. 15 | /// 16 | public static class NetCache 17 | { 18 | private static LimitingHttpMessageHandler speculative; 19 | [ThreadStatic] 20 | private static LimitingHttpMessageHandler? unitTestSpeculative; 21 | private static HttpMessageHandler userInitiated; 22 | [ThreadStatic] 23 | private static HttpMessageHandler? unitTestUserInitiated; 24 | private static HttpMessageHandler background; 25 | [ThreadStatic] 26 | private static HttpMessageHandler? unitTestBackground; 27 | private static HttpMessageHandler offline; 28 | [ThreadStatic] 29 | private static HttpMessageHandler? unitTestOffline; 30 | private static OperationQueue operationQueue = new(4); 31 | [ThreadStatic] 32 | private static OperationQueue? unitTestOperationQueue; 33 | private static IRequestCache? requestCache; 34 | [ThreadStatic] 35 | private static IRequestCache? unitTestRequestCache; 36 | private static IReadonlyDependencyResolver? Current; 37 | 38 | /// 39 | /// Initializes static members of the class. 40 | /// 41 | static NetCache() 42 | { 43 | var innerHandler = GetCurrent().GetService() ?? new HttpClientHandler(); 44 | 45 | // NB: In vNext this value will be adjusted based on the user's 46 | // network connection, but that requires us to go fully platformy 47 | // like Splat. 48 | speculative = new RateLimitedHttpMessageHandler(innerHandler, Priority.Speculative, 0, 1048576 * 5); 49 | userInitiated = new RateLimitedHttpMessageHandler(innerHandler, Priority.UserInitiated, 0); 50 | background = new RateLimitedHttpMessageHandler(innerHandler, Priority.Background, 0); 51 | offline = new OfflineHttpMessageHandler(null); 52 | } 53 | 54 | /// 55 | /// Gets or sets a handler of that allow a certain number of bytes to be 56 | /// read before cancelling all future requests. This is designed for 57 | /// reading data that may or may not be used by the user later, in order 58 | /// to improve response times should the user later request the data. 59 | /// 60 | public static LimitingHttpMessageHandler Speculative 61 | { 62 | get => unitTestSpeculative ?? GetCurrent().GetService("Speculative") ?? speculative; 63 | set 64 | { 65 | if (ModeDetector.InUnitTestRunner()) 66 | { 67 | unitTestSpeculative = value; 68 | speculative ??= value; 69 | } 70 | else 71 | { 72 | speculative = value; 73 | } 74 | } 75 | } 76 | 77 | /// 78 | /// Gets or sets a scheduler that should be used for requests initiated by a user 79 | /// action such as clicking an item, they have the highest priority. 80 | /// 81 | public static HttpMessageHandler UserInitiated 82 | { 83 | get => unitTestUserInitiated ?? GetCurrent().GetService("UserInitiated") ?? userInitiated; 84 | set 85 | { 86 | if (ModeDetector.InUnitTestRunner()) 87 | { 88 | unitTestUserInitiated = value; 89 | userInitiated ??= value; 90 | } 91 | else 92 | { 93 | userInitiated = value; 94 | } 95 | } 96 | } 97 | 98 | /// 99 | /// Gets or sets a scheduler that should be used for requests initiated in the 100 | /// background, and are scheduled at a lower priority. 101 | /// 102 | public static HttpMessageHandler Background 103 | { 104 | get => unitTestBackground ?? GetCurrent().GetService("Background") ?? background; 105 | set 106 | { 107 | if (ModeDetector.InUnitTestRunner()) 108 | { 109 | unitTestBackground = value; 110 | background ??= value; 111 | } 112 | else 113 | { 114 | background = value; 115 | } 116 | } 117 | } 118 | 119 | /// 120 | /// Gets or sets a scheduler that fetches results solely from the cache specified in 121 | /// RequestCache. 122 | /// 123 | public static HttpMessageHandler Offline 124 | { 125 | get => unitTestOffline ?? GetCurrent().GetService("Offline") ?? offline; 126 | set 127 | { 128 | if (ModeDetector.InUnitTestRunner()) 129 | { 130 | unitTestOffline = value; 131 | offline ??= value; 132 | } 133 | else 134 | { 135 | offline = value; 136 | } 137 | } 138 | } 139 | 140 | /// 141 | /// Gets or sets a scheduler that should be used for requests initiated in the 142 | /// operationQueue, and are scheduled at a lower priority. You don't 143 | /// need to mess with this. 144 | /// 145 | public static OperationQueue OperationQueue 146 | { 147 | get => unitTestOperationQueue ?? GetCurrent().GetService("OperationQueue") ?? operationQueue; 148 | set 149 | { 150 | if (ModeDetector.InUnitTestRunner()) 151 | { 152 | unitTestOperationQueue = value; 153 | operationQueue ??= value; 154 | } 155 | else 156 | { 157 | operationQueue = value; 158 | } 159 | } 160 | } 161 | 162 | /// 163 | /// Gets or sets a request cache that if set indicates that HTTP handlers should save and load 164 | /// requests from a cached source. 165 | /// 166 | public static IRequestCache? RequestCache 167 | { 168 | get => unitTestRequestCache ?? requestCache; 169 | set 170 | { 171 | if (ModeDetector.InUnitTestRunner()) 172 | { 173 | unitTestRequestCache = value; 174 | requestCache ??= value; 175 | } 176 | else 177 | { 178 | requestCache = value; 179 | } 180 | } 181 | } 182 | 183 | internal static void CreateDefaultInstances(IReadonlyDependencyResolver? current) 184 | { 185 | // This method is just here to force the static constructor to run 186 | Current = current; 187 | } 188 | 189 | private static IReadonlyDependencyResolver GetCurrent() 190 | { 191 | return Current ??= AppLocator.Current; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Fusillade 2 | 3 | We'd love for you to contribute to our source code and to make Fusillade even better than it is 4 | today! Here are the guidelines we'd like you to follow: 5 | 6 | - [Code of Conduct](#coc) 7 | - [Question or Problem?](#question) 8 | - [Issues and Bugs](#issue) 9 | - [Feature Requests](#feature) 10 | - [Submission Guidelines](#submit) 11 | - [Coding Rules](#rules) 12 | - [Commit Message Guidelines](#commit) 13 | 14 | ## Code of Conduct 15 | 16 | Help us keep the project open and inclusive. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md). 17 | 18 | ## Got a Question or Problem? 19 | 20 | 21 | ## Found an Issue? 22 | 23 | If you find a bug in the source code or a mistake in the documentation, you can help us by 24 | submitting an issue to our [GitHub Repository](https://github.com/reactiveui/Fusillade). Even better you can submit a Pull Request 25 | with a fix. 26 | 27 | **Please see the [Submission Guidelines](#submit) below.** 28 | 29 | ## Want a Feature? 30 | 31 | You can request a new feature by submitting an issue to our [GitHub Repository](https://github.com/paulcbetts/Fusillade). If you 32 | would like to implement a new feature then consider what kind of change it is: 33 | 34 | prevent duplication of work, and help you to craft the change so that it is successfully accepted 35 | into the project. 36 | * **Small Changes** can be crafted and submitted to the [GitHub Repository](https://github.com/reactiveui/Fusillade) as a Pull 37 | Request. 38 | 39 | ## Submission Guidelines 40 | 41 | ### Submitting an Issue 42 | 43 | If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize 44 | the effort we can spend fixing issues and adding new features, by not reporting duplicate issues. 45 | 46 | Providing the following information will increase the chances of your issue being dealt with 47 | quickly: 48 | 49 | * **Overview of the Issue** - if an error is being thrown a stack trace helps 50 | * **Motivation for or Use Case** - explain why this is a bug for you 51 | * **Fusillade Version(s)** - is it a regression? 52 | * **Operating System** - is this a problem with all browsers or only specific ones? 53 | * **Reproduce the Error** - provide a example or an unambiguous set of steps. 54 | * **Related Issues** - has a similar issue been reported before? 55 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 56 | causing the problem (line of code or commit) 57 | 58 | **If you get help, help others. Good karma rulez!** 59 | 60 | ### Submitting a Pull Request 61 | Before you submit your pull request consider the following guidelines: 62 | 63 | * Search [GitHub](https://github.com/reactiveui/Fusillade/pulls) for an open or closed Pull Request 64 | that relates to your submission. You don't want to duplicate effort. 65 | * Make your changes in a new git branch: 66 | 67 | ```shell 68 | git checkout -b my-fix-branch master 69 | ``` 70 | 71 | * Create your patch, **including appropriate test cases**. 72 | * Follow our [Coding Rules](#rules). 73 | * Run the test suite, as described below. 74 | * Commit your changes using a descriptive commit message that follows our 75 | [commit message conventions](#commit). 76 | 77 | ```shell 78 | git commit -a 79 | ``` 80 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 81 | 82 | * Build your changes locally to ensure all the tests pass: 83 | 84 | ```shell 85 | build.cmd 86 | ``` 87 | 88 | * Push your branch to GitHub: 89 | 90 | ```shell 91 | git push origin my-fix-branch 92 | ``` 93 | 94 | In GitHub, send a pull request to `Fusillade:master`. 95 | 96 | If we suggest changes, then: 97 | 98 | * Make the required updates. 99 | * Re-run the test suite to ensure tests are still passing. 100 | * Commit your changes to your branch (e.g. `my-fix-branch`). 101 | * Push the changes to your GitHub repository (this will update your Pull Request). 102 | 103 | If the PR gets too outdated we may ask you to rebase and force push to update the PR: 104 | 105 | ```shell 106 | git rebase master -i 107 | git push origin my-fix-branch -f 108 | ``` 109 | 110 | _WARNING: Squashing or reverting commits and force-pushing thereafter may remove GitHub comments 111 | on code that were previously made by you or others in your commits. Avoid any form of rebasing 112 | unless necessary._ 113 | 114 | That's it! Thank you for your contribution! 115 | 116 | #### After your pull request is merged 117 | 118 | After your pull request is merged, you can safely delete your branch and pull the changes 119 | from the main (upstream) repository: 120 | 121 | * Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows: 122 | 123 | ```shell 124 | git push origin --delete my-fix-branch 125 | ``` 126 | 127 | * Check out the master branch: 128 | 129 | ```shell 130 | git checkout master -f 131 | ``` 132 | 133 | * Delete the local branch: 134 | 135 | ```shell 136 | git branch -D my-fix-branch 137 | ``` 138 | 139 | * Update your master with the latest upstream version: 140 | 141 | ```shell 142 | git pull --ff upstream master 143 | ``` 144 | 145 | ## Coding Rules 146 | 147 | To ensure consistency throughout the source code, keep these rules in mind as you are working: 148 | 149 | * All features or bug fixes **must be tested** by one or more unit tests. 150 | * All public API methods **must be documented** with XML documentation. 151 | 152 | ## Git Commit Guidelines 153 | 154 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 155 | format that includes a **type** and a **subject**: 156 | 157 | ``` 158 | : 159 | 160 | 161 |