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}