diff --git a/sdk/lib/_internal/js_runtime/lib/js_helper.dart b/sdk/lib/_internal/js_runtime/lib/js_helper.dart index 24417463745..3011fd81114 100644 --- a/sdk/lib/_internal/js_runtime/lib/js_helper.dart +++ b/sdk/lib/_internal/js_runtime/lib/js_helper.dart @@ -2782,8 +2782,8 @@ String getIsolateAffinityTag(String name) { return JS('String', '#(#)', isolateTagGetter, name); } -final Map?> _loadingLibraries = ?>{}; -final Set _loadedLibraries = new Set(); +final Map?> _loadingLibraries = {}; +final Set _loadedLibraries = {}; typedef void DeferredLoadCallback(); @@ -2881,8 +2881,9 @@ Future loadDeferredLibrary(String loadId, int priority) { part: uri, hash: hash, event: 'alreadyInitialized', loadId: loadId); continue; } - // On strange scenarios, e.g. if js encounters parse errors, we might get - // an "success" callback on the script load but the hunk will be null. + // This check is just an extra precaution, `isHunkLoaded(hash)` should + // always be true at this point. `_loadHunk` does this check and throws + // for any part that failed to load. if (JS('bool', '#(#)', isHunkLoaded, hash)) { _addEvent(part: uri, hash: hash, event: 'initialize', loadId: loadId); JS('void', '#(#)', initializer, hash); @@ -2897,11 +2898,12 @@ Future loadDeferredLibrary(String loadId, int priority) { } Future loadAndInitialize(int i) { - if (JS('bool', '#(#)', isHunkLoaded, hashes[i])) { + final hash = hashes[i]; + if (JS('bool', '#(#)', isHunkLoaded, hash)) { waitingForLoad[i] = false; return new Future.value(); } - return _loadHunk(uris[i], loadId, priority).then((Null _) { + return _loadHunk(uris[i], loadId, priority, hash, 0).then((Null _) { waitingForLoad[i] = false; initializeSomeLoadedHunks(); }); @@ -2993,16 +2995,16 @@ Object _computePolicy() { /// loading is changed to use a more structured layout with subdirectories, this /// method will need to be updated to make the URL still clearly safe by /// construction. -Object _getBasedScriptUrl(String component) { +Object _getBasedScriptUrl(String component, String suffix) { final base = _thisScriptBaseUrl; final encodedComponent = _encodeURIComponent(component); - final url = '$base$encodedComponent'; + final url = '$base$encodedComponent$suffix'; final policy = _deferredLoadingTrustedTypesPolicy; return JS('', '#.createScriptURL(#)', policy, url); } Object getBasedScriptUrlForTesting(String component) => - _getBasedScriptUrl(component); + _getBasedScriptUrl(component, ''); String _encodeURIComponent(String component) { return JS('', 'self.encodeURIComponent(#)', component); @@ -3057,17 +3059,21 @@ String _computeThisScriptFromTrace() { throw new UnsupportedError('Cannot extract URI from "$stack"'); } -Future _loadHunk(String hunkName, String loadId, int priority) { +Future _loadHunk( + String hunkName, String loadId, int priority, String hash, int retryCount) { + const int maxRetries = 3; var initializationEventLog = JS_EMBEDDED_GLOBAL('', INITIALIZATION_EVENT_LOG); - var future = _loadingLibraries[hunkName]; + var completer = _loadingLibraries[hunkName]; _addEvent(part: hunkName, event: 'startLoad', loadId: loadId); - if (future != null) { + if (completer != null && retryCount == 0) { _addEvent(part: hunkName, event: 'reuse', loadId: loadId); - return future.then((Null _) => null); + return completer.future; } + completer ??= _loadingLibraries[hunkName] = Completer(); - Object trustedScriptUri = _getBasedScriptUrl(hunkName); + Object trustedScriptUri = _getBasedScriptUrl( + hunkName, retryCount > 0 ? '?dart2jsRetry=$retryCount' : ''); // [trustedScriptUri] is either a String, in which case `toString()` is an // identity function, or it is a TrustedScriptURL and `toString()` returns the // sanitized URL. @@ -3076,22 +3082,33 @@ Future _loadHunk(String hunkName, String loadId, int priority) { _addEvent(part: hunkName, event: 'download', loadId: loadId); var deferredLibraryLoader = JS('', 'self.dartDeferredLibraryLoader'); - Completer completer = Completer(); - - void success() { - _addEvent(part: hunkName, event: 'downloadSuccess', loadId: loadId); - completer.complete(null); - } void failure(error, String context, StackTrace? stackTrace) { - _addEvent(part: hunkName, event: 'downloadFailure', loadId: loadId); - _loadingLibraries[hunkName] = null; - stackTrace ??= StackTrace.current; - completer.completeError( - DeferredLoadException('Loading $uriAsString failed: $error\n' - 'Context: $context\n' - 'event log:\n${_getEventLog()}\n'), - stackTrace); + if (retryCount < maxRetries) { + _addEvent(part: hunkName, event: 'retry$retryCount', loadId: loadId); + _loadHunk(hunkName, loadId, priority, hash, retryCount + 1); + } else { + _addEvent(part: hunkName, event: 'downloadFailure', loadId: loadId); + _loadingLibraries[hunkName] = null; + stackTrace ??= StackTrace.current; + completer!.completeError( + DeferredLoadException('Loading $uriAsString failed: $error\n' + 'Context: $context\n' + 'event log:\n${_getEventLog()}\n'), + stackTrace); + } + } + + void success() { + var isHunkLoaded = JS_EMBEDDED_GLOBAL('', IS_HUNK_LOADED); + + if (JS('bool', '#(#)', isHunkLoaded, hash)) { + _addEvent(part: hunkName, event: 'downloadSuccess', loadId: loadId); + completer!.complete(null); + } else { + failure( + 'Success callback invoked but part $hunkName not loaded.', '', null); + } } var jsSuccess = convertDartClosureToJS(success, 0); @@ -3160,7 +3177,6 @@ Future _loadHunk(String hunkName, String loadId, int priority) { JS('', '#.addEventListener("error", #, false)', script, jsFailure); JS('', 'document.body.appendChild(#)', script); } - _loadingLibraries[hunkName] = completer.future; return completer.future; } diff --git a/tests/web/deferred_fail_and_retry_test.dart b/tests/web/deferred_fail_and_retry_test.dart index 5dc34c54137..5c62639502e 100644 --- a/tests/web/deferred_fail_and_retry_test.dart +++ b/tests/web/deferred_fail_and_retry_test.dart @@ -14,20 +14,31 @@ main() { // invocation. js.context.callMethod("eval", [ """ + retryCount = 0; if (self.document) { oldAppendChild = document.body.appendChild; - document.body.appendChild = function(element) { + replacement = function(element) { element.src = "non_existing.js"; document.body.appendChild = oldAppendChild; document.body.appendChild(element); + if (retryCount < 3) { + retryCount++; + document.body.appendChild = replacement; + } } + document.body.appendChild = replacement; } if (self.load) { oldLoad = load; - load = function(uri) { + replacement = function(uri) { load = oldLoad; load("non_existing.js"); + if (retryCount < 3) { + retryCount++; + load = replacement; + } } + load = replacement; } """ ]); diff --git a/tests/web_2/deferred_fail_and_retry_test.dart b/tests/web_2/deferred_fail_and_retry_test.dart index 23a6caa5d7a..c57c709054f 100644 --- a/tests/web_2/deferred_fail_and_retry_test.dart +++ b/tests/web_2/deferred_fail_and_retry_test.dart @@ -16,20 +16,31 @@ main() { // invocation. js.context.callMethod("eval", [ """ + retryCount = 0; if (self.document) { oldAppendChild = document.body.appendChild; - document.body.appendChild = function(element) { + replacement = function(element) { element.src = "non_existing.js"; document.body.appendChild = oldAppendChild; document.body.appendChild(element); + if (retryCount < 3) { + retryCount++; + document.body.appendChild = replacement; + } } + document.body.appendChild = replacement; } if (self.load) { oldLoad = load; - load = function(uri) { + replacement = function(uri) { load = oldLoad; load("non_existing.js"); + if (retryCount < 3) { + retryCount++; + load = replacement; + } } + load = replacement; } """ ]);