dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.25k stars 1.58k forks source link

package:js refuses to interloop Future function #49048

Open TheOneWithTheBraid opened 2 years ago

TheOneWithTheBraid commented 2 years ago

It seems like the package:js has problems interlooping simple Future callbacks, such as:

import 'dart:async';

import 'package:js/js.dart';

@JS('myDartFunction')
external set _myDartFunction(Future<List<String>> Function() f);

@JS()
external Future<List<String>> myDartFunction();

void main() {
  _myDartFunction = allowInterop(myActualDartFunction);
}

Future<List<String>> myActualDartFunction() async {
  Future.delayed(Duration(seconds: 1));
  return ['test', 'test'];
}

When calling this function by JS (myDartFunction().then((value) => {console.log(value);})), the following error is thrown:

Uncaught (in promise) TypeError: f is not a function
    runUnary zone.dart:1685
    handleValue future_impl.dart:147
    handleValueCallback future_impl.dart:766
    _propagateToListeners future_impl.dart:795
    _completeWithValue future_impl.dart:566
    load__dart_sdk/async._Future$</</< future_impl.dart:639
    _microtaskLoop schedule_microtask.dart:40
    _startMicrotaskLoop schedule_microtask.dart:49
    _scheduleImmediateWithPromise async_patch.dart:166
    promise callback*_scheduleImmediateWithPromise async_patch.dart:164
    _scheduleImmediate async_patch.dart:136
    _scheduleAsyncCallback schedule_microtask.dart:69
    _rootScheduleMicrotask zone.dart:1493
    scheduleMicrotask zone.dart:1705
    _asyncCompleteWithValue future_impl.dart:638
    _asyncComplete future_impl.dart:598
    runBody async_patch.dart:108
    _async async_patch.dart:123
    myActualDartFunction background_executor.dart:15
    _checkAndCall operations.dart:334
    dcall operations.dart:339
    ret dart_sdk.sound.js:57378
    <anonymous> debugger eval code:1
zone.dart:1685:53
srujzs commented 2 years ago

IIRC, this isn't allowed as Futures aren't Promises. The Future returned by your Dart function in JS won't necessarily have the methods you're expecting, and there isn't a futureToPromise function like there is for promiseToFuture. @sigmundch to confirm, but it seems like it might be useful to add errors here in allowInterop to inform users.

sigmundch commented 2 years ago

Yes, I believe you are correct @srujzs

yjbanov commented 2 years ago

I had to do some weird gymnastics to create a JS object with a method that's expected to return a promise: https://github.com/flutter/engine/pull/36253/files#diff-c12aeab766e5e21494fb2a61877c898072f52aa21a8f8663ad9e74144f17357a

futureToPromise would be handy. Or perhaps allowInterop could support async functions (since async is part of the language both for Dart and JS).

navaronbracke commented 1 year ago

@yjbanov I had a similar problem just now. I needed to be able to create a Promise in Dart so that I could pass it through allowInterop() back to a library for which I'm writing some interop for.

I did manage to write a wrapper using the existing @staticInterop annotation. Perhaps we could include a static interop definition for Promise in the SDK? (a la futureToPromise)

Code sample:

// main.dart This file just wires up a function to the window so that could easily test it.
import 'package:js/js.dart';
import 'package:js/js_util.dart';

import 'dart:html' as html;

@JS()
@staticInterop
class JSWindow {}

extension JSWindowExtension on JSWindow {
  external void Function(Promise input) get testFunction;
}

// This one is all I needed, don't forget to call allowInterop() on the executor! 
@JS()
@staticInterop
class Promise {
  external factory Promise(
    void Function(Promise? Function(Object?) resolve, Promise? Function(Object?) reject) executor,
  );
}

void main() async {
  var jsWindow = html.window as JSWindow;

  await Future.delayed(const Duration(seconds: 2));

  jsWindow.testFunction(Promise(allowInterop(
    (resolve, reject) async {
      await Future.delayed(const Duration(seconds: 2));

      resolve(
        Promise(
          allowInterop((resolve2, reject2) {
            print('RESOLVING!');
            resolve2(42);
          }),
        ),
      );

      //resolve(42);
      //reject(500);
      //reject(jsify(<String, dynamic>{
      //  'foo': 42,
      //}));
    },
  )));
}
// api.js
window.testFunction = async function(input) {
    if(input instanceof Promise) {
        input.then((value) => {
                console.log(`resolved with: ${value}`);
            },
            (reason) => {
                console.log(`rejected with: ${JSON.stringify(reason)}`);
            },
        );
    }
}

cc @srujzs Another pain point :)

srujzs commented 1 year ago

I think the Flutter engine has a similar futureToPromise function to the code above. We do have a JSPromise now in dart:js_interop that users can use as part of JS types, but it's very minimal. We only have a toDart function on it that calls promiseToFuture, but I can totally see a toJS that calls JSPromise's external constructor.