Skip to content
4 changes: 3 additions & 1 deletion src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
// {
Expand Down
22 changes: 20 additions & 2 deletions src/Bswup/Bit.Bswup/BswupProgress.razor
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,27 @@
</div>
<p id="bit-bswup-percent">0 %</p>
<ul id="bit-bswup-assets" style="display: @(ShowAssets ? "block" : "none");"></ul>
<div id="bit-bswup-error" class="bit-bswup-error" style="display: none;" role="alert">
<p class="bit-bswup-error-title">Update failed to install</p>
<p id="bit-bswup-error-message" class="bit-bswup-error-message"></p>
<pre id="bit-bswup-error-details" class="bit-bswup-error-details"></pre>
<button id="bit-bswup-error-retry" type="button">Retry</button>
</div>
</div>
<button id="bit-bswup-reload">Update ready to install!</button>
}
<img style="display: none" src=""
onerror="BitBswupProgress.start(@(AutoReload ? "true" : "false"), @(ShowLogs ? "true" : "false"), @(ShowAssets ? "true" : "false"), '@(AppContainer)', @(HideApp ? "true" : "false"), @(AutoHide ? "true" : "false"), '@(Handler)')">
<script>
if (window.BitBswupProgress !== undefined) {
window.BitBswupProgress.start(
@(AutoReload ? "true" : "false"),
@(ShowLogs ? "true" : "false"),
@(ShowAssets ? "true" : "false"),
'@(AppContainer)',
@(HideApp ? "true" : "false"),
@(AutoHide ? "true" : "false"),
'@(Handler)');
Comment thread
msynk marked this conversation as resolved.
Outdated
Comment thread
msynk marked this conversation as resolved.
Outdated
Comment thread
msynk marked this conversation as resolved.
Outdated
} else {
console.error('BitBswupProgress not found');
}
</script>
</div>
91 changes: 91 additions & 0 deletions src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
window['bit-bswup.progress version'] = '10.4.5';

// Default progress/splash UI for Bswup. This script registers the global
// `bitBswupHandler` that bit-bswup.ts calls with every BswupMessage, and drives the
// built-in splash markup (progress bar, percentage, asset log, reload/retry buttons,
// error panel) rendered by BswupProgress.razor. Apps can layer their own behavior by
// passing a custom `handler` name, which is invoked after the built-in handling.
(function () {
// Live config overrides applied via BitBswupProgress.config(); each value, when set,
// takes precedence over the corresponding argument passed to start().
const _config: IBswupProgressConfigs = {};

(window as any).BitBswupProgress = {
start,
config
};

// Initializes the splash UI and installs the message handler. Called once from the
// generated startup markup with the app's display preferences:
// autoReload - reload automatically when an update finishes, instead of
// showing a manual reload button
// showLogs - console.log lifecycle messages
// showAssets - list each downloaded asset in the UI
// appContainerSelector - element whose visibility is toggled while installing
// hideApp - hide the app element during download
// autoHide - hide the splash automatically when the download finishes
// handler - optional name of a user handler invoked after the built-in one
function start(autoReload: boolean,
showLogs: boolean,
showAssets: boolean,
Expand All @@ -22,12 +39,19 @@
const percentEl = document.getElementById('bit-bswup-percent');
const assetsEl = document.getElementById('bit-bswup-assets');
const reloadButton = document.getElementById('bit-bswup-reload');
const errorEl = document.getElementById('bit-bswup-error');
const errorMessageEl = document.getElementById('bit-bswup-error-message');
const errorDetailsEl = document.getElementById('bit-bswup-error-details');
const errorRetryButton = document.getElementById('bit-bswup-error-retry');

const appElOriginalDisplay = appEl && appEl.style.display;

(window as any).bitBswupHandler = bitBswupHandler;
const handlerFn = (handler ? window[handler] : undefined) as (message: any, data: any) => void;

// The global handler bit-bswup.ts invokes for every lifecycle message. It runs the
// built-in UI handling first, then forwards to the optional user handler (errors in
// the user handler are caught so they can't break the splash).
function bitBswupHandler(message: string, data: any) {
handleInternal(message, data);

Expand Down Expand Up @@ -100,13 +124,80 @@
reloadButton && (reloadButton.onclick = data.reload);
}
return showLogs_ ? console.log('new update is ready.') : undefined;

case BswupMessage.error:
// Reveal the install panel even if no progress event landed first
// (manifest validation failures fire before any progress message).
hideApp_ && appEl && (appEl.style.display = 'none');
bswupEl && (bswupEl.style.display = 'block');

Comment thread
msynk marked this conversation as resolved.
// A failed install supersedes any earlier "update ready" prompt. Leaving
// the reload button visible would invite the user to activate an update
// that has already failed, promoting a broken worker / caches. Hide and
// unwire it so the only actionable control is the (conditional) Retry.
if (reloadButton) {
reloadButton.style.display = 'none';
reloadButton.onclick = null;
}

// The error supersedes any in-flight progress. Hide the bar and the
// percentage so a stale partial value (e.g. "47%") isn't left sitting
// next to the failure message.
if (progressEl && progressEl.parentElement) progressEl.parentElement.style.display = 'none';
if (percentEl) percentEl.style.display = 'none';

if (errorEl) {
errorEl.style.display = 'block';
if (errorMessageEl) errorMessageEl.textContent = (data && data.message) || 'Service worker install failed.';
if (errorDetailsEl) {
const reasonText = data && data.reason ? `[${data.reason}] ` : '';
const urlText = data && data.url ? `\nasset: ${data.url}` : '';
const hashText = data && data.hash ? `\nhash: ${data.hash}` : '';
errorDetailsEl.textContent = `${reasonText}${urlText}${hashText}`.trim();
}
if (errorRetryButton) {
// Some failures are deterministic - a plain reload re-fetches the
// same broken bytes and fails identically. A manifest that won't
// parse or an SRI/integrity mismatch needs a redeploy (or fixed
// CDN/proxy), not a retry. For those, hide the retry button so we
// don't invite a pointless reload loop; keep it for transient
// failures (network/fetch/cache) where reloading can genuinely help.
const nonRetriableReasons = ['manifest', 'integrity', 'install-incomplete'];
const isRetriable = !(data && nonRetriableReasons.indexOf(data.reason) !== -1);
if (isRetriable) {
errorRetryButton.style.display = 'inline-block';
errorRetryButton.onclick = () => {
if (data && typeof data.reload === 'function') {
data.reload();
} else {
window.location.reload();
}
};
} else {
errorRetryButton.style.display = 'none';
errorRetryButton.onclick = null;
Comment thread
msynk marked this conversation as resolved.
}
}
}
// Always log errors regardless of showLogs - this is actionable info.
console.error('BitBswup install error:', data);
return;
}
}
}
};

function config(newConfig: IBswupProgressConfigs) {
Object.assign(_config, newConfig);

// Keep the assets list visibility in sync when toggled at runtime.
// The <ul> is server-rendered with an inline display style based on the
// initial ShowAssets parameter, so flipping the config alone wouldn't
// reveal/hide it without also updating the element here.
if (newConfig.showAssets !== undefined) {
const assetsEl = document.getElementById('bit-bswup-assets');
if (assetsEl) assetsEl.style.display = newConfig.showAssets ? 'block' : 'none';
}
}
}());

Expand Down
13 changes: 12 additions & 1 deletion src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw-cleanup.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
self['bit-bswup.sw-cleanup version'] = '10.4.5';
(self as any)['bit-bswup.sw-cleanup version'] = '10.4.5';

// Self-destructing "uninstall" service worker. Deploy this in place of the real
// bit-bswup.sw.js when an app needs to fully back out of Bswup (e.g. switching a site away
// from offline support, or recovering clients stuck on a broken worker/cache). On install
// it wipes every Bswup/Blazor cache, immediately takes over all clients, and tells each one
// to unregister and reload - leaving the app running purely from the network with no SW.
self.addEventListener('install', e => e.waitUntil(removeBswup()));

// Purges the caches this library (and Blazor) created, then activates immediately and
// signals every open client to tear down. Runs once, at install time.
async function removeBswup() {
const cacheKeys = await caches.keys();
const cachePromises = cacheKeys.filter(key => key.startsWith('bit-bswup') || key.startsWith('blazor-resources')).map(key => caches.delete(key));
await Promise.all(cachePromises);

// skipWaiting() so this cleanup worker activates without waiting for existing clients to
// close, then message every client (controlled or not) to unregister itself. The delayed
// 'WAITING_SKIPPED' nudge is a fallback reload signal for clients that don't act on
// 'UNREGISTER' fast enough, so no tab is left running against the now-deleted caches.
self.skipWaiting().then(() => self.clients
.matchAll({ includeUncontrolled: true })
.then(clients => (clients || []).forEach(client => {
Expand Down
Loading
Loading