ServiceWorker
Register, inspect, update and unregister service workers, and exchange messages with the worker controlling the page - a strongly-typed wrapper over navigator.serviceWorker.
@inject Bit.Butil.ServiceWorker serviceWorkerMDN reference
/sw.js that does no caching - it exists purely so the registration-based APIs on this page
(and on the BackgroundSync and Push pages) have something real to register and inspect.
Returns true when the runtime exposes navigator.serviceWorker. Service workers are available in every modern browser, but not in insecure contexts.
var supported = await serviceWorker.IsSupported();Registers a same-origin worker script and resolves with a snapshot of the created registration. Optional parameters control the scope, the update-via-cache strategy (imports, all or none) and whether the script is loaded as an ES module.
var info = await serviceWorker.Register("/sw.js");
// info.IsRegistered -> true
// info.Scope -> e.g. "https://example.com/"
// info.ActiveState -> "activated" once the worker is running
// info.InstallingState / info.WaitingState / info.UpdateViaCacheGetRegistration returns the registration matching a scope, or the most specific one for the current URL when the scope is omitted; IsRegistered is false when none exists. Update forces the browser to re-fetch the worker script and check for a newer version.
var info = await serviceWorker.GetRegistration();
if (info.IsRegistered)
{
await serviceWorker.Update();
}PostMessage sends a payload to the worker currently controlling this page and returns false when there is no controller yet. SubscribeMessage receives payloads the worker posts back as JsonElement values, and SubscribeControllerChange fires whenever the controlling worker changes - for example right after the demo worker activates and claims this page. Both return a ButilSubscription; dispose it to detach.
var messageSub = await serviceWorker.SubscribeMessage(data =>
{
// data is a JsonElement carrying whatever the worker posted
});
var controllerSub = await serviceWorker.SubscribeControllerChange(() =>
{
// the page is now controlled by a (new) worker
});
var delivered = await serviceWorker.PostMessage("hi worker");
await messageSub.DisposeAsync();
await controllerSub.DisposeAsync();Both of these can only be done by the worker to itself, so these post a message and the worker acts on it. SkipWaiting promotes a worker that has installed and is waiting for the old one's pages to go away - the 'reload to update' button, without the reload. Claim takes control of pages that loaded before the worker activated, which is why the first visit after an install is the one where offline support quietly does not work.
// the page side
var hadWaitingWorker = await serviceWorker.SkipWaiting();
var claimed = await serviceWorker.Claim();
// the worker side - this protocol is what the two calls expect
self.addEventListener('message', event => {
if (event.data?.__butil === 'skipWaiting') self.skipWaiting();
if (event.data?.__butil === 'claim') {
event.waitUntil(self.clients.claim().then(() => event.ports[0]?.postMessage(true)));
}
});Which tabs, iframes and workers the service worker controls. The Clients API exists only on the worker's global scope, so this is a question asked over a MessageChannel - and an empty result is ambiguous: the worker may have answered with no clients (this page is not in the list while it is uncontrolled and includeUncontrolled is false), or there was no active worker, or none answered.
// the page side
ServiceWorkerClientInfo[] clients = await serviceWorker.MatchAllClients(
includeUncontrolled: true, type: "window");
// the worker side
self.addEventListener('message', event => {
if (event.data?.__butil !== 'clients') return;
event.waitUntil(self.clients
.matchAll({ includeUncontrolled: event.data.includeUncontrolled, type: event.data.type })
.then(clients => event.ports[0]?.postMessage(clients.map(c => ({
id: c.id, url: c.url, type: c.type, frameType: c.frameType,
focused: c.focused, visibilityState: c.visibilityState
})))));
});Removes the registration matching the given scope, or the current one when omitted, and returns true when a registration was actually removed. Butil never unregisters workers automatically on disposal - the consuming app decides when a worker goes away.
var removed = await serviceWorker.Unregister();API reference
ValueTask<bool> IsSupported()ValueTask<ServiceWorkerRegistrationInfo> Register(string scriptUrl, string? scope = null, string? updateViaCache = null, bool moduleType = false)ValueTask<ServiceWorkerRegistrationInfo> GetRegistration(string? scope = null)ValueTask Update(string? scope = null)ValueTask<bool> Unregister(string? scope = null)ValueTask<bool> PostMessage<T>(T message)Task<ButilSubscription> SubscribeMessage(Action<JsonElement> handler)Task<ButilSubscription> SubscribeControllerChange(Action handler)ValueTask DisposeAsync()void InvokeServiceWorkerMessage(Guid id, JsonElement data)void InvokeServiceWorkerControllerChange(Guid id)bool IsRegistered { get; set; }string Scope { get; set; }string? ActiveState { get; set; }string? InstallingState { get; set; }string? WaitingState { get; set; }string? UpdateViaCache { get; set; }ValueTask<ServiceWorkerRegistrationInfo[]> GetRegistrations()ValueTask<ServiceWorkerRegistrationInfo> Ready(int timeoutMs = 10000)ValueTask<bool> EnableNavigationPreload(string? scope = null)ValueTask<bool> DisableNavigationPreload(string? scope = null)ValueTask<bool> SetNavigationPreloadHeader(string value, string? scope = null)ValueTask<NavigationPreloadState> GetNavigationPreloadState(string? scope = null)ValueTask<bool> SkipWaiting(string? scope = null)ValueTask<bool> Claim(string? scope = null, int timeoutMs = 5000)ValueTask<ServiceWorkerClientInfo[]> MatchAllClients(bool includeUncontrolled = false, string type = "window", string? scope = null, int timeoutMs = 5000)class NavigationPreloadState { bool IsSupported; bool Enabled; string HeaderValue; }class ServiceWorkerClientInfo { string Id; string Url; string Type; string FrameType; bool Focused; string VisibilityState; }