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 a4d4e2d626..a673ea3d6b 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 ef03a6e790..c3d7ead125 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.slnx b/src/Bswup/Bit.Bswup.slnx index 2b41b652c7..6bf34c2593 100644 --- a/src/Bswup/Bit.Bswup.slnx +++ b/src/Bswup/Bit.Bswup.slnx @@ -7,12 +7,12 @@ - - + + - - + + diff --git a/src/Bswup/Bit.Bswup/Bit.Bswup.csproj b/src/Bswup/Bit.Bswup/Bit.Bswup.csproj index 0335bfd2f5..ff8c95a1a1 100644 --- a/src/Bswup/Bit.Bswup/Bit.Bswup.csproj +++ b/src/Bswup/Bit.Bswup/Bit.Bswup.csproj @@ -5,10 +5,6 @@ net10.0;net9.0;net8.0 true - - BeforeBuildTasks; - $(ResolveStaticWebAssetsInputsDependsOn) - @@ -21,41 +17,117 @@ + - - + + - - - + + - - - - + + + + + + + + + + + + + + - + + + + + + + - + + + - - - + + + + + + + + + + + + + + + + + - - - - - - - - \ No newline at end of file + diff --git a/src/Bswup/Bit.Bswup/BswupProgress.razor b/src/Bswup/Bit.Bswup/BswupProgress.razor index 3d6344e5a2..e00d8b6dd8 100644 --- a/src/Bswup/Bit.Bswup/BswupProgress.razor +++ b/src/Bswup/Bit.Bswup/BswupProgress.razor @@ -10,24 +10,54 @@ [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 + + + + + + + + diff --git a/src/Bswup/FullDemo/Server/Components/_Imports.razor b/src/Bswup/FullDemo/Server/Components/_Imports.razor new file mode 100644 index 0000000000..23aacbed1f --- /dev/null +++ b/src/Bswup/FullDemo/Server/Components/_Imports.razor @@ -0,0 +1,13 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Bit.Bswup +@using Bit.Bswup.FullDemo.Server +@using Bit.Bswup.FullDemo.Server.Components +@using Bit.Bswup.FullDemo.Client +@using Bit.Bswup.FullDemo.Client.Shared diff --git a/src/Bswup/FullDemo/Server/Pages/_Host.cshtml b/src/Bswup/FullDemo/Server/Pages/_Host.cshtml deleted file mode 100644 index 9f671f70ec..0000000000 --- a/src/Bswup/FullDemo/Server/Pages/_Host.cshtml +++ /dev/null @@ -1,14 +0,0 @@ -@page "/" -@namespace Bit.Bswup.Demo.Web.Pages -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers - -@using RenderMode = Microsoft.AspNetCore.Mvc.Rendering.RenderMode - -@{ - Layout = "_Layout"; - - var noPrerender = Request.Query["no-prerender"].Count > 0; - var renderMode = noPrerender ? RenderMode.WebAssembly : RenderMode.WebAssemblyPrerendered; -} - - \ No newline at end of file diff --git a/src/Bswup/FullDemo/Server/Pages/_Layout.cshtml b/src/Bswup/FullDemo/Server/Pages/_Layout.cshtml deleted file mode 100644 index 34dacd5b93..0000000000 --- a/src/Bswup/FullDemo/Server/Pages/_Layout.cshtml +++ /dev/null @@ -1,44 +0,0 @@ -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@namespace Bit.Bswup.Demo.Web.Pages - -@using Bit.Bswup.Demo.Web -@using Microsoft.AspNetCore.Http -@using Microsoft.AspNetCore.Components.Web -@using RenderMode = Microsoft.AspNetCore.Mvc.Rendering.RenderMode - - - - - - - bit Bswup Full Demo - - - - - - -
- @RenderBody() -
- - - - - - - - @* - *@ - - \ No newline at end of file diff --git a/src/Bswup/FullDemo/Server/Program.cs b/src/Bswup/FullDemo/Server/Program.cs index 49c224c338..37bf9cc289 100644 --- a/src/Bswup/FullDemo/Server/Program.cs +++ b/src/Bswup/FullDemo/Server/Program.cs @@ -1,20 +1,50 @@ -var builder = WebApplication.CreateBuilder(args); +using System.IO.Compression; +using Bit.Bswup.FullDemo.Server.Components; +using Microsoft.AspNetCore.ResponseCompression; -#if DEBUG -if (OperatingSystem.IsWindows()) +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveWebAssemblyComponents(); + +builder.Services.AddControllers(); +builder.Services.AddHttpContextAccessor(); + +builder.Services.AddResponseCompression(opts => { - builder.WebHost.UseUrls("https://localhost:5021", "http://localhost:5020", "https://*:5021", "http://*:5020"); + opts.EnableForHttps = true; + opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]); + opts.Providers.Add(); + opts.Providers.Add(); +}) + .Configure(opt => opt.Level = CompressionLevel.Fastest) + .Configure(opt => opt.Level = CompressionLevel.Fastest); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseWebAssemblyDebugging(); } else { - builder.WebHost.UseUrls("https://localhost:5021", "http://localhost:5020"); + 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.UseResponseCompression(); } -#endif -Bit.Bswup.Demo.Server.Startup.Services.Add(builder.Services); +app.UseHttpsRedirection(); -var app = builder.Build(); +app.MapStaticAssets(); +app.UseAntiforgery(); + +app.MapControllers(); -Bit.Bswup.Demo.Server.Startup.Middlewares.Use(app, builder.Environment); +app.MapRazorComponents() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(Bit.Bswup.FullDemo.Client._Imports).Assembly); app.Run(); diff --git a/src/Bswup/FullDemo/Server/Properties/launchSettings.json b/src/Bswup/FullDemo/Server/Properties/launchSettings.json index c60dbf41d8..a257a7aa19 100644 --- a/src/Bswup/FullDemo/Server/Properties/launchSettings.json +++ b/src/Bswup/FullDemo/Server/Properties/launchSettings.json @@ -1,7 +1,7 @@ { "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "Bit.Bswup.Demo.Server": { + "Bit.Bswup.FullDemo.Server": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, diff --git a/src/Bswup/FullDemo/Server/Startup/Middlewares.cs b/src/Bswup/FullDemo/Server/Startup/Middlewares.cs deleted file mode 100644 index 2cadf34289..0000000000 --- a/src/Bswup/FullDemo/Server/Startup/Middlewares.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.Net.Http.Headers; - -namespace Bit.Bswup.Demo.Server.Startup; - -public static class Middlewares -{ - public static void Use(IApplicationBuilder app, IHostEnvironment env) - { - //app.Use(async (context, next) => - //{ - // await Task.Delay(new Random().Next(500, 800)); - // await next.Invoke(context); - //}); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.UseWebAssemblyDebugging(); - } - - app.UseBlazorFrameworkFiles(); - - if (env.IsDevelopment() is false) - { - app.UseResponseCompression(); - } - app.UseStaticFiles(new StaticFileOptions - { - OnPrepareResponse = ctx => - { - ctx.Context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromDays(7), - Public = true - }; - } - }); - - app.UseRouting(); - app.UseCors(options => options.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()); - - app.UseEndpoints(endpoints => - { - endpoints.MapDefaultControllerRoute(); - - endpoints.MapFallbackToPage("/_Host"); - }); - } -} diff --git a/src/Bswup/FullDemo/Server/Startup/Services.cs b/src/Bswup/FullDemo/Server/Startup/Services.cs deleted file mode 100644 index 7137f85770..0000000000 --- a/src/Bswup/FullDemo/Server/Startup/Services.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.IO.Compression; -using Microsoft.AspNetCore.ResponseCompression; - -namespace Bit.Bswup.Demo.Server.Startup; - -public static class Services -{ - public static void Add(IServiceCollection services) - { - services.AddRazorPages(); - - services.AddCors(); - - services.AddControllers(); - - services.AddHttpContextAccessor(); - - services.AddResponseCompression(opts => - { - opts.EnableForHttps = true; - opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "application/octet-stream" }).ToArray(); - opts.Providers.Add(); - opts.Providers.Add(); - }) - .Configure(opt => opt.Level = CompressionLevel.Fastest) - .Configure(opt => opt.Level = CompressionLevel.Fastest); - } -} diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Bit.Bswup.NewDemo.Client.csproj b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Bit.Bswup.NewDemo.Client.csproj similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Bit.Bswup.NewDemo.Client.csproj rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Bit.Bswup.NewDemo.Client.csproj diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor.css b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor.css similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor.css rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor.css diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor.css b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor.css similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor.css rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor.css diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Pages/Counter.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Pages/Counter.razor similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Pages/Counter.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Pages/Counter.razor diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Pages/Home.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Pages/Home.razor similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Pages/Home.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Pages/Home.razor diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Program.cs b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Program.cs similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Program.cs rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Program.cs diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Routes.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Routes.razor similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Routes.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Routes.razor diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/_Imports.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/_Imports.razor similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/_Imports.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/_Imports.razor diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/app.css b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/app.css similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/app.css rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/app.css diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/bit-bw-64.png b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/bit-bw-64.png similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/bit-bw-64.png rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/bit-bw-64.png diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/favicon.ico b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/favicon.ico similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/favicon.ico rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/favicon.ico diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/icon-512.png b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/icon-512.png similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/icon-512.png rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/icon-512.png diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/manifest.json b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/manifest.json similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/manifest.json rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/manifest.json diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.csproj b/src/Bswup/NewDemo/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.csproj similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.csproj rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.csproj diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/App.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo/Components/App.razor similarity index 98% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/App.razor rename to src/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/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.