Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add isUpdate and wasWaitingBeforeRegistration to workbox-window lifecycle events #1905

Merged
merged 6 commits into from
Feb 15, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
204 changes: 195 additions & 9 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 Down Expand Up @@ -117,12 +131,23 @@ 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...');
// 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,
}));
logger.warn('A service worker was already waiting to activate ' +
'before this script was registered...');
});
}

const currentPageIsOutOfScope = () => {
Expand Down Expand Up @@ -356,8 +381,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,7 +402,7 @@ 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) {
Expand Down Expand Up @@ -430,10 +460,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 +478,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};
48 changes: 48 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,7 @@ describe(`[workbox-window] Workbox`, function() {
}
});

expect(result.isUpdate).to.equal(undefined);
expect(result.installedSpyCallCount).to.equal(1);
expect(result.activatedSpyCallCount).to.equal(1);
expect(result.controllingSpyCallCount).to.equal(1);
Expand All @@ -128,6 +130,52 @@ 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});
}
});

expect(result.wb1IsUpdate).to.equal(undefined);
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