d-markey / squadron

Multithreading and worker thread pool for Dart / Flutter, to offload CPU-bound and heavy I/O tasks to Isolate or Web Worker threads.
https://pub.dev/packages/squadron
MIT License
79 stars 0 forks source link

Uncaught Error: Expected a value of type 'FutureOr<Map<String, dynamic>>?', but got one of type 'LinkedMap<dynamic, dynamic> #2

Closed Fraa-124 closed 2 years ago

Fraa-124 commented 2 years ago

Hi @d-markey, I think I have found a bug, it just happens on web. I want to return a Map<String, dynamic> but It seems I can't. Given this code,

class ComputeService implements WorkerService {
  Future<Map<String, dynamic>> whateverFnc() async {
    final Map<String, dynamic> res = {
      'num': 24,
    };
    return res;
  }

  static const whateverFncCommand = 1;

  @override
  Map<int, CommandHandler> get operations {
    return {
      whateverFncCommand: (WorkerRequest r) {
        return whateverFnc();
      }
    };
  }
}

class ComputeWorkerPool extends WorkerPool<ComputeWorker> implements ComputeService {
  ComputeWorkerPool(ConcurrencySettings concurrencySettings)
      : super(
          createWorker,
          concurrencySettings: concurrencySettings,
        );

  @override
  Future<Map<String, dynamic>> whateverFnc() => execute((w) => w.whateverFnc());
}

class ComputeWorker extends Worker implements ComputeService {
  ComputeWorker(dynamic entryPoint, {String? id, List args = const []}) : super(entryPoint, id: id, args: args);

  @override
  Future<Map<String, dynamic>> whateverFnc() {
    return send(ComputeService.whateverFncCommand, []);
  }
}

Uncaught Error: Expected a value of type 'FutureOr<Map<String, dynamic>>?', but got one of type 'LinkedMap<dynamic, dynamic>' at Object.throw_ [as throw] (errors.dart:251:49) at Object.castError (errors.dart:84:3) at Object.cast [as as] (operations.dart:452:10) at dart.NullableType.new.as (types.dart:367:9) at channel.dart:62:18 at Object._checkAndCall (operations.dart:334:14) at Object.dcall (operations.dart:339:39) at MessagePort.<anonymous> (html_dart2js.dart:37230:58)

If I return String or number it works great.

Thanks for your work,

d-markey commented 2 years ago

Hello @Fraa-124,

unfortunately this will be "by design", data structures cannot cross the border between a worker and the caller while retaining all details of its original type. Specifically in this case, the type of keys and values do not survive serialization when the data is sent from the Isolate / Web Worker back to the caller.

You might want to try Map<dynamic, dynamic> (or simply Map) which should work as long as keys and values are simple types that can be sent across worker channels. Otherwise, if you want to send structured data (eg. with a class you've implemented), you will need to implement de/serialization methods and call them before sending/receiving the data.

There is an example in the CacheService where high-level CacheStat objects are transfered from the cache worker to the caller. The command handler calls the serialize() method:

late final Map<int, CommandHandler> operations = {
    getOperation: (r) => get(r.args[0]),
    containsOperation: (r) => containsKey(r.args[0]),
    setOperation: (r) => set(r.args[0], r.args[1],
        timeToLive:
            (r.args[2] == null) ? null : Duration(microseconds: r.args[2])),
    statsOperation: (r) => getStats().serialize()
  };

and the CacheClient calls deserialize():

Future<CacheStat> getStats() async => CacheStat.deserialize(
      await _remote.sendRequest(CacheService.statsOperation, []));
Fraa-124 commented 2 years ago

You are right, it works now. Thank you very much!

d-markey commented 2 years ago

Alternatively for the sake of type safety, you can implement a function to ensure type compatibility.

Keep your services as Map<String, dynamic> or Map<String, int>. You know this will be serialized by channels as Map<dynamic, dynamic>, it only comes down to casting entry types eg. something along the lines of:

Map<K, V> toTypedMap<K, V>(Map data) => data.map((k, v) => MapEntry<K, V>(k as K, v as V));