Created
November 11, 2019 01:53
-
-
Save bseib/06164a973552fe9f814df97bb4d30305 to your computer and use it in GitHub Desktop.
Manage service worker installation and monitor for updates
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface InstalledWorkers { | |
waitingWorker: ServiceWorker | null; | |
activeWorker: ServiceWorker | null; | |
} | |
type ListenerCallback = (waitingWorker: ServiceWorker | null) => any; | |
export default class ServiceWorkerManager { | |
private static singleton: ServiceWorkerManager | null = null; | |
public static instance(): ServiceWorkerManager { | |
if ( null === ServiceWorkerManager.singleton ) { | |
ServiceWorkerManager.singleton = new ServiceWorkerManager(); | |
} | |
return ServiceWorkerManager.singleton; | |
} | |
private waitingWorkerListeners: ListenerCallback[] = []; | |
private isDoingPageReload: boolean = false; | |
private _waitingWorker: ServiceWorker | null = null; | |
private _activeWorker: ServiceWorker | null = null; | |
private _checkForUpdate: (() => Promise<boolean>) | null = null; | |
private _initializer: Promise<any>; | |
private _isInitialized: boolean = false; | |
private constructor() { | |
this._initializer = new Promise<any>((resolve, reject) => { | |
if ( !('serviceWorker' in navigator) ) { | |
reject('service workers are not available'); | |
} | |
if ( this._isInitialized ) { | |
resolve(); | |
} | |
else { | |
this.initialize() | |
.then(() => { | |
this._isInitialized = true; | |
resolve(); | |
}) | |
.catch((err) => { | |
reject('initialization failed: ' + err); | |
}); | |
} | |
}); | |
} | |
public get isInitialized(): boolean { | |
return this._isInitialized; | |
} | |
public get waitingWorker(): ServiceWorker | null { | |
return this._waitingWorker; | |
} | |
public get activeWorker(): ServiceWorker | null { | |
return this._activeWorker; | |
} | |
public addOnWaitingWorkerListener(callback: ListenerCallback) { | |
this.waitingWorkerListeners.push(callback); | |
} | |
// The promise is resolved when one or both `activeWorker` and `waitingWorker` have values. | |
// If a new worker is installing, then the promise is not resolved until it becomes a | |
// `waitingWorker`. | |
public whenReady(): Promise<any> { | |
return this._initializer; | |
} | |
public activateWaitingWorker() { | |
console.log('activate waiting worker'); | |
console.log(' `--> waitingWorker = ', this.waitingWorker); | |
if ( this.waitingWorker ) { | |
const worker = this.waitingWorker; | |
console.log(' `--> in 5 seconds, tell service worker to skipWaiting'); | |
setTimeout(() => { | |
worker.postMessage({ name: 'skipWaiting'}); | |
}, 5000); | |
} | |
} | |
public checkForUpdate(): Promise<boolean> { | |
return new Promise((resolve, reject) => { | |
if ( this._checkForUpdate ) { | |
this._checkForUpdate() | |
.then((isUpdateFound: boolean) => { | |
resolve(isUpdateFound); | |
}) | |
.catch((err) => { | |
reject(err); | |
}); | |
} | |
else { | |
reject('there is no service worker registration to update'); | |
} | |
}); | |
} | |
private initialize(): Promise<any> { | |
console.log("initialize service worker"); | |
// document.addEventListener('service-worker-installed', (event) => { this.onNewServiceWorkerWasInstalled(event); }, { once: true }); | |
navigator.serviceWorker.addEventListener('controllerchange', () => { this.onWaitingWorkerWasActivated(); }); | |
// when this resolves, one or both `_waitingWorker` and `_activeWorker` will be assigned. | |
return new Promise<any>((resolve, reject) => { | |
window.addEventListener('load', () => { | |
const swUrl = `${process.env.BASE_URL}service-worker.js`; | |
const registrationOptions = {}; | |
console.log(` \`--> registering service worker: ${swUrl}`); | |
navigator.serviceWorker.register(swUrl, registrationOptions) | |
.then((registration: ServiceWorkerRegistration) => { | |
console.log(` \`--> registration done`); | |
// Watch for new updates that are found. I presume that calling navigator.serviceWorker.register() causes | |
// an internal call to registration.update(), which fetches `swUrl` and can trigger the `onupdatefound` handler. | |
registration.onupdatefound = this.createUpdateFoundHandler(registration, (installedWorkers: InstalledWorkers) => { | |
this.assignToWaitingWorker(installedWorkers.waitingWorker); | |
this._activeWorker = installedWorkers.activeWorker; | |
this.logCurrentWorkers(registration); | |
resolve(); | |
}); | |
this.logCurrentWorkers(registration); | |
// capture the update function for later use | |
this._checkForUpdate = this.createCheckForUpdateHandler(registration, swUrl); | |
this._activeWorker = registration.active; // could be null if none have been activated | |
if ( !registration.installing ) { | |
// ignore the 'installing' case because the `onupdatefound` handler above will resolve when it becomes 'waiting' | |
if ( registration.waiting) { | |
this.assignToWaitingWorker(registration.waiting); | |
} | |
// we must have an active or waiting worker to resolve. But if one is installing, then above handler will resolve it. | |
if ( this._activeWorker || this._waitingWorker ) { | |
resolve(); | |
} | |
} | |
}) | |
.catch((err) => { | |
reject(err); | |
}); | |
}); | |
}); | |
} | |
private logCurrentWorkers(registration: ServiceWorkerRegistration) { | |
console.log(` \`--> the installing worker: `, registration.installing); | |
console.log(` \`--> the waiting worker: `, registration.waiting); | |
console.log(` \`--> the active worker: `, registration.active); | |
} | |
private createUpdateFoundHandler(registration: ServiceWorkerRegistration, onInstalled: (installedWorkers: InstalledWorkers) => any ) { | |
return () => { | |
console.log(` \`--> an updated service worker was found`); | |
const installingWorker = (registration.installing as ServiceWorker); // cast because it won't be null | |
installingWorker.onstatechange = () => { | |
console.log(` \`--> an installing worker state changed to: ${installingWorker.state}`); | |
if ( installingWorker.state === 'installed' ) { | |
if ( registration.active ) { // installed, but now waiting | |
console.log(` \`--> a previous active worker is already in place, so become the waiting worker`); | |
onInstalled({ | |
waitingWorker: registration.waiting, | |
activeWorker: registration.active, | |
}); | |
} | |
} | |
else if ( installingWorker.state === 'activated' ) { // installed and now waiting | |
if ( registration.active ) { | |
console.log(` \`--> no previous active worker is in the way, so become the active worker`); | |
onInstalled({ | |
waitingWorker: registration.waiting, | |
activeWorker: registration.active, | |
}); | |
} | |
} | |
}; | |
}; | |
} | |
private createCheckForUpdateHandler(registration: ServiceWorkerRegistration, swUrl: string): () => Promise<boolean> { | |
return (): Promise<boolean> => { | |
return new Promise<boolean>((resolve, reject) => { | |
console.log(`checking for an update to ${swUrl}`); | |
registration.update().then(() => { | |
this.logCurrentWorkers(registration); | |
if ( registration.installing ) { | |
registration.onupdatefound = this.createUpdateFoundHandler(registration, (installedWorkers: InstalledWorkers) => { | |
this.assignToWaitingWorker(installedWorkers.waitingWorker); | |
this._activeWorker = installedWorkers.activeWorker; | |
this.logCurrentWorkers(registration); | |
resolve(true); | |
}); | |
} | |
else { | |
if ( registration.waiting ) { | |
resolve(true); | |
} | |
else { | |
resolve(false); | |
} | |
} | |
}).catch((err) => { | |
reject(err); | |
}); | |
}); | |
}; | |
} | |
private assignToWaitingWorker(worker: ServiceWorker | null) { | |
this._waitingWorker = worker; | |
this.waitingWorkerListeners.forEach((callback) => { | |
callback(worker); | |
}); | |
} | |
private onWaitingWorkerWasActivated() { | |
console.log('onWaitingWorkerWasActivated()'); | |
if ( ! this.isDoingPageReload) { | |
this.isDoingPageReload = true; | |
window.location.replace('/'); // This was specific to my needs... A callback fn might be more general... | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Where does that
skipWaiting
event end up? Inservice-worker.js
: