Search code examples
blazorprogressive-web-appsservice-workerblazor-webassembly

Force a total refresh of Blazor WebAssembly PWA app from Javascript


This code is in index.html. I call the updateVersion with the latest version of my Blazor WebAssembly PWA app. The first line is the original registration of the service worker that was part of the Blazor app template. The rest is added by me.

navigator.serviceWorker.register('service-worker.js');

function updateVersion(newVersion) {
    var key = 'x-photish-version';

    var oldVersion = localStorage.getItem(key);

    if (oldVersion == null) {
        localStorage.setItem(key, newVersion);
    }
    else if (newVersion != oldVersion) {
        localStorage.setItem(key, newVersion);

        // Reload service worker
        navigator.serviceWorker.register('service-worker.js').then(function (registration) {

            caches.delete("blazor-resources-/").then(function (e) {
                console.log("'blazor-resources-/' cache deleted");
            });

            registration.update();

            window.location.reload(); 
        }).catch(function (error) {
            // registration failed
            console.log(`Registration failed with ${error}`);
        });
    }
}

The version part works. It detects that the version is new and it correctly enters the newVersion != oldVersion part of the code where I want to make sure the app is completely refreshed.

To test it, I release a new version of my app with some trivial changes to my app and it detects it's a new version and reloads the page. And my small app changes do not appear. It shows the old version of the app.

It is essential I get a way to do this from the code as I don't want the users to retrieve the latest content on every page load. Only if I actually deployed a new version of the code.

What can I do to ensure the serviceworker is refreshed and the cache of the blazor app itself is not cached?

UPDATE: I thought it was solved after changing the code to the following but I am still seeing some cases where it does not work.

navigator.serviceWorker.register('service-worker.js');

function updateVersion(newVersion) {
    var key = 'x-photish-version';

    var oldVersion = localStorage.getItem(key);

    if (oldVersion == null) {
        localStorage.setItem(key, newVersion);
    }
    else if (newVersion != oldVersion) {
        localStorage.setItem(key, newVersion);

        caches.delete("blazor-resources-/").then(function (e) {
            console.log("'blazor-resources-/' cache deleted");
        });

        // Reload service worker
        navigator.serviceWorker.register('/service-worker.js', { updateViaCache: 'none' }).then(function (registration) {

            window.location.reload(); 

        }).catch(function (error) {
            // registration failed
            console.log(`Registration failed with ${error}`);
        });
    }
}

Still hoping someone out there can fix the code and make it bulletproof or simply declare that this is technically impossible for some reason.


Solution

  • I found a solution and confirmed over several days that it works. It also works from my PWA loaded into iOS.

    My existing function from the question works if combined with an updated const CACHE inside the wwwroot/service-worker.published.js.

    My post-build Powershell script looks like this:

    $currentDate = get-date -format yyyy.MM.dd.HHmm;
    
    # Update service worker
    $curDir = Get-Location
    $inputFile = $curDir.Path +"\wwwroot\service-worker.published.js"
    $findString = "const CACHE_VERSION =.+"
    $replaceString = "const CACHE_VERSION = '$currentDate'"
    (Get-Content $inputFile) | ForEach-Object { $_ -replace $findString , $replaceString } | Set-Content $inputFile
    
    Write-Host "Service Worker Updated:     $currentDate"
    

    For it to work I have to manually add this line to service-worker.published.js:

    const CACHE_VERSION = '2022.08.10.2222'
    

    I am also using the following post-build Powershell script to update the version in version.txt which I always know will be updated. And then I also update the assembly version inside the csproj. That way I can compare the two values to see if I am actually running the newest code:

    param([string]$ProjectDir, [string]$ProjectPath);
    
    $currentDate = get-date -format yyyy.MM.dd.HHmm;
    
    # Update version.txt
    $versionFilePath = $ProjectDir + "/wwwroot/version.txt"
    Set-Content -Path $versionFilePath -Value $currentDate -NoNewline
    
    Write-Host "version.txt Updated:        $currentDate"
    
    # Update actual assembly version
    $find = "<Version>(.|\n)*?</Version>";
    $replace = "<Version>" + $currentDate + "</Version>";
    $csproj = Get-Content $ProjectPath
    $csprojUpdated = $csproj -replace $find, $replace
    
    Set-Content -Path $ProjectPath -Value $csprojUpdated
    
    Write-Host "Assembly Version Updated:   $currentDate"
    

    For your reference, I check the assembly version like this in C#:

    assemblyVersion = Assembly.GetExecutingAssembly()?.
                GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.
                InformationalVersion;
    

    And I check the latest version with a simple http call but indicating no-cache in C#.

    var message = new HttpRequestMessage
    {
        Method = HttpMethod.Get,
        RequestUri = new Uri(baseUri + "version.txt?nocache=" + DateTime.Now.ToString("yyyyMMddHHmmss"))
    };
    message.Headers.CacheControl = new CacheControlHeaderValue
    {
        NoCache = true
    };
    
    var versionResponse = await http.SendAsync(message);
    var version = await versionResponse.Content.ReadAsStringAsync();
    
    return version;
    

    I call my function in index.html with this line in Blazor:

    await js.InvokeAsync<string>("updateVersion", state.Version);
    

    And finally here is the javascript in index.html that will clear the cache and refresh the service-worker.

    navigator.serviceWorker.register('service-worker.js');
    
    function updateVersion(newVersion) {
        var key = 'x-photish-version';
    
        var oldVersion = localStorage.getItem(key);
    
        if (oldVersion == null) {
            localStorage.setItem(key, newVersion);
        }
        else if (newVersion != oldVersion) {
            localStorage.setItem(key, newVersion);
    
            caches.delete("blazor-resources-/").then(function (e) {
                console.log("'blazor-resources-/' cache deleted");
            });
    
            // Reload service worker
            navigator.serviceWorker.register('/service-worker.js', { updateViaCache: 'none' }).then(function (registration) {
    
                window.location.reload(); 
    
            }).catch(function (error) {
                // registration failed
                console.log(`Registration failed with ${error}`);
            });
        }
    }
    

    The first line is included in the standard Blazor template. The remaining is the function that is called from the interop in Blazor.

    Voila: Just code. On every build the version number is updated. If a user opens production code that is outdated, the page will automatically reload and it will then be running the latest code.