go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/sw/ui_sw.ts (about) 1 // Copyright 2021 The LUCI Authors. 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 // TODO(weiweilin): add integration tests to ensure the SW works properly. 16 17 import 'virtual:configs.js'; 18 import { 19 cleanupOutdatedCaches, 20 createHandlerBoundToURL, 21 precacheAndRoute, 22 } from 'workbox-precaching'; 23 import { NavigationRoute, registerRoute } from 'workbox-routing'; 24 25 import { Prefetcher } from '@/common/service_workers/prefetch'; 26 27 // Tell TSC that this is a ServiceWorker script. 28 declare const self: ServiceWorkerGlobalScope; 29 30 /** 31 * A regex for the defined routes. 32 */ 33 declare const DEFINED_ROUTES_REGEXP: string; 34 35 // Unconditionally skip waiting so the clients can always get the newest version 36 // when the page is refreshed. The only downside is that clients on an old 37 // version may encounter errors when lazy loading cached static assets. This is 38 // not critical because 39 // 1. This is unlikely to happen since all JS assets are prefetched. 40 // 2. All lazy-loadable assets (i.e. excluding entry files) have content hashes 41 // in their filenames. This means in case of a cache miss, 42 // 1. it's virtually impossible to lazy load an asset of an incompatible 43 // version because the content hash won't match, and 44 // 2. they can have a much longer cache duration (currently configured to be 45 // 4 weeks), the asset loading request can likely be fulfilled by other 46 // cache layers (e.g. AppEngine server cache, browser HTTP cache). 47 // 3. When the client failed to lazy load an asset, a simple refresh will be 48 // able to fix the issue anyway. 49 self.skipWaiting(); 50 51 const prefetcher = new Prefetcher(SETTINGS, self.fetch.bind(self)); 52 53 self.addEventListener('fetch', async (e) => { 54 if (e.request.mode === 'navigate') { 55 const url = new URL(e.request.url); 56 prefetcher.prefetchResources(url.pathname); 57 return; 58 } 59 60 if (prefetcher.respondWithPrefetched(e)) { 61 return; 62 } 63 64 // Ensure all clients served by this service worker use the same config. 65 if (e.request.url === self.origin + '/configs.js') { 66 const res = new Response( 67 `self.VERSION = '${VERSION}';\n` + 68 `self.SETTINGS = Object.freeze(${JSON.stringify(SETTINGS)});\n`, 69 ); 70 res.headers.set('content-type', 'application/javascript'); 71 e.respondWith(res); 72 return; 73 } 74 }); 75 76 // Enable workbox specific features AFTER we registered our own event handlers 77 // to make sure our own event handlers have the chance to intercept the 78 // requests. 79 { 80 self.addEventListener('message', (event) => { 81 // This is not used currently. We keep it here anyway to ensure that calling 82 // `Workbox.prototype.messageSkipWaiting` is not a noop should it be used in 83 // the future. 84 if (event.data?.type === 'SKIP_WAITING') { 85 self.skipWaiting(); 86 } 87 }); 88 89 cleanupOutdatedCaches(); 90 precacheAndRoute(self.__WB_MANIFEST); 91 registerRoute( 92 new NavigationRoute(createHandlerBoundToURL('/ui/index.html'), { 93 // Only handle defined routes so when the user visits a newly added route, 94 // the service worker won't serve an old cache, causing the user to see a 95 // 404 page until the new version is activated. 96 allowlist: [new RegExp(DEFINED_ROUTES_REGEXP, 'i')], 97 }), 98 ); 99 }