// 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. if (!_flutter) { var _flutter = {}; } _flutter.loader = null; (function () { "use strict"; const baseUri = ensureTrailingSlash(getBaseURI()); function getBaseURI() { const base = document.querySelector("base"); return (base && base.getAttribute("href")) || ""; } function ensureTrailingSlash(uri) { if (uri == "") { return uri; } return uri.endsWith("/") ? uri : `${uri}/`; } /** * 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 the creation of a TrustedTypes `policy` that validates URLs based * on an (optional) incoming array of RegExes. */ class FlutterTrustedTypesPolicy { /** * Constructs the policy. * @param {[RegExp]} validPatterns the patterns to test URLs * @param {String} policyName the policy name (optional) */ constructor(validPatterns, policyName = "flutter-js") { const patterns = validPatterns || [ /\.js$/, ]; if (window.trustedTypes) { this.policy = trustedTypes.createPolicy(policyName, { createScriptURL: function(url) { const parsed = new URL(url, window.location); const file = parsed.pathname.split("/").pop(); const matches = patterns.some((pattern) => pattern.test(file)); if (matches) { return parsed.toString(); } console.error( "URL rejected by TrustedTypes policy", policyName, ":", url, "(download prevented)"); } }); } } } /** * Handles loading/reloading Flutter's service worker, if configured. * * @see: https://developers.google.com/web/fundamentals/primers/service-workers */ 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 {*} settings Service worker settings * @returns {Promise} that resolves when the latest serviceWorker is ready. */ loadServiceWorker(settings) { if (settings == null) { // 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(this._getNewServiceWorker) .then(this._waitForServiceWorkerActivation); // Timeout race promise return timeout( serviceWorkerActivation, timeoutMillis, "prepareServiceWorker" ); } /** * Returns the latest service worker for the given `serviceWorkerRegistrationPromise`. * * This might return the current service worker, if there's no new service worker * awaiting to be installed/updated. * * @param {Promise} serviceWorkerRegistrationPromise * @returns {Promise} */ async _getNewServiceWorker(serviceWorkerRegistrationPromise) { const reg = await serviceWorkerRegistrationPromise; if (!reg.active && (reg.installing || reg.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 reg.installing || reg.waiting; } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) { // When the app updates the serviceWorkerVersion changes, so we // need to ask the service worker to update. return reg.update().then((newReg) => { console.debug("Updating service worker."); return newReg.installing || newReg.waiting || newReg.active; }); } else { console.debug("Loading from existing service worker."); return reg.active; } } /** * Returns a Promise that resolves when the `latestServiceWorker` changes its * state to "activated". * * @param {Promise} latestServiceWorkerPromise * @returns {Promise} */ async _waitForServiceWorkerActivation(latestServiceWorkerPromise) { const serviceWorker = await latestServiceWorkerPromise; if (!serviceWorker || serviceWorker.state == "activated") { if (!serviceWorker) { return Promise.reject( new Error("Cannot activate a null service worker!") ); } else { console.debug("Service worker already active."); return Promise.resolve(); } } return new Promise((resolve, _) => { serviceWorker.addEventListener("statechange", () => { if (serviceWorker.state == "activated") { console.debug("Activated new service worker."); resolve(); } }); }); } } /** * Handles injecting the main Flutter web entrypoint (main.dart.js), and notifying * the user when Flutter is ready, through `didCreateEngineInitializer`. * * @see https://docs.flutter.dev/development/platform-integration/web/initialization */ class FlutterEntrypointLoader { /** * Creates a FlutterEntrypointLoader. */ constructor() { // Watchdog to prevent injecting the main entrypoint multiple times. this._scriptLoaded = false; } /** * Injects a TrustedTypesPolicy (or undefined if the feature is not supported). * @param {TrustedTypesPolicy | undefined} policy */ setTrustedTypesPolicy(policy) { this._ttPolicy = policy; } /** * Loads flutter main entrypoint, specified by `entrypointUrl`, and calls a * user-specified `onEntrypointLoaded` callback with an EngineInitializer * object when it's done. * * @param {*} options * @returns {Promise | undefined} that will eventually resolve with an * EngineInitializer, or will be rejected with the error caused by the loader. * Returns undefined when an `onEntrypointLoaded` callback is supplied in `options`. */ async loadEntrypoint(options) { const { entrypointUrl = `${baseUri}main.dart.js`, onEntrypointLoaded } = options || {}; return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded); } /** * Resolves the promise created by loadEntrypoint, and calls the `onEntrypointLoaded` * function supplied by the user (if needed). * * Called by Flutter through `_flutter.loader.didCreateEngineInitializer` method, * which is bound to the correct instance of the FlutterEntrypointLoader by * the FlutterLoader object. * * @param {Function} engineInitializer @see https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/js_interop/js_loader.dart#L42 */ didCreateEngineInitializer(engineInitializer) { if (typeof this._didCreateEngineInitializerResolve === "function") { this._didCreateEngineInitializerResolve(engineInitializer); // Remove the resolver after the first time, so Flutter Web can hot restart. this._didCreateEngineInitializerResolve = null; // Make the engine revert to "auto" initialization on hot restart. delete _flutter.loader.didCreateEngineInitializer; } if (typeof this._onEntrypointLoaded === "function") { this._onEntrypointLoaded(engineInitializer); } } /** * Injects a script tag into the DOM, and configures this loader to be able to * handle the "entrypoint loaded" notifications received from Flutter web. * * @param {string} entrypointUrl the URL of the script that will initialize * Flutter. * @param {Function} onEntrypointLoaded a callback that will be called when * Flutter web notifies this object that the entrypoint is * loaded. * @returns {Promise | undefined} a Promise that resolves when the entrypoint * is loaded, or undefined if `onEntrypointLoaded` * is a function. */ _loadEntrypoint(entrypointUrl, onEntrypointLoaded) { const useCallback = typeof onEntrypointLoaded === "function"; if (!this._scriptLoaded) { this._scriptLoaded = true; const scriptTag = this._createScriptTag(entrypointUrl); if (useCallback) { // Just inject the script tag, and return nothing; Flutter will call // `didCreateEngineInitializer` when it's done. console.debug("Injecting