Service Worker is also Being cached in Chrome? - javascript

I built a Progressive Web App, https://www.tavest.com.
I don't understand why my service worker is also being cached in Chrome? https://www.tavest.com/service-worker-tavest.js So when I change the service-worker, the chrome doesn't detect the change thus the service worker is not updating.
Eventough I refresh the page many times, it's still the same. However, in Mozilla it works just fine.
Here's my code for installing the service worker
if ('serviceWorker' in navigator && (window.location.protocol === 'https:')) {
navigator.serviceWorker.register('/service-worker-tavest.js')
.then(function(registration) {
// updatefound is fired if service-worker.js changes.
registration.onupdatefound = function() {
// updatefound is also fired the very first time the SW is installed,
// and there's no need to prompt for a reload at that point.
// So check here to see if the page is already controlled,
// i.e. whether there's an existing service worker.
if (navigator.serviceWorker.controller) {
// The updatefound event implies that registration.installing is set:
// https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#service-worker-container-updatefound-event
var installingWorker = registration.installing;
installingWorker.onstatechange = function() {
switch (installingWorker.state) {
case 'installed':
// At this point, the old content will have been purged and the
// fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in the page's interface.
console.warn('New content is available, please refresh the page');
break;
case 'redundant':
throw new Error('The installing ' +
'service worker became redundant.');
default:
// Ignore
}
};
}
};
}).catch(function(e) {
console.error('Error during service worker registration:', e);
});
}
Thank you for your help
Warm regards,

It's because the service worker is just a normal JS file and it will be browser-cached.
You can set no-cache header to the /service-worker-tavest.js so that the browser will stop caching your service worker file. That way you can have your service worker updated immediately when the new file is uploaded.
Here is how to do that in Nginx:
location /service-worker-tavest.js {
# Don't use browser cache for service worker file
add_header cache-control "no-store, no-cache, must-revalidate";
}

I think I know the answer regarding to this caching.
It is because of "Service worker Lifecycle" in Chrome.
https://www.youtube.com/watch?v=TF4AB75PyIc
Conclusion: The cache in chrome browser is okay because chrome will update the service worker by itself.

Related

Blazor - Service worker not installing due to integrity check failure

I'm trying to setup PWA for my blazor application. I followed the instructions on: https://learn.microsoft.com/en-us/aspnet/core/blazor/progressive-web-app?view=aspnetcore-6.0&tabs=visual-studio
But when I open the deployed website the following error occurs:
Failed to find a valid digest in the 'integrity' attribute for resource 'domain/manifest.json' with computed SHA-256 integrity 'uDWnAIEnaz9hFx7aEpJJVS1a+QB/W7fMELDfHWSOFkQ='. The resource has been blocked.
Unknown error occurred while trying to verify integrity.
service-worker.js:22
Uncaught (in promise) TypeError: Failed to fetch
at service-worker.js:22:54
at async onInstall (service-worker.js:22:5)
In the source file this happens here:
async function onInstall(event) {
console.info('Service worker: Install');
// Fetch and cache all matching items from the assets manifest
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
I think the error is happening since the entry in assetsRequests has a wrong hash and the resource is blocked. If I remove the file from the service-worker-assets.js, the service worker installs and the PWA can be used. But I think this is not a reliable solution.
This also happens sometimes for the appsettings.json. In the service-worker-assets.js I can find the following entry:
{
"hash": "sha256-+Py0\/ezc+0k1sm\/aruGPrVhS1jOCTfPKMhOSS+bunek=",
"url": "manifest.json"
},
So the hash does not seem to match. Where does the browser take the wrong hash from? How can I fix this so it does match?
Also it seems that the app is caching older files sometimes. Even when I do a "Reset Cache & Hard Reload" in Chrome the service-worker.js file is still an older version. Any idea how to fix this as well, since it might be related?
Edit: I was also checking this solution: https://stackoverflow.com/a/69935118/11385442. But in the mentioned blazor.boot.json I cannot find any reference to the manifest.json or the appsettings.json. Only Dlls are listed. So the problem only seems to relate to files not listed in blazor.boot.json.
Edit2: What I can see on the webserver is that the following files are published:
appsettings.json
appsettings.json.br
appsettings.json.gzip
So it seems like compressed version are added. Also the appsettings.json has a different size than the one in the solution. My guess is that somewhere in the build or release pipeline (Azure) the files are modified. But even when I copy the appsettings.json manually to the webserver the error still occurs. I was following Information provided here: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly?view=aspnetcore-5.0
(Diagnosing integrity problems)
My guess was right. The appsettings.json was modified probably due to the xml transformation in the azure pipeline. My current solution is to exclude integrity validation for such resources as described in the following answer: Error loading appsettings.Production.json due to digest integrity issue
Also I changed the "sw-registrator.js" mentioned in the original posts comments to work correctly, because it didn't load the new files into the cache:
function invokeServiceWorkerUpdateFlow(registration) {
if (confirm("New version available, reload?") == true) {
if (registration.waiting) {
console.info(`Service worker registrator: Post skip_waiting...`);
// let waiting Service Worker know it should became active
registration.waiting.postMessage('SKIP_WAITING')
}
}
}
function checkServiceWorkerUpdate(registration) {
setInterval(() => {
console.info(`Service worker registrator: Checking for update... (scope: ${registration.scope})`);
registration.update();
}, 60 * 1000); // 60000ms -> check each minute
}
// check if the browser supports serviceWorker at all
if ('serviceWorker' in navigator) {
// wait for the page to load
window.addEventListener('load', async () => {
// register the service worker from the file specified
const registration = await navigator.serviceWorker.register('/service-worker.js');
// ensure the case when the updatefound event was missed is also handled
// by re-invoking the prompt when there's a waiting Service Worker
if (registration.waiting) {
invokeServiceWorkerUpdateFlow(registration);
}
// detect Service Worker update available and wait for it to become installed
registration.addEventListener('updatefound', () => {
if (registration.installing) {
// wait until the new Service worker is actually installed (ready to take over)
registration.installing.addEventListener('statechange', () => {
if (registration.waiting) {
// if there's an existing controller (previous Service Worker), show the prompt
if (navigator.serviceWorker.controller) {
invokeServiceWorkerUpdateFlow(registration);
} else {
// otherwise it's the first install, nothing to do
console.log('Service worker registrator: Initialized for the first time.')
}
}
});
}
});
checkServiceWorkerUpdate(registration);
let refreshing = false;
// detect controller change and refresh the page
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.info(`Service worker registrator: Refreshing app... (refreshing: ${refreshing})`);
if (!refreshing) {
window.location.reload();
refreshing = true
}
});
});
}
else
{
console.error(`Service worker registrator: This browser doesn't support service workers.`);
}
Also I had to add this in service-worker.js:
self.addEventListener('message', (event) => {
console.info('Service worker: Message received');
if (event.data === 'SKIP_WAITING') {
// Cause the service worker to update
self.skipWaiting();
}
});
This code was mostly taken from https://whatwebcando.today/articles/handling-service-worker-updates/

How to make persistent PWA cache?

I've been trying to make offline only PWAs for Android, but the site's cache keeps clearing every so often. Is there any way to make the cache stay permanently?
You can define caching strategies for static assets and data requests for your service worker.
In the following article about service workers and caching strategies I list the different strategies and describe when it makes more sense to implement a specific one.
You can cache static assets and provide them offline when the SW is installing. Those files should be only the "minimum" version of your app (usually called app shell). Because of this, the cache.addAll method rejects the promise if it is not possible to get any of the resources. This means the service worker will install successfully only if all the targeted resources are cached.
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('staticAssetsCache').then(function(cache) {
return cache.addAll(
[
'/css/bootstrap.css',
'/css/styles.css',
'/js/jquery.min.js',
'/offline.html'
]
);
})
);
});
You can also cache HTTP GET Requests, for example below the stale while revalidate strategy that returns the data from the cache, if available, and in the background attempts to fetch and cache a newer version from the network:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open('www.my-web-app.com')
.then(function(cache) {
return cache.match(event.request)
.then(function(response) {
var fetchPromise = fetch(event.request).then(function(networkResponse) {
cache.put(event.request, networkResponse.clone());
return networkResponse;
})
// response contains cached data, if available
return response || fetchPromise;
})
})
);
});
If you are using Angular or Workbox library, https://dev.to/paco_ita/create-progressive-web-apps-with-angular-workbox-pwa-builder-step-4-27d for more details.
I believe I read somewhere iOS Safari and Chrome would invalidate the cache frequently to get new updates. No logic behind it, just re-fetching the files.
Solution:
(In a recent Chrome devlog, it mentions a reduction in frequency from 3 days to 1)
to prevent the clearing of the cache / IndexDB I found this.
if (navigator.storage && navigator.storage.persist)
// '.persist()' will silently pass or trigger a dialog
navigator.storage.persist().then(function(persistent) {
alert(persistent ? 'persistent' : 'denied');
})
else
alert('not available - iOS / ancient Android?');

Blank Service worker script

The service worker script is blank
I am trying to implement service workers into my web app however i noticed nothing was working. I get confirmation that the service worker starts however when i try to view it in the chrome tools it shows a blank document. All efforts to console log etc are unsuccessful which is leading me to believe that the file is truly blank despite it obviously not being.
I have tried unregistered the service worker and updating manually.
SCRIPT ON INDEX
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw_basic.js')
.then(function(reg) {
// registration worked
console.log('Registration succeeded. Scope is ' + reg.scope);
}).catch(function(error) {
// registration failed
console.log('Registration failed with ' + error);
});
}
SERVICE WORKER SCRIPT
var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
'./', './index.php',
'./profile.php',
'./support.php',
'./img/dance3-min.png',
'./css/agency',
'./css/agency.min.css',
'./css/eventform.css',
'./css/loginmodal.css',
'./css/profile.css',
'./css/support.css',
'./css/table.css',
'./css/timer.css'
]
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened Cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', function(event)) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
console.log('Successfully fetched resource from chache: ${event.request.url}');
return response;
} else {
console.error('Failed to fetch resource: ${event.request.url}');
}
return fetch(event.request);
}
)
)
}
EDIT* I have gotten it to update by changing the name of the js file and manually unregistering the service worker in chrome however it doesn't always update this way sometimes requiring several attempts
I still feel like there must be a better way for doing this and in all the tutorials / documentation it seems like it should install the new one and activate once all tabs are unloaded but its not even installing the updated one at all.
EDIT*
I noticed the service worker tries to install and then disappears.
Example- Service worker #12 is active and running. I refresh and then for a second service worker #24 is installing and then suddenly its gone. At this point i really don't know whats going on other feeds are saying its a problem with the cache max age but I have it set to 0 in the htaccess
Cache-Control: max-age= 0
EDIT*
I have tried taking the service worker onto a different page remove the caching and just try to get it to update.
Currently my index looks like
<html>
<head>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/beta/sw.js', {scope: '/beta/'})
.then(function(reg) {
// registration worked
console.log('Registration succeeded. Scope is ' + reg.scope);
}).catch(function(error) {
// registration failed
console.log('Registration failed with ' + error);
});
}
</script>
</head>
online page v2.0
</html>
and the service worker looks like
self.addEventListener('install', function(event) {
console.log("SW installed");
});
self.addEventListener('activate', function(event) {
console.log("SW activated");
});
self.addEventListener('fetch', function(event) {
console.log("Hijacked Signal");
event.respondWith(new Response("offline page"));
});
This Works when the user refreshes after visiting the page the text changes from online to offline. The problem occurs when i change the desired text (eg to offline 2.0). Anyone who has already visited the website is running the old service worker and so will see offline and not offline 2.0
a link to the page if anyone wishes to see whats going on
https://pakcollegelive.tk/beta/index.php
It turns out Cloudflare wasn't playing nice with the service worker files for whatever reason. It wasn't imperative that Cloudflare was used in my case so disabling it fixed the problem.

JavaScript (PWA) - What is the correct way to initialize service-worker?

What is the better way to initialize service-worker used for PWA caching?
Method 1:
I expect that in this case will be installed new SW instance. I guess that's not necessary and I want to re-activate existing SW already running in the browser - am I right?
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js').then(registration => {
// Registration success
}, function(err) {
// Registration failed
});
});
}
Method 2: In this case I'm checking that there SW already registered and I'm installing registering a new one only if there is non registered.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.getRegistrations().then(registrations => {
const isServiceWorkerNotRegistered = registrations.length === 0;
if (isServiceWorkerNotRegistered) {
navigator.serviceWorker.register('sw.js').then(registration => {
// Registration success
}).catch(error => {
// Registration failed
});
} else {
console.log('Service worker already registered.');
}
});
});
}
On top of that... Is it really necessary to wait for window to load (line 2)? Since SW is running asynchronously in the browser I guess I don't need the app to be completely loaded.
Waiting for the load event is so that assets being downloaded by the service worker don't take bandwidth and device resources from rendering the currently requested page. See improving the boilerplate.
For your registration question, use the simple first version.
Unless you change the URL of the service worker script, navigator.serviceWorker.register() is effectively a no-op during subsequent visits.
subsequent visits
If the service worker is not registered, register will register it. If a service worker is already registered, register will basically not do anything.

Refresh page after load on cache-first Service Worker

I'm currently considering adding service workers to a Web app I'm building.
This app is, essentially, a collection manager. You can CRUD items of various types and they are usually tightly linked together (e.g. A hasMany B hasMany C).
sw-toolbox offers a toolbox.fastest handler which goes to the cache and then to the network (in 99% of the cases, cache will be faster), updating the cache in the background. What I'm wondering is how you can be notified that there's a new version of the page available. My intent is to show the cached version and, then, if the network fetch got a newer version, to suggest to the user to refresh the page in order to see the latest edits. I saw something in a YouTube video a while ago but the presenter gives no clue of how to deal with this.
Is that possible? Is there some event handler or promise that I could bind to the request so that I know when the newer version is retrieved? I would then post a message to the page to show a notification.
If not, I know I can use toolbox.networkFirst along with a reasonable timeout to make the pages available even on Lie-Fi, but it's not as good.
I just stumbled accross the Mozilla Service Worker Cookbooj, which includes more or less what I wanted: https://serviceworke.rs/strategy-cache-update-and-refresh.html
Here are the relevant parts (not my code: copied here for convenience).
Fetch methods for the worker
// On fetch, use cache but update the entry with the latest contents from the server.
self.addEventListener('fetch', function(evt) {
console.log('The service worker is serving the asset.');
// You can use respondWith() to answer ASAP…
evt.respondWith(fromCache(evt.request));
// ...and waitUntil() to prevent the worker to be killed until the cache is updated.
evt.waitUntil(
update(evt.request)
// Finally, send a message to the client to inform it about the resource is up to date.
.then(refresh)
);
});
// Open the cache where the assets were stored and search for the requested resource. Notice that in case of no matching, the promise still resolves but it does with undefined as value.
function fromCache(request) {
return caches.open(CACHE).then(function (cache) {
return cache.match(request);
});
}
// Update consists in opening the cache, performing a network request and storing the new response data.
function update(request) {
return caches.open(CACHE).then(function (cache) {
return fetch(request).then(function (response) {
return cache.put(request, response.clone()).then(function () {
return response;
});
});
});
}
// Sends a message to the clients.
function refresh(response) {
return self.clients.matchAll().then(function (clients) {
clients.forEach(function (client) {
// Encode which resource has been updated. By including the ETag the client can check if the content has changed.
var message = {
type: 'refresh',
url: response.url,
// Notice not all servers return the ETag header. If this is not provided you should use other cache headers or rely on your own means to check if the content has changed.
eTag: response.headers.get('ETag')
};
// Tell the client about the update.
client.postMessage(JSON.stringify(message));
});
});
}
Handling of the "resource was updated" message
navigator.serviceWorker.onmessage = function (evt) {
var message = JSON.parse(evt.data);
var isRefresh = message.type === 'refresh';
var isAsset = message.url.includes('asset');
var lastETag = localStorage.currentETag;
// ETag header usually contains the hash of the resource so it is a very effective way of check for fresh content.
var isNew = lastETag !== message.eTag;
if (isRefresh && isAsset && isNew) {
// Escape the first time (when there is no ETag yet)
if (lastETag) {
// Inform the user about the update.
notice.hidden = false;
}
//For teaching purposes, although this information is in the offline cache and it could be retrieved from the service worker, keeping track of the header in the localStorage keeps the implementation simple.
localStorage.currentETag = message.eTag;
}
};

Categories

Resources