Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)');
} else {
console.error('BitBswupProgress not found');
}
</script>
Comment thread
msynk marked this conversation as resolved.
Comment thread
msynk marked this conversation as resolved.
Comment thread
msynk marked this conversation as resolved.
Comment thread
msynk marked this conversation as resolved.
</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