Spaces:
Running
Running
| // 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((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(); | |
| } | |
| }); | |
| }); | |
| } | |
| } | |
| /** | |
| * 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 <script> tag. Using callback."); | |
| this._onEntrypointLoaded = onEntrypointLoaded; | |
| document.body.append(scriptTag); | |
| } else { | |
| // Inject the script tag and return a promise that will get resolved | |
| // with the EngineInitializer object from Flutter when it calls | |
| // `didCreateEngineInitializer` later. | |
| return new Promise((resolve, reject) => { | |
| console.debug( | |
| "Injecting <script> tag. Using Promises. Use the callback approach instead!" | |
| ); | |
| this._didCreateEngineInitializerResolve = resolve; | |
| scriptTag.addEventListener("error", reject); | |
| document.body.append(scriptTag); | |
| }); | |
| } | |
| } | |
| } | |
| /** | |
| * Creates a script tag for the given URL. | |
| * @param {string} url | |
| * @returns {HTMLScriptElement} | |
| */ | |
| _createScriptTag(url) { | |
| const scriptTag = document.createElement("script"); | |
| scriptTag.type = "application/javascript"; | |
| // Apply TrustedTypes validation, if available. | |
| let trustedUrl = url; | |
| if (this._ttPolicy != null) { | |
| trustedUrl = this._ttPolicy.createScriptURL(url); | |
| } | |
| scriptTag.src = trustedUrl; | |
| return scriptTag; | |
| } | |
| } | |
| /** | |
| * The public interface of _flutter.loader. Exposes two methods: | |
| * * loadEntrypoint (which coordinates the default Flutter web loading procedure) | |
| * * didCreateEngineInitializer (which is called by Flutter to notify that its | |
| * Engine is ready to be initialized) | |
| */ | |
| class FlutterLoader { | |
| /** | |
| * Initializes the Flutter web app. | |
| * @param {*} options | |
| * @returns {Promise?} a (Deprecated) Promise that will eventually resolve | |
| * with an EngineInitializer, or will be rejected with | |
| * any error caused by the loader. Or Null, if the user | |
| * supplies an `onEntrypointLoaded` Function as an option. | |
| */ | |
| async loadEntrypoint(options) { | |
| const { serviceWorker, ...entrypoint } = options || {}; | |
| // A Trusted Types policy that is going to be used by the loader. | |
| const flutterTT = new FlutterTrustedTypesPolicy(); | |
| // The FlutterServiceWorkerLoader instance could be injected as a dependency | |
| // (and dynamically imported from a module if not present). | |
| const serviceWorkerLoader = new FlutterServiceWorkerLoader(); | |
| serviceWorkerLoader.setTrustedTypesPolicy(flutterTT.policy); | |
| await serviceWorkerLoader.loadServiceWorker(serviceWorker).catch(e => { | |
| // Regardless of what happens with the injection of the SW, the show must go on | |
| console.warn("Exception while loading service worker:", e); | |
| }); | |
| // The FlutterEntrypointLoader instance could be injected as a dependency | |
| // (and dynamically imported from a module if not present). | |
| const entrypointLoader = new FlutterEntrypointLoader(); | |
| entrypointLoader.setTrustedTypesPolicy(flutterTT.policy); | |
| // Install the `didCreateEngineInitializer` listener where Flutter web expects it to be. | |
| this.didCreateEngineInitializer = | |
| entrypointLoader.didCreateEngineInitializer.bind(entrypointLoader); | |
| return entrypointLoader.loadEntrypoint(entrypoint); | |
| } | |
| } | |
| _flutter.loader = new FlutterLoader(); | |
| })(); | |