{"id":14966900,"url":"https://github.com/reactiveui/fusillade","last_synced_at":"2025-10-04T20:12:49.448Z","repository":{"id":16340138,"uuid":"19089866","full_name":"reactiveui/Fusillade","owner":"reactiveui","description":"An opinionated HTTP library for Mobile Development","archived":false,"fork":false,"pushed_at":"2025-10-04T13:29:35.000Z","size":2174,"stargazers_count":312,"open_issues_count":12,"forks_count":32,"subscribers_count":20,"default_branch":"main","last_synced_at":"2025-10-04T15:22:28.927Z","etag":null,"topics":["cross-platform","dotnet","dotnet-core","http","http-client","mobile","speculative-requests","volley","xamarin"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/reactiveui.png","metadata":{"funding":{"github":["reactivemarbles"]},"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2014-04-24T00:08:11.000Z","updated_at":"2025-09-30T02:45:28.000Z","dependencies_parsed_at":"2023-11-11T08:31:39.716Z","dependency_job_id":"5145e261-a2cd-40d1-8752-f6e73d7342fb","html_url":"https://github.com/reactiveui/Fusillade","commit_stats":{"total_commits":316,"total_committers":22,"mean_commits":"14.363636363636363","dds":0.5411392405063291,"last_synced_commit":"4c755d63d2d0551161185260f7eea92684f274f7"},"previous_names":["paulcbetts/fusillade"],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/reactiveui/Fusillade","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reactiveui%2FFusillade","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reactiveui%2FFusillade/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reactiveui%2FFusillade/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reactiveui%2FFusillade/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/reactiveui","download_url":"https://codeload.github.com/reactiveui/Fusillade/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reactiveui%2FFusillade/sbom","scorecard":{"id":765957,"data":{"date":"2025-08-11","repo":{"name":"github.com/reactiveui/Fusillade","commit":"d007098eca8b040f5abc0655a8d6bb1752d6afeb"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":4.8,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Code-Review","score":-1,"reason":"Found no human activity in the last 30 changesets","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":5,"reason":"6 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 5","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci-build.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/reactiveui/Fusillade/ci-build.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/lock.yml:19: update your workflow using https://app.stepsecurity.io/secureworkflow/reactiveui/Fusillade/lock.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/release.yml:9: update your workflow using https://app.stepsecurity.io/secureworkflow/reactiveui/Fusillade/release.yml/main?enable=pin","Info:   0 out of   3 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/ci-build.yml:1","Warn: no topLevel permission defined: .github/workflows/release.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 30 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-23T00:48:04.013Z","repository_id":16340138,"created_at":"2025-08-23T00:48:04.013Z","updated_at":"2025-08-23T00:48:04.013Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278366673,"owners_count":25975097,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-04T02:00:05.491Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cross-platform","dotnet","dotnet-core","http","http-client","mobile","speculative-requests","volley","xamarin"],"created_at":"2024-09-24T13:37:07.196Z","updated_at":"2025-10-04T20:12:49.442Z","avatar_url":"https://github.com/reactiveui.png","language":"C#","readme":"[![NuGet Stats](https://img.shields.io/nuget/v/fusillade.svg)](https://www.nuget.org/packages/fusillade) ![Build](https://github.com/reactiveui/Fusillade/workflows/Build/badge.svg) [![Code Coverage](https://codecov.io/gh/reactiveui/fusillade/branch/main/graph/badge.svg)](https://codecov.io/gh/reactiveui/fusillade)\n\n\u003cbr /\u003e\n\u003ca href=\"https://github.com/reactiveui/fusillade\"\u003e\n  \u003cimg width=\"120\" heigth=\"120\" src=\"https://raw.githubusercontent.com/reactiveui/styleguide/master/logo_fusillade/main.png\"\u003e\n\u003c/a\u003e\n\n## Fusillade: An opinionated HTTP library for .NET apps\n\nFusillade helps you write efficient, resilient networked apps by composing HttpMessageHandlers for HttpClient. It focuses on:\n\n- Request de-duplication for relevant HTTP methods\n- Concurrency limiting via a priority-aware operation queue\n- Request prioritization for predictable UX\n- Speculative background fetching with byte-budget limits\n- Optional caching of responses and an offline replay handler\n\nDesign inspirations include Android's Volley and Picasso.\n\nSupported targets: library is built for .NET Standard 2.0 and is used from modern .NET (e.g., .NET 8/9), Xamarin/Mono, and .NET for iOS/Android/Mac Catalyst apps.\n\n\n## Install\n\n- Package Manager: Install-Package fusillade\n- .NET CLI: dotnet add package fusillade\n\nOptional (examples below): Akavache for caching.\n\n- .NET CLI: dotnet add package Akavache.SystemTextJson\n\n\n## Quick start\n\nCreate HttpClient instances by picking the right handler from NetCache:\n\n```csharp\nusing Fusillade;\nusing System.Net.Http;\n\n// Highest priority: the user is waiting now\nvar client = new HttpClient(NetCache.UserInitiated);\nvar json = await client.GetStringAsync(\"https://httpbin.org/get\");\n```\n\nAvailable built-ins:\n\n- NetCache.UserInitiated: foreground work the user is waiting for\n- NetCache.Background: background work that should not block UI work\n- NetCache.Speculative: background prefetching with a byte budget\n- NetCache.Offline: fetch from cache only (no network)\n\nBy default, requests are processed four at a time via an operation queue.\n\n\n## Core ideas\n\n### 1) Request de-duplication\n\nFusillade de-duplicates concurrent requests for the same resource when the method is GET, HEAD, or OPTIONS. If multiple callers request the same URL concurrently, only one on-the-wire request is made; the others join the same in-flight response.\n\nThis happens transparently in RateLimitedHttpMessageHandler.\n\n### 2) Concurrency limiting and prioritization\n\nAll work is scheduled through an OperationQueue (default parallelism is 4). Each handler has an effective priority:\n\n- Priority.UserInitiated (100)\n- Priority.Background (20)\n- Priority.Speculative (10)\n- Priority.Explicit (custom base with offset)\n\nHigher numbers run before lower ones. You can set a custom base (Explicit) and an offset to fit your scenario.\n\n```csharp\nusing Fusillade;\nusing Punchclock;\nusing System.Net.Http;\n\n// Custom queue with 2 concurrent slots\nvar queue = new OperationQueue(2);\n\nvar handler = new RateLimitedHttpMessageHandler(\n    new HttpClientHandler(),\n    basePriority: Priority.Explicit,\n    priority: 500,           // higher runs earlier\n    opQueue: queue);\n\nvar client = new HttpClient(handler);\n```\n\n### 3) Speculative background fetching with byte budgets\n\nUse NetCache.Speculative for prefetching scenarios. Limit the total number of bytes fetched; once the limit is reached, further speculative requests are canceled.\n\n```csharp\n// Reset byte budget to 5 MB (e.g., on app resume)\nNetCache.Speculative.ResetLimit(5 * 1024 * 1024);\n\nvar prefetch = new HttpClient(NetCache.Speculative);\n_ = prefetch.GetStringAsync(\"https://example.com/expensive-data\");\n```\n\nTo stop speculative fetching immediately:\n\n```csharp\nNetCache.Speculative.ResetLimit(-1); // any further requests will be canceled\n```\n\n\n## Caching and offline\n\nFusillade can optionally cache responses (body bytes + headers) and replay them when offline.\n\nThere are two ways to wire caching:\n\n1) Provide a cacheResultFunc to RateLimitedHttpMessageHandler, which gets called with the response and a unique request key when a response is received.\n\n2) Set NetCache.RequestCache with an implementation of IRequestCache. Fusillade will invoke Save and Fetch automatically.\n\n### IRequestCache\n\n```csharp\npublic interface IRequestCache\n{\n    Task Save(HttpRequestMessage request, HttpResponseMessage response, string key, CancellationToken ct);\n    Task\u003cbyte[]\u003e Fetch(HttpRequestMessage request, string key, CancellationToken ct);\n}\n```\n\n- Save is called once the handler has fully buffered the body (as ByteArrayContent) and cloned headers.\n- Fetch should return the previously saved body bytes for the key (or null if not found).\n\nKeys are generated by RateLimitedHttpMessageHandler.UniqueKeyForRequest(request). Treat the key as an implementation detail; persist what you receive and return it during Fetch.\n\n### Simple Akavache-based cache\n\n```csharp\nusing Akavache;\nusing Akavache.SystemTextJson;\nusing Fusillade;\nusing System.Net.Http;\n\n// Initialize a simple in-memory Akavache cache\nvar database = CacheDatabase.CreateBuilder().WithSerializerSystemTextJson().Build();\nvar blobCache = new InMemoryBlobCache(database.Serializer);\n\n// Option A: Provide a cacheResultFunc directly\nvar cachingHandler = new RateLimitedHttpMessageHandler(\n    new HttpClientHandler(),\n    Priority.UserInitiated,\n    cacheResultFunc: async (rq, resp, key, ct) =\u003e\n    {\n        var data = await resp.Content.ReadAsByteArrayAsync(ct);\n        await blobCache.Insert(key, data);\n    });\n\nvar client = new HttpClient(cachingHandler);\nvar fresh = await client.GetStringAsync(\"https://httpbin.org/get\");\n\n// Option B: Implement IRequestCache and set NetCache.RequestCache\nNetCache.RequestCache = new MyRequestCache(blobCache);\n```\n\n```csharp\n// Example IRequestCache wrapper over Akavache\nclass MyRequestCache : IRequestCache\n{\n    private readonly IBlobCache _cache;\n    public MyRequestCache(IBlobCache cache) =\u003e _cache = cache;\n\n    public async Task Save(HttpRequestMessage request, HttpResponseMessage response, string key, CancellationToken ct)\n    {\n        var bytes = await response.Content.ReadAsByteArrayAsync(ct);\n        await _cache.Insert(key, bytes);\n    }\n\n    public Task\u003cbyte[]\u003e Fetch(HttpRequestMessage request, string key, CancellationToken ct)\n        =\u003e _cache.Get(key);\n}\n```\n\n### Offline replay\n\nUse OfflineHttpMessageHandler to serve cached data only (no network). This handler asks IRequestCache (or your custom retrieveBodyFunc) for the cached body and returns:\n\n- 200 OK with the cached body, or\n- 503 Service Unavailable if not found\n\n```csharp\n// Use NetCache.Offline after setting NetCache.RequestCache\nvar offline = new HttpClient(NetCache.Offline);\nvar data = await offline.GetStringAsync(\"https://httpbin.org/get\");\n\n// Or construct explicitly\nvar offlineExplicit = new HttpClient(new OfflineHttpMessageHandler(\n    async (rq, key, ct) =\u003e await blobCache.Get(key)));\n```\n\n\n## Dependency injection and Splat integration\n\nIf you use Splat, you can initialize NetCache to use your container’s services via the provided extension:\n\n```csharp\nusing Splat.Builder;\n\nvar app = AppBuilder.CreateSplatBuilder().Build();\napp.CreateFusilladeNetCache();\n```\n\nYou can also register a platform-specific HttpMessageHandler (e.g., NSUrlSessionHandler on iOS, AndroidMessageHandler on Android) in your container beforehand; NetCache will pick it up as the inner HTTP handler.\n\n\n## Advanced configuration\n\n- Custom OperationQueue: override NetCache.OperationQueue with your own queue to control concurrency for the entire app.\n\n```csharp\nusing Punchclock;\nNetCache.OperationQueue = new OperationQueue(maxConcurrency: 6);\n```\n\n- Custom priorities: compose RateLimitedHttpMessageHandler with Priority.Explicit and an offset to place certain pipelines ahead or behind the defaults.\n\n```csharp\nvar urgent = new RateLimitedHttpMessageHandler(new HttpClientHandler(), Priority.Explicit, priority: 1_000);\nvar slow   = new RateLimitedHttpMessageHandler(new HttpClientHandler(), Priority.Explicit, priority: -50);\n```\n\n- Deduplication scope: deduplication is per-HttpMessageHandler instance via an in-memory in-flight map. Multiple handlers mean multiple scopes.\n\n\n## Usage recipes\n\n- Image gallery / avatars\n  - Use RateLimitedHttpMessageHandler for GETs\n  - De-dup prevents duplicate downloads for the same URL\n  - Use Background for preloading next images; switch to UserInitiated for visible images\n\n- Boot-time warmup\n  - On app start/resume, set NetCache.Speculative.ResetLimit to a sensible budget\n  - Queue speculative GETs for likely-next screens to reduce perceived latency\n\n- Offline-first data views\n  - Populate cache during online sessions using cacheResultFunc or IRequestCache\n  - When network is unavailable, point HttpClient to NetCache.Offline\n\n\n## FAQ\n\n- How many requests run at once?\n  - Default is 4 (OperationQueue with concurrency 4). Override via NetCache.OperationQueue or pass a custom queue to a handler.\n\n- Which methods are de-duplicated?\n  - GET, HEAD, and OPTIONS.\n\n- How are cache keys generated?\n  - Via RateLimitedHttpMessageHandler.UniqueKeyForRequest(request). Treat this as an implementation detail; persist and reuse as given.\n\n- Can I cancel a request?\n  - Use CancellationToken in HttpClient APIs; dedup ensures the underlying request cancels only when all dependents cancel.\n\n\n## Contribute\n\nFusillade is developed under an OSI-approved open source license, making it freely usable and distributable, even for commercial use. We ❤ our contributors and welcome new contributors of all experience levels.\n\n- Answer questions on StackOverflow: https://stackoverflow.com/questions/tagged/fusillade\n- Share knowledge and mentor the next generation of developers\n- Donations: https://reactiveui.net/donate and Corporate Sponsorships: https://reactiveui.net/sponsorship\n- Ask your employer to support open-source: https://github.com/github/balanced-employee-ip-agreement\n- Improve documentation and examples\n- Contribute features and bugfixes via PRs\n\n\n## What’s with the name?\n\n“Fusillade” is a synonym for Volley 🙂\n","funding_links":["https://github.com/sponsors/reactivemarbles"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freactiveui%2Ffusillade","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Freactiveui%2Ffusillade","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freactiveui%2Ffusillade/lists"}