blob: 7c4ff3e6c39bdbce9429029eec92e480ed32a3c2 [file] [log] [blame]
// 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();
}
});
});
}
}