Skip to content

Commit

Permalink
Changed UI interface slightly and made other small tweaks.
Browse files Browse the repository at this point in the history
  • Loading branch information
daffinm committed Apr 5, 2020
1 parent 083aa85 commit ee1e117
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 51 deletions.
9 changes: 4 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,12 @@
<h1>Service Worker Test</h1>
<div id="status-messages"></div>
<ol>
<li>Open the dev tools console asap and check the console messages to see what's happening.</li>
<li>Open the dev tools console ASAP and check the console messages to see what's happening in the background.</li>
<li>Simulate an upate by adding/removing spaces from the end of the <i>sw.js</i> file.</li>
<li>Press the 'Check for updates' button</li>
<li>
Play with accepting and rejecting updates at different times. (Note what happens if you reject an update
when the page is not controlled by a service worker.)
</li>
<li>Play with accepting and rejecting updates at different times.</li>
<li>Note what happens if an update is found when the page is NOT controlled by the service worker.</li>
<li>Go offline and then press 'check for updates'. </li>
</ol>
<p>
Note that changing any file other than the sw.js file <a href="https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#updates">does NOT count as an update</a>.
Expand Down
95 changes: 67 additions & 28 deletions js/sw-client.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
// Uses assert from assert.js

function ServiceWorkerClient(url, debug, ui) {

if (serviceWorkerIsUnsupported()) return;

assert.isDefined(url);
assert.isDefined(debug);
assert.isDefined(ui);
assert.isFunction(ui.noUpdateFound);
assert.isFunction(ui.updateError);
assert.isFunction(ui.updateFoundConfirmWithUser);
assert.isFunction(ui.updateFoundReloadNeeded);
assert.isFunction(ui.confirmUpdateWithUser);
assert.isFunction(ui.updateNotFound);
assert.isFunction(ui.reload);

// Brilliant prolyfil by dfabulich
Expand All @@ -27,6 +30,9 @@ function ServiceWorkerClient(url, debug, ui) {
});
}
this.register = function () {

if (serviceWorkerIsUnsupported()) return;

debug.log('Registering service worker...');
navigator.serviceWorker.register(url)
.then(function (reg) {
Expand All @@ -41,32 +47,53 @@ function ServiceWorkerClient(url, debug, ui) {
debug.error('Error registering service worker: ', err);
});
};
// Mostly this will be called as a result of a user interaction such as a 'check for updates' button being pressed.
// If not then override this default by calling this method with false.
this.update = function (updateButtonPressed) {

if (serviceWorkerIsUnsupported()) return;

// Default
if (typeof updateButtonPressed === 'undefined' || updateButtonPressed === null) {
updateButtonPressed = true;
}

debug.log(`Checking for updates to service worker (updateButtonPressed=${updateButtonPressed})`);

// Call register as this will trigger an error if the user if offline.
navigator.serviceWorker.register(url).then(async function (reg) {
await fetch(url, {method: 'HEAD'}); // trigger error if offline.
reg.update().then(function (reg) {
logRegistration(reg, 'Checking for updates', debug);
logRegistration(reg, 'Update check complete. Registration state:');
if (updateIsAvailable(reg)) {
debug.log('Update found by update checker. Handling it...');
handleUpdateTo(reg, updateButtonPressed);
}
else {
debug.log('No update found.');
if (updateButtonPressed) {
ui.noUpdateFound();
ui.updateNotFound();
}
}
})
})
.catch(function(err){
debug.error('Error getting service worker registration: ', err);
ui.updateError(err);
});
.catch(function(err){
debug.error('Error getting service worker registration: ', err);
ui.updateError(err);
});
};
//--------------------------------------------------------------------------------------------------------------
// Private methods
//--------------------------------------------------------------------------------------------------------------
function serviceWorkerIsUnsupported() {
if (!('serviceWorker' in navigator)) {
debug.warn('Service Workers are not supported by this browser.');
return true;
}
else {
return false;
}
}
function logRegistration(reg, message) {
message = message ? message : 'Service Worker registration';
const yes = '✓';
Expand All @@ -87,11 +114,11 @@ function ServiceWorkerClient(url, debug, ui) {
navigator.serviceWorker.oncontrollerchange = function (e) {
debug.log('Controller has changed!');
if (navigator.serviceWorker.controller) {
debug.log('Controller is new. Reloading....');
debug.log('Controller is NEW. Reloading....');
ui.reload();
}
else {
debug.log('Controller has died :-( Doing nothing.');
debug.log('Controller is DEAD :-( Doing nothing.');
}
};
}
Expand Down Expand Up @@ -122,10 +149,11 @@ function ServiceWorkerClient(url, debug, ui) {
}
if (isUpdate) {
if (navigator.serviceWorker.controller) {
ui.confirmUpdateWithUser(function (acceptUpdate) {
ui.updateFoundConfirmWithUser(function (acceptUpdate) {
if (acceptUpdate) {
debug.log('++OK: Proceeding with update...');
debug.log('Sending SKIP_WAITING command to new service worker so that it activates.')
debug.log('Sending SKIP_WAITING command to new service worker so that it activates.');
// TODO ensure your service worker implements a message listener that looks for messages like this.
newSw.postMessage({message: 'SKIP_WAITING'});
} else {
debug.log('++CANCEL: update rejected by user.');
Expand All @@ -140,8 +168,13 @@ function ServiceWorkerClient(url, debug, ui) {
// So we need to play catch up, and reload so we become controlled by our service worker.
// If we don't do this now then we will have to wait until we reload or restart, or for another update,
// in order to become controlled by our service worker.
debug.log('New service worker is activating automatically. Reloading to become controlled by it.');
ui.updateFoundReloadNeeded();
// The assumption here is that this app is not running in multiple tabs in the same browser, in different
// states in each tab. If you have an advanced use case that needs this then - DIY :)
debug.warn('This client is NOT controlled by the service worker.\n - New service worker should activate automatically.\n - Reload is needed NOW.');
ui.updateFoundReloadNeeded(function () {
debug.warn('User has acknowledged. Reloading application...');
ui.reload();
});
}
}
}
Expand All @@ -150,31 +183,37 @@ function ServiceWorkerClient(url, debug, ui) {
// Demo ui callback object - implements an interface (I wish) that enables us to decouple the service worker
// client from the app that's using it.
function SimpleUI(debug) {
this.noUpdateFound = function () {
alert(`No update found\n\nYou are already on the latest version.`);
};
this.updateError = function (err) {
alert(`Error\n\nCannot check for updates.\n\nAre you offline?`);
};
this.updateFoundReloadNeeded = function () {
// Called when an update is found but the client is not controlled by the service worker. This means that
// the update will activate automatically. So the client will need to play catch up with the service worker:
// to reload so that it becomes controlled by it.
// Inform the user with OK dialog and reload when this returns (to give some sense of control).
alert('Update found!\n\nApp will reload when you press OK.');
this.reload();
alert(`Error!\n\nCannot check for updates.\n\nAre you offline?`);
};
this.confirmUpdateWithUser = function (callback) {
this.updateFoundConfirmWithUser = function (callback) {
if (confirm(`Update available!\n\nAn update is available for this app.\n\nUse it now?`)) {
callback(true);
}
else {
callback(false);
}
};
this.updateFoundReloadNeeded = function (callback) {
// Called when an update is found but the client is not controlled by the service worker. This means that
// the update will activate automatically. So the client will need to play catch up with the service worker:
// to reload so that it becomes controlled by it (assuming it is routed through it).
//
// This method enables you to inform the user that an update/reload is about to happen.
// - Give them time to register and respond to this message with with an alert dialog of some kind, then call
// the callback to signal that this has been done.
// - The aim here is to give the user some sense of control over something that just has to happen, whether
// they like it or not.
// - ui.reload() will then be called automatically by the swc as the next step.
alert('Update found!\n\nApp will reload when you press OK.');
callback();
};
this.updateNotFound = function () {
alert(`No update found\n\nYou are already on the latest version.`);
};
this.reload = function () {
debug.warn('Reloading app...');
document.body.style = 'color:red;';
document.body.style = 'color: red;';
setTimeout(() => window.location.reload(), 1000);
};
}
Expand Down
34 changes: 16 additions & 18 deletions sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,21 @@ function logRegistration(reg, message) {
message = `${message}\n - installing: ${installing}\n - waiting: ${waiting}\n - active: ${active}`;
debug.log(message);
}
function countControlledClients() {
self.clients.matchAll().then(function (clientArray) {
let controlledClients = clientArray.length;
debug.log(`I am currently controlling ${controlledClients} client(s).`);
});
}

importScripts('js/debug-console.js');
const debug = new DebugConsole(DEBUG_LOGGING, 'Service Worker', 'indianred');
debug.heading('New Service Worker installing...');

logRegistration(self.registration, 'At startup: registration state', debug);
const FIRST_TIME = (!self.registration.active);
if (FIRST_TIME) {
debug.log('Installing for the first time. Activating automatically...');
}

const WORKBOX_VERSION = "5.1.2";
importScripts(`https://storage.googleapis.com/workbox-cdn/releases/${WORKBOX_VERSION}/workbox-sw.js`);
Expand All @@ -27,6 +36,7 @@ workbox.core.setCacheNameDetails({
precache: 'installtime',
runtime: 'runtime',
});

self.addEventListener('message', (event) => {
debug.log(`Received message from client: ${event.data.message}`);
if (event.data.message === 'SKIP_WAITING') {
Expand All @@ -35,27 +45,14 @@ self.addEventListener('message', (event) => {
}
});
self.addEventListener('install', function (event) {
logRegistration(self.registration, 'On install: registration state', debug);
logRegistration(self.registration, 'install event handler. Registration state:', debug);
countControlledClients();
});
const FIRST_TIME = (!self.registration.active);
if (FIRST_TIME) {
debug.log('Installing for the first time. Activating automatically...');
}
let controlledClients = 0;
self.clients.matchAll().then(function (clientArray) {
controlledClients = clientArray.length;
debug.log(`I am currently controlling ${controlledClients} client(s)`);
if (controlledClients === 0) {
debug.log('Activating automatically...');
} else {
debug.log('Not activating automatically.');
}
});

// If there are no controlled clients then a new service worker will activate automatically, even if there
// is a previous active version. And it will carry on doing this each time until a client becomes controlled.
self.addEventListener('activate', function(event) {
logRegistration(self.registration, 'On activate: registration state', debug);
logRegistration(self.registration, 'activate event handler. Registration state:', debug);
countControlledClients();
// Note. Our client page is initially uncontrolled by a service worker because it was not served by a the
// service worker. So how do we bring it under control in a way that does not take control away from the user?
// ---
Expand Down Expand Up @@ -99,3 +96,4 @@ workbox.routing.registerRoute(
);



0 comments on commit ee1e117

Please sign in to comment.