BackgroundFetch
Downloads the browser owns: they keep running after the tab is closed, resume after a dropped connection, and show the user their own progress UI. Start one from C#, watch its progress, and let the service worker keep what arrives.
@inject Bit.Butil.BackgroundFetch backgroundFetchMDN reference
IsSupported
is false until one is registered - even in a browser that implements it. Firefox and Safari do
not. Register the demo worker below first.
The same minimal /sw.js the rest of this category uses. Its backgroundfetchsuccess handler logs what arrived to the DevTools console.
Bit.Butil.ServiceWorker serviceWorker
<button @onclick="RegisterWorker">Register /sw.js</button>
{
private async Task RegisterWorker()
{
await serviceWorker.Register("/sw.js");
// Background fetch hangs off the *active* registration, so a Fetch issued before the worker
// has activated finds nothing to hang off.
await serviceWorker.Ready();
}
}// A background fetch outlives the page that started it, so its results are delivered here rather
// than to a tab. By the time a page could ask, the browser has usually released the records - so
// this handler is the only place they can be kept.
self.addEventListener('backgroundfetchsuccess', event => {
event.waitUntil((async () => {
const cache = await caches.open('downloads-v1');
const records = await event.registration.matchAll();
await Promise.all(records.map(async record => {
const response = await record.responseReady;
await cache.put(record.request, response);
}));
// Turns the browser's download UI into a link back into the app.
await event.updateUI({ title: 'Downloaded' });
})());
});
self.addEventListener('backgroundfetchfail', event => {
console.log('[sw] background fetch failed:', event.registration.failureReason);
});
self.addEventListener('backgroundfetchabort', event => {
console.log('[sw] background fetch aborted:', event.registration.id);
});
// Fired when the user taps the entry in the browser's download UI.
self.addEventListener('backgroundfetchclick', event => {
event.waitUntil(self.clients.openWindow('/downloads'));
});The id is your name for the transfer and has to be unique among the ones currently running. downloadTotal is what the browser's progress UI counts against - and a limit it enforces, so an estimate that is too low aborts the fetch. Pass 0 for an indeterminate UI.
var registration = await backgroundFetch.Fetch(
id: "manual-2024",
urls: ["/media/chapter-1.pdf", "/media/chapter-2.pdf"],
title: "Downloading the manual",
downloadTotal: 12 * 1024 * 1024,
icons: [new BackgroundFetchIcon { Src = "/icon-192.png", Sizes = "192x192", Type = "image/png" }]);
// null when the browser refused it - a duplicate id, or no supportGetIds lists what is running for this registration - which is how a page that has just been reopened finds the transfer it started in an earlier session. A fetch disappears from here soon after it ends, so a null Get is 'not running now', not 'never existed'.
string[] ids = await backgroundFetch.GetIds();
BackgroundFetchRegistrationInfo? info = await backgroundFetch.Get("manual-2024");
if (info is not null)
{
var progress = info.DownloadTotal > 0
? (double)info.Downloaded / info.DownloadTotal
: 0;
}
await backgroundFetch.Abort("manual-2024");Progress events fire as bytes move, for as long as this page is open. They are a convenience, not the way to observe a transfer: the fetch outlives the page and the events do not - the service worker's backgroundfetchsuccess handler is what always runs.
var subscription = await backgroundFetch.SubscribeProgress("manual-2024", info =>
{
var done = info.Downloaded;
var total = info.DownloadTotal;
InvokeAsync(StateHasChanged);
});
// null when nothing is running under that id
await using var _ = subscription;A fetch is made of records - one request/response pair each. They can be read while the fetch is live; once it ends the browser releases them and RecordsAvailable goes false, which is why keeping the responses is the service worker's job.
string[] recordUrls = await backgroundFetch.GetRecordUrls("manual-2024");
string? body = await backgroundFetch.ReadRecordText("manual-2024", recordUrls[0], timeoutMs: 5000);backgroundfetchsuccess,
backgroundfetchfail and backgroundfetchabort, and that handler is
where the responses should be stored - in a Cache or in the
origin private file system. By the time a page asks
for them, they are usually gone.
API reference
ValueTask<bool> IsSupported()ValueTask<BackgroundFetchRegistrationInfo?> Fetch(string id, string[] urls, string title = "", long downloadTotal = 0, BackgroundFetchIcon[]? icons = null)ValueTask<BackgroundFetchRegistrationInfo?> Get(string id)ValueTask<string[]> GetIds()ValueTask<bool> Abort(string id)ValueTask<string[]> GetRecordUrls(string id)ValueTask<string?> ReadRecordText(string id, string url, int timeoutMs = 10000)Task<ButilSubscription?> SubscribeProgress(string id, Action<BackgroundFetchRegistrationInfo> handler)class BackgroundFetchRegistrationInfo { string Id; long UploadTotal; long Uploaded; long DownloadTotal; long Downloaded; string Result; string FailureReason; bool RecordsAvailable; }class BackgroundFetchIcon { string Src; string? Sizes; string? Type; string? Label; }