├── 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 |