Skip to content

Commit

Permalink
Merge pull request #1905 from GoogleChrome/workbox-window-events
Browse files Browse the repository at this point in the history
Add isUpdate and wasWaitingBeforeRegistration to workbox-window lifecycle events
  • Loading branch information
philipwalton authored Feb 15, 2019
2 parents 0b3937f + 5650dcc commit 3033185
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 17 deletions.
11 changes: 9 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,15 @@ module.exports = {
WorkboxSW: false,
},
rules: {
"jsdoc/check-types": 2,
"jsdoc/newline-after-description": 2,
'jsdoc/check-types': 2,
'jsdoc/newline-after-description': 2,
'max-len': [2, {
code: 80,
tabWidth: 2,
ignoreComments: true,
ignoreUrls: true,
ignorePattern: '^\\s*import',
}],
},
plugins: [
'jsdoc',
Expand Down
212 changes: 200 additions & 12 deletions packages/workbox-window/Workbox.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ const REGISTRATION_TIMEOUT_DURATION = 60000;
* A class to aid in handling service worker registration, updates, and
* reacting to service worker lifecycle events.
*
* @fires [message]{@link module:workbox-window.Workbox#message}
* @fires [installed]{@link module:workbox-window.Workbox#installed}
* @fires [waiting]{@link module:workbox-window.Workbox#waiting}
* @fires [controlling]{@link module:workbox-window.Workbox#controlling}
* @fires [activated]{@link module:workbox-window.Workbox#activated}
* @fires [redundant]{@link module:workbox-window.Workbox#redundant}
* @fires [externalinstalled]{@link module:workbox-window.Workbox#externalinstalled}
* @fires [externalwaiting]{@link module:workbox-window.Workbox#externalwaiting}
* @fires [externalactivated]{@link module:workbox-window.Workbox#externalactivated}
*
* @memberof module:workbox-window
*/
class Workbox extends EventTargetShim {
Expand Down Expand Up @@ -84,6 +94,10 @@ class Workbox extends EventTargetShim {
await new Promise((res) => addEventListener('load', res));
}

// Set this flag to true if any service worker was controlling the page
// at registration time.
this._isUpdate = Boolean(navigator.serviceWorker.controller);

// Before registering, attempt to determine if a SW is already controlling
// the page, and if that SW script (and version, if specified) matches this
// instance's script.
Expand All @@ -103,6 +117,27 @@ class Workbox extends EventTargetShim {
'statechange', this._onStateChange, {once: true});
}

// If there's a waiting service worker with a matching URL before the
// `updatefound` event fires, it likely means the this site is open
// in another tab, or the user refreshed the page without unloading it
// first.
// https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#waiting
if (this._registration.waiting &&
urlsMatch(this._registration.waiting.scriptURL, this._scriptURL)) {
// Run this in the next microtask, so any code that adds an event
// listener after awaiting `register()` will get this event.
Promise.resolve().then(() => {
this.dispatchEvent(new WorkboxEvent('waiting', {
sw: this._registration.waiting,
wasWaitingBeforeRegister: true,
}));
if (process.env.NODE_ENV !== 'production') {
logger.warn('A service worker was already waiting to activate ' +
'before this script was registered...');
}
});
}

if (process.env.NODE_ENV !== 'production') {
logger.log('Successfully registered service worker.', this._scriptURL);

Expand All @@ -117,14 +152,6 @@ class Workbox extends EventTargetShim {
}
}

// If there's an active and waiting service worker before the
// `updatefound` event fires, it means there was a waiting service worker
// in the queue before this one was registered.
if (this._registration.waiting && this._registration.active) {
logger.warn('A service worker was already waiting to activate ' +
'before this script was registered...');
}

const currentPageIsOutOfScope = () => {
const scopeURL = new URL(
this._registerOptions.scope || this._scriptURL, document.baseURI);
Expand Down Expand Up @@ -356,8 +383,13 @@ class Workbox extends EventTargetShim {
const isExternal = sw === this._externalSW;
const eventPrefix = isExternal ? 'external' : '';

const eventProps = {sw, originalEvent};
if (!isExternal && this._isUpdate) {
eventProps.isUpdate = true;
}

this.dispatchEvent(new WorkboxEvent(
eventPrefix + state, {sw, originalEvent}));
eventPrefix + state, eventProps));

if (state === 'installed') {
// This timeout is used to ignore cases where the service worker calls
Expand All @@ -372,15 +404,15 @@ class Workbox extends EventTargetShim {
// Ensure the SW is still waiting (it may now be redundant).
if (state === 'installed' && this._registration.waiting === sw) {
this.dispatchEvent(new WorkboxEvent(
eventPrefix + 'waiting', {sw, originalEvent}));
eventPrefix + 'waiting', eventProps));

if (process.env.NODE_ENV !== 'production') {
if (isExternal) {
logger.warn('An external service worker has installed but is ' +
'waiting for this client to close before activating...');
} else {
logger.warn('The service worker has installed but is waiting ' +
' for existing clients to close before activating...');
'for existing clients to close before activating...');
}
}
}
Expand Down Expand Up @@ -430,10 +462,10 @@ class Workbox extends EventTargetShim {
_onControllerChange(originalEvent) {
const sw = this._sw;
if (sw === navigator.serviceWorker.controller) {
this.dispatchEvent(new WorkboxEvent('controlling', {sw, originalEvent}));
if (process.env.NODE_ENV !== 'production') {
logger.log('Registered service worker now controlling this page.');
}
this.dispatchEvent(new WorkboxEvent('controlling', {sw, originalEvent}));
this._controllingDeferred.resolve(sw);
}
}
Expand All @@ -448,4 +480,160 @@ class Workbox extends EventTargetShim {
}
}

// The jsdoc comments below outline the events this instance may dispatch:
// -----------------------------------------------------------------------

/**
* The `message` event is dispatched any time a `postMessage` (or a
* `BroadcastChannel` message with the `workbox` channel name) is received.
*
* @event module:workbox-window.Workbox#message
* @type {WorkboxEvent}
* @property {*} data The `data` property from the original `message` event.
* @property {Event} originalEvent The original [`message`]{@link https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent}
* event.
* @property {string} type `message`.
* @property {Workbox} target The `Workbox` instance.
*/

/**
* The `installed` event is dispatched if the state of a
* [`Workbox`]{@link module:workbox-window.Workbox} instance's
* [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw}
* changes to `installed`.
*
* Then can happen either the very first time a service worker is installed,
* or after an update to the current service worker is found. In the case
* of an update being found, the event's `isUpdate` property will be `true`.
*
* @event module:workbox-window.Workbox#installed
* @type {WorkboxEvent}
* @property {ServiceWorker} sw The service worker instance.
* @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange}
* event.
* @property {boolean|undefined} isUpdate True if a service worker was already
* controlling when this `Workbox` instance called `register()`.
* @property {string} type `installed`.
* @property {Workbox} target The `Workbox` instance.
*/

/**
* The `waiting` event is dispatched if the state of a
* [`Workbox`]{@link module:workbox-window.Workbox} instance's
* [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw}
* changes to `installed` and then doesn't immediately change to `activating`.
* It may also be dispatched if a service worker with the same
* [`scriptURL`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/scriptURL}
* was already waiting when the [`register()`]{@link module:workbox-window.Workbox#register}
* method was called.
*
* @event module:workbox-window.Workbox#waiting
* @type {WorkboxEvent}
* @property {ServiceWorker} sw The service worker instance.
* @property {Event} originalEvent The native `controllerchange` event
* @property {boolean|undefined} isUpdate True if a service worker was already
* controlling when this `Workbox` instance called `register()`.
* @property {boolean|undefined} wasWaitingBeforeRegister True if a service worker with
* a matching `scriptURL` was already waiting when this `Workbox`
* instance called `register()`.
* @property {string} type `waiting`.
* @property {Workbox} target The `Workbox` instance.
*/

/**
* The `controlling` event is dispatched if a
* [`controllerchange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/oncontrollerchange}
* fires on the service worker [container]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer}
* and the [`scriptURL`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/scriptURL}
* of the new [controller]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/controller}
* matches the `scriptURL` of the `Workbox` instance's
* [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw}.
*
* @event module:workbox-window.Workbox#controlling
* @type {WorkboxEvent}
* @property {ServiceWorker} sw The service worker instance.
* @property {Event} originalEvent The original [`controllerchange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/oncontrollerchange}
* event.
* @property {boolean|undefined} isUpdate True if a service worker was already
* controlling when this service worker was registered.
* @property {string} type `controlling`.
* @property {Workbox} target The `Workbox` instance.
*/

/**
* The `activated` event is dispatched if the state of a
* [`Workbox`]{@link module:workbox-window.Workbox} instance's
* [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw}
* changes to `activated`.
*
* @event module:workbox-window.Workbox#activated
* @type {WorkboxEvent}
* @property {ServiceWorker} sw The service worker instance.
* @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange}
* event.
* @property {boolean|undefined} isUpdate True if a service worker was already
* controlling when this `Workbox` instance called `register()`.
* @property {string} type `activated`.
* @property {Workbox} target The `Workbox` instance.
*/

/**
* The `redundant` event is dispatched if the state of a
* [`Workbox`]{@link module:workbox-window.Workbox} instance's
* [registered service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-registered-sw}
* changes to `redundant`.
*
* @event module:workbox-window.Workbox#redundant
* @type {WorkboxEvent}
* @property {ServiceWorker} sw The service worker instance.
* @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange}
* event.
* @property {boolean|undefined} isUpdate True if a service worker was already
* controlling when this `Workbox` instance called `register()`.
* @property {string} type `redundant`.
* @property {Workbox} target The `Workbox` instance.
*/

/**
* The `externalinstalled` event is dispatched if the state of an
* [external service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-external-sw}
* changes to `installed`.
*
* @event module:workbox-window.Workbox#externalinstalled
* @type {WorkboxEvent}
* @property {ServiceWorker} sw The service worker instance.
* @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange}
* event.
* @property {string} type `externalinstalled`.
* @property {Workbox} target The `Workbox` instance.
*/

/**
* The `externalwaiting` event is dispatched if the state of an
* [external service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-external-sw}
* changes to `waiting`.
*
* @event module:workbox-window.Workbox#externalwaiting
* @type {WorkboxEvent}
* @property {ServiceWorker} sw The service worker instance.
* @property {Event|undefined} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange}
* event.
* @property {string} type `externalwaiting`.
* @property {Workbox} target The `Workbox` instance.
*/

/**
* The `externalactivated` event is dispatched if the state of an
* [external service worker]{@link https://developers.google.com/web/tools/workbox/modules/workbox-precaching#def-external-sw}
* changes to `activated`.
*
* @event module:workbox-window.Workbox#externalactivated
* @type {WorkboxEvent}
* @property {ServiceWorker} sw The service worker instance.
* @property {Event} originalEvent The original [`statechange`]{@link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/onstatechange}
* event.
* @property {string} type `externalactivated`.
* @property {Workbox} target The `Workbox` instance.
*/

export {Workbox};
52 changes: 52 additions & 0 deletions test/workbox-window/integration/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ describe(`[workbox-window] Workbox`, function() {

wb.addEventListener('activated', () => {
cb({
isUpdate: installedSpy.args[0][0].isUpdate,
installedSpyCallCount: installedSpy.callCount,
waitingSpyCallCount: waitingSpy.callCount,
controllingSpyCallCount: controllingSpy.callCount,
Expand All @@ -120,6 +121,9 @@ describe(`[workbox-window] Workbox`, function() {
}
});

// Test for truthiness because some browsers structure clone
// `undefined` to `null`.
expect(result.isUpdate).to.not.be.ok;
expect(result.installedSpyCallCount).to.equal(1);
expect(result.activatedSpyCallCount).to.equal(1);
expect(result.controllingSpyCallCount).to.equal(1);
Expand All @@ -128,6 +132,54 @@ describe(`[workbox-window] Workbox`, function() {
expect(result.waitingSpyCallCount).to.equal(0);
});

it(`reports all events for an updated SW registration`, async function() {
const result = await executeAsyncAndCatch(async (cb) => {
try {
const wb1 = new Workbox('sw-clients-claim.tmp.js?v=1');
const redundantSpy = sinon.spy();
wb1.addEventListener('redundant', redundantSpy);

await wb1.register();
await wb1.controlling;

const wb2 = new Workbox('sw-clients-claim.tmp.js?v=2');
await wb2.register();

const installedSpy = sinon.spy();
const waitingSpy = sinon.spy();
const activatedSpy = sinon.spy();
const controllingSpy = sinon.spy();

wb2.addEventListener('installed', installedSpy);
wb2.addEventListener('waiting', waitingSpy);
wb2.addEventListener('controlling', controllingSpy);
wb2.addEventListener('activated', activatedSpy);

wb2.addEventListener('activated', () => {
cb({
wb1IsUpdate: redundantSpy.args[0][0].isUpdate,
wb2IsUpdate: installedSpy.args[0][0].isUpdate,
installedSpyCallCount: installedSpy.callCount,
waitingSpyCallCount: waitingSpy.callCount,
controllingSpyCallCount: controllingSpy.callCount,
activatedSpyCallCount: activatedSpy.callCount,
});
});
} catch (error) {
cb({error: error.stack});
}
});

// Test for truthiness because some browsers structure clone
// `undefined` to `null`.
expect(result.wb1IsUpdate).to.not.be.ok;
expect(result.wb2IsUpdate).to.equal(true);
expect(result.installedSpyCallCount).to.equal(1);
expect(result.waitingSpyCallCount).to.equal(0);
expect(result.activatedSpyCallCount).to.equal(1);
expect(result.controllingSpyCallCount).to.equal(1);
});

it(`reports all events for an external SW registration`, async function() {
const firstTab = await getLastWindowHandle();

Expand Down
Loading

0 comments on commit 3033185

Please sign in to comment.