Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
redDwarf03 opened this issue Jun 21, 2024 · 13 comments
Open

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

redDwarf03 opened this issue Jun 21, 2024 · 13 comments
Labels
area-api-missing-event type-enhancement A request for a change that isn't a bug

Comments

@redDwarf03
Copy link

redDwarf03 commented Jun 21, 2024

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 kevmoo added the type-enhancement A request for a change that isn't a bug label Jun 21, 2024
@kevmoo
Copy link
Member

kevmoo commented Jun 21, 2024

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
Copy link
Member

kevmoo commented Jun 21, 2024

We can add helpers here!

@redDwarf03
Copy link
Author

redDwarf03 commented Jun 21, 2024

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
Copy link
Member

kevmoo commented Jun 21, 2024

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

@redDwarf03
Copy link
Author

redDwarf03 commented Jun 23, 2024

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
Copy link
Author

redDwarf03 commented Jun 24, 2024

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
Copy link
Member

kevmoo commented Jun 24, 2024

@srujzs ?

@redDwarf03
Copy link
Author

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
Copy link
Member

kevmoo commented Jun 24, 2024

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

@redDwarf03
Copy link
Author

Thx for the advice

@srujzs
Copy link
Contributor

srujzs commented Jun 25, 2024

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.

@redDwarf03
Copy link
Author

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
Copy link
Contributor

srujzs commented Jun 26, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-api-missing-event type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests

3 participants