diff --git a/dev/bots/service_worker_test.dart b/dev/bots/service_worker_test.dart index 2f4d3b0a78e8a..ce148d28233ac 100644 --- a/dev/bots/service_worker_test.dart +++ b/dev/bots/service_worker_test.dart @@ -29,6 +29,7 @@ enum ServiceWorkerTestType { withoutFlutterJs, withFlutterJs, withFlutterJsShort, + withFlutterJsEntrypointLoadedEvent, } // Run a web service worker test as a standalone Dart program. @@ -36,9 +37,11 @@ Future main() async { await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs); await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJs); await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort); + await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent); await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs); await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJs); await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort); + await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent); await runWebServiceWorkerTestWithBlockedServiceWorkers(headless: false); } @@ -67,6 +70,9 @@ String _testTypeToIndexFile(ServiceWorkerTestType type) { case ServiceWorkerTestType.withFlutterJsShort: indexFile = 'index_with_flutterjs_short.html'; break; + case ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent: + indexFile = 'index_with_flutterjs_entrypoint_loaded.html'; + break; } return indexFile; } diff --git a/dev/bots/test.dart b/dev/bots/test.dart index cbb1bf6ed43c3..b8913fc38e806 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -1092,9 +1092,11 @@ Future _runWebLongRunningTests() async { () => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs), () => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJs), () => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort), + () => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent), () => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs), () => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJs), () => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort), + () => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent), () => runWebServiceWorkerTestWithBlockedServiceWorkers(headless: true), () => _runWebStackTraceTest('profile', 'lib/stack_trace.dart'), () => _runWebStackTraceTest('release', 'lib/stack_trace.dart'), diff --git a/dev/integration_tests/web/web/index_with_flutterjs_entrypoint_loaded.html b/dev/integration_tests/web/web/index_with_flutterjs_entrypoint_loaded.html new file mode 100644 index 0000000000000..a364e597103f3 --- /dev/null +++ b/dev/integration_tests/web/web/index_with_flutterjs_entrypoint_loaded.html @@ -0,0 +1,42 @@ + + + + + + + + Codestin Search App + + + + + + + + + + + + + diff --git a/dev/md b/dev/md new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/flutter_tools/lib/src/web/file_generators/flutter_js.dart b/packages/flutter_tools/lib/src/web/file_generators/flutter_js.dart index 257cb42d3918a..6c8020a97837f 100644 --- a/packages/flutter_tools/lib/src/web/file_generators/flutter_js.dart +++ b/packages/flutter_tools/lib/src/web/file_generators/flutter_js.dart @@ -8,169 +8,301 @@ /// flutter.js should be completely static, so **do not use any parameter or /// environment variable to generate this file**. String generateFlutterJsFile() { - return ''' + return r''' // 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. -/** - * This script installs service_worker.js to provide PWA functionality to - * application. For more information, see: - * https://developers.google.com/web/fundamentals/primers/service-workers - */ - if (!_flutter) { var _flutter = {}; } _flutter.loader = null; -(function() { +(function () { "use strict"; - class FlutterLoader { - /** - * Creates a FlutterLoader, and initializes its instance methods. - */ - constructor() { - // TODO: Move the below methods to "#private" once supported by all the browsers - // we support. In the meantime, we use the "revealing module" pattern. - - // Watchdog to prevent injecting the main entrypoint multiple times. - this._scriptLoaded = null; - - // Resolver for the pending promise returned by loadEntrypoint. - this._didCreateEngineInitializerResolve = null; - - // Called by Flutter web. - // Bound to `this` now, so "this" is preserved across JS <-> Flutter jumps. - this.didCreateEngineInitializer = this._didCreateEngineInitializer.bind(this); + /** + * 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 + */ + class FlutterServiceWorkerLoader { /** - * Initializes the main.dart.js with/without serviceWorker. - * @param {*} options - * @returns a Promise that will eventually resolve with an EngineInitializer, - * or will be rejected with the error caused by the loader. + * 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. */ - loadEntrypoint(options) { + loadServiceWorker(settings) { + if (!("serviceWorker" in navigator) || settings == null) { + // In the future, settings = null -> uninstall service worker? + return Promise.reject( + new Error("Service worker not supported (or configured).") + ); + } const { - entrypointUrl = "main.dart.js", - serviceWorker, - } = (options || {}); - return this._loadWithServiceWorker(entrypointUrl, serviceWorker); + serviceWorkerVersion, + serviceWorkerUrl = "flutter_service_worker.js?v=" + + serviceWorkerVersion, + timeoutMillis = 4000, + } = settings; + + const serviceWorkerActivation = navigator.serviceWorker + .register(serviceWorkerUrl) + .then(this._getNewServiceWorker) + .then(this._waitForServiceWorkerActivation); + + // Timeout race promise + return timeout( + serviceWorkerActivation, + timeoutMillis, + "prepareServiceWorker" + ); } /** - * Resolves the promise created by loadEntrypoint. - * Called by Flutter through the public `didCreateEngineInitializer` method, - * which is bound to the correct instance of the FlutterLoader on the page. - * @param {*} engineInitializer + * 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} */ - _didCreateEngineInitializer(engineInitializer) { - if (typeof this._didCreateEngineInitializerResolve != "function") { - console.warn("Do not call didCreateEngineInitializer by hand. Start with loadEntrypoint instead."); - } - this._didCreateEngineInitializerResolve(engineInitializer); - // Remove the public method after it's done, so Flutter Web can hot restart. - delete this.didCreateEngineInitializer; - } + async _getNewServiceWorker(serviceWorkerRegistrationPromise) { + const reg = await serviceWorkerRegistrationPromise; - _loadEntrypoint(entrypointUrl) { - if (!this._scriptLoaded) { - console.debug("Injecting