| // Copyright 2013 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. |
| |
| import { baseUri } from "./base_uri.js"; |
| |
| /** |
| * Wraps `promise` in a timeout of the given `duration` in ms. |
| * |
| * Resolves/rejects with whatever the original `promises` does, or rejects |
| * if `promise` takes longer to complete than `duration`. In that case, |
| * `debugName` is used to compose a legible error message. |
| * |
| * If `duration` is < 0, the original `promise` is returned unchanged. |
| * @param {Promise} promise |
| * @param {number} duration |
| * @param {string} debugName |
| * @returns {Promise} a wrapped promise. |
| */ |
| async function timeout(promise, duration, debugName) { |
| if (duration < 0) { |
| return promise; |
| } |
| let timeoutId; |
| const _clock = new Promise((_, reject) => { |
| timeoutId = setTimeout(() => { |
| reject( |
| new Error( |
| `${debugName} took more than ${duration}ms to resolve. Moving on.`, |
| { |
| cause: timeout, |
| } |
| ) |
| ); |
| }, duration); |
| }); |
| return Promise.race([promise, _clock]).finally(() => { |
| clearTimeout(timeoutId); |
| }); |
| } |
| |
| /** |
| * Handles loading/reloading Flutter's service worker, if configured. |
| * |
| * @see: https://developers.google.com/web/fundamentals/primers/service-workers |
| */ |
| export class FlutterServiceWorkerLoader { |
| /** |
| * Injects a TrustedTypesPolicy (or undefined if the feature is not supported). |
| * @param {TrustedTypesPolicy | undefined} policy |
| */ |
| setTrustedTypesPolicy(policy) { |
| this._ttPolicy = policy; |
| } |
| /** |
| * Returns a Promise that resolves when the latest Flutter service worker, |
| * configured by `settings` has been loaded and activated. |
| * |
| * Otherwise, the promise is rejected with an error message. |
| * @param {import("./types").ServiceWorkerSettings} settings Service worker settings |
| * @returns {Promise} that resolves when the latest serviceWorker is ready. |
| */ |
| loadServiceWorker(settings) { |
| if (!settings) { |
| // In the future, settings = null -> uninstall service worker? |
| console.debug("Null serviceWorker configuration. Skipping."); |
| return Promise.resolve(); |
| } |
| if (!("serviceWorker" in navigator)) { |
| let errorMessage = "Service Worker API unavailable."; |
| if (!window.isSecureContext) { |
| errorMessage += "\nThe current context is NOT secure." |
| errorMessage += "\nRead more: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts"; |
| } |
| return Promise.reject( |
| new Error(errorMessage) |
| ); |
| } |
| const { |
| serviceWorkerVersion, |
| serviceWorkerUrl = `${baseUri}flutter_service_worker.js?v=${serviceWorkerVersion}`, |
| timeoutMillis = 4000, |
| } = settings; |
| // Apply the TrustedTypes policy, if present. |
| let url = serviceWorkerUrl; |
| if (this._ttPolicy != null) { |
| url = this._ttPolicy.createScriptURL(url); |
| } |
| const serviceWorkerActivation = navigator.serviceWorker |
| .register(url) |
| .then((serviceWorkerRegistration) => this._getNewServiceWorker(serviceWorkerRegistration, serviceWorkerVersion)) |
| .then(this._waitForServiceWorkerActivation); |
| // Timeout race promise |
| return timeout( |
| serviceWorkerActivation, |
| timeoutMillis, |
| "prepareServiceWorker" |
| ); |
| } |
| /** |
| * Returns the latest service worker for the given `serviceWorkerRegistration`. |
| * |
| * This might return the current service worker, if there's no new service worker |
| * awaiting to be installed/updated. |
| * |
| * @param {ServiceWorkerRegistration} serviceWorkerRegistration |
| * @param {string} serviceWorkerVersion |
| * @returns {Promise<ServiceWorker>} |
| */ |
| async _getNewServiceWorker(serviceWorkerRegistration, serviceWorkerVersion) { |
| if (!serviceWorkerRegistration.active && (serviceWorkerRegistration.installing || serviceWorkerRegistration.waiting)) { |
| // No active web worker and we have installed or are installing |
| // one for the first time. Simply wait for it to activate. |
| console.debug("Installing/Activating first service worker."); |
| return serviceWorkerRegistration.installing || serviceWorkerRegistration.waiting; |
| } else if (!serviceWorkerRegistration.active.scriptURL.endsWith(serviceWorkerVersion)) { |
| // When the app updates the serviceWorkerVersion changes, so we |
| // need to ask the service worker to update. |
| const newRegistration = await serviceWorkerRegistration.update(); |
| console.debug("Updating service worker."); |
| return newRegistration.installing || newRegistration.waiting || newRegistration.active; |
| } else { |
| console.debug("Loading from existing service worker."); |
| return serviceWorkerRegistration.active; |
| } |
| } |
| /** |
| * Returns a Promise that resolves when the `serviceWorker` changes its |
| * state to "activated". |
| * |
| * @param {ServiceWorker} serviceWorker |
| * @returns {Promise<void>} |
| */ |
| async _waitForServiceWorkerActivation(serviceWorker) { |
| if (!serviceWorker || serviceWorker.state === "activated") { |
| if (!serviceWorker) { |
| throw new Error("Cannot activate a null service worker!"); |
| } else { |
| console.debug("Service worker already active."); |
| return; |
| } |
| } |
| return new Promise((resolve, _) => { |
| serviceWorker.addEventListener("statechange", () => { |
| if (serviceWorker.state === "activated") { |
| console.debug("Activated new service worker."); |
| resolve(); |
| } |
| }); |
| }); |
| } |
| } |