diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/HomePage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/HomePage.razor
index 4ef62a7a95..bb30021363 100644
--- a/src/Bswup/Bit.Bswup.Demo/Pages/HomePage.razor
+++ b/src/Bswup/Bit.Bswup.Demo/Pages/HomePage.razor
@@ -3,7 +3,7 @@
Home
-
222
+111
Hello, world!
diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js
index aeddf5019e..31089441dc 100644
--- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js
+++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js
@@ -2,13 +2,15 @@
self.assetsExclude = [/\.scp\.css$/, /weather\.json$/];
self.caseInsensitiveUrl = true;
-self.precachedAssetsInclude = [/favicon\.ico$/, /icon-512\.png$/, /bit-bw-64\.png$/];
self.externalAssets = [
{
"url": "not-found/script.file.js"
}
];
+// 'lax' opts into best-effort installs: the demo intentionally references a non-existent
+// asset to exercise the progress / error reporting UI. Under the default 'strict' setting
+// that would abort the install. See README.md > errorTolerance.
self.errorTolerance = 'lax';
self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js');
diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js
index d854eca798..b7bc9b3caf 100644
--- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js
+++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js
@@ -2,7 +2,6 @@
self.assetsExclude = [/\.scp\.css$/, /weather\.json$/];
self.caseInsensitiveUrl = true;
-self.precachedAssetsInclude = [/favicon\.ico$/, /icon-512\.png$/, /bit-bw-64\.png$/];
//self.externalAssets = [
// {
diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/App.razor b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/App.razor
index 3110bb5054..a7a2710331 100644
--- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/App.razor
+++ b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/App.razor
@@ -21,7 +21,6 @@
- 111
+ handler="bitBswupHandler"
+ updateInterval="3600"
+ updateOnVisibility="true">
```
- `scope`: The scope of the service-worker ([read more](https://developer.chrome.com/docs/workbox/service-worker-lifecycle/#scope)).
-- `log`: The log level of the Bswup logger. available options are: `info`, `verbose`, `debug`, and `error`. (not implemented yet)
+- `log`: The log level of the Bswup logger. Available options are: `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.
- `handler`: The name of the handler function for the service-worker events.
+- `blazorScript`: The path of the Blazor entry-point script (the one you added `autostart="false"` to in step 3). 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.
+- `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 this to 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 (the browser throttles those timers anyway) and resume when it becomes visible again. Omit or set to `0` to disable (the default).
+- `updateOnVisibility`: When set to `true`, Bswup checks for an update every time the tab returns to the foreground (the `visibilitychange` event). This is a lightweight way to catch updates right when a user comes back to a tab they left open. Disabled by default.
> You can remove any of these attributes, and use the default values mentioned above.
@@ -73,6 +78,7 @@ function bitBswupHandler(type, data) {
return console.log('downloading assets started:', data?.version);
case BswupMessage.downloadProgress:
+ const percent = Math.round(data.percent);
progressBar.style.width = `${percent}%`;
return console.log('asset downloaded:', data);
@@ -92,10 +98,32 @@ function bitBswupHandler(type, data) {
reloadButton.style.display = 'block';
reloadButton.onclick = data.reload;
return console.log('new update ready.');
+
+ case BswupMessage.updateNotFound:
+ return console.log('checked for an update, already on the latest version.');
+
+ case BswupMessage.error:
+ // Structured install failure. data.reason is one of 'manifest' | 'integrity' |
+ // 'fetch' | 'cache' | 'request' | 'install-incomplete'; data.message is human
+ // readable, and data.url / data.hash point at the offending asset when known.
+ console.error('Bswup install error:', data.reason, data.message,
+ ...(data.url ? [`url: ${data.url}`] : []),
+ ...(data.hash ? [`hash: ${data.hash}`] : []),
+ data);
+ return;
}
}
```
+> **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 the `controllerchange` event) 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.
+
6. Configure additional settings in the service-worker file like the following code:
```js
@@ -115,6 +143,7 @@ self.externalAssets = [
];
self.assetsUrl = '/service-worker-assets.js';
self.noPrerenderQuery = 'no-prerender=true';
+self.cacheVersion = '2026.05.31-abc1234';
self.caseInsensitiveUrl = true;
self.ignoreDefaultInclude = true;
@@ -133,6 +162,26 @@ The most important line here is the last line which is the only mandatory config
self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js');
```
+> **Security note - the service worker is part of your trusted base.** Unlike the assets in
+> `service-worker-assets.js` (which Bswup verifies with Subresource Integrity), the
+> service-worker script itself cannot be integrity-pinned: browsers do not support an
+> `integrity` option on `navigator.serviceWorker.register()`, and `importScripts()` has no
+> SRI mechanism either. This is not Bswup-specific - Workbox and every other SW library share
+> the limitation - but it matters because a service worker can intercept every request, so a
+> tampered `service-worker.js` or `bit-bswup.sw.js` is effectively persistent, fully-privileged
+> XSS. Treat the origin/CDN that serves these two files as part of your trusted computing base:
+> serve them over HTTPS from an origin you control, and apply a strict Content-Security-Policy.
+>
+> To keep clients from getting stuck on a stale worker, Bswup registers with
+> `updateViaCache: 'none'`, which tells the browser to bypass the HTTP cache for the
+> service-worker script **and** the scripts it pulls in via `importScripts()` during update
+> checks (the browser default, `'imports'`, would still serve imported scripts from the HTTP
+> cache). That covers the whole `service-worker.js` -> `bit-bswup.sw.js` -> `service-worker-assets.js`
+> import chain. As defense-in-depth - and because `updateViaCache` support is uneven (older
+> Safari/iOS in particular) and intermediary proxies are not bound by it - also configure your
+> server to send `Cache-Control: no-cache` (or `no-store`) for `service-worker.js` and
+> `_content/Bit.Bswup/bit-bswup.sw.js` so every fetch revalidates against the origin.
+
The other settings are:
- `assetsInclude`: The list of file names from the assets list to **include** when the Bswup tries to store them in the cache storage (regex supported).
@@ -141,7 +190,7 @@ The other settings are:
- `defaultUrl`: The default page URL. Use `/` when using `_Host.cshtml`.
- `assetsUrl`: The file path of the service-worker assets file generated at compile time (the default file name is `service-worker-assets.js`).
- `prohibitedUrls`: The list of file names that should not be accessed (regex supported).
-- `caseInsensitiveUrl`: Enables the case insensitivity in the URL checking of the cache process.
+- `caseInsensitiveUrl`: Enables case-insensitive URL checking. This applies both to the asset cache matching and to every URL-matching regex list (`prohibitedUrls`, `serverHandledUrls`, `serverRenderedUrls`, `assetsInclude`, `assetsExclude`): when enabled, those patterns are compiled with the `i` flag so e.g. `prohibitedUrls: [/\/admin\//]` also blocks `/ADMIN/`. Patterns that already specify the `i` flag are left unchanged.
- `serverHandledUrls`: The list of URLs that do not enter the service-worker offline process and will be handled only by server (regex supported). such as `/api`, `/swagger`, ...
- `serverRenderedUrls`: The list of URLs that should be rendered by the server and not client while navigating (regex supported). such as `/about.html`, `/privacy`, ...
- `noPrerenderQuery`: The query string attached to the default document request to disable the prerendering from the server so an unwanted prerendered result not be cached.
@@ -156,14 +205,55 @@ The other settings are:
#### Keep in mind that caching service-worker related files will corrupt the update cycle of the service-worker. Only the browser should handle these files.
- `isPassive`: Enables the Bswup's passive mode. In this mode, the assets won't be cached in advance but rather upon initial request.
- `enableIntegrityCheck`: Enables the default integrity check available in browsers by setting the `integrity` attribute of the request object created in the service-worker to fetch the assets.
-- `errorTolerance`: Determines how the Bswup should handle the errors while downloading assets. Possible values are: `strict`, `lax`, `config`.
+- `errorTolerance`: Controls how the service worker reacts to asset download / cache failures during install. Possible values:
+ - `strict` (default): mirrors the standard Microsoft template / Workbox behavior. If any required asset fails to fetch or store during install, the install promise rejects, the partially populated cache is discarded, and the previous service-worker (if any) keeps serving the app. Failed assets are reported via the `error` message and are *not* counted toward the progress percentage, so 100% means every asset succeeded.
+ - `lax`: best-effort install. The install always succeeds; missing assets are filled in lazily on the first fetch (in both passive and non-passive modes). Failed assets are still reported as errors but are counted toward the progress so the bar can reach 100% even with failures. Use this only when you knowingly accept a partial cache, for example when listing optional `externalAssets` that may legitimately 404.
- `enableDiagnostics`: Enables diagnostics by pushing service-worker logs to the browser console.
- `enableFetchDiagnostics`: Enables fetch event diagnostics by pushing service-worker fetch event logs to the browser console.
-- `disableHashlessAssetsUpdate`: Disables the update of the hash-less assets. By default, the Bswup tries to automatically update all of the hash-less assets (e.g. the external assets) every time an update found for the app.
+- `disableHashlessAssetsUpdate`: Disables the update of hash-less assets. By default, Bswup automatically updates all hash-less assets (e.g. the external assets) every time an update is found for the app.
- `forcePrerender`: Forces the prerendering of the default document for every navigation request to ensure that the server always has the latest version of the app. This is useful when you have a server-rendered app and you want to make sure that the client always has the latest version of the app.
- `enableCacheControl`: Enables the cache-control mechanism by providing cache busting setting and header to each request (`cache:no-store` settings and `cache-control:no-cache` header).
+- `cacheVersion`: Overrides the value used to name the cache storage bucket (`bit-bswup - `). By default this tracks Blazor's `assetsManifest.version` (a hash over the published assets), which means the cache is rotated automatically whenever any asset hash changes - and *only* then. Set `cacheVersion` to take manual control: pin it to a stable string so noisy dev rebuilds that perturb asset hashes don't needlessly evict the whole cache (runtime `.dll`/`.wasm` included), or bump it to force a refresh when a meaningful change lives outside Blazor's asset manifest. Only the cache bucket name (`CACHE_NAME`) is affected. Per-asset cache busting (`?v=`) is set in `createNewAssetRequest()` from each asset's `asset.hash` (falling back to `assetsManifest.version`), and Subresource Integrity uses `asset.hash` when integrity checking is enabled. When unset (or not a non-empty string) it falls back to the manifest version. Tip: feed it a build-stamped value (commit SHA, build timestamp, or your app's informational version) so it bumps automatically per publish.
- `mode`: Determines the mode of the Bswup. Possible values are:
- `NoPrerender`: Disables the prerendering of the default document for every navigation request.
- `InitialPrerender`: Enables the prerendering of the default document only for the initial navigation request.
- `AlwaysPrerender`: Enables the prerendering of the default document for every navigation request.
- - `FullOffline`: Enables the full offline mode where all assets are cached and served from the cache from first time the app is loaded.
\ No newline at end of file
+ - `FullOffline`: Enables the full offline mode where all assets are cached and served from the cache from the first time the app is loaded.
+
+## JavaScript API
+
+Bswup exposes a small global `BitBswup` object on the page so you can drive the update lifecycle from your own code (a "check for updates" button, a custom poller, a "reset app" action, etc.):
+
+- `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 the `updateNotFound` event 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. This is the registration-aware version that powers the built-in polling; it is safe to call as often as you like.
+- `BitBswup.skipWaiting()`: If an update has finished downloading and is waiting, this activates it immediately (equivalent to calling the `reload` callback you receive in `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 (the one whose scope controls the current page, via `navigator.serviceWorker.getRegistration()`), not every same-origin service worker - so other apps or sub-apps mounted under different scopes on the same origin are left untouched. By default it clears **every** CacheStorage bucket (Bswup, Blazor framework, and any app-owned caches such as Workbox add-ons or API caches) so nothing stale survives the reload. To narrow what gets cleared, pass an optional `cacheFilter`: a string (prefix match against the cache name, e.g. `'bit-bswup'`), a `RegExp` (tested against the cache name), or a predicate function `(key) => boolean` that returns `true` for caches to delete.
+
+### 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:
+
+1. Set `updateInterval` (and/or `updateOnVisibility`) on the script tag for built-in polling (see the options above). This is the simplest approach and requires no extra code.
+2. Call `BitBswup.checkForUpdate()` yourself, for example from a timer or after a user action.
+
+```js
+// check every hour from your own code (equivalent to updateInterval="3600")
+setInterval(() => BitBswup.checkForUpdate(), 60 * 60 * 1000);
+
+// or check whenever the user clicks a button, and react to the result
+document.getElementById('check-updates').onclick = () => BitBswup.checkForUpdate();
+```
+
+Either way, the result surfaces through your `bitBswupHandler`: a found update flows through `updateFound`/`updateReady`, "nothing new" flows through `updateNotFound`, and a transient check failure flows through `updateCheckFailed` (handle it the same way as the other events, e.g. stop your spinner and optionally show a "couldn't check right now" hint - the app keeps running on the current version):
+
+```js
+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
+ }
+};
+```
+
+> Built-in polling skips checks while the tab is in the background (the browser throttles
+> those timers anyway) and catches up automatically when the tab becomes visible again.