• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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';
22import {raf} from '../core/raf_scheduler';
23
24import {globals} from './globals';
25
26// We use a dedicated |caches| object to share a global boolean beween the main
27// thread and the SW. SW cannot use local-storage or anything else other than
28// IndexedDB (which would be overkill).
29const BYPASS_ID = 'BYPASS_SERVICE_WORKER';
30
31class BypassCache {
32  static async isBypassed(): Promise<boolean> {
33    try {
34      return await caches.has(BYPASS_ID);
35    } catch (_) {
36      // TODO(288483453): Reinstate:
37      // return ignoreCacheUnactionableErrors(e, false);
38      return false;
39    }
40  }
41
42  static async setBypass(bypass: boolean): Promise<void> {
43    try {
44      if (bypass) {
45        await caches.open(BYPASS_ID);
46      } else {
47        await caches.delete(BYPASS_ID);
48      }
49    } catch (_) {
50      // TODO(288483453): Reinstate:
51      // ignoreCacheUnactionableErrors(e, undefined);
52    }
53  }
54}
55
56export class ServiceWorkerController {
57  private _bypassed = false;
58  private _installing = false;
59
60  // Caller should reload().
61  async setBypass(bypass: boolean) {
62    if (!('serviceWorker' in navigator)) return; // Not supported.
63    this._bypassed = bypass;
64    if (bypass) {
65      await BypassCache.setBypass(true); // Create the entry.
66      for (const reg of await navigator.serviceWorker.getRegistrations()) {
67        await reg.unregister();
68      }
69    } else {
70      await BypassCache.setBypass(false);
71      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
72      if (window.localStorage) {
73        window.localStorage.setItem('bypassDisabled', '1');
74      }
75      this.install();
76    }
77    raf.scheduleFullRedraw();
78  }
79
80  onStateChange(sw: ServiceWorker) {
81    raf.scheduleFullRedraw();
82    if (sw.state === 'installing') {
83      this._installing = true;
84    } else if (sw.state === 'activated') {
85      this._installing = false;
86    }
87  }
88
89  monitorWorker(sw: ServiceWorker | null) {
90    if (!sw) return;
91    sw.addEventListener('error', (e) => reportError(e));
92    sw.addEventListener('statechange', () => this.onStateChange(sw));
93    this.onStateChange(sw); // Trigger updates for the current state.
94  }
95
96  async install() {
97    if (!('serviceWorker' in navigator)) return; // Not supported.
98
99    if (location.pathname !== '/') {
100      // Disable the service worker when the UI is loaded from a non-root URL
101      // (e.g. from the CI artifacts GCS bucket). Supporting the case of a
102      // nested index.html is too cumbersome and has no benefits.
103      return;
104    }
105
106    // If this is localhost disable the service worker by default, unless the
107    // user manually re-enabled it (in which case bypassDisabled = '1').
108    const hostname = location.hostname;
109    const isLocalhost = ['127.0.0.1', '::1', 'localhost'].includes(hostname);
110    const bypassDisabled =
111      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
112      window.localStorage &&
113      window.localStorage.getItem('bypassDisabled') === '1';
114    if (isLocalhost && !bypassDisabled) {
115      await this.setBypass(true); // Will cause the check below to bail out.
116    }
117
118    if (await BypassCache.isBypassed()) {
119      this._bypassed = true;
120      console.log('Skipping service worker registration, disabled by the user');
121      return;
122    }
123    // In production cases versionDir == VERSION. We use this here for ease of
124    // testing (so we can have /v1.0.0a/ /v1.0.0b/ even if they have the same
125    // version code).
126    const versionDir = globals.root.split('/').slice(-2)[0];
127    const swUri = `/service_worker.js?v=${versionDir}`;
128    navigator.serviceWorker.register(swUri).then((registration) => {
129      // At this point there are two options:
130      // 1. This is the first time we visit the site (or cache was cleared) and
131      //    no SW is installed yet. In this case |installing| will be set.
132      // 2. A SW is already installed (though it might be obsolete). In this
133      //    case |active| will be set.
134      this.monitorWorker(registration.installing);
135      this.monitorWorker(registration.active);
136
137      // Setup the event that shows the "Updated to v1.2.3" notification.
138      registration.addEventListener('updatefound', () => {
139        this.monitorWorker(registration.installing);
140      });
141    });
142  }
143
144  get bypassed() {
145    return this._bypassed;
146  }
147  get installing() {
148    return this._installing;
149  }
150}
151