// Copyright (C) 2020 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Handles registration, unregistration and lifecycle of the service worker. // This class contains only the controlling logic, all the code in here runs in // the main thread, not in the service worker thread. // The actual service worker code is in src/service_worker. // Design doc: http://go/perfetto-offline. import {reportError} from '../base/logging'; import {globals} from './globals'; // We use a dedicated |caches| object to share a global boolean beween the main // thread and the SW. SW cannot use local-storage or anything else other than // IndexedDB (which would be overkill). const BYPASS_ID = 'BYPASS_SERVICE_WORKER'; export class ServiceWorkerController { private _initialWorker: ServiceWorker|null = null; private _bypassed = false; private _installing = false; // Caller should reload(). async setBypass(bypass: boolean) { if (!('serviceWorker' in navigator)) return; // Not supported. this._bypassed = bypass; if (bypass) { await caches.open(BYPASS_ID); // Create the entry. for (const reg of await navigator.serviceWorker.getRegistrations()) { await reg.unregister(); } } else { await caches.delete(BYPASS_ID); this.install(); } globals.rafScheduler.scheduleFullRedraw(); } onStateChange(sw: ServiceWorker) { globals.rafScheduler.scheduleFullRedraw(); if (sw.state === 'installing') { this._installing = true; } else if (sw.state === 'activated') { this._installing = false; // Don't show the notification if the site was served straight // from the network (e.g., on the very first visit or after // Ctrl+Shift+R). In these cases, we are already at the last // version. if (sw !== this._initialWorker && this._initialWorker) { globals.frontendLocalState.newVersionAvailable = true; } } } monitorWorker(sw: ServiceWorker|null) { if (!sw) return; sw.addEventListener('error', (e) => reportError(e)); sw.addEventListener('statechange', () => this.onStateChange(sw)); this.onStateChange(sw); // Trigger updates for the current state. } async install() { if (!('serviceWorker' in navigator)) return; // Not supported. if (location.pathname !== '/') { // Disable the service worker when the UI is loaded from a non-root URL // (e.g. from the CI artifacts GCS bucket). Supporting the case of a // nested index.html is too cumbersome and has no benefits. return; } if (await caches.has(BYPASS_ID)) { this._bypassed = true; console.log('Skipping service worker registration, disabled by the user'); return; } // In production cases versionDir == VERSION. We use this here for ease of // testing (so we can have /v1.0.0a/ /v1.0.0b/ even if they have the same // version code). const versionDir = globals.root.split('/').slice(-2)[0]; const swUri = `/service_worker.js?v=${versionDir}`; navigator.serviceWorker.register(swUri).then(registration => { this._initialWorker = registration.active; // At this point there are two options: // 1. This is the first time we visit the site (or cache was cleared) and // no SW is installed yet. In this case |installing| will be set. // 2. A SW is already installed (though it might be obsolete). In this // case |active| will be set. this.monitorWorker(registration.installing); this.monitorWorker(registration.active); // Setup the event that shows the "Updated to v1.2.3" notification. registration.addEventListener('updatefound', () => { this.monitorWorker(registration.installing); }); }); } get bypassed() {  return this._bypassed; } get installing() { return this._installing; } }