dart-lang / web

Lightweight browser API bindings built around JS static interop.
https://pub.dev/packages/web
BSD 3-Clause "New" or "Revised" License
107 stars 18 forks source link

The getter 'onMessage' isn't defined for the type 'MessagePort'. #261

Open redDwarf03 opened 3 weeks ago

redDwarf03 commented 3 weeks ago

Hello

I have a class


import 'dart:async';
import 'dart:html';

class MessagePortStreamChannel
    with StreamChannelMixin<String>
    implements StreamChannel<String> {
  MessagePortStreamChannel({required this.port}) {
    _onReceiveMessageSubscription = port.onMessage.listen((message) {
      _in.add(message.data);
    });

    _onPostMessageSubscription = _out.stream.listen((event) {
      port.postMessage(event);
    });
  }

  final MessagePort port;
  final _in = StreamController<String>(sync: true);
  final _out = StreamController<String>(sync: true);

  late final StreamSubscription<MessageEvent> _onReceiveMessageSubscription;
  late final StreamSubscription<String> _onPostMessageSubscription;

  Future<void> dispose() async {
    await _onReceiveMessageSubscription.cancel();
    await _onPostMessageSubscription.cancel();
    await _in.close();
    await _out.close();
  }

  @override
  StreamSink<String> get sink => _out.sink;

  @override
  Stream<String> get stream => _in.stream;
}

When i use import 'package:web/web.dart'; , onMessage method isn't defined for the type 'MessagePort'. but in the class we have external EventHandler get onmessage;

i don't understand something to migrate my class to web package.

thx

kevmoo commented 3 weeks ago

Try this for now

import 'dart:async';
import 'dart:js_interop';

import 'package:web/web.dart';

class MessagePortStreamChannel {
  MessagePortStreamChannel({required this.port}) {
    _onReceiveMessageSubscription = port.onMessage.listen((message) {
      _in.add(message.data as String);
    });

    _onPostMessageSubscription = _out.stream.listen(port.postMessage);
  }

  final MessagePort port;
  final _in = StreamController<String>(sync: true);
  final _out = StreamController<JSAny?>(sync: true);

  late final StreamSubscription<MessageEvent> _onReceiveMessageSubscription;
  late final StreamSubscription<JSAny?> _onPostMessageSubscription;

  Future<void> dispose() async {
    await _onReceiveMessageSubscription.cancel();
    await _onPostMessageSubscription.cancel();
    await _in.close();
    await _out.close();
  }
}

extension on MessagePort {
  Stream<MessageEvent> get onMessage =>
      EventStreamProviders.messageEvent.forTarget(this);
}
kevmoo commented 3 weeks ago

We can add helpers here!

redDwarf03 commented 3 weeks ago

With your example, i don't understand now how to call the class


class MessageChannelArchethicDappClient extends AWCJsonRPCClient
    implements ArchethicDAppClient {
  MessageChannelArchethicDappClient({
    required super.origin,
  }) : super(
          channelBuilder: () async {
            if (awcAvailable != true) throw Failure.connectivity;

            return MessagePortStreamChannel(
              port: await asyncAWC,
            );
          },
          disposeChannel: (StreamChannel<String> channel) async {
            await (channel as MessagePortStreamChannel).dispose();
          },
        );

  static bool get isAvailable => kIsWeb && awcAvailable == true;
}

The return type 'MessagePortStreamChannel' isn't a 'Future<StreamChannel<String>>', as required by the closure's context.dart[return_of_invalid_type_from_closure](https://dart.dev/diagnostics/return_of_invalid_type_from_closure)

For info, awc is


@JS()
library awc;

import 'dart:async';
import 'dart:developer';

import 'dart:js_interop';
import 'package:web/web.dart';

external MessagePort? get awc;
external bool? get awcAvailable;

@JS('onAWCReady')
external set onAWCReady(void Function(MessagePort awc) f);

Future<MessagePort> get asyncAWC async {
  if (awc != null) return awc!;
  log('Wait for awc');
  final awcReadyCompleter = Completer<MessagePort>();

  if (awc != null) awcReadyCompleter.complete(awc!);

  onAWCReady = (awc) {
    log('AWC ready !');
  };

  return awcReadyCompleter.future;
}
kevmoo commented 3 weeks ago

You might have to translate the StreamChannel from JSAny or similar. I'm not sure...

redDwarf03 commented 3 weeks ago

Finally, it works with


import 'dart:async';
import 'dart:js_interop';

import 'package:archethic_wallet_client/archethic_wallet_client.dart';
import 'package:archethic_wallet_client/src/transport/common/awc_json_rpc_client.dart';
import 'package:archethic_wallet_client/src/transport/message_channel/message_channel.js.dart';
import 'package:flutter/foundation.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:web/web.dart';

class MessageChannelArchethicDappClient extends AWCJsonRPCClient
    implements ArchethicDAppClient {
  MessageChannelArchethicDappClient({
    required super.origin,
  }) : super(
          channelBuilder: () async {
            if (awcAvailable != true) throw Failure.connectivity;

            return MessagePortStreamChannel(
              port: await asyncAWC,
            );
          },
          disposeChannel: (StreamChannel<String> channel) async {
            await (channel as MessagePortStreamChannel).dispose();
          },
        );

  static bool get isAvailable => kIsWeb && awcAvailable == true;
}

class MessagePortStreamChannel
    with StreamChannelMixin<String>
    implements StreamChannel<String> {
  MessagePortStreamChannel({required this.port}) {
    _onReceiveMessageSubscription = port.onMessage.listen((message) {
      _in.add(message.data! as String);
    });

    _onPostMessageSubscription = _out.stream.listen((event) {
      port.postMessage(event as JSAny?);
    });
  }

  final MessagePort port;
  final _in = StreamController<String>(sync: true);
  final _out = StreamController<String>(sync: true);

  late final StreamSubscription<MessageEvent> _onReceiveMessageSubscription;
  late final StreamSubscription<String> _onPostMessageSubscription;

  Future<void> dispose() async {
    await _onReceiveMessageSubscription.cancel();
    await _onPostMessageSubscription.cancel();
    await _in.close();
    await _out.close();
  }

  @override
  StreamSink<String> get sink => _out.sink;

  @override
  Stream<String> get stream => _in.stream;
}

extension on MessagePort {
  Stream<MessageEvent> get onMessage =>
      EventStreamProviders.messageEvent.forTarget(this);
}
redDwarf03 commented 3 weeks ago

hello @kevmoo

I couldn't generate Chrome extension with my code in flutter 3.22 I read https://dart.dev/interop/js-interop/mock but that's not help me :/ Any idea ?

flutter build web --web-renderer html --csp
@JS()
library awc;

import 'dart:async';
import 'dart:developer';
import 'dart:js_interop';
import 'package:web/web.dart';

@JS()
external MessagePort? get awc;

@JS()
external bool? get awcAvailable;

@JS('onAWCReady')
external set onAWCReady(void Function(MessagePort awc) f);

Future<MessagePort> get asyncAWC async {
  if (awc != null) {
    return awc!;
  }

  log('Wait for awc');
  final awcReadyCompleter = Completer<MessagePort>();

  onAWCReady = (port) {
    awcReadyCompleter.complete(port);
    log('AWC ready !');
  };

  // Handle potential timeout or error (optional)
  await Future.delayed(const Duration(seconds: 5), () {
    if (!awcReadyCompleter.isCompleted) {
      awcReadyCompleter.completeError(Exception('Timeout waiting for awc'));
    }
  });

  return awcReadyCompleter.future;
}
Target dart2js failed: ProcessException: Process exited abnormally with exit code 1:
../archethic-wallet-client-dart/lib/src/transport/message_channel/message_channel.js.dart:16:14:
Error: External JS interop member contains an invalid type: 'void Function(MessagePort)'.
external set onAWCReady(void Function(MessagePort awc) f);

Other errors with other part of my code: initial code with js.dart (this code worked before flutter 3.22)

// ignore_for_file: avoid_setters_without_getters

@JS()
library awc;

import 'dart:async';

import 'package:js/js.dart';

@JS('archethic')
external ArchethicJS? get archethic;

@JS()
class ArchethicJS {
  @JS('streamChannel')
  external AWCStreamChannelJS? get streamChannel;
}

@JS()
class AWCStreamChannelJS {
  @JS('state')
  external AWCStreamChannelState get state;

  /// This returns a promise.
  /// You must use `promiseTofuture` to call this from Dart code.
  @JS('connect')
  external Object connect();

  /// This returns a promise.
  /// You must use `promiseTofuture` to call this from Dart code.
  @JS('close')
  external Object close();

  /// This returns a promise.
  /// You must use `promiseTofuture` to call this from Dart code.
  @JS('send')
  external Object send(String data);

  @JS('onReceive')
  external set onReceive(Future<void> Function(String data) callback);

  @JS('onReady')
  external set onReady(Future<void> Function() callback);

  @JS('onClose')
  external set onClose(Future<void> Function(String reason) callback);
}

enum AWCStreamChannelState {
  connecting,
  open,
  closing,
  closed,
}

to my new code with js_interop

// ignore_for_file: avoid_setters_without_getters

@JS()
library awc;

import 'dart:js_interop';

extension type ArchethicJS._(JSObject _) implements JSObject {
  @JS('streamChannel')
  external AWCStreamChannelJS? get streamChannel;
}

@JS('archethic')
external ArchethicJS? get archethic;

extension type AWCStreamChannelJS._(JSObject _) implements JSObject {
  @JS('state')
  external AWCStreamChannelState get state;

  /// This returns a promise.
  /// You must use `promiseTofuture` to call this from Dart code.
  @JS('connect')
  external JSObject connect();

  /// This returns a promise.
  /// You must use `promiseTofuture` to call this from Dart code.
  @JS('close')
  external JSObject close();

  /// This returns a promise.
  /// You must use `promiseTofuture` to call this from Dart code.
  @JS('send')
  external JSObject send(JSString data);

  @JS('onReceive')
  external set onReceive(void Function(JSString data) callback);

  @JS('onReady')
  external set onReady(void Function() callback);

  @JS('onClose')
  external set onClose(void Function(JSString reason) callback);
}

enum AWCStreamChannelState {
  connecting,
  open,
  closing,
  closed,
}

i have these error too.

    ^
../archethic-wallet-client-dart/lib/src/transport/webbrowser_extension/webbrowser_extension.js.dart:18:38:
Error: External JS interop member contains an invalid type: 'AWCStreamChannelState'.
 - 'AWCStreamChannelState' is from 'package:archethic_wallet_client/src/transport/webbrowser_extension/webbrowser_extension.js.dart'
 ('../archethic-wallet-client-dart/lib/src/transport/webbrowser_extension/webbrowser_extension.js.dart').
  external AWCStreamChannelState get state;
                                     ^
../archethic-wallet-client-dart/lib/src/transport/webbrowser_extension/webbrowser_extension.js.dart:36:16:
Error: External JS interop member contains an invalid type: 'void Function(JSString)'.
  external set onReceive(void Function(JSString data) callback);
               ^
../archethic-wallet-client-dart/lib/src/transport/webbrowser_extension/webbrowser_extension.js.dart:39:16:
Error: External JS interop member contains an invalid type: 'void Function()'.
  external set onReady(void Function() callback);
               ^
../archethic-wallet-client-dart/lib/src/transport/webbrowser_extension/webbrowser_extension.js.dart:42:16:
Error: External JS interop member contains an invalid type: 'void Function(JSString)'.
  external set onClose(void Function(JSString reason) callback);
kevmoo commented 3 weeks ago

@srujzs ?

redDwarf03 commented 3 weeks ago

to be confirmed but to help community with js_interop, i share a solution i think

// ignore_for_file: avoid_setters_without_getters

@JS()
library awc;

import 'dart:js_interop';

extension type ArchethicJS._(JSObject _) implements JSObject {
  @JS('streamChannel')
  external AWCStreamChannelJS? get streamChannel;
}

@JS('archethic')
external ArchethicJS? get archethic;

extension type AWCStreamChannelJS._(JSObject _) implements JSObject {
  @JS('state')
  external AWCStreamChannelState get state;

  /// This returns a promise.
  /// You must use `promiseTofuture` to call this from Dart code.
  @JS('connect')
  external JSObject connect();

  /// This returns a promise.
  /// You must use `promiseTofuture` to call this from Dart code.
  @JS('close')
  external JSObject close();

  /// This returns a promise.
  /// You must use `promiseTofuture` to call this from Dart code.
  @JS('send')
  external JSObject send(JSString data);

  @JS('onReceive')
  external set onReceive(JSFunction callback);

  @JS('onReady')
  external set onReady(JSFunction callback);

  @JS('onClose')
  external set onClose(JSFunction callback);
}

@JS()
extension type AWCStreamChannelState._(JSObject _) implements JSObject {
  external static AWCStreamChannelState get connecting;
  external static AWCStreamChannelState get open;
  external static AWCStreamChannelState get closing;
  external static AWCStreamChannelState get closed;
}
kevmoo commented 3 weeks ago

FYI: you don't need to use @JS('whatever') if the name is the same! You don't need the annotation at all!

redDwarf03 commented 3 weeks ago

Thx for the advice

srujzs commented 3 weeks ago

A few drive-by comments:

redDwarf03 commented 2 weeks ago

A few drive-by comments:

  • The general errors around "invalid types" (which should be a little clearer with a newer version of the SDK) are related to https://dart.dev/interop/js-interop/js-types#requirements-on-external-declarations-and-function-tojs. Specifically, you can't pass arbitrary Dart functions to interop APIs, they need to be converted to a JSFunction.
  • You can be more specific that JSObject when dealing with Promises by using JSPromise. This allows you to convert them to a Future that you can then await using .toJS.

Thank you for advices. I change types:

// ignore_for_file: avoid_setters_without_getters

@JS()
library awc;

import 'dart:js_interop';

extension type ArchethicJS._(JSObject _) implements JSObject {
  @JS('streamChannel')
  external AWCStreamChannelJS? get streamChannel;
}

@JS('archethic')
external ArchethicJS? get archethic;

extension type AWCStreamChannelJS._(JSObject _) implements JSObject {
  external AWCStreamChannelState get state;

  /// This returns a promise.
  /// You must use `promiseTofuture` to call this from Dart code.
  external JSPromise connect();

  /// This returns a promise.
  /// You must use `promiseTofuture` to call this from Dart code.
  external JSPromise close();

  /// This returns a promise.
  /// You must use `promiseTofuture` to call this from Dart code.
  external JSPromise send(JSString data);
  external set onReceive(JSFunction callback);
  external set onReady(JSFunction callback);
  external set onClose(JSFunction callback);
}

@JS()
extension type AWCStreamChannelState._(JSObject _) implements JSObject {
  external static AWCStreamChannelState get connecting;
  external static AWCStreamChannelState get open;
  external static AWCStreamChannelState get closing;
  external static AWCStreamChannelState get closed;
}

but i met execution error on streamChannel.send(event as JSString); :

import 'dart:async';
import 'dart:developer';
import 'dart:js_interop';

import 'package:archethic_wallet_client/archethic_wallet_client.dart';
import 'package:archethic_wallet_client/src/transport/common/awc_json_rpc_client.dart';
import 'package:archethic_wallet_client/src/transport/webbrowser_extension/webbrowser_extension.js.dart';
import 'package:stream_channel/stream_channel.dart';

class WebBrowserExtensionDappClient extends AWCJsonRPCClient
    implements ArchethicDAppClient {
  WebBrowserExtensionDappClient({
    required super.origin,
  }) : super(
          channelBuilder: () async {
            if (archethic?.streamChannel == null) {
              throw Failure.connectivity;
            }
            final streamChannel = WebBrowserExtensionStreamChannel(
              streamChannel: archethic!.streamChannel!,
            );

            await streamChannel.connect();
            return streamChannel;
          },
          disposeChannel: (channel) async {
            await (channel as WebBrowserExtensionStreamChannel).dispose();
          },
        );

  static bool get isAvailable => archethic?.streamChannel != null;
}

class WebBrowserExtensionStreamChannel
    with StreamChannelMixin<String>
    implements StreamChannel<String> {
  WebBrowserExtensionStreamChannel({required this.streamChannel}) {
    streamChannel.onReceive = (message) async {
      log('[WBE] command received $message');
      _in.add(message.toString());
      log('[WBE] command received Done');
    }.toJS;

    _onPostMessageSubscription = _out.stream.listen((event) {
      log('[WBE] send command $event');
      streamChannel.send(event as JSString);
      log('[WBE] send command Done');
    });

    streamChannel.onClose = (reason) async {
      await dispose();
    }.toJS;
  }

  Future<void> connect() async => streamChannel.connect();

  final AWCStreamChannelJS streamChannel;
  final _in = StreamController<String>(sync: true);
  final _out = StreamController<String>(sync: true);

  late final StreamSubscription<String> _onPostMessageSubscription;

  Future<void> dispose() async {
    await _onPostMessageSubscription.cancel();
    await _in.close();
    await _out.close();
  }

  @override
  StreamSink<String> get sink => _out.sink;

  @override
  Stream<String> get stream => _in.stream;
}
srujzs commented 2 weeks ago

I'm guessing you're running with dart2wasm. Avoid casting String to JSString. Prefer using .toJS instead to convert the String instead.

If you're using 3.5.0-1XX.X.beta, you can enable the lint invalid_runtime_check_with_js_interop_types to catch invalid casts like this.