diff --git a/pkg/_js_interop_checks/lib/src/transformations/js_util_wasm_optimizer.dart b/pkg/_js_interop_checks/lib/src/transformations/js_util_wasm_optimizer.dart index 07fa54fee2d..5d77967f7b7 100644 --- a/pkg/_js_interop_checks/lib/src/transformations/js_util_wasm_optimizer.dart +++ b/pkg/_js_interop_checks/lib/src/transformations/js_util_wasm_optimizer.dart @@ -58,6 +58,7 @@ class JsUtilWasmOptimizer extends Transformer { final Field _pragmaOptions; final Member _globalThisMember; int _functionTrampolineN = 1; + late Library _library; final CoreTypes _coreTypes; final StatefulStaticTypeContext _staticTypeContext; @@ -93,6 +94,7 @@ class JsUtilWasmOptimizer extends Transformer { @override Library visitLibrary(Library lib) { + _library = lib; _staticTypeContext.enterLibrary(lib); lib.transformChildren(this); _staticTypeContext.leaveLibrary(lib); @@ -227,7 +229,6 @@ class JsUtilWasmOptimizer extends Transformer { /// matches. String _createFunctionTrampoline(Procedure node, FunctionType function) { int fileOffset = node.fileOffset; - Library library = node.enclosingLibrary; // Create arguments for each positional parameter in the function. These // arguments will be converted in JS to Dart objects. The generated wrapper @@ -256,11 +257,12 @@ class JsUtilWasmOptimizer extends Transformer { // a native JS value before being returned to JS. DartType nullableWasmAnyRefType = _wasmAnyRefClass.getThisType(_coreTypes, Nullability.nullable); + final String libraryName = _library.name ?? 'Unnamed'; final functionTrampolineName = - '|_functionTrampoline${_functionTrampolineN++}'; + '|_functionTrampoline${_functionTrampolineN++}For$libraryName'; final functionTrampolineImportName = '\$$functionTrampolineName'; final functionTrampoline = Procedure( - Name(functionTrampolineName, library), + Name(functionTrampolineName, _library), ProcedureKind.Method, FunctionNode( ReturnStatement(StaticInvocation( @@ -285,7 +287,7 @@ class JsUtilWasmOptimizer extends Transformer { _pragmaOptions.fieldReference: StringConstant(functionTrampolineImportName) }))); - library.addProcedure(functionTrampoline); + _library.addProcedure(functionTrampoline); return functionTrampolineImportName; } diff --git a/pkg/dart2wasm/bin/run_wasm.js b/pkg/dart2wasm/bin/run_wasm.js index 0d60fb195f6..beb39840086 100644 --- a/pkg/dart2wasm/bin/run_wasm.js +++ b/pkg/dart2wasm/bin/run_wasm.js @@ -357,6 +357,9 @@ var dart2wasm = { areEqualInJS: function(l, r) { return l === r; }, + promiseThen: function(promise, successFunc, failureFunc) { + promise.then(successFunc, failureFunc); + }, }; function instantiate(filename, imports) { diff --git a/sdk/lib/_internal/wasm/lib/js_helper.dart b/sdk/lib/_internal/wasm/lib/js_helper.dart index 39cebf20b50..963573ecb94 100644 --- a/sdk/lib/_internal/wasm/lib/js_helper.dart +++ b/sdk/lib/_internal/wasm/lib/js_helper.dart @@ -307,6 +307,10 @@ external String stringify(WasmAnyRef? object); @pragma("wasm:import", "dart2wasm.objectKeys") external WasmAnyRef objectKeysRaw(WasmAnyRef? o); +@pragma("wasm:import", "dart2wasm.promiseThen") +external void promiseThen( + WasmAnyRef promise, WasmAnyRef successFunc, WasmAnyRef failureFunc); + // Currently, `allowInterop` returns a Function type. This is unfortunate for // Dart2wasm because it means arbitrary Dart functions can flow to JS util // calls. Our only solutions is to cache every function called with diff --git a/sdk/lib/_internal/wasm/lib/js_util_patch.dart b/sdk/lib/_internal/wasm/lib/js_util_patch.dart index eb59f4a24d5..1ecc57ccc49 100644 --- a/sdk/lib/_internal/wasm/lib/js_util_patch.dart +++ b/sdk/lib/_internal/wasm/lib/js_util_patch.dart @@ -4,8 +4,10 @@ library dart.js_util; +import "dart:_js_annotations" as js; import "dart:_internal"; import "dart:_js_helper"; +import "dart:async" show Completer, FutureOr; import "dart:collection"; import "dart:typed_data"; import "dart:wasm"; @@ -141,8 +143,32 @@ bool lessThan(Object? first, Object? second) => throw 'unimplemented'; @patch bool lessThanOrEqual(Object? first, Object? second) => throw 'unimplemented'; +typedef _PromiseSuccessFunc = void Function(Object? value); +typedef _PromiseFailureFunc = void Function(Object? error); + @patch -Future promiseToFuture(Object jsPromise) => throw 'unimplemented'; +Future promiseToFuture(Object jsPromise) { + Completer completer = Completer(); + + final success = js.allowInterop<_PromiseSuccessFunc>((r) { + return completer.complete(r as FutureOr?); + }); + final error = js.allowInterop<_PromiseFailureFunc>((e) { + // Note that `completeError` expects a non-nullable error regardless of + // whether null-safety is enabled, so a `NullRejectionException` is always + // provided if the error is `null` or `undefined`. + // TODO(joshualitt): At this point `undefined` has been replaced with `null` + // so we cannot tell them apart. In the future we should reify `undefined` + // in Dart. + if (e == null) { + return completer.completeError(NullRejectionException._(false)); + } + return completer.completeError(e); + }); + + promiseThen(jsifyRaw(jsPromise)!, jsifyRaw(success)!, jsifyRaw(error)!); + return completer.future; +} @patch Object? objectGetPrototypeOf(Object? object) => throw 'unimplemented'; diff --git a/tests/web/wasm/js_util_test.dart b/tests/web/wasm/js_util_test.dart index c30748d350c..56b47b9e1c8 100644 --- a/tests/web/wasm/js_util_test.dart +++ b/tests/web/wasm/js_util_test.dart @@ -5,6 +5,7 @@ import 'dart:js_util'; import 'dart:typed_data'; +import 'package:async_helper/async_helper.dart'; import 'package:expect/expect.dart'; import 'package:js/js.dart'; @@ -252,10 +253,65 @@ void deepConversionsTest() { callMethod(gt, 'invoke', [dartify(getProperty(gt, 'f'))])); } -void main() { +Future promiseToFutureTest() async { + Object gt = globalThis; + eval(r''' + globalThis.rejectedPromise = new Promise((resolve, reject) => reject('rejected')); + globalThis.resolvedPromise = new Promise(resolve => resolve('resolved')); + globalThis.getResolvedPromise = function() { + return resolvedPromise; + } + //globalThis.nullRejectedPromise = Promise.reject(null); + '''); + + // Test resolved + { + Future f = promiseToFuture(getProperty(gt, 'resolvedPromise')); + Expect.equals('resolved', await f); + } + + // Test rejected + { + String result = await asyncExpectThrows( + promiseToFuture(getProperty(gt, 'rejectedPromise'))); + Expect.equals('rejected', result); + } + + // Test return resolved + { + Future f = promiseToFuture(callMethod(gt, 'getResolvedPromise', [])); + Expect.equals('resolved', await f); + } + + // Test promise chaining + { + bool didThen = false; + Future f = promiseToFuture(callMethod(gt, 'getResolvedPromise', [])); + f.then((resolved) { + Expect.equals(resolved, 'resolved'); + didThen = true; + }); + await f; + Expect.isTrue(didThen); + } + + // Test rejecting promise with null should trigger an exception. + // TODO(joshualitt): Fails with an illegal cast. + // { + // Future f = promiseToFuture(getProperty(gt, 'nullRejectedPromise')); + // f.then((_) { Expect.fail("Expect promise to reject"); }).catchError((e) { + // print('A'); + // Expect.isTrue(e is NullRejectionException); + // }); + // await f; + // } +} + +void main() async { createObjectTest(); equalTest(); evalAndConstructTest(); dartObjectRoundTripTest(); deepConversionsTest(); + await promiseToFutureTest(); }