bit Bswup

Preparing the app for offline use, please wait...

0%

bit platform logo Bswup
NuGet GitHub

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

BswupMessageValueRaised when
updateFoundUPDATE_FOUNDThe browser found a new service worker version and started installing it.
stateChangedSTATE_CHANGEDThe installing worker's lifecycle state changed (data.currentTarget.state).
downloadStartedDOWNLOAD_STARTEDAsset download began. data.version, data.firstInstall.
downloadProgressDOWNLOAD_PROGRESSAn asset finished. data.percent (0-100), data.index (1-based count), data.asset (url, reqUrl, hash).
downloadFinishedDOWNLOAD_FINISHEDAll assets are staged. data.firstInstall, data.reload(), data.cleanup().
updateReadyUPDATE_READYA fully staged update is waiting to be activated (data.reload()).
activateACTIVATEA new version activated (data.version).
updateNotFoundUPDATE_NOT_FOUNDAn update check completed and the app is already on the latest version.
updateCheckFailedUPDATE_CHECK_FAILEDAn update check itself failed transiently (offline, server hiccup). Non-blocking - the app keeps running.
errorERRORA structured install failure - see below.

A complete handler

javascript
const appEl = document.getElementById('app');
const bswupEl = document.getElementById('bit-bswup');
const progressBar = document.getElementById('bit-bswup-progress-bar');
const reloadButton = document.getElementById('bit-bswup-reload');

function bitBswupHandler(type, data) {
    switch (type) {
        case BswupMessage.updateFound: return console.log('an update found.');

        case BswupMessage.stateChanged: return console.log('state:', data.currentTarget.state);

        case BswupMessage.activate: return console.log('new version activated:', data.version);

        case BswupMessage.downloadStarted:
            // A background update downloads behind the running app - only a first
            // install owns the screen (firstInstall rides on every message).
            if (data?.firstInstall === false) return;
            appEl.style.display = 'none';
            bswupEl.style.display = 'block';
            return console.log('downloading assets started:', data?.version);

        case BswupMessage.downloadProgress:
            progressBar.style.width = `${Math.round(data.percent)}%`;
            return console.log('asset downloaded:', data.asset.url, data);

        case BswupMessage.downloadFinished:
            if (data.firstInstall) {
                // First install: claim + start Blazor, no page reload. Reveal the app even if
                // the claim/start stalls or rejects - a hidden #app with no way out is worse
                // than an app shown behind a stale splash.
                const reveal = () => {
                    appEl.style.display = 'block';
                    bswupEl.style.display = 'none';
                };
                const failSafe = setTimeout(reveal, 10000);
                data.reload()
                    .catch(err => console.error('Bswup first install failed to start:', err))
                    .then(() => { clearTimeout(failSafe); reveal(); });
            } else {
                // Update: let the user accept it.
                reloadButton.style.display = 'block';
                reloadButton.onclick = data.reload;
            }
            return console.log('downloading assets finished.');

        case BswupMessage.updateReady:
            reloadButton.style.display = 'block';
            reloadButton.onclick = data.reload;
            return console.log('new update ready.');

        case BswupMessage.updateNotFound:
            return console.log('already on the latest version.');

        case BswupMessage.updateCheckFailed:
            return console.warn('could not check for updates right now:', data);

        case BswupMessage.error:
            if (data.fatal === false) {
                // lax tolerance: the install continued; the asset lazy-fills later.
                return console.warn('Bswup asset skipped:', data.reason, data.message);
            }
            console.error('Bswup install error:', data.reason, data.message, data);
            if (data.firstInstall) {
                // Fatal first-install failure: Bswup force-starts Blazor from the network;
                // reveal the app instead of leaving it booted behind the splash.
                appEl.style.display = 'block';
                bswupEl.style.display = 'none';
            }
            return;
    }
}

Finishing a download: reload and cleanup

  • 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.
Breaking change: updates no longer auto-reload by default
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:

FieldMeaning
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).
messageHuman-readable description.
url / hashThe 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).
javascript
case BswupMessage.error:
    if (data.fatal === false) {
        console.warn('Bswup asset skipped:', data.reason, data.message, data);
        return;
    }
    if (data.firstInstall === false) {
        // A background update failed - the running app is untouched.
        return;
    }
    // Fatal first-install failure: Bswup force-starts Blazor from the network;
    // reveal the app instead of leaving it booted behind the splash.
    appEl.style.display = 'block';
    bswupEl.style.display = 'none';
    return;