| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /// The caching strategy for the generated service worker. |
| enum ServiceWorkerStrategy { |
| /// Download the app shell eagerly and all other assets lazily. |
| /// Prefer the offline cached version. |
| offlineFirst, |
| /// Do not generate a service worker, |
| none, |
| } |
| |
| /// Generate a service worker with an app-specific cache name a map of |
| /// resource files. |
| /// |
| /// The tool embeds file hashes directly into the worker so that the byte for byte |
| /// invalidation will automatically reactivate workers whenever a new |
| /// version is deployed. |
| String generateServiceWorker( |
| Map<String, String> resources, |
| List<String> coreBundle, { |
| required ServiceWorkerStrategy serviceWorkerStrategy, |
| }) { |
| if (serviceWorkerStrategy == ServiceWorkerStrategy.none) { |
| return ''; |
| } |
| return ''' |
| 'use strict'; |
| const MANIFEST = 'flutter-app-manifest'; |
| const TEMP = 'flutter-temp-cache'; |
| const CACHE_NAME = 'flutter-app-cache'; |
| const RESOURCES = { |
| ${resources.entries.map((MapEntry<String, String> entry) => '"${entry.key}": "${entry.value}"').join(",\n")} |
| }; |
| |
| // The application shell files that are downloaded before a service worker can |
| // start. |
| const CORE = [ |
| ${coreBundle.map((String file) => '"$file"').join(',\n')}]; |
| // During install, the TEMP cache is populated with the application shell files. |
| self.addEventListener("install", (event) => { |
| self.skipWaiting(); |
| return event.waitUntil( |
| caches.open(TEMP).then((cache) => { |
| return cache.addAll( |
| CORE.map((value) => new Request(value, {'cache': 'reload'}))); |
| }) |
| ); |
| }); |
| |
| // During activate, the cache is populated with the temp files downloaded in |
| // install. If this service worker is upgrading from one with a saved |
| // MANIFEST, then use this to retain unchanged resource files. |
| self.addEventListener("activate", function(event) { |
| return event.waitUntil(async function() { |
| try { |
| var contentCache = await caches.open(CACHE_NAME); |
| var tempCache = await caches.open(TEMP); |
| var manifestCache = await caches.open(MANIFEST); |
| var manifest = await manifestCache.match('manifest'); |
| // When there is no prior manifest, clear the entire cache. |
| if (!manifest) { |
| await caches.delete(CACHE_NAME); |
| contentCache = await caches.open(CACHE_NAME); |
| for (var request of await tempCache.keys()) { |
| var response = await tempCache.match(request); |
| await contentCache.put(request, response); |
| } |
| await caches.delete(TEMP); |
| // Save the manifest to make future upgrades efficient. |
| await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES))); |
| return; |
| } |
| var oldManifest = await manifest.json(); |
| var origin = self.location.origin; |
| for (var request of await contentCache.keys()) { |
| var key = request.url.substring(origin.length + 1); |
| if (key == "") { |
| key = "/"; |
| } |
| // If a resource from the old manifest is not in the new cache, or if |
| // the MD5 sum has changed, delete it. Otherwise the resource is left |
| // in the cache and can be reused by the new service worker. |
| if (!RESOURCES[key] || RESOURCES[key] != oldManifest[key]) { |
| await contentCache.delete(request); |
| } |
| } |
| // Populate the cache with the app shell TEMP files, potentially overwriting |
| // cache files preserved above. |
| for (var request of await tempCache.keys()) { |
| var response = await tempCache.match(request); |
| await contentCache.put(request, response); |
| } |
| await caches.delete(TEMP); |
| // Save the manifest to make future upgrades efficient. |
| await manifestCache.put('manifest', new Response(JSON.stringify(RESOURCES))); |
| return; |
| } catch (err) { |
| // On an unhandled exception the state of the cache cannot be guaranteed. |
| console.error('Failed to upgrade service worker: ' + err); |
| await caches.delete(CACHE_NAME); |
| await caches.delete(TEMP); |
| await caches.delete(MANIFEST); |
| } |
| }()); |
| }); |
| |
| // The fetch handler redirects requests for RESOURCE files to the service |
| // worker cache. |
| self.addEventListener("fetch", (event) => { |
| if (event.request.method !== 'GET') { |
| return; |
| } |
| var origin = self.location.origin; |
| var key = event.request.url.substring(origin.length + 1); |
| // Redirect URLs to the index.html |
| if (key.indexOf('?v=') != -1) { |
| key = key.split('?v=')[0]; |
| } |
| if (event.request.url == origin || event.request.url.startsWith(origin + '/#') || key == '') { |
| key = '/'; |
| } |
| // If the URL is not the RESOURCE list then return to signal that the |
| // browser should take over. |
| if (!RESOURCES[key]) { |
| return; |
| } |
| // If the URL is the index.html, perform an online-first request. |
| if (key == '/') { |
| return onlineFirst(event); |
| } |
| event.respondWith(caches.open(CACHE_NAME) |
| .then((cache) => { |
| return cache.match(event.request).then((response) => { |
| // Either respond with the cached resource, or perform a fetch and |
| // lazily populate the cache only if the resource was successfully fetched. |
| return response || fetch(event.request).then((response) => { |
| if (response && Boolean(response.ok)) { |
| cache.put(event.request, response.clone()); |
| } |
| return response; |
| }); |
| }) |
| }) |
| ); |
| }); |
| |
| self.addEventListener('message', (event) => { |
| // SkipWaiting can be used to immediately activate a waiting service worker. |
| // This will also require a page refresh triggered by the main worker. |
| if (event.data === 'skipWaiting') { |
| self.skipWaiting(); |
| return; |
| } |
| if (event.data === 'downloadOffline') { |
| downloadOffline(); |
| return; |
| } |
| }); |
| |
| // Download offline will check the RESOURCES for all files not in the cache |
| // and populate them. |
| async function downloadOffline() { |
| var resources = []; |
| var contentCache = await caches.open(CACHE_NAME); |
| var currentContent = {}; |
| for (var request of await contentCache.keys()) { |
| var key = request.url.substring(origin.length + 1); |
| if (key == "") { |
| key = "/"; |
| } |
| currentContent[key] = true; |
| } |
| for (var resourceKey of Object.keys(RESOURCES)) { |
| if (!currentContent[resourceKey]) { |
| resources.push(resourceKey); |
| } |
| } |
| return contentCache.addAll(resources); |
| } |
| |
| // Attempt to download the resource online before falling back to |
| // the offline cache. |
| function onlineFirst(event) { |
| return event.respondWith( |
| fetch(event.request).then((response) => { |
| return caches.open(CACHE_NAME).then((cache) => { |
| cache.put(event.request, response.clone()); |
| return response; |
| }); |
| }).catch((error) => { |
| return caches.open(CACHE_NAME).then((cache) => { |
| return cache.match(event.request).then((response) => { |
| if (response != null) { |
| return response; |
| } |
| throw error; |
| }); |
| }); |
| }) |
| ); |
| } |
| '''; |
| } |