[js_types] Add JSPromise and JSVoid.
CoreLibraryReviewExempt: Changes to Web specific libraries.
Change-Id: I71cc720dcf1cea3ca8a219259ccd35912ed00d9d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/282940
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Commit-Queue: Joshua Litt <joshualitt@google.com>
diff --git a/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart b/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart
index a451837..87f8dd7 100644
--- a/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart
+++ b/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart
@@ -5,6 +5,7 @@
import 'dart:_internal' show patch;
import 'dart:_js_types';
import 'dart:js';
+import 'dart:js_util';
import 'dart:typed_data';
/// [JSExportedDartFunction] <-> [Function]
@@ -29,6 +30,12 @@
JSExportedDartObject get toJS => this;
}
+/// [JSPromise] -> [Future<JSAny?>].
+extension JSPromiseToFuture on JSPromise {
+ @patch
+ Future<JSAny?> get toDart => promiseToFuture<JSAny?>(this);
+}
+
/// [JSArrayBuffer] <-> [ByteBuffer]
extension JSArrayBufferToByteBuffer on JSArrayBuffer {
@patch
diff --git a/sdk/lib/_internal/js_shared/lib/js_types.dart b/sdk/lib/_internal/js_shared/lib/js_types.dart
index 48e9d30..e1f66e3 100644
--- a/sdk/lib/_internal/js_shared/lib/js_types.dart
+++ b/sdk/lib/_internal/js_shared/lib/js_types.dart
@@ -8,6 +8,7 @@
/// library.
library _js_types;
+import 'dart:_js_annotations';
import 'dart:_interceptors' as interceptors;
import 'dart:_internal' show patch;
import 'dart:typed_data';
@@ -25,7 +26,7 @@
typedef JSObject = interceptors.JSObject;
typedef JSFunction = Function;
typedef JSExportedDartFunction = Function;
-typedef JSArray = List;
+typedef JSArray = List<JSAny?>;
typedef JSExportedDartObject = Object;
typedef JSArrayBuffer = ByteBuffer;
typedef JSDataView = ByteData;
@@ -42,3 +43,8 @@
typedef JSNumber = double;
typedef JSBoolean = bool;
typedef JSString = String;
+typedef JSVoid = void;
+
+@JS()
+@staticInterop
+class JSPromise {}
diff --git a/sdk/lib/_internal/js_shared/lib/js_util_patch.dart b/sdk/lib/_internal/js_shared/lib/js_util_patch.dart
index fe45f9b..eb72b93 100644
--- a/sdk/lib/_internal/js_shared/lib/js_util_patch.dart
+++ b/sdk/lib/_internal/js_shared/lib/js_util_patch.dart
@@ -465,7 +465,7 @@
// provided if the error is `null` or `undefined`.
if (e == null) {
return completer.completeError(
- NullRejectionException._(JS('bool', '# === undefined', e)));
+ NullRejectionException(JS('bool', '# === undefined', e)));
}
return completer.completeError(e);
}, 1);
diff --git a/sdk/lib/_internal/wasm/lib/js_interop_patch.dart b/sdk/lib/_internal/wasm/lib/js_interop_patch.dart
index a11fe57..56731fc 100644
--- a/sdk/lib/_internal/wasm/lib/js_interop_patch.dart
+++ b/sdk/lib/_internal/wasm/lib/js_interop_patch.dart
@@ -4,6 +4,8 @@
import 'dart:_internal' show patch;
import 'dart:_js_helper';
+import 'dart:async' show Completer;
+import 'dart:js_util' show NullRejectionException;
import 'dart:typed_data';
import 'dart:wasm';
@@ -37,6 +39,30 @@
_box<JSExportedDartObject>(jsObjectFromDartObject(this));
}
+/// [JSPromise] -> [Future<JSAny?>].
+extension JSPromiseToFuture on JSPromise {
+ @patch
+ Future<JSAny?> get toDart {
+ final completer = Completer<JSAny>();
+ final success = (JSAny r) {
+ return completer.complete(r);
+ }.toJS;
+ final error = (JSAny e) {
+ // TODO(joshualitt): Investigate reifying `JSNull` and `JSUndefined` on
+ // all backends and if it is feasible, or feasible for some limited use
+ // cases, then we should pass [e] directly to `completeError`.
+ // TODO(joshualitt): Use helpers to avoid conflating `null` and `JSNull` /
+ // `JSUndefined`.
+ if (e == null) {
+ return completer.completeError(NullRejectionException(false));
+ }
+ return completer.completeError(e);
+ }.toJS;
+ promiseThen(_ref(this), _ref(success), _ref(error));
+ return completer.future;
+ }
+}
+
/// [JSArrayBuffer] <-> [ByteBuffer]
extension JSArrayBufferToByteBuffer on JSArrayBuffer {
@patch
diff --git a/sdk/lib/_internal/wasm/lib/js_types.dart b/sdk/lib/_internal/wasm/lib/js_types.dart
index 65ae68d..a06cbb1 100644
--- a/sdk/lib/_internal/wasm/lib/js_types.dart
+++ b/sdk/lib/_internal/wasm/lib/js_types.dart
@@ -61,6 +61,10 @@
@staticInterop
class JSExportedDartFunction implements JSFunction {}
+@JS()
+@staticInterop
+class JSPromise implements JSObject {}
+
@JS('Array')
@staticInterop
class JSArray implements JSObject {
@@ -131,3 +135,8 @@
@JS()
@staticInterop
class JSString implements JSAny {}
+
+/// [JSVoid] is just a typedef for [void]. While we could just use
+/// `JSUndefined`, in the future we may be able to use this to elide `return`s
+/// in JS trampolines.
+typedef JSVoid = void;
diff --git a/sdk/lib/_internal/wasm/lib/js_util_patch.dart b/sdk/lib/_internal/wasm/lib/js_util_patch.dart
index a14a25e..df4010a 100644
--- a/sdk/lib/_internal/wasm/lib/js_util_patch.dart
+++ b/sdk/lib/_internal/wasm/lib/js_util_patch.dart
@@ -161,7 +161,7 @@
// 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(NullRejectionException(false));
}
return completer.completeError(e);
});
diff --git a/sdk/lib/js_interop/js_interop.dart b/sdk/lib/js_interop/js_interop.dart
index d3c7ea9..a85b15f 100644
--- a/sdk/lib/js_interop/js_interop.dart
+++ b/sdk/lib/js_interop/js_interop.dart
@@ -49,6 +49,9 @@
/// TODO(joshualitt): Detail exactly what are the requirements.
typedef JSExportedDartFunction = js_types.JSExportedDartFunction;
+/// The type of JS promises and promise-like objects, [JSPromise] <: [JSObject].
+typedef JSPromise = js_types.JSPromise;
+
/// The type of all JS arrays, [JSArray] <: [JSObject].
typedef JSArray = js_types.JSArray;
@@ -92,6 +95,10 @@
/// TODO(joshualitt): Figure out how we want to handle JSUndefined and JSNull.
+/// The type of `JSUndefined` when returned from functions. Unlike pure JS,
+/// no actual object will be returned.
+typedef JSVoid = js_types.JSVoid;
+
/// Extension members to support conversions between Dart types and JS types.
/// Not all Dart types can be converted to JS types and vice versa.
/// TODO(joshualitt): We might want to investigate using inline classes instead
@@ -115,6 +122,11 @@
external JSExportedDartObject get toJS;
}
+/// [JSPromise] -> [Future<JSAny?>].
+extension JSPromiseToFuture on JSPromise {
+ external Future<JSAny?> get toDart;
+}
+
/// TODO(joshualitt): On Wasm backends List / Array conversion methods will
/// copy, and on JS backends they will not. We should find a path towards
/// consistent semantics.
diff --git a/sdk/lib/js_util/js_util.dart b/sdk/lib/js_util/js_util.dart
index 513daaf..1196871 100644
--- a/sdk/lib/js_util/js_util.dart
+++ b/sdk/lib/js_util/js_util.dart
@@ -131,7 +131,7 @@
// Indicates whether the value is `undefined` or `null`.
final bool isUndefined;
- NullRejectionException._(this.isUndefined);
+ NullRejectionException(this.isUndefined);
@override
String toString() {
diff --git a/tests/lib/js/static_interop_test/js_types_test.dart b/tests/lib/js/static_interop_test/js_types_test.dart
index 373c5b3..cb77cc9 100644
--- a/tests/lib/js/static_interop_test/js_types_test.dart
+++ b/tests/lib/js/static_interop_test/js_types_test.dart
@@ -5,6 +5,7 @@
// Check that JS types work.
import 'dart:js_interop';
+import 'dart:js_util';
import 'dart:typed_data';
import 'package:expect/minitest.dart';
@@ -91,7 +92,7 @@
String get foo => 'bar';
}
-void main() {
+void syncTests() {
eval('''
globalThis.obj = {
'foo': 'bar',
@@ -218,3 +219,90 @@
String dartStr = str.toDart;
expect(dartStr, 'foo');
}
+
+@JS()
+external JSPromise get resolvedPromise;
+
+@JS()
+external JSPromise get rejectedPromise;
+
+@JS()
+external JSPromise getResolvedPromise();
+
+@JS()
+external JSPromise getRejectablePromise();
+
+@JS()
+external JSVoid rejectPromiseWithNull();
+
+@JS()
+external JSVoid rejectPromiseWithUndefined();
+
+Future<void> asyncTests() async {
+ eval(r'''
+ globalThis.resolvedPromise = new Promise(resolve => resolve('resolved'));
+ globalThis.getResolvedPromise = function() {
+ return resolvedPromise;
+ }
+ globalThis.getRejectablePromise = function() {
+ return new Promise(function(_, reject) {
+ globalThis.rejectPromise = reject;
+ });
+ }
+ globalThis.rejectPromiseWithNull = function() {
+ globalThis.rejectPromise(null);
+ }
+ globalThis.rejectPromiseWithUndefined = function() {
+ globalThis.rejectPromise(undefined);
+ }
+ ''');
+
+ // [JSPromise] -> [Future].
+ // Test resolved
+ {
+ Future<JSAny?> f = resolvedPromise.toDart;
+ expect(((await f) as JSString).toDart, 'resolved');
+ }
+
+ // Test rejected
+ // TODO(joshualitt): Write a test for rejected promises that works on all
+ // backends.
+
+ // Test return resolved
+ {
+ Future<JSAny?> f = getResolvedPromise().toDart;
+ expect(((await f) as JSString).toDart, 'resolved');
+ }
+
+ // Test promise chaining
+ {
+ bool didThen = false;
+ Future<JSAny?> f = getResolvedPromise().toDart;
+ f.then((resolved) {
+ expect((resolved as JSString).toDart, 'resolved');
+ didThen = true;
+ });
+ await f;
+ expect(didThen, true);
+ }
+
+ // Test rejecting promise with null should trigger an exception.
+ // TODO(joshualitt): `catchError` doesn't seem to clear the JS exception on
+ // Dart2Wasm.
+ //{
+ // bool threw = false;
+ // Future<JSAny?> f = getRejectablePromise().toDart;
+ // f.then((_) {}).catchError((e) {
+ // threw = true;
+ // expect(e is NullRejectionException, true);
+ // });
+ // rejectPromiseWithNull();
+ // await f;
+ // expect(threw, true);
+ //}
+}
+
+void main() async {
+ syncTests();
+ await asyncTests();
+}