1// Copyright (C) 2020 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// Handles registration, unregistration and lifecycle of the service worker. 16// This class contains only the controlling logic, all the code in here runs in 17// the main thread, not in the service worker thread. 18// The actual service worker code is in src/service_worker. 19// Design doc: http://go/perfetto-offline. 20 21import {reportError} from '../base/logging'; 22 23import {globals} from './globals'; 24 25// We use a dedicated |caches| object to share a global boolean beween the main 26// thread and the SW. SW cannot use local-storage or anything else other than 27// IndexedDB (which would be overkill). 28const BYPASS_ID = 'BYPASS_SERVICE_WORKER'; 29 30export class ServiceWorkerController { 31 private _initialWorker: ServiceWorker|null = null; 32 private _bypassed = false; 33 private _installing = false; 34 35 // Caller should reload(). 36 async setBypass(bypass: boolean) { 37 if (!('serviceWorker' in navigator)) return; // Not supported. 38 this._bypassed = bypass; 39 if (bypass) { 40 await caches.open(BYPASS_ID); // Create the entry. 41 for (const reg of await navigator.serviceWorker.getRegistrations()) { 42 await reg.unregister(); 43 } 44 } else { 45 await caches.delete(BYPASS_ID); 46 this.install(); 47 } 48 globals.rafScheduler.scheduleFullRedraw(); 49 } 50 51 onStateChange(sw: ServiceWorker) { 52 globals.rafScheduler.scheduleFullRedraw(); 53 if (sw.state === 'installing') { 54 this._installing = true; 55 } else if (sw.state === 'activated') { 56 this._installing = false; 57 // Don't show the notification if the site was served straight 58 // from the network (e.g., on the very first visit or after 59 // Ctrl+Shift+R). In these cases, we are already at the last 60 // version. 61 if (sw !== this._initialWorker && this._initialWorker) { 62 globals.frontendLocalState.newVersionAvailable = true; 63 } 64 } 65 } 66 67 monitorWorker(sw: ServiceWorker|null) { 68 if (!sw) return; 69 sw.addEventListener('error', (e) => reportError(e)); 70 sw.addEventListener('statechange', () => this.onStateChange(sw)); 71 this.onStateChange(sw); // Trigger updates for the current state. 72 } 73 74 async install() { 75 if (!('serviceWorker' in navigator)) return; // Not supported. 76 77 if (location.pathname !== '/') { 78 // Disable the service worker when the UI is loaded from a non-root URL 79 // (e.g. from the CI artifacts GCS bucket). Supporting the case of a 80 // nested index.html is too cumbersome and has no benefits. 81 return; 82 } 83 84 if (await caches.has(BYPASS_ID)) { 85 this._bypassed = true; 86 console.log('Skipping service worker registration, disabled by the user'); 87 return; 88 } 89 // In production cases versionDir == VERSION. We use this here for ease of 90 // testing (so we can have /v1.0.0a/ /v1.0.0b/ even if they have the same 91 // version code). 92 const versionDir = globals.root.split('/').slice(-2)[0]; 93 const swUri = `/service_worker.js?v=${versionDir}`; 94 navigator.serviceWorker.register(swUri).then(registration => { 95 this._initialWorker = registration.active; 96 97 // At this point there are two options: 98 // 1. This is the first time we visit the site (or cache was cleared) and 99 // no SW is installed yet. In this case |installing| will be set. 100 // 2. A SW is already installed (though it might be obsolete). In this 101 // case |active| will be set. 102 this.monitorWorker(registration.installing); 103 this.monitorWorker(registration.active); 104 105 // Setup the event that shows the "Updated to v1.2.3" notification. 106 registration.addEventListener('updatefound', () => { 107 this.monitorWorker(registration.installing); 108 }); 109 }); 110 } 111 112 get bypassed() { 113 return this._bypassed; 114 } 115 get installing() { 116 return this._installing; 117 } 118}