<!DOCTYPE html>
<html>

<head>
  <script src="helper.js"></script>
  <script src="sw-registration.js"></script>
</head>

<body>
  <h1>Service Worker events</h1>

  <button type="button" onclick="registerWorker()">Register SW</button>
  <button type="button" onclick="unregisterWorker()">Unregister SW</button>
  <button type="button" onclick="clearOutput()">Clear output</button>
  
  <pre id="output"></pre>
</body>

</html>
/**
 * Handler for messages coming from SW registration
 */
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.addEventListener("message", function(event) {
    writeLine("event " + event.data.type + ":" + event.data.message + " " + event.data.client)
    if(event.data.type === "check" && !event.data.message){
      removeServiceWorkerScreenCover()
    }
  });
}

/**
 * Wrapped in a function as it helps testing
 */
function registerWorker() {
  if ('serviceWorker' in navigator) {
    
    addServiceWorkerScreenCover();
    
    navigator.serviceWorker.register('sw.js').then(function(reg) {

      reg.onupdatefound = function() {

        var installingWorker = reg.installing;

        installingWorker.onstatechange = function() {
          switch (installingWorker.state) {
            case 'installed':
              if (navigator.serviceWorker.controller) {
                writeLine("new or updated content available")
                removeServiceWorkerScreenCover();
              } else {
                writeLine("content available offline");
                removeServiceWorkerScreenCover();
              }
              break;
          }
        };
      };

      if (reg.active) {
        // here I know that `service-worker.js` was already installed
        // but not sure it there are changes
        writeLine("service worker is active")
        navigator.serviceWorker.controller.postMessage("check");
      }


    }).catch(function(e) {
      writeLine("Error during service worker registration")
    });
  } else {
    window.alert(`I'm affraid this demo won't work in this browser, please try latest Chrome or Firefox for the best results`);
  }
}


/**
 * Utility function that unregisters SW, but you still need to reload to see SW removed completely
 * This does not delete items in Cache
 */
function unregisterWorker() {
  navigator.serviceWorker.getRegistrations()
    .then(function(registrations) {
      return Promise.all(
        registrations.map(function(reg) {
          return reg.unregister().then(function(boolean) {
            if (boolean) {
              writeLine("unregistered")
            } else {
              writeLine("unable to deregister")
            }
          });
        })
      )
    })
    .then(function() {
      writeLine("unregistrations complete")
    })
}
# Checking active Service Worker

Here **SW** is Service Worker

Issue: no event handler in the case when active **SW** will not be updated

Solution: `postMessage` communication between **SW** client and **SW** registration

onload:

- **SW** registration is always invoked
- **SW** client sets up handler for `postMessage` coming from **SW** registration
- in case of **new registration** `onupdatefound` will be called
  - `onstatechange` will be setup
  - `onstatechange` will be called
  - `installingWorker.state` changes to `installed` 
  - there is no `navigator.serviceWorker.controller` found
- in case of **update** `onupdatefound` will be called
  - `serviceWorkerRegistration.active` is set - always before updates
  - `onstatechange` will be setup
  - `onstatechange` will be called
  - `installingWorker.state` changes to `installed` 
  - `navigator.serviceWorker.controller` exists
- otherwise
  - `serviceWorkerRegistration.active` is set
  - `postMessage` to **SW** registration with name **check**
  - **SW** registration checks `message` in handler, if it matches **check**, then checks for changes
  - **check** looks for changes between **SW** registration and **cache**
  - value is returned to **SW** client via `postMessage`
// Most of the content here is from `sw-precache` library

// Precache config
//////////////////////////////

var cacheName = 'v1';

var precacheConfig = [
  ["helper.js", "a"],
  ["sw-registration.js", "b"],
  ["index.html", "c"]
]

var hashParamName = '_sw-precache';
var urlsToCacheKeys = new Map(
  precacheConfig.map(function(item) {
    var relativeUrl = item[0];
    var hash = item[1];
    var absoluteUrl = new URL(relativeUrl, self.location);
    var cacheKey = createCacheKey(absoluteUrl, hashParamName, hash, false);
    return [absoluteUrl.toString(), cacheKey];
  })
);

function setOfCachedUrls(cache) {
  return cache.keys().then(function(requests) {
    return requests.map(function(request) {
      return request.url;
    });
  }).then(function(urls) {
    return new Set(urls);
  });
}

function createCacheKey(originalUrl, paramName, paramValue,
  dontCacheBustUrlsMatching) {
  // Create a new URL object to avoid modifying originalUrl.
  var url = new URL(originalUrl);

  // If dontCacheBustUrlsMatching is not set, or if we don't have a match,
  // then add in the extra cache-busting URL parameter.
  if (!dontCacheBustUrlsMatching ||
    !(url.toString().match(dontCacheBustUrlsMatching))) {
    url.search += (url.search ? '&' : '') +
      encodeURIComponent(paramName) + '=' + encodeURIComponent(paramValue);
  }

  return url.toString();
}

//=================

/**
 * Put configured files to cache
 */
self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheName).then(function(cache) {
      return setOfCachedUrls(cache).then(function(cachedUrls) {
        return Promise.all(
          Array.from(urlsToCacheKeys.values()).map(function(cacheKey) {
            // If we don't have a key matching url in the cache already, add it.
            if (!cachedUrls.has(cacheKey)) {
              return cache.add(new Request(cacheKey, {
                credentials: 'same-origin',
                redirect: 'follow'
              }));
            }
          })
        );
      });
    }).then(function() {
        // Force the SW to transition from installing -> active state
      return self.skipWaiting();
    })
  );
});

/**
 * Purge redundant files and take control of clients
 */
self.addEventListener('activate', function(event) {

  var setOfExpectedUrls = new Set(urlsToCacheKeys.values());

  event.waitUntil(
    caches.open(cacheName).then(function(cache) {
      return cache.keys().then(function(existingRequests) {
        return Promise.all(
          existingRequests.map(function(existingRequest) {
            if (!setOfExpectedUrls.has(existingRequest.url)) {
              return cache.delete(existingRequest);
            }
          })
        );
      });
    }).then(function() {
      return self.clients.claim();
    })
  );
});


/**
 * Import additional functionality
 * mimick sw-precache library
 */
importScripts('sw-helper.js');
function getOutputEl() {
  return document.getElementById("output")
}

function getOutputText() {
  return getOutputEl().innerHTML
}

function setOutputText(text) {
  getOutputEl().innerHTML = text
}

function writeLine(text) {
  setOutputText(getOutputText() + "\n" + text)
}

function clearOutput() {
  setOutputText("")
}

var COVER_ID = "service-worker-screen-cover";

function addServiceWorkerScreenCover() {
  var screenCover = '<div id="' + COVER_ID + '" style="' +
    '    position: absolute;' +
    '    top: 0;' +
    '    left: 0;' +
    '    right: 0;' +
    '    bottom: 0;' +
    '    background: green;' +
    '    z-index: 9999999999;' +
    '    position: fixed;' +
    '">' +
    '  <div>' +
    '    <p>Loading ...</p>' +
    '  </div>' +
    '</div>';
  var bodyElements = document.querySelectorAll('body script');
  if(!bodyElements.length){
    bodyElements = document.querySelectorAll('body div');
  }
  var lastBodyEl = bodyElements[bodyElements.length - 1];
  var newDiv = document.createElement("div");
  newDiv.innerHTML = screenCover;
  document.body.insertBefore(newDiv, lastBodyEl);
}

function removeServiceWorkerScreenCover(){
  var el = document.getElementById(COVER_ID);
  if(el){
    el.parentNode.removeChild(el);
  }
}
/**
 * Handler for messages coming from clients that registered this SW
 */
self.addEventListener('message', function(event) {
  if (event.data === "check")
    hasChanges().then(function(changed) {
      sendMessageToAllClients(event.data, changed)
    })
  else
    sendMessageToAllClients(event.data)
})

/**
 * Utility function to send messages back to clients using this worker
 */
function sendMessageToAllClients(type, msg) {
  self.clients.matchAll().then(function(clientList) {
    clientList.forEach(function(client) {
      client.postMessage({
        client: client.id,
        type: type,
        message: msg
      });
    })
  })
}

/**
 * Utility function to check if this SW has changes that affect cache
 * Logic taken from `install` `activate` events.
 * 
 * It also could be some just a timestamp generated on server
 * 
 * It DOES NOT reflect the logic of a browser:
 * > If there is even a byte's difference in the service worker file 
 * > compared to what it currently has, it considers it new.
 * ^^ taken from https://developers.google.com/web/fundamentals/getting-started/primers/service-workers
 */
function hasChanges() {

  return caches.open(cacheName)
    .then(function(cache) {

      var changed = false

      return setOfCachedUrls(cache).then(function(cachedUrls) {
        Array.from(urlsToCacheKeys.values()).forEach(function(cacheKey) {
          if (!cachedUrls.has(cacheKey)) {
            changed = true
          }
        })
        return changed;
      }).then(function() {

        if (changed) {
          return changed
        }

        return cache.keys().then(function(existingRequests) {
          var setOfExpectedUrls = new Set(urlsToCacheKeys.values());
          existingRequests.forEach(function(existingRequest) {
            if (!setOfExpectedUrls.has(existingRequest.url)) {
              changed = true
            }
          })
          return changed;
        });
      });

    });
}