+
+
+
diff --git a/src/Bswup/Bit.Bswup.Demo/Bit.Bswup.Demo.csproj b/src/Bswup/Bit.Bswup.Demo/Bit.Bswup.Demo.csproj
index 2b8520fff4..cd2808a4cf 100644
--- a/src/Bswup/Bit.Bswup.Demo/Bit.Bswup.Demo.csproj
+++ b/src/Bswup/Bit.Bswup.Demo/Bit.Bswup.Demo.csproj
@@ -20,4 +20,4 @@
-
\ No newline at end of file
+
diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/CleanupPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/CleanupPage.razor
new file mode 100644
index 0000000000..6489a8db60
--- /dev/null
+++ b/src/Bswup/Bit.Bswup.Demo/Pages/CleanupPage.razor
@@ -0,0 +1,52 @@
+@page "/cleanup"
+
+Backing Out of Bswup - bit Bswup
+
+
Backing Out of Bswup
+
+ Fully remove Bswup from a deployed app - dropping offline support, or recovering clients stuck on a
+ broken worker or cache - with the self-destructing cleanup worker.
+
+
+
+ Replace the content of your service-worker.jsand
+ service-worker.published.js (the file deployed builds actually ship, via the
+ ServiceWorker item's PublishedContent mapping) with:
+
+
+
+
What happens next
+
On its next update check, every client installs this self-destructing worker instead. It:
+
+
activates immediately,
+
purges this app's Bswup and Blazor caches,
+
unregisters its own registration, and
+
signals open tabs to detach.
+
+
+ Tabs the previous worker controlled reload once; everything afterwards runs purely from the network,
+ even while the page keeps referencing bit-bswup.js - each later load just repeats the
+ register/self-unregister cycle silently, with no reloads.
+
+
+ Once no client has loaded the old app for as long as your cache headers require, the
+ bit-bswup.js script tag can be removed from the host document too.
+
+
+
Client-side reset without backing out
+
+ If you only need to recover a single broken client (not remove Bswup from the deployment),
+ prefer BitBswup.forceRefresh() - it clears this
+ app's caches, unregisters its worker, and reloads, without touching the deployed service worker file.
+
diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/EventsPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/EventsPage.razor
new file mode 100644
index 0000000000..c7e8796b26
--- /dev/null
+++ b/src/Bswup/Bit.Bswup.Demo/Pages/EventsPage.razor
@@ -0,0 +1,220 @@
+@page "/events"
+
+Events & Handler - bit Bswup
+
+
Events & Handler
+
+ Bswup reports its whole lifecycle through a single global handler function. Use the built-in
+ progress UI, or write your own handler and drive any markup you like.
+
+
+
+ The handler's name is configured through the handler
+ script attribute (default bitBswupHandler). It receives (type, data) where
+ type is one of the BswupMessage constants:
+
+
+
Event catalog
+
+
+
+
BswupMessage
Value
Raised when
+
+
+
updateFound
UPDATE_FOUND
The browser found a new service worker version and started installing it.
+
stateChanged
STATE_CHANGED
The installing worker's lifecycle state changed (data.currentTarget.state).
+ data.reload() activates the staged version. On a first install it
+ claims the clients and starts Blazor with no reload; on an update it
+ performs SKIP_WAITING and reloads.
+
+
+ data.cleanup() (optional) asks the active service worker to prune this app's stale
+ cache buckets right away. It is safe to call at any time - the worker declines while an update is
+ staged or staging (pruning then happens automatically on activation), and it never touches another
+ app's caches. Most apps never need it: the same pruning already runs on activation and after every
+ accepted update.
+
+
+
+
+ Since v-10-6-0, the built-in BswupProgress component's AutoReload parameter
+ defaults to false: when an update finishes downloading, the reload button is shown and the
+ new version activates when the user accepts it - an unprompted reload discards whatever in-page state
+ the user has mid-session. Set AutoReload="true" to restore the old behavior. First
+ installs are unaffected: they always complete the seamless claim-and-start flow (no reload).
+
+
+
Multi-tab updates
+
+ Service workers are single-instance per origin, so accepting an update in one tab activates the new
+ version for every open tab. When that happens, Bswup has the new worker claim all clients and each
+ other tab reloads itself automatically (via controllerchange) onto the new
+ version. This keeps every tab consistent and avoids the classic failure where an old tab keeps running
+ old app code while its asset requests are served from the new version's cache (mismatched boot config /
+ DLL hashes). The first install is exempt: claiming a client for the first time starts Blazor and does
+ not trigger a reload.
+
+
+
The error payload
+
Install failures are structured:
+
+
+
+
Field
Meaning
+
+
+
+
reason
+
+ One of manifest, integrity, fetch,
+ cache, request, install-incomplete,
+ install-aborted, or install-infra (the install died
+ before/while touching CacheStorage - storage pressure, a broken private mode - always
+ fatal).
+
+
+
message
Human-readable description.
+
url / hash
The offending asset, when known.
+
+
fatal
+
+ Whether the install actually stopped. Under the default lax tolerance a
+ failed asset is reported with fatal: false - the install still succeeds and
+ the asset is fetched from the network on first use - so treat it as a warning, not a
+ dead app. fatal: true means no usable staged version is available to this
+ page - though a worker may still have been installed (a lax
+ install-infra failure resolves the install so the worker can keep serving
+ as a network pass-through).
+
+
+
+
firstInstall
+
+ Where a fatal failure landed. true: before the app ever booted - Bswup
+ starts the app without a service worker so it still boots. false: a
+ background update failed - the app keeps running on the current version (the
+ previous worker keeps serving).
+
+ Long-lived HTTP caching makes repeat visits and updates dramatically faster - the service worker
+ revalidates content by hash, so stale HTTP caches are never a correctness problem.
+
+
+
+
3. Defer Blazor's startup
+
+ Bswup must start Blazor at the right moment (after the first install completes, or immediately when
+ the app is already cached), so disable auto-start on the Blazor script in your host document
+ (index.html or App.razor):
+
+
+
+
4. Add the Bswup script
+
Reference bit-bswup.js right after the Blazor script:
+
+
+ Every attribute is optional - see the Script Options reference for all
+ of them and their defaults.
+
+
+
5. Configure the service worker
+
+ Create (or edit) wwwroot/service-worker.js and wwwroot/service-worker.published.js.
+ The only mandatory line is the importScripts call; everything else is tuning:
+
+
+
Then make sure the service worker is registered in your project file:
+ Use the built-in progress UI - the BswupProgress component
+ plus bit-bswup.progress.js - and you are done. This is what this site uses.
+
+
+ Write your own handler function and drive any markup you like - see
+ Events & Handler for the full event catalog and a complete sample.
+
+
+
+
+ This documentation site is a live Bswup app. Open dev tools → Application → Service Workers to
+ inspect the registration, or visit the Live Playground to watch events and
+ drive the update lifecycle by hand.
+
+
+
Ship Blazor PWAs that install, update & work offline - beautifully.
+
+ Bswup replaces the default Blazor service worker experience with a real install progress UI,
+ controlled background updates, resilient downloads, and a tiny JavaScript API - all with a
+ single script tag.
+
+ bit Bswup (Blazor service-worker update progress) is a
+ NuGet package for Blazor WebAssembly apps that takes over the service worker lifecycle:
+ it downloads and caches your app's assets with visible progress, verifies their integrity, serves them
+ offline, and manages version updates without ever leaving your users staring at a blank page - or
+ running a stale build for days.
+
+
+ This documentation site is itself a Blazor WebAssembly app powered by Bswup - open the browser dev
+ tools, go offline, and reload. Then head over to the Live Playground to watch
+ the service worker events in real time.
+
-
+
How it fits together
+
+
+
+
Piece
What it does
+
+
+
+
bit-bswup.js
+
Page-side script: registers the service worker, coordinates the install/update handshake, starts Blazor at the right moment, polls for updates, and raises lifecycle events.
+
+
+
bit-bswup.sw.js
+
The service worker engine: precaches assets (with retries and integrity checks), serves fetches from cache, handles SPA navigation fallback, and stages new versions. Imported from your service-worker.js.
+
+
+
bit-bswup.progress.js + BswupProgress
+
The optional built-in progress UI: splash screen, progress bar, reload button, and failure panel - CSP-friendly and screen-reader aware.
+
+
+
BitBswup
+
A small JavaScript API to check for updates, activate staged versions, request persistent storage, and force-reset a broken client.
+
+
+
bit-bswup.sw-cleanup.js
+
A self-destructing worker to back out of Bswup or recover clients stuck on a broken worker or cache.
Besides this documentation site, the repository ships two runnable samples:
+
+
Demos/BasicDemo (Bit.Bswup.BasicDemo) - a minimal standalone Blazor WebAssembly app with a hand-written handler, deliberately including a failing external asset to exercise the error flow.
+
Demos/FullDemo (Bit.Bswup.FullDemo.*) - a Blazor Web App (server + client) using the BswupProgress component with a custom progress bar.
diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/JsApiPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/JsApiPage.razor
new file mode 100644
index 0000000000..14c5e79668
--- /dev/null
+++ b/src/Bswup/Bit.Bswup.Demo/Pages/JsApiPage.razor
@@ -0,0 +1,129 @@
+@page "/javascript-api"
+
+JavaScript API - bit Bswup
+
+
JavaScript API
+
+ Bswup exposes a small global BitBswup object so you can drive the update lifecycle from
+ your own code - a "check for updates" button, a custom poller, a "reset app" action.
+
+
+
+ The Live Playground wires each of these methods to a button on this very site.
+
+
+
BitBswup.checkForUpdate()
+
+ Asks the browser to re-fetch the service worker script and check for a new version. If a new version is
+ found, the normal update flow runs (updateFound → stateChanged →
+ updateReady/downloadFinished). If the app is already on the latest version,
+ Bswup raises updateNotFound so you can stop a spinner or show an "up to date" message.
+
+
+ If the check itself fails for a transient reason (offline, server hiccup, a throttled background tab),
+ Bswup raises the non-blocking updateCheckFailed event instead of the install-path
+ error event, so the default progress handler does not hide the app or
+ show the install-failed UI; the payload still carries reason/message so you
+ can surface it yourself. Registration-aware and safe to call as often as you like - it is what powers
+ the built-in polling.
+
+
+
BitBswup.persistStorage()
+
+ Requests durable, eviction-resistant storage for the origin via navigator.storage.persist()
+ and resolves with a boolean saying whether storage is now persistent. Without it the caches are
+ best-effort and can be reclaimed by the browser (Safari deletes all storage for a site not interacted
+ with for seven days). Calling it from a user gesture - after login, from an "install app" button - has
+ the best chance of being granted. Safe to call repeatedly: an already-persistent origin resolves
+ true without prompting again, and unsupported browsers resolve false with a
+ console warning.
+
+
+
BitBswup.skipWaiting()
+
+ If an update has finished downloading and is waiting, this activates it immediately (equivalent to
+ calling the reload callback from updateReady/downloadFinished).
+ Returns true when there was a waiting worker to activate, otherwise false.
+
+
+
BitBswup.forceRefresh(cacheFilter?)
+
+ Clears caches, unregisters the service worker controlling the current page, and reloads. Use this as a
+ last-resort "reset" when a client gets into a bad state. It only removes this app's own registration -
+ other apps mounted under different scopes on the same origin are left untouched.
+
+
+ By default it clears only the caches this app and Blazor own: the app's scope-qualified Bswup buckets,
+ legacy scope-less buckets, and blazor-resources caches. A sibling Bswup app's scoped
+ buckets are spared, and app-owned CacheStorage buckets (Workbox add-ons, offline app data, cached API
+ responses) are left intact, since those can hold data with no other copy. To change what gets cleared,
+ pass an optional cacheFilter: a string (prefix match), a RegExp, or a
+ predicate (key) => boolean:
+
+
+
+
Polling for updates
+
+ By default a service worker is only re-checked by the browser on navigation and roughly every 24 hours,
+ so a tab that stays open for a long time can keep running an old version. There are two ways to check
+ more often:
+
+
+
+ Set updateInterval (and/or
+ updateOnVisibility) on the script tag
+ for built-in polling. Simplest, no extra code - this site uses both.
+
+
Call BitBswup.checkForUpdate() yourself, from a timer or after a user action:
+
+
+
+ Either way, the result surfaces through your handler: a found update flows through
+ updateFound/updateReady, "nothing new" flows through
+ updateNotFound, and a transient check failure flows through
+ updateCheckFailed:
+
+
+
+ Built-in polling skips checks while the tab is in the background (the browser throttles those timers
+ anyway); the next timer tick after the tab is foregrounded runs normally. For an immediate
+ check the moment the user comes back, also set updateOnVisibility="true".
+
+
+
+
+@code {
+ private const string ForceRefreshCode = @"BitBswup.forceRefresh(); // Bswup + Blazor caches (default)
+BitBswup.forceRefresh(() => true); // every cache on the origin
+BitBswup.forceRefresh('bit-bswup'); // only Bswup's own caches
+BitBswup.forceRefresh(/^(bit-bswup|my-app-data)/) // a specific set";
+
+ private const string PollingCode = @"// an ordinary failed check (offline, server hiccup) does NOT reject - it is reported through
+// the UPDATE_CHECK_FAILED message below. This catch is only for unexpected errors, such as a
+// call made before the registration is ready.
+const checkNow = () => BitBswup.checkForUpdate()
+ .catch(err => console.warn('unexpected update check error:', err));
+
+// check every hour from your own code (equivalent to updateInterval=""3600"")
+setInterval(checkNow, 60 * 60 * 1000);
+
+// or check whenever the user clicks a button, and react to the result
+document.getElementById('check-updates').onclick = checkNow;";
+
+ private const string PollingHandlerCode = @"window.bitBswupHandler = (message, data) => {
+ switch (message) {
+ case 'UPDATE_NOT_FOUND': /* already up to date - stop the spinner */ break;
+ case 'UPDATE_CHECK_FAILED': /* transient failure - keep running, optionally notify */ break;
+ // updateFound / stateChanged / updateReady / downloadFinished drive the update UI
+ }
+};";
+}
diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/MigrationPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/MigrationPage.razor
new file mode 100644
index 0000000000..cc189317bf
--- /dev/null
+++ b/src/Bswup/Bit.Bswup.Demo/Pages/MigrationPage.razor
@@ -0,0 +1,58 @@
+@page "/migration"
+
+Migrating to 10-6-0 - bit Bswup
+
+
Migrating to v-10-6-0
+
+ v-10-6-0 is a resilience-focused release. Most changes are internal hardening; this page lists what is
+ visible when upgrading.
+
+
+
Action items
+
+
1. Updates no longer auto-reload.BswupProgress.AutoReload now defaults to false - updates announce themselves through the reload button. Set AutoReload="true" to restore the old behavior.
+
2. Blocked URLs answer 403.prohibitedUrls matches are answered with 403 Forbidden (previously 405). Code detecting a blocked URL by status must check for 403.
+
3. String entries now take effect. Strings in the URL-matching lists (assetsInclude, assetsExclude, prohibitedUrls, serverHandledUrls, serverRenderedUrls) are now matched literally as substrings. Previous releases silently ignored them - audit any strings sitting in those lists, because they take effect for the first time after upgrading.
+
4. Hand-written splash markup needs one edit. Move <button id="bit-bswup-reload">outside the #bit-bswup overlay (and give it a z-index), optionally adding a visually-hidden <span id="bit-bswup-reload-status" role="status"> next to it. The built-in handling no longer reveals the overlay for updates, so a button left inside it would never become visible. The BswupProgress component ships this layout already.
+
+
+
Behavior changes
+
+
Cache buckets are scope-qualified: bit-bswup:<scope-path> - <version> (previously bit-bswup - <version>). Multiple Bswup apps on one origin no longer evict each other's caches; the migration from legacy-named buckets is automatic and does not re-download.
+
Under the default lax tolerance, the asset download now runs inside the install event (waitUntil): the browser keeps the worker alive for the whole download, and updateReady is only raised once the new version is fully staged - previously it could fire while the download had barely started.
+
assetsUrl defaults to a path resolved against the service worker script's own location (previously root-absolute), so apps mounted on a sub-path work without configuration.
+
The cleanup callback only prunes when no update is staged or staging, and BitBswup.forceRefresh()'s default filter clears only this app's own, legacy, and Blazor caches. The same staged-or-staging guard protects every cache-pruning path, so a prune can never race a newer install's freshly written bucket.
+
Requests carrying a Range header are answered with real 206 Partial Content slices from cached bodies (cached media now plays in Safari), and partial responses are never written to the cache.
+
Updates found later in the same session as the first install are treated as real updates: updateReady is raised, downloadFinished carries firstInstall: false, and accepting them runs the normal SKIP_WAITING flow. Previously a long-lived tab kept classifying every later update as another first install.
+
Navigations whose URL is itself a managed asset (e.g. opening /manifest.json directly) are served that asset instead of the SPA default document.
+
An update's re-download of the default document and of hash-less assets no longer deletes the existing cache entry first: if the refresh fails (offline mid-update), the previous copy keeps serving - including offline navigation.
+
The built-in progress UI stays out of the way during background updates: the full-screen splash is first-install only (downloadStarted/downloadProgress payloads carry firstInstall so custom handlers can do the same), the reload button lives outside the overlay, it is announced via a role="status" region, and the overlay no longer swallows clicks outside its content.
+
A first install completes even when no handler function is registered at all (previously the app waited out the full stallTimeout behind the splash).
+
The cleanup worker unregisters its own registration during teardown and no longer claims clients; the page reloads on UNREGISTER only while actually controlled, removing a reload-loop hazard.
+
The passive-mode background top-up after first boot is deterministic: it fills every asset still missing from the cache.
+
The default asset excludes cover all shipped worker-script variants (bit-bswup.sw.min.js, bit-bswup.sw-cleanup.js, bit-bswup.sw-cleanup.min.js).
+
A manifest or externalAssets entry whose URL cannot be parsed is skipped with a non-fatal request error instead of killing the whole service worker at startup.
+
A navigation to a URL that only a RegExp externalAssets pattern matches is served the app shell, never the pattern asset - and a shell cache miss during navigation is refilled from the shell's own URL, never from the navigated route's URL.
+
WAITING_SKIPPED and UNREGISTER never reload an uncontrolled page anymore; they make sure the app is booted instead. Activating a first install through BitBswup.skipWaiting() completes the seamless claim-and-start flow instead of reloading.
+
A page that loads while an update is already mid-install now observes it: updateReady / stateChanged fire in that tab when the update finishes staging.
+
An exception thrown by the app's bitBswupHandler no longer breaks the update pipeline (it is logged, and the remaining messages still dispatch).
+
+
+
New capabilities
+
+
Update polling: the updateInterval / updateOnVisibility script attributes, a registration-aware BitBswup.checkForUpdate(), and the updateNotFound / updateCheckFailed events.
+
Install robustness:errorTolerance (lax/strict), transient-failure retries (maxRetries, retryDelay), the stallTimeout first-install watchdog, and structured error payloads (reason, fatal, firstInstall - including the terminal install-infra reason).
+
Storage:persistStorage / BitBswup.persistStorage() for eviction-resistant storage, and cacheVersion for manual control of cache-bucket rotation.
+
Registration: automatic retry with the default scope when the browser rejects the configured scope, and fingerprint-tolerant Blazor entry-script detection for .NET 9+ @@Assets[...] references.
diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/PlaygroundPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/PlaygroundPage.razor
new file mode 100644
index 0000000000..f81331e11b
--- /dev/null
+++ b/src/Bswup/Bit.Bswup.Demo/Pages/PlaygroundPage.razor
@@ -0,0 +1,93 @@
+@page "/playground"
+
+Live Playground - bit Bswup
+
+
+
Live Playground
+
+ This documentation site is itself a Blazor WebAssembly app powered by Bswup. Inspect its service
+ worker, drive the JavaScript API, and watch every lifecycle event
+ arrive in real time.
+
+
+
+
+ Open dev tools → Network and switch to Offline, then reload - the app keeps working from
+ cache. Or open this site in a second tab, accept an update there, and watch this tab reload
+ itself onto the new version.
+
+
+
+
+
+
Service worker status
+
+
Service worker
…
+
This page
…
+
Scope
…
+
Active worker
…
+
Update staged
…
+
Storage
…
+
+
+
+
+
+
+
+
Cache buckets
+
+ Bswup's buckets are scope-qualified:
+ bit-bswup:<scope-path> - <version>.
+
+
+
+
+
+
Update lifecycle
+
+ checkForUpdate() re-fetches the service worker script; if nothing changed you
+ will see UPDATE_NOT_FOUND in the log below. skipWaiting()
+ activates a staged update immediately.
+
+
+
+
+
+
+
+
+
Storage & reset
+
+ persistStorage() requests eviction-resistant storage (browsers may prompt or
+ apply engagement heuristics). forceRefresh() clears this app's caches,
+ unregisters its worker, and reloads - the last-resort client reset.
+
+
+
+
+
+
+
+
+
Event log
+
+ Every BswupMessage raised on this page - fed by a custom handler chained after the
+ built-in progress UI via the Handler/data-bit-bswup-handler option
+ (newest first, capped at 200 entries).
+
+ @* aria-live pairs with demo.js rendering incrementally: new entries are prepended rather
+ than the list being rebuilt, so screen readers announce only what is new. *@
+
diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/ProgressUIPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/ProgressUIPage.razor
new file mode 100644
index 0000000000..861f5d67ab
--- /dev/null
+++ b/src/Bswup/Bit.Bswup.Demo/Pages/ProgressUIPage.razor
@@ -0,0 +1,150 @@
+@page "/progress-ui"
+
+Progress UI - bit Bswup
+
+
The Built-in Progress UI
+
+ Instead of writing a handler yourself, use the built-in splash/progress UI: the
+ BswupProgress Razor component (the markup) plus bit-bswup.progress.js
+ (the behavior). It registered the splash you saw when this site first installed.
+
+
+
Reference both in your host document:
+
+
Then render the component:
+
+
+
Component parameters
+
+ Each parameter maps to a data-bit-bswup-* attribute that the script reads at load time.
+ The component emits no inline <script>, so it works under a strict
+ Content-Security-Policy and when rendered by an interactive Blazor renderer.
+
+
+
+
+
Parameter
Default
Description
+
+
+
AutoReload
false
Reload automatically when an update finishes instead of showing the reload button. Changed in v-10-6-0 - previously defaulted to true.
+
ShowLogs
false
Log lifecycle messages to the console.
+
ShowAssets
false
List each downloaded asset inside the splash.
+
AppContainer
#app
Selector of the element to hide while installing (used with HideApp). An invalid selector is tolerated - only the hiding is skipped.
+
HideApp
false
Hide the app container while the first install downloads.
+
AutoHide
false
Hide the splash automatically when the download finishes.
+
Handler
-
Name of an additional custom handler invoked after the built-in handling, so you can layer your own behavior without replacing the UI. (Do not point it at bitBswupHandler itself - self-references are detected and ignored.)
+
ChildContent
-
Replaces the default splash markup with your own - see below.
+
+
+
+
+
Behavior worth knowing
+
+
+ The full-screen splash is first-install only. A background update downloads
+ silently behind the running app; when it finishes - or when an update is already staged at page
+ load - the reload button appears on its own, with a screen-reader announcement through a
+ role="status" region.
+
+
+ Clicks outside the splash content pass through the overlay, so an app force-started under a failure
+ panel stays usable.
+
+
+ A failure panel with retry appears for fatal first-install errors; deterministic failures
+ (an unparseable manifest, an integrity mismatch) hide the retry button since reloading would fail
+ identically.
+
+
+ Runtime toggles are available via
+ BitBswupProgress.config({ autoReload, showLogs, showAssets, hideApp, autoHide }).
+
+
+
+
Custom splash content
+
+ Pass ChildContent to replace the default splash markup. The component keeps initializing
+ automatically, and the built-in behavior drives your markup through the documented element ids -
+ include whichever you want driven:
+
+
+
+
+
Element id
Driven as
+
+
+
bit-bswup-progress-bar
Width + aria-valuenow set to the download percentage; the splash root also gets --bit-bswup-percent / --bit-bswup-percent-text CSS variables.
+
bit-bswup-percent
Text content set to NN%.
+
bit-bswup-assets
Each downloaded asset prepended as a list item.
+
bit-bswup-error (+ -message, -details, -retry)
The failure panel for fatal first-install errors.
+
bit-bswup-reload / bit-bswup-reload-status
The update-ready button and its screen-reader status region.
+
+
+
+
+ The update-ready button (#bit-bswup-reload) and its status region are always rendered by
+ the component itself, outside the overlay, even with custom content - they are the only way a
+ finished update surfaces under the default AutoReload="false". A custom splash should not
+ render its own copy of those two; restyle them by id instead.
+
+
+
Standalone WebAssembly apps (like this site)
+
+ In a standalone Blazor WebAssembly app the splash must exist before Blazor starts - on a first
+ install, Blazor only boots after the download completes - so the BswupProgress component
+ (which renders as part of the app) cannot paint the first-install splash. Hand-write the same markup in
+ index.html instead; bit-bswup.progress.js self-initializes from the
+ data-bit-bswup-* attributes exactly as it does for the component:
+
+
+
+ Keep #bit-bswup-reload and its status region outside the #bit-bswup
+ overlay (a finished background update must surface the button without revealing the splash), and give
+ the button its own z-index if your app has fixed chrome in the top-right corner. View the
+ source of this very page for a complete working example.
+
+
+
Name of the global function receiving lifecycle events.
+
blazorScript
auto-detected
Path of the Blazor entry script, when it lives at a non-default path.
+
updateInterval
0 (off)
Seconds between automatic update checks.
+
updateOnVisibility
false
Check for an update whenever the tab returns to the foreground.
+
stallTimeout
60
Seconds of total service worker silence before a first install falls back to a network boot.
+
persistStorage
false
Request eviction-resistant storage at startup.
+
options
bitBswup
Name of a global object to read all of these settings from.
+
+
+
+
+
scope
+
+ The scope of the service worker. Defaults to /. A service worker can only control URLs
+ beneath its own folder unless the server sends a Service-Worker-Allowed header, so if
+ your app is mounted on a sub-path (e.g. https://host/myapp/) set this to that sub-path.
+
+
+ If the browser refuses the configured scope, Bswup automatically retries with the default scope -
+ the folder containing the service worker script - so the app keeps offline support rather than losing
+ the service worker entirely; the fallback is reported as a console warning.
+
+
+ The scope also namespaces the caches: buckets are named bit-bswup:<scope-path> - <version>,
+ so several Bswup apps mounted under different scopes on one origin keep fully independent caches.
+ The previous scope-less name made sibling apps evict each other's caches on every update. On upgrade,
+ entries from a legacy-named bucket are migrated without re-downloading.
+
+
+
log
+
+ The log level of the Bswup logger. Available options: none, error,
+ warn, info, verbose, and debug. Each level
+ includes everything above it (e.g. info also shows warn and
+ error). Defaults to warn; use none to silence all output.
+
+
+
sw
+
The file path of the service worker file. Defaults to service-worker.js.
+
+
handler
+
+ The name of the global handler function for service worker events. Defaults to
+ bitBswupHandler - which is also the name the built-in progress script registers, so the
+ two wire up without configuration. The handler is re-resolved until found, so it may be registered
+ after bit-bswup.js loads.
+
+
+ If no handler is ever registered, Bswup still completes a first install on its own - it drives the
+ finish handshake itself so the app boots instead of waiting for the stall watchdog. Updates are simply
+ left staged until the next full restart.
+
+
+
blazorScript
+
+ The path of the Blazor entry-point script (the one you added autostart="false" to).
+ When omitted, Bswup auto-detects both the Blazor Web App script (_framework/blazor.web.js)
+ and the standalone Blazor WebAssembly script (_framework/blazor.webassembly.js), so you
+ only need to set this if your script lives at a non-default path.
+
+
+ Matching is fingerprint-tolerant: the fingerprinted names .NET 9+ emits when the script is referenced
+ through @@Assets["..."] / the ImportMap (e.g. _framework/blazor.web.<fingerprint>.js)
+ are recognized automatically, both for the auto-detected defaults and for an explicitly configured value.
+
+
+
updateInterval
+
+ Number of seconds between automatic update checks. By default the browser only re-checks the service
+ worker on navigation and roughly every 24 hours, so a long-lived SPA tab can run a stale version for a
+ long time. Set a positive number (e.g. 3600 for hourly) to have Bswup call
+ reg.update() on a timer. Checks are skipped while the tab is in the background and resume
+ when it becomes visible again. Omit or set 0 to disable (the default).
+
+
+
updateOnVisibility
+
+ When true, Bswup checks for an update every time the tab returns to the foreground (the
+ visibilitychange event) - a lightweight way to catch updates right when a user comes back
+ to a tab they left open. Disabled by default.
+
+
+
stallTimeout
+
+ Number of seconds of complete service worker silence (no message, no lifecycle event) after
+ which, on a first install only, Bswup stops waiting and starts Blazor directly from
+ the network. This is the last line of defense against install failures that report nothing - most
+ notably the browser terminating the service worker mid-install (Chromium kills installs after
+ ~5 minutes) - which would otherwise leave the app frozen behind the splash forever.
+
+
+ The page is uncontrolled at that point, so it behaves exactly as if no service worker existed, and the
+ install is retried on the next load. Every progress message resets the timer, so a slow-but-healthy
+ download never triggers it - only true silence does. Defaults to 60; set 0
+ to disable. Updates are unaffected: the app is already running when an update stalls.
+
+
+
persistStorage
+
+ When true, Bswup asks the browser to make the origin's storage persistent
+ (navigator.storage.persist()) at startup. By default everything Bswup caches lives in
+ best-effort storage: browsers silently reclaim it under disk pressure, and Safari deletes
+ all storage for a site that has not been interacted with for seven days - the user
+ comes back offline to an app that no longer boots. Persistent storage exempts the origin from that
+ eviction.
+
+
+ This is disabled by default because the request can show a permission prompt (Firefox) and grant odds
+ are engagement-based elsewhere. For the best odds, leave it off and call
+ BitBswup.persistStorage() yourself from a user
+ gesture - after login, or from an "install app" button.
+
+
+
options
+
+ The name of a global configuration object to read settings from. Defaults to bitBswup.
+ Every option above can also be supplied as a property of that object; the object is merged over the
+ built-in defaults first, and any script-tag attribute then overrides the matching property. This is
+ the way to configure Bswup when the script is injected dynamically, where attribute-based
+ configuration may not be readable.
+
+ Everything you can configure in service-worker.js before importing the Bswup engine.
+
+
+
+
+
+ The most important line is the last one - the only mandatory config in this file:
+
+
+
+
+
+ Unlike the assets in service-worker-assets.js (which Bswup verifies with Subresource
+ Integrity), the service worker script itself cannot be integrity-pinned: browsers support neither an
+ integrity option on register() nor SRI for importScripts().
+ A tampered service-worker.js or bit-bswup.sw.js is effectively persistent,
+ fully-privileged XSS. Serve both over HTTPS from an origin you control and apply a strict
+ Content-Security-Policy.
+
+
+ Bswup registers with updateViaCache: 'none' so update checks bypass the HTTP cache for
+ the whole service-worker.js → bit-bswup.sw.js → service-worker-assets.js
+ import chain. As defense-in-depth (support is uneven on older Safari/iOS, and proxies are not bound
+ by it), also send Cache-Control: no-cache for these two files so every fetch
+ revalidates against the origin.
+
+
+
+
How the URL-matching lists are matched
+
+ assetsInclude, assetsExclude, prohibitedUrls,
+ serverHandledUrls and serverRenderedUrls all accept the same two kinds of entry:
+
+
+
a RegExp (e.g. /\/admin\//) is used as the pattern it is - what you want for anything non-trivial;
+
a string (e.g. '/admin/') is matched literally as a substring of the URL. It is regex-escaped, so 'v1.0' matches only v1.0 and never v1X0.
+
+
+ 'app.css' matches /css/app.css anywhere in the URL; anchor with a
+ RegExp such as /\/app\.css$/ if that is too broad. Prefer a RegExp
+ whenever you need anchoring, alternation or wildcards. Also note: before v-10-6-0 string entries were
+ silently ignored - see the migration guide.
+
+
+
Asset selection
+
+
assetsInclude
+
The list of file names from the assets list to include when Bswup stores them in cache storage (regex supported).
+
+
assetsExclude
+
The list of file names from the assets list to exclude when Bswup stores them in cache storage (regex supported).
+
+
ignoreDefaultInclude / ignoreDefaultExclude
+
Ignore the default include / exclude arrays Bswup provides. The defaults are:
+
+
+ /\.wasm/, /\.pdb/ and /\.html/ are deliberately unanchored
+ (mirroring the standard Blazor template), so variants such as foo.wasm.br also match.
+ The default excludes keep the service worker's own scripts out of the cache:
+
+
+
+ Caching service-worker-related files corrupts the update cycle of the service worker. Only the browser
+ should handle these files.
+
+
+
externalAssets
+
+ The list of external assets to cache that are not included in the auto-generated assets file. For
+ example, if you're not using index.html (like _host.cshtml), add
+ { "url": "/" }.
+
+
+
Accepted entry shapes and behaviors:
+
+
An object with a url (a concrete string, or a RegExp for server-generated names unknown ahead of time), a bare string, or a bare RegExp; a single value also works without the array.
+
An entry may carry a hash (an SRI digest, sha256-...) that participates in ?v= cache busting and - when enableIntegrityCheck is on - in integrity verification, exactly like a manifest asset.
+
Entries whose url cannot be parsed are skipped with a non-fatal request error instead of breaking the worker.
+
Cross-origin entries are fetched in CORS mode first; when the host sends no CORS headers, Bswup retries with no-cors and caches the resulting opaque response so the asset still works offline. This fallback is skipped for integrity-checked assets. Browsers pad opaque responses in quota accounting (Chromium reserves several megabytes per entry), so prefer CORS-enabled hosts when you control them.
+
Media works too: requests carrying a Range header are answered with a real 206 Partial Content sliced from the cached full body (Safari refuses media served as 200 for a ranged request); uncached ranged requests pass through with their Range intact, and partial responses are never written to the cache.
+
Entries cached for RegExp patterns are kept across updates so the app still boots offline, but only the newest three generations per pattern survive each update.
+
+
+
Navigation & URLs
+
+
defaultUrl
+
+ The default page URL, served from cache for navigation requests (the SPA fallback). Defaults to
+ index.html; use / when using _Host.cshtml. The value must match
+ an entry that actually exists in service-worker-assets.js or externalAssets;
+ the comparison uses resolved URLs, so equivalent spellings match. When nothing matches,
+ offline navigation cannot work and Bswup logs a defaultUrl ... matches no asset warning
+ at startup.
+
+
+
Navigations whose URL is itself a managed asset are served that asset instead of the default document (changed in v-10-6-0): opening /manifest.json directly shows that file, while route URLs (/counter, ...) still get the app shell.
+
If your host answers the shell URL with a redirect (e.g. / → /index.html, common on Cloudflare Pages and Netlify), Bswup rebuilds that response so offline deep-link navigation keeps working instead of failing with a redirect-mode error.
+
+
+
assetsUrl
+
+ The path of the compile-time assets manifest (default file name service-worker-assets.js).
+ The default is resolved relative to the service worker script's own location - which is also where
+ Blazor publishes the file - so it works unchanged for apps mounted on a sub-path. Set it explicitly
+ only when the file lives somewhere else; a leading / makes the path origin-absolute.
+
+
+
prohibitedUrls
+
+ URLs that should not be accessed (regex supported). Matching requests are answered by the service
+ worker with 403 Forbidden for every HTTP method (changed in v-10-6-0 - previously
+ 405).
+
+
+ Enforcement happens only inside the service worker, which is bypassed whenever the page is not
+ controlled (the very first visit, a hard reload, browsers without service worker support) and by any
+ client that talks to the server directly. Access control must be enforced on the server.
+
+
+
serverHandledUrls
+
URLs that skip the service worker offline pipeline entirely and are handled only by the server (regex supported) - e.g. /api, /swagger.
+
+
serverRenderedUrls
+
URLs that should be rendered by the server rather than the client while navigating (regex supported) - e.g. /about.html, /privacy.
+
+
caseInsensitiveUrl
+
+ Enables case-insensitive URL checking - for asset cache matching and every URL-matching regex
+ list: when enabled, patterns are compiled with the i flag so e.g.
+ prohibitedUrls: [/\/admin\//] also blocks /ADMIN/.
+
+
+
noPrerenderQuery
+
+ The query string attached to the default document request to disable server prerendering, so an
+ unwanted prerendered result is not cached - e.g. no-prerender=true.
+
+
+
forcePrerender
+
+ Forces prerendering of the default document for every navigation request to ensure the server always
+ has the latest version of the app. Useful for server-rendered apps.
+
+
+
Install behavior & resiliency
+
+
isPassive
+
+ Enables passive mode: assets are not cached in advance but upon first request. Passive mode does not
+ skip the full download entirely - on a first install, once Blazor has started, the service
+ worker still tops up the cache in the background with every asset not yet fetched, so the app ends up
+ fully offline-capable. What passive mode buys is that the first paint is never blocked behind a full
+ precache. Assets lazily fetched by the app while the top-up runs can be downloaded twice in that
+ window - a bandwidth cost, not a correctness issue.
+
+
+
errorTolerance
+
Controls how the service worker reacts to asset download / cache failures during install:
+
+
+
+
Value
Behavior
+
+
+
+
lax (default)
+
+ Best-effort install. Asset failures never fail the install; missing assets are filled in
+ lazily on first fetch. Failures are reported through the error event with
+ fatal: false and still count toward progress so the bar reaches 100%.
+ Tolerates optional externalAssets that may legitimately 404, and avoids
+ leaving a first visit with no service worker to complete the startup handshake. The
+ download runs under the install event's waitUntil, so
+ updateReady is only announced once the background fill has settled.
+
+
+
+
strict
+
+ Mirrors the standard Microsoft template / Workbox behavior. If any required asset fails,
+ the install rejects, the partial cache is discarded, and the previous service worker (if
+ any) keeps serving. Failed assets are not counted toward progress, so 100% means
+ every asset succeeded; the abort is reported with reason: 'install-aborted'
+ and fatal: true. On a first install (no previous version to fall back to)
+ Bswup starts the app without a service worker so it still boots, and the install is
+ retried on the next load.
+
+
+
+
+
+
+
maxRetries / retryDelay
+
+ maxRetries (default 2) is the number of additional download attempts
+ after the first when an asset fails transiently during install (a rejected fetch, or HTTP 408/429/5xx).
+ Deterministic failures - 404/403 and other permanent statuses, and SRI mismatches - are never retried,
+ since identical bytes would fail identically.
+
+
+ retryDelay (default 300) is the base backoff in milliseconds: attempt
+ n waits retryDelay * 2^(n-1) plus random jitter, so a mass failure doesn't re-hit
+ the origin in one synchronized burst.
+
+
+
enableIntegrityCheck
+
+ Enables the browsers' built-in integrity verification by setting the integrity attribute
+ on the requests the service worker creates to fetch assets.
+
+
+
Caching & updates
+
+
cacheVersion
+
+ Overrides the value used to name the cache bucket (bit-bswup:<scope-path> - <version>).
+ By default this tracks Blazor's assetsManifest.version (a hash over the published assets),
+ so the cache rotates automatically whenever any asset hash changes - and only then. Set it to take
+ manual control:
+
+
+
Pin it to a stable string so noisy dev rebuilds don't needlessly evict the whole cache.
+
Bump it to force a refresh when a meaningful change lives outside Blazor's asset manifest.
+
Feed it a build-stamped value (commit SHA, build timestamp) so it bumps automatically per publish.
+
+
+ Only the cache bucket name is affected: per-asset ?v= cache busting and Subresource
+ Integrity keep using each asset's own hash.
+
+
+
disableHashlessAssetsUpdate
+
+ Disables the automatic re-download of hash-less assets (e.g. external assets) that Bswup performs every
+ time an update is found.
+
+
+
enableCacheControl
+
+ Adds cache-busting to each asset request (cache: 'no-store' plus a
+ cache-control: no-cache header). The header is only attached to same-origin requests: it
+ is not CORS-safelisted, so on a cross-origin asset it would force a preflight most third-party hosts
+ reject; cross-origin requests rely on the cache: no-store option alone.
+
+
+
Diagnostics
+
+
enableDiagnostics: pushes service worker logs to the browser console.
+
enableFetchDiagnostics: additionally logs every fetch event the worker handles.
+
+
+
Modes
+
+ A mode is a preset bundle of defaults for the individual settings above
+ (isPassive, defaultUrl, forcePrerender,
+ errorTolerance, caseInsensitiveUrl, noPrerenderQuery): it only
+ fills settings you have not assigned yourself, so any explicit assignment always wins over the preset -
+ including explicit falsy values such as caseInsensitiveUrl = false.
+
+
+
+
+
Mode
Behavior
+
+
+
NoPrerender
Disables prerendering of the default document for every navigation request.
+
InitialPrerender
Enables prerendering of the default document only for the initial navigation request.
+
AlwaysPrerender
Enables prerendering of the default document for every navigation request.
+
FullOffline
Full offline mode: all assets are cached and served from cache from the first time the app loads.
- Swapping to Development environment will display more detailed information about the error that occurred.
-
-
- The Development environment shouldn't be enabled for deployed applications.
- It can result in displaying sensitive information from exceptions to end users.
- For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
- and restarting the app.
-
-
-@code{
- [CascadingParameter]
- private HttpContext? HttpContext { get; set; }
-
- private string? RequestId { get; set; }
- private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
-
- protected override void OnInitialized() =>
- RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
-}
diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Program.cs b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Program.cs
deleted file mode 100644
index 668876cc8a..0000000000
--- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Program.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Bit.Bswup.NewDemo.Components;
-
-var builder = WebApplication.CreateBuilder(args);
-
-// Add services to the container.
-builder.Services.AddRazorComponents()
- .AddInteractiveServerComponents()
- .AddInteractiveWebAssemblyComponents();
-
-var app = builder.Build();
-
-// Configure the HTTP request pipeline.
-if (app.Environment.IsDevelopment())
-{
- app.UseWebAssemblyDebugging();
-}
-else
-{
- app.UseExceptionHandler("/Error", createScopeForErrors: true);
- // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
- app.UseHsts();
-}
-
-app.UseHttpsRedirection();
-
-app.MapStaticAssets();
-app.UseAntiforgery();
-
-app.MapRazorComponents()
- .AddInteractiveServerRenderMode()
- .AddInteractiveWebAssemblyRenderMode()
- .AddAdditionalAssemblies(typeof(Bit.Bswup.NewDemo.Client._Imports).Assembly);
-
-app.Run();
diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.Development.json b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.Development.json
deleted file mode 100644
index 0c208ae918..0000000000
--- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.Development.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- }
-}
diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.json b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.json
deleted file mode 100644
index 10f68b8c8b..0000000000
--- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- },
- "AllowedHosts": "*"
-}
diff --git a/src/Bswup/Bit.Bswup.slnx b/src/Bswup/Bit.Bswup.slnx
index 2b41b652c7..85af14d2f4 100644
--- a/src/Bswup/Bit.Bswup.slnx
+++ b/src/Bswup/Bit.Bswup.slnx
@@ -6,14 +6,13 @@
-
-
-
+
+
-
-
-
+
+
+
-
+
diff --git a/src/Bswup/Bit.Bswup/Bit.Bswup.csproj b/src/Bswup/Bit.Bswup/Bit.Bswup.csproj
index 0335bfd2f5..e355026245 100644
--- a/src/Bswup/Bit.Bswup/Bit.Bswup.csproj
+++ b/src/Bswup/Bit.Bswup/Bit.Bswup.csproj
@@ -5,10 +5,15 @@
net10.0;net9.0;net8.0true
-
- BeforeBuildTasks;
- $(ResolveStaticWebAssetsInputsDependsOn)
-
+
+ es2019
@@ -21,41 +26,142 @@
+
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/src/Bswup/Bit.Bswup/BswupProgress.razor b/src/Bswup/Bit.Bswup/BswupProgress.razor
index 3d6344e5a2..35c432f0b0 100644
--- a/src/Bswup/Bit.Bswup/BswupProgress.razor
+++ b/src/Bswup/Bit.Bswup/BswupProgress.razor
@@ -1,7 +1,16 @@
@code {
[Parameter] public RenderFragment ChildContent { get; set; } = default!;
- [Parameter] public bool AutoReload { get; set; } = true;
+ // Whether a finished update is activated (and every open tab reloaded) automatically, or
+ // announced through the reload button for the user to accept. Defaults to false: an
+ // unprompted reload discards whatever in-page state the user has mid-session (a
+ // half-filled form has no other copy), which is a worse failure than running one version
+ // behind for a while - and it matches the prompt-then-reload pattern of the standard
+ // Blazor template and this package's own README sample. First installs are unaffected:
+ // they always complete the claim-and-start flow (no reload) regardless of this setting.
+ // CHANGED in v-10-6-0 - previous versions defaulted to true; set AutoReload="true"
+ // explicitly to keep the old behavior.
+ [Parameter] public bool AutoReload { get; set; } = false;
[Parameter] public bool ShowLogs { get; set; } = false;
[Parameter] public bool ShowAssets { get; set; } = false;
[Parameter] public string AppContainer { get; set; } = "#app";
@@ -10,24 +19,83 @@
[Parameter] public string? Handler { get; set; }
}
-
- @if (ChildContent is not null)
+@* Configuration is published as data-* attributes and read by bit-bswup.progress.js when it
+ loads (it self-initializes from these attributes). This deliberately avoids emitting an
+ inline
+
+
+
+