blob: ea0c2855d520b0c33196cf7b65ad2bf06812eb2e [file] [log] [blame]
// 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 {ignoreCacheUnactionableErrors} from '../common/errors';
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';
class BypassCache {
static async isBypassed(): Promise<boolean> {
try {
return await caches.has(BYPASS_ID);
} catch (e) {
return ignoreCacheUnactionableErrors(e, false);
}
}
static async setBypass(bypass: boolean): Promise<void> {
try {
if (bypass) {
await caches.open(BYPASS_ID);
} else {
await caches.delete(BYPASS_ID);
}
} catch (e) {
ignoreCacheUnactionableErrors(e, undefined);
}
}
}
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 BypassCache.setBypass(true); // Create the entry.
for (const reg of await navigator.serviceWorker.getRegistrations()) {
await reg.unregister();
}
} else {
await BypassCache.setBypass(false);
if (window.localStorage) {
window.localStorage.setItem('bypassDisabled', '1');
}
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 this is localhost disable the service worker by default, unless the
// user manually re-enabled it (in which case bypassDisabled = '1').
const hostname = location.hostname;
const isLocalhost = ['127.0.0.1', '::1', 'localhost'].includes(hostname);
const bypassDisabled = window.localStorage &&
window.localStorage.getItem('bypassDisabled') === '1';
if (isLocalhost && !bypassDisabled) {
await this.setBypass(true); // Will cause the check below to bail out.
}
if (await BypassCache.isBypassed()) {
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;
}
}