Reload all clients pages on Service Worker skipWaiting call - javascript

I want to be able to fully control my Service Worker verions,
I created signals and functions that allow me to update or skipWaiting an exsting Service Worker which works prefect!
self.addEventListener('install',event=>
event.waitUntil(caches.open(version).then(cache=>
cache.addAll([
...
]).then(_=>
condition?
self.skipWaiting():
false
)
))
)
self.addEventListener('activate',event=>
event.waitUntil(clients.claim())
)
When I call skipWaiting, the client page is claimed by the new Service Worker as it supposed to,
But I can't make it immediately reload from the new cache of the newly installed Service Worker.
How can I skipWaiting and force all clients pages to reload themself from the cache of the newly installed Service Worker?

Try this code snippet:
// If service workers are supported...
if ('serviceWorker' in navigator) {
// Check to see if the page is currently controlled.
let isControlled = Boolean(navigator.serviceWorker.controller);
// Listen for a change to the current controller...
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (isControlled) {
// ...and if the page was previosly controlled, reload the page.
window.location.reload();
} else {
// ...otherwise, set the flag for future updates, but don't reload.
isControlled = true;
}
});
}
I'd recommend basing this logic on the controllerchange event rather than some other approaches, like listening to the statechange of an installing/waiting service worker, since there's a small gap in time between when a service worker activates and when it takes control of existing clients. Waiting on each page for the controller to change will guarantee that the reload takes place under control of the updated service worker.
Using this code in conjunction with a service worker that calls skipWaiting() should give you the behavior you're looking for.

Related

Period background sync does't fire after successfully registered

I'm trying to make periodic bg sync where service worker updates badge.
When I run my page and test it via Chrome DevTools, Service worker process the request. But when the page is closed, it doesnt't do anything. Same on mobile phone.
On my page (this part is working and output in console is periodic update set):
navigator.permissions.query({name:'periodic-background-sync'}).then(function(result) {
if (result.state === 'granted') {
console.log('periodic background granted');
navigator.serviceWorker.ready.then(function(registration){
if ('periodicSync' in registration) {
try {
registration.periodicSync.register('update-badge', {
minInterval: 60 * 60 * 1000,
}).then(function(ret){
console.log('periodic update set');
});
} catch (error) {
console.log('Periodic background sync cannot be used.');
}
}
});
}
});
Service worker:
async function updateBadge() {
const unreadCount = 5; //fixed value for testing
navigator.setAppBadge(unreadCount).catch((error) => {
console.log('Error setting badge.');
});
}
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'update-badge') {
event.waitUntil(updateBadge());
}
});
So when I manually fire background sync from DevTools, badge is set, but not automatically in the background as I thought it will work.
I can't find anything wrong with the code. I've seen variants where you request periodic-background-sync after navigator.serviceWorker.ready but I think that both work (especially since you can trigger the registered event manually).
I think the problem is that not all conditions for periodic background sync to fire are true. These are the conditions from the initial implementation in Chrome (2019):
Chrome version 80 or later
The PWA needs to be installed
The website needs to have an engagement score (can be viewed here chrome://site-engagement/). Websites with 0 engagement is deleted from the list and won't trigger the periodicsync event, even if it is installed. Once a user hasn’t interacted with a website for 2 hours, its engagement levels begin to decay. With minimal engagement, a periodicsync event will be triggered at an interval of 36h. source
In your case I think your code is correct and working but you haven't interacted enough with you app continuously over 36h so the engagement is purged and periodicsync fire timer cancelled (if you have installed your PWA).
For the record, here is a complete working demo (event registration) (and sw code).
The specific interval at which the periodicsync event is fired varies; what I've seen on, e.g., installed Android web apps is that you'll get periodicsync around once a day. It may be different on desktop platforms (and will only happen if your browser is actually running at the time).
Have you waited a full day or so after installing your PWA, and see if it's fired?

Set $location in AngularJS / Electron app

Is it possible to change the URL for $location in AngularJS within an Electon app but without implicitly loading that URL? The problem is that Electron is loading index.html (and other resources) locally, which is intended. But then of course it also sets $location to the local file system.
Background: The reason why I need $location to point to the server is that there is some existing legacy code (which I must not change) and this code uses e.g. $location.search. So after the Electron app has started I'd need to set the location correctly, so that this legacy code can work.
UPDATE 17.07.2020
Here is the requested example code:
I'm trying to set the location with window.location = "https://example.com?param1=test" so that the AngularJS function $location.search() returns param1=test. The problem is, as mentioned above, that when setting window.location, Electron loads the index.html from that server and replaces the content of the BrowserWindow. But I want to load those resources (index.html, *.js, *.css locally) I also tried:
window.location.href = ...
window.location.assign (...)
window.location.replace (...)
but all of these are reloading the page as well.
I think you'll want to add an event listener for will-navigate. Docs can be found here. The important piece:
[The will-event event will be] emitted when a user or the page wants to start navigation. It can happen when the window.location object is changed or a user clicks a link in the page.
I'd imagine your main.js file will look something like this, it's bare-bones but I hope you get the idea.
const {
app
} = require("electron");
let window;
function createWindow(){
window = new BrowserWindow(){...};
// etc....
}
app.on("ready", createWindow);
// For all BrowserWindows you make, the inner bindings will be applied to each;
// more information for "web-contents-created" is here: https://www.electronjs.org/docs/api/app#event-web-contents-created
app.on("web-contents-created", (event, contents) => {
contents.on("will-redirect", (event, navigationUrl) => {
// prevent the window from changing path via "window.location = '....'"
event.preventDefault();
return;
});
});
FYI: This event listener is mainly used for security reasons, but I don't see why you can't use it in your case.

Is there a way to tell in the page that a service worker has recently upgraded?

I have a SPA with a service worker.
I don't want to force users to update, so usually the new worker updates in the background, but I notify users and they can click a button to message the worker to skipWaiting. When this happens I rely on oncontrollerchange to force any other tabs they have open to refresh.
I want to show a link to release notes after the worker has upgraded, either due to refresh of all tabs or they force the refresh.
However, I don't want to show these notes the first time they visit, or every time the service worker activates. If they don't read the release notes I don't want to keep nagging them about it.
Is there an event or reliable design pattern I can use to tell (in the page) that the service worker has updated to a new version?
A straightforward solution would be to add a query parameter to the current URL, and instead of calling location.reload() to reload your page, instead change the location value to that URL.
Something like:
// Inside your `controllerchange` listener, call reloadWithNotes()
function reloadWithNotes() {
const url = new URL(location.href);
url.searchParams.set('showNotes', '');
location = url.href;
}
// Elsewhere...
window.addEventListener('load', () => {
const url = new URL(location.href);
if (url.searchParams.has('showNotes')) {
// Show your release notes.
// Remove the parameter.
url.searchParams.delete('showNotes');
window.history.replaceState({}, document.title, url.href);
}
});

How: ServiceWorker check if ready to update

What I am trying to achieve:
render page with loader/spinner
if service-worker.js is registered and active, then check for updates
if no updates, then remove loader
if updatefound and new version installed, then reload the page
else register service-worker.js
when updatefound, meaning new one was installed, remove loader
I am using sw-precache module for me to generate service-worker.js and following registration code:
window.addEventListener('load', function() {
// show loader
addLoader();
navigator.serviceWorker.register('service-worker.js')
.then(function(swRegistration) {
// react to changes in `service-worker.js`
swRegistration.onupdatefound = function() {
var installingWorker = swRegistration.installing;
installingWorker.onstatechange = function() {
if(installingWorker.state === 'installed' && navigator.serviceWorker.controller){
// updated content installed
window.location.reload();
} else if (installingWorker.state === 'installed'){
// new sw registered and content cached
removeLoader();
}
};
}
if(swRegistration.active){
// here I know that `service-worker.js` was already installed
// but not sure it there are changes
// If there are no changes it is the last thing I can check
// AFAIK no events are fired afterwards
}
})
.catch(function(e) {
console.error('Error during service worker registration:', e);
});
});
After reading the spec it is clear that there are no handlers for something like updatenotfound. Looks like serviceWorker.register checks if service-worker.js changed internally by running get-newest-worker-algorithm, but I cannot see similar methods exposed via public api.
I think my options are:
wait for couple of seconds after service worker registration becomes active to see if onupdatefound is fired
fire custom events from service-worker.js code if cache was not updated
Any other suggestions?
Edit:
I've came up with some code which solves this issue by using postMessage() between SW registration and SW client (as #pate suggested)
Following demo tries to achieve checks through postMessage between SW client and SW registration, but fails as SW code is already cached DEMO
Edit:
So by now it looks like I cannot implement what I want because:
when service worker is active you cannot check for updates by evaluating some code in SW - this is still the same cached SW, no changes there
you need to wait for onupdatefound, there is nothing else that will notify of changes in SW
activation of older SW comes before onupdatefound
if there is no change, nothing fires after activation
SW registration update() is immature, keeps changing, Starting with Chrome 46, update() returns a promise that resolves with 'undefined' if the operation completed successfully or there was no update
setting timeout to postpone view rendering is suboptimal as there is no clear answer to how long should it be set to, it depends on SW size as well
The other answer, provided by Fabio, doesn't work. The Service Worker script has no access to the DOM. It's not possible to remove anything from the DOM or, for instance, manipulate any data that is handling DOM elements from inside the Service Worker. That script runs separately with no shared state.
What you can do, though, is send messages between the actual page-running JS and the Service Worker. I'm not sure if this is the best possible way to do what the OP is asking but can be used to achieve it.
Register an onmessage handler on the page
Send a message from the SW's activate or install event to the page
Act accordingly when the message is received on the page
I have myself kept SW version number in a variable inside the SW. My SW has then posted that version number to the page and the page has stored it into the localStorage of the browser. The next time the page is loaded SW posts it current version number to the page and the onmessage handler compares it to the currently stored version number. If they are not the same, then the SW has been updated to some version number that was included in the mssage. After that I've updated the localStorage copy of the version number and done this and that.
This flow could also be done in the other way around: send a message from the page to the SW and let SW answer something back, then act accordingly.
I hope I was able to explain my thoughts clearly :)
The only way that pops up in my mind is to start the loader normally and then remove it in the service-worker, in the install function. I will give this a try, in your service-worker.js:
self.addEventListener('install', function(e) {
console.log('[ServiceWorker] Install');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(filesToCache);
}).then(function() {
***removeLoader();***
return self.skipWaiting();
})
);
});

Windows Store HTML app with popup window

I'm wrapping a web app in a Windows Store app shell using a x-ms-webview. This works fine, but I have one problem. I use PayPal and since PayPal doesn't allow to be iframed I need to open PayPal in a new browser window.
On regular browsers this isn't a problem. The window open and when the user returns from PayPal I can a callback on "opener" and update the users account.
But when doing this in a Windows Store app the window.open triggers IE to launch. The problem is to return to my app and let it know that the user finished the transaction.
My first idea was just to use a URI activation. This kind of works, but I having trouble knowing if the PayPal page was launch from a regular browser or an app. I also think it is confusing for the user to be taken to another app to make the purchase.
I would prefer to have the window open in my app, but I'm not sure how I would open open a new x-ms-webview as a modal window overlapping existing webview.
What is the best way to communicate from the current web view and the app?
Can I use postMessage to send messages between the app and the x-ms-webview even though the src of the web view is a http hosted site?
Thank you for your help.
I found a solution to this.
First, you will need to use a https url for the embedded site. The reason for this is that the solution include postMessage and invokeScriptAsync.
First, my markup in my app looks something like this to have one webview for the app and one web view for the PayPal popup.
<x-ms-webview id="webview" src="https://myapp"></x-ms-webview>
<div id="paypalContainer">
<div class="paypal-header"><button id="paypalClose" type="reset">Close</button></div>
<div class="paypal-body"><x-ms-webview id="paypalWebView" src="about:blank"></x-ms-webview></div>
</div>
Then, when the web app is ready to use PayPal, I use window.external.notify to send a message to the Windows Store app.
if (window.external && 'notify' in window.external) {
window.external.notify(JSON.stringify({ action: 'paypal' }));
}
The windows store app listens for Script Notify events and displays the paypalWebView.
webview.addEventListener("MSWebViewScriptNotify", scriptNotify);
function scriptNotify(e) {
var data = JSON.parse(e.value);
if (data.action === "paypal") {
var loading = document.getElementById("loading-indicator");
var container = document.getElementById("paypalContainer");
var paypalWebView = document.getElementById("paypalWebView");
var paypalClose = document.getElementById("paypalClose");
if (paypalWebView.src === "about:blank") {
paypalWebView.addEventListener('MSWebViewNavigationCompleted', function (e) {
loading.classList.remove('loading');
var successUrl = '/paypal/success';
if (event.target.src.indexOf(successUrl) !== -1) {
var operation = webview.invokeScriptAsync("updateUser");
operation.oncomplete = function () {
(new Windows.UI.Popups.MessageDialog("Your account is refreshed", "")).showAsync().done();
};
operation.start();
}
});
paypalWebView.addEventListener('MSWebViewNavigationStarting', function (e) {
console.log("Started loading");
loading.classList.add('loading');
});
paypalClose.addEventListener('click', function () {
container.classList.remove("visible");
});
}
paypalWebView.src = "https://myapp/paypal/";
container.classList.add("visible");
}
}
So, basically, when the script notify event fires, I parse the sent json string to an object and check what kind of action it is. If it's the first time I run this I setup some naviation event handlers that check if the web view reach the Success page. If we have, I use incokeScriptAsync to let the web app know that we're done so it can refresh the user account the new payment.
I think you can use a similar solution for authentication and just check your your return URL after authenticating.
Hope this helps!

Categories

Resources