squadron_builder icon indicating copy to clipboard operation
squadron_builder copied to clipboard

[Squadron Builder 7] - issues with IdentityMarshaler

Open AlexDochioiu opened this issue 8 months ago • 10 comments

Starting with version 7, I have issues using Identity Marshaler for VM.

import "marshaler.vm.dart" //
    if (dart.library.js) "marshaler.web.dart"
    if (dart.library.js_interop) "marshaler.web.dart"
    if (dart.library.html) "marshaler.web.dart";

For VM I have:

<redacted>
const dataSignatureMarshaler = IdentityMarshaler<DataSignature>();
const utxoListMarshaler = IdentityMarshaler<List<Utxo>>();
<redacted>

for WEB I Have:

<redacted>
const dataSignatureMarshaler = _DataSignatureMarshaler();

class _DataSignatureMarshaler implements SquadronMarshaler<DataSignature, Uint8List> {
  const _DataSignatureMarshaler();

  @override
  Uint8List marshal(DataSignature data, [MarshalingContext? context]) => data.marshal();

  @override
  DataSignature unmarshal(Uint8List data, [MarshalingContext? context]) => DataSignature.unmarshal(data);
}
<redacted>

and the output Deser in worker is:

final class _$Deser extends MarshalingContext {
  _$Deser({super.contextAware});
  late final $0 = value<String>();
  late final $1 = list<String>($0);
  late final $2 = value<int>();
  late final $3 = value<Uint8List>();
  late final $4 = value<T>();
  late final $5 = (($) => hdWalletMarshaler.unmarshal($4($), this));
  late final $6 = (($) => networkIdMarshaler.unmarshal($4($), this));
  <redacted>
}

Problem is that all the values get wrapped with $4 which is causing problems (cannot compile to web because of it). I believe the issue is caused by IdentityMarshaler.

Trying to change the imports to always use web instead of conditional imports leads to correct worker being generated (without that value wrapper).

AlexDochioiu avatar May 07 '25 00:05 AlexDochioiu

Trying to change the imports to always use web instead of conditional imports leads to correct worker being generated (without that value wrapper).

Seems that if I do that it actually screws up and I cannot compile/run the app for VM anymore, since the types do not match anymore.

AlexDochioiu avatar May 07 '25 00:05 AlexDochioiu

Hello @AlexDochioiu ,

I'll be looking at this issue, but I'm not sure I understand everything. You mention dataSignatureMarshaler and utxoListMarshaler but the generated code uses hdWalletMarshaler and networkIdMarshaler. Not sure how all this is related?

However just one thought: $4 aka value<T> hints that you have implemented a generic service method? eg.

@SquadronService()
class myService {
   @squadronMethod
   Future<bool> doSomething<T>(T input) {
      // do something
   }
}

However generic method will likely not work on Web because the T instance (whatever it is) will be serialized, converted to JS and leave the Dart type system only to resurface on the worker side as plain JavaScript data after going through the structured clone algorithm (https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). So on the worker side, the service method will be called with T = dynamic. It might work on VM because the data remains in the Dart environment (however, I'm not even so sure about that). Web requires more caution with regard to types.

You could probably work around the problem like so:

@SquadronService()
class myService {
   Future<bool> _doSomething<T>(T input) {
      // do something
   }

   @squadronMethod
   Future<bool> doSomethingWithT1(T1 input) {
      // do something
   }

   @squadronMethod
   Future<bool> doSomethingWithT2(T2 input) {
      // do something
   }
}

where T1 and T2 are classes that implement T.

I'll continue investigating and will keep you posted.

d-markey avatar May 08 '25 12:05 d-markey

Hello @d-markey . Sorry for not being clear enough:

  1. Those classes contain many marshalers, that's why I just put <redacted> in there to imply that there's a lot more.
  2. I don't use generics. The value<T> is generated because of the IdentityMarshaler which is practically a SquadronMarshaler<T, T>.

AlexDochioiu avatar May 08 '25 19:05 AlexDochioiu

Also, the problem seems to (also) be that the generated code looks at and uses the types from "marshaler.vm.dart", which are not compatible with the types from "marshaler.web.dart".

All in all, I believe that using IdentityMarshaler doesn't work anymore at all in squadron v7?

import "marshaler.vm.dart" //
    if (dart.library.js) "marshaler.web.dart"
    if (dart.library.js_interop) "marshaler.web.dart"
    if (dart.library.html) "marshaler.web.dart";

AlexDochioiu avatar May 08 '25 19:05 AlexDochioiu

Hello,

could you post the full .worker.g.dart file that was generated by Squadron Builder? Also I'd like to have your service's interface, ie. just the class with service method signatures (no bodies -- I don't need the implementation).

I want to understand where that T comes from. FYI there is an example for VM-specific marshalers based on IdentityMarshaler in https://github.com/d-markey/squadron_sample/blob/main/lib/src/perf/marshalers/marshalers.dart and the generated file https://github.com/d-markey/squadron_sample/blob/main/lib/src/perf/_gen/perf.worker.g.dart doesn't use any unresolved type in the de/ser classes.

d-markey avatar May 11 '25 14:05 d-markey

Hello, any news? Did you manage to work around the issue?

d-markey avatar May 29 '25 09:05 d-markey

Sorry @d-markey. I did not have the time to re-visit this yet. I am still on v6 currently. Looking at the sample code, one difference that stands out to me which may or may not impact anything, is that for unmarshal, I see data is always defined as dynamic on your example, while for me it is typed.

Seems to me like this could be why late final $4 = value<T>(); is generated and used always for unmarshal.

AlexDochioiu avatar May 29 '25 09:05 AlexDochioiu

Hello @AlexDochioiu, that's the point, if squadron_builder used a T, it means your service method must mention or somehow refer to that T one way or another. Could you post the generated code that calls the $4 deserializer (and also the signature of the corresponding original service method)? You only provided the code of your marshaler + the generated deserializer, but I'd like to see what's going on in the service per se.

d-markey avatar Jun 06 '25 07:06 d-markey

@d-markey Finally got back to this a bit. It seems that the root issue comes from IdentityMarshaler.

Failing code

<redacted>
const dataSignatureMarshaler = IdentityMarshaler<DataSignature>();
const utxoListMarshaler = IdentityMarshaler<List<Utxo>>();
<redacted>

Working code

const dataSignatureMarshaler = GenericIdentityMarshaler<DataSignature>();
const utxoListMarshaler = GenericIdentityMarshaler<List<Utxo>>();

WHERE GenericIdentityMarshaler is defined as:

class GenericIdentityMarshaler<T> implements GenericMarshaler<T> {
  const GenericIdentityMarshaler();

  @override
  T marshal(T data, [MarshalingContext? context]) => data;

  @override
  T unmarshal(dynamic data, [MarshalingContext? context]) => data as T;
}

P.s. to be clear, the IdentityMarshaler is used on the VM side. The web marshalers have proper implementation.

AlexDochioiu avatar Jun 30 '25 17:06 AlexDochioiu

This is probably because IdentityMarshaler is a SquadronMarshaler<T, T> instead of SquadronMarshaler<T, dynamic>, which (I guess) causes the cast attempt.

AlexDochioiu avatar Jun 30 '25 17:06 AlexDochioiu