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

Getting double as returned type in WASM build when saved as int #46

Closed
rydmike opened this issue Nov 9, 2024 · 34 comments · Fixed by #64
Closed

Getting double as returned type in WASM build when saved as int #46

rydmike opened this issue Nov 9, 2024 · 34 comments · Fixed by #64

Comments

@rydmike
Copy link

rydmike commented Nov 9, 2024

Odd issue in Web WASM build with int type

First off, thanks for this lovely fork of Hive v2 with WASM support, looks great and pleased to see this simple classic version of Hive continued.

I was experimenting with making a WASM build of the companion app Themes Playground for the FlexColorScheme package.

There was a lot of things to deal with t get the WASM build to work, the last part was Hive and that is when I landed on this landed on this nice fork and port to support and use package:web instead of html.

However, when using hive_ce I noticed this difference:

In Classic WEB JS (canvaskit) and VM builds, when values are stored as int in Hive, I always get them back as int, so all is fine:

Screenshot 2024-11-09 at 19 45 47

However, building the same app as Wasm, Hive consistently returns values stored as int as double.

Screenshot 2024-11-09 at 19 52 34

The stored int values certainly visually look like ints in the IndexedDB, same as they do when using the Web JS build.

Screenshot 2024-11-09 at 20 47 05

Have you seen this issue before?


When storing double types in Hive, with int like values, like e.g. 5.0, 4.0, 20.0, they seem to get stored as ints as well and come back as ints, but this is fine as ints can be assigned to the expected double type in Dart for such cases. The JS build has always done this, and it is fine. However, the other way around is not OK, and crashes. Well my error handling catches it and just returns the default int value for the key in question, but it effectively means the persistence does not work.


I was looking at the hive code for the web persistence, to me it in a quick look seems like the hive Dart implementation code is the same for Web WASM and classic WEB JS, so it seems to me the issue might be at a lower level.

Code sample

Sorry, no code sample yet. I need to write a simple repro of it, the sample code base is way too big to make sense here.

Might edit and add one later.

Version

  • Platform: Web WASM
  • Flutter version: 3.24.4
  • Hive version: hive_ce 2.7.0 1
@Rexios80
Copy link
Member

Rexios80 commented Nov 9, 2024

late final Box<dynamic> _hiveBox;

I believe this needs to be late final Box<int> _hiveBox; to function properly. All numbers are doubles by default in WASM builds. Without the explicit type, Hive can't know what type of number it's supposed to be reading.

@rydmike
Copy link
Author

rydmike commented Nov 10, 2024

Hi @Rexios80, thanks for your quick reply and again thanks again for this cool port and revival of hive v2.

The single Hive box needs to hold keys of all kind of types and cannot be for just one single type, it must be dynamic. This is also the typical usage of Hive, but typically this dynamic would just be written as:

late final Box _hiveBox;

But since my linting rules require specifying all types, I must write out the implicit dynamic.

This has all worked fine in JS and VM builds.

JS also treats all numbers as floating point by default, and it probably is in this case too in a JS build, but the difference is that in JS builds a number with type int, e.g. 5, gets saved as 5 and returned as 5 from the web IndexedDb. Dart then type converts this number 5 to an int type in type checks, that still holds in both JS and VM builds. And it still does so with this hive_se version on JS builds too, despite using updated package:web and not html as before.

But in the WASM builds, while it looks like an int value e.g. 5 is saved as 5 into the IndexedDB, it comes back as 5.0, which is typed converted into to a double and the type checks then fails.

I added an epic and horrific work-around hack as shown below, that is only used on the WASM build that converts the returned double to an int, when it is known to expect an int value for a used key:

  /// Loads a setting from the Theme service, using a key to access it from
  /// the Hive storage box.
  ///
  /// If type <T> is not an atomic Dart type, there must be a
  /// Hive type adapter that converts <T> into one.
  @override
  Future<T> load<T>(String key, T defaultValue) async {
    try {
      final dynamic gotValue = _hiveBox.get(key, defaultValue: defaultValue);
      if (_debug) {
        debugPrint('Hive LOAD _______________');
        debugPrint(' Type expected: $key as ${defaultValue.runtimeType}');
        debugPrint(' Type loaded  : $key as ${gotValue.runtimeType}');
        debugPrint(' Value loaded : $gotValue');
      }
      // Add workaround for hive WASM returning double instead of int, when
      // values saved were int.
      // See issue: https://github.com/IO-Design-Team/hive_ce/issues/46
      if (App.isRunningWithWasm &&
          gotValue != null &&
          (gotValue is double) &&
          (defaultValue is int || defaultValue is int?)) {
        final T loaded = gotValue.round() as T;
        if (_debug) {
          debugPrint('   WASM Error : Expected int got double, '
              'returning as int: $loaded');
        }
        return loaded;
      } else {
        final T loaded = gotValue as T;
        return loaded;
      }
    } catch (e) {
      debugPrint('Hive load (get) ERROR');
      debugPrint(' Error message ...... : $e');
      debugPrint(' Store key .......... : $key');
      debugPrint(' defaultValue ....... : $defaultValue');
      if (e is HiveError && e.message.contains('missing type adapter')) {
        // Skip the offending key
        debugPrint(' Missing type adapter : SKIP and return default');
      }
      // If something goes wrong we return the default value.
      return defaultValue;
    }
  }

This is of course a horrific hack, that only gets used for WASM builds, when an int or int? is expected, so it won't affect any other build mode, but it should of course not be needed.

I'm going to make some simpler use cases than this, and dig deeper. This is actually very bad and creates big difference in how past VM and JS builds have worked, basically rendering the conversion to a WASM build unusable, or at least very doubtful and unsafe imo, since same Dart code results in builds that runs differently and fails for WASM builds.

I don't think this is necessarily an issue in hive_ce, it might be rooted deeper, it might not. It could e.g. be some difference in how WASM builds interfaces with web IndexedDB.


I tried the build also on Flutter latest beta channel. Works the same with it.

At latest beta does fix the issues that WASM builds are reported as kReleaseMode true, even if you build with --debug flag using flutter run.

@Rexios80
Copy link
Member

All unit tests in hive_ce pass in both JS in WASM builds. I also have used hive_ce in WASM compiled apps that store ints and doubles with no issue. I am going to need a code sample that reproduces this to investigate further.

@rydmike
Copy link
Author

rydmike commented Nov 10, 2024

Agreed, I need a simpler repro case as well to dig deeper, so I will make one and add it when I have investigated this more.

@rydmike
Copy link
Author

rydmike commented Nov 19, 2024

Hi @Rexios80, just as a status follow-up.

I have finally had time to make a consistent simple repro sample of the issues I am experiencing with hive_ce and the WASM build. I also had video meeting yesterday with Kevin Moore from Google about it, who works on WASM among other things. He reached out and wanted to discuss my WASM issue findings, I had mentioned them on Twitter and Bluesky.

He thought it is possible that the issues are WASM compiler rooted, related to type optimizations, but looking at it with the repro sample, when I can now trigger it consistently, I would not rule out that it may also be in hive_ce, although even if the issue would be happening there, it is still possible that it is WASM type issues.

Since I'm not familiar enough with the hive_se code base to quickly figure out what is going on there exactly, I'm going to post my findings here, before I post them in the flutter repo where Kevin suggested I post them. My thinking was we can try to first rule out hive_ce, to make sure it is not the culprit, before I post the same issue in the Flutter repo and tag Kevins as he suggsted. If we find that it is in hive_ce itself, well that is also a good step towards fixing it.

I should be able to finish writing up my findings tomorrow and clean up the repro sample.

@rydmike
Copy link
Author

rydmike commented Nov 24, 2024

Hi @Rexios80,

Here is a reproduction sample of the type issues. It has also been reported in the Flutter repo here too flutter/flutter#186300, since after a video meeting with Kevin Moore, he was interested in seeing a report in the Flutter repo.

Pease take a look and see what you can figure out from the hive_ce viewpoint.

The report below is the same as the one I used in the Flutter repo.


Type conversion issues in WASM builds

When saving key-value pair to a Hive box in a Flutter WASM web build, the of types int, int? and double? are not type converted correctly when retrieving values from the Hive box, that uses WEB IndexedDB for the locally persisted data.

When building the same code using native Dart VM build as target or more relevant, when building with JavaScript as target, the type conversions all work as expected.

Issue sample code

The reproduction sample is provided in a separate repository here: https://github.com/rydmike/hive_wasm_case

The sample uses the hive_ce package that on web builds persists key-value pairs in the WEB browser IndexedDB. The hive_ce package is a community fork of the original hive package, that has been modified to work with Flutter Web WASM builds, by using package:web instead of html library.

The package works as expected when building for Web JS target with no --wasm option, but when building with --wasm option, the here presented type conversion issues are present.

Expected results

Expect retrieving int, int? and double? values to work, when values of known saved types are loaded and converted back their original types.

Build and run the sample code using WEB target in Chrome browser.
Open up developer tools, show the console.

Hit the (Increase values and save to DB) button to add some key-value pairs to the IndexedDB for local storage.

1-wasm-issue

We can verify that the values have been persisted as expected in comparing console log debug prints and values persisted in browser IndexedDB.

2-wasm issue

Next hit refresh page on the browser, an empty start page is shown, then hit the (Load values from DB) button, to load values from the IndexedDB.

Same values as before are loaded and printed to the console without any errors.

3-wasm-issue

This works the same way on all native VM platform builds and WEB JS builds and on ALL Flutter channels. We expect the same results in a WEB WASM build.

Actual results

Let's repeat this process again, but this time we build for web target with --wasm option.

The sample app header will change confirming that the build is for web target with WASM-GC being used, the app will also run with a red theme, indicating this will have some issues.

Again open up developer tools, show the console.

Hit the (Increase values and save to DB) button to add some key-value pairs to the IndexedDB for local storage.

4-wasm-issue

We can again verify that the values have been persisted as expected by checking the console.

5-wasm-issue

Issue 1) double? type conversion issue

Next clear the console and then hit (Load values from DB) button.

6-wasm-issue

We can see that the saved value that had type nullable double double?, threw an exception error when trying to convert it back to double.

In this sample code, an attempt was made to try handle the error before it is thrown, by checking for the conditions that seem to trigger it and work around it:

      // We should catch the 2nd issue here, but we do not see it in this
      // if branch, we should see the debugPrint, but we do not see it.
      // We get a caught error in the catch block instead.
      } else if (App.isRunningWithWasm && storedValue != null &&
                 isNullableDoubleT && defaultValue == null) {
        debugPrint(
          '   WASM Error : Expected double? but thinks T is int, '
          'returning as double: $storedValue',
        );
        final double loaded = storedValue as double;
        return loaded as T;
      } 

However, the attempts to do so did not work as expected, and the error was thrown and caught in the catch block instead.

Store LOAD ERROR ********
  Error message ...... : Type 'int' is not a subtype of type 'double?' in type cast
  Store key .......... : D: doubleNullable
  Store value ........ : 0.1
  defaultValue ....... : null

If we add a stacktrace to the error message, can get some additional information:

  Stacktrace ......... :     
     at module0._TypeError._throwAsCheckError (http://localhost:55415/main.dart.wasm:wasm-function[921]:0x244e22)
     at module0._asSubtype (http://localhost:55415/main.dart.wasm:wasm-function[975]:0x245c0d)
     at module0.StorageServiceHive.load inner (http://localhost:55415/main.dart.wasm:wasm-function[15615]:0x36e600)
     at module0.StorageServiceHive.load (http://localhost:55415/main.dart.wasm:wasm-function[15607]:0x36e1d0)
     at module0._MyHomePageState._loadCounters inner (http://localhost:55415/main.dart.wasm:wasm-function[15602]:0x36e03b)
     at module0._awaitHelper closure at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5 (http://localhost:55415/main.dart.wasm:wasm-function[19862]:0x3c6172)
     at module0.closure wrapper at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5 trampoline (http://localhost:55415/main.dart.wasm:wasm-function[19867]:0x3c6213)
     at module0._RootZone.runUnary (http://localhost:55415/main.dart.wasm:wasm-function[1561]:0x24e333)

Next comes an interesting twist.

Issue 2) int type conversion issue, when loading values from fresh start.

Next hit refresh page on the browser, empty start page is shown, and hit (Load values from DB) button again.

7-wasm-issue

This time we get more errors!

In the sample reproduction code there is a special condition to catch this issue without throwing an error:

      // Add workaround for hive WASM returning double instead of int, when
      // values saved to IndexedDb were int.
      // In this reproduction sample we see this FAIL triggered ONLY when
      // loading the values from the DB without having written anything to it
      // first. We can reproduce this issue by running the sample as WASM build
      // hitting Increase button a few times, then hot restart the app or 
      // reload the browser and hit Load Values. We then hit this issue.
      // Without this special if case handling, we would get an error thrown.
      // This path is never entered on native VM or JS builds.
      } else
      if (App.isRunningWithWasm &&
          storedValue != null &&
          (storedValue is double) &&
          (defaultValue is int || defaultValue is int?)) {
        final T loaded = storedValue.round() as T;
        debugPrint(
          '  ** WASM Error : Expected int but got double, '
          'returning as int: $loaded',
        );
        return loaded;
      }

We can see that in this case when the values are loaded from the IndexedDB, the int values are not type converted correctly, because they are for some reason returned as "1.0" instead of "1", as they were before re-loading the web app and starting it again.

We can again check that values are actually persisted in the IndexedDB and that the values are correct, they are "1" and not "1.0" as returned when loading the values from the IndexedDB when starting the app without having saved anything to it first.

8-wasm-issue

If we hit (Increase values and save to DB) button again and update the int values to 2, and hit (Load values from DB) again, we can see that the values are now loaded as "2" as they should be, and not "2.0".

9-wasm-issue

Again if we restart the app with a page refresh (or Flutter hot restart), the int values are again returned as "2.0" and not "2".

10-wasm-issue

It is worth noticing that issue 1) is present all the time, but the second issue 2) with int being returned as double is only present when loading values from the IndexedDB, without having written anything to it first after starting the app, it seem like if you write something to it after opening the DB, the read values will be returned with the correct int type.

Flutter version

Used Flutter version: Channel master
Channel master, 3.27.0-1.0.pre.621

It reproduces on beta and stable 3.24.5 too.

Flutter doctor
flutter doctor -v
[✓] Flutter (Channel master, 3.27.0-1.0.pre.621, on macOS 15.1.1 24B91 darwin-arm64, locale en-US)
    • Flutter version 3.27.0-1.0.pre.621 on channel master at /Users/rydmike/fvm/versions/master
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision da188452a6 (55 minutes ago), 2024-11-23 19:55:24  0100
    • Engine revision b382d17a27
    • Dart version 3.7.0 (build 3.7.0-183.0.dev)
    • DevTools version 2.41.0-dev.2

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at /Users/rydmike/Library/Android/sdk
    • Platform android-34, build-tools 34.0.0
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
      This is the JDK bundled with the latest Android Studio installation on this machine.
      To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment (build 17.0.9 0-17.0.9b1087.7-11185874)
    • All Android licenses accepted.

[!] Xcode - develop for iOS and macOS (Xcode 16.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 16B40

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2023.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
    • Dart plugin can be installed from:
    • Java version OpenJDK Runtime Environment (build 17.0.9 0-17.0.9b1087.7-11185874)

[✓] IntelliJ IDEA Community Edition (version 2024.2.4)
    • IntelliJ at /Applications/IntelliJ IDEA CE.app
    • Flutter plugin version 82.1.3
    • Dart plugin version 242.22855.32

[✓] VS Code (version 1.95.3)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.100.0

[✓] Connected device (4 available)
    • MrPinkPro (mobile)              • 74120d6ef6769c3a2e53d61051da0147d0279996 • ios            • iOS 17.7.2 21H221
    • macOS (desktop)                 • macos                                    • darwin-arm64   • macOS 15.1.1 24B91 darwin-arm64
    • Mac Designed for iPad (desktop) • mac-designed-for-ipad                    • darwin         • macOS 15.1.1 24B91 darwin-arm64
    • Chrome (web)                    • chrome                                   • web-javascript • Google Chrome 131.0.6778.86

[✓] Network resources
    • All expected network resources are available.

@Rexios80
Copy link
Member

I think this might be a type resolution issue. If I change the following line:

static const double? doubleDefaultNullable = null;

to

static const double? doubleDefaultNullable = 0.0;

the app works fine. Interestingly, the following does not work:

static const double? doubleDefaultNullable = 0;

which makes the case that this is a WASM compiler bug more plausible.

@rydmike
Copy link
Author

rydmike commented Nov 25, 2024

Interesting find on type issue 1. I tried the options too, but for me the:

static const double? doubleDefaultNullable = 0;

Worked fine! I tried with both stable and master. Weird. Wonder why we see different results on this?

In any case, the

static const double? doubleDefaultNullable = null;

Still throws in a bad way.


Any clues on why we get the issue-2?

And I don't mean the type conversion error when we get "1.0" from Hive, even if IndexedDB has value "1", it is obvious that this will throw an error when cast to int or `int?´, that is clear and not the issue. The issue is why are we sometimes getting "1" and sometimes "1.0"?

The odd part is that in a WEB JS build we NEVER get "1.0" from Hive, when we have an int value 1 stored.

But with WEB WASM build, we get "1.0" if we after saving value int value 1, reload the browser, or hot reload the app and load the data from hive.

If the IndexedDB had a saved int value of 1 we can see it is saved as "1", always also in a WASM build, but in the restart or browser reload scenario it comed back as "1.0", but if we reload the values after saving them, without a restart, we get the core "1" value.

Perhaps that is because hive returns as cached value in the latter case? And only in the reload browser app, restart app plus load values case, are we actually getting the values read and returned from the IndexedDB. As far as I can see that would then mean that .dartify() returns "1.0" when the DB contain value "1", but that it only does so in the WASM build but not in the JS build?

From hive_ce:storage_backend_js.dart

  /// Not part of public API
  @visibleForTesting
  Object? decodeValue(JSAny? value) {
    if (value.isA<JSArrayBuffer>()) {
      value as JSArrayBuffer;
      final bytes = Uint8List.view(value.toDart);
      if (_isEncoded(bytes)) {
        final reader = BinaryReaderImpl(bytes, _registry);
        reader.skip(2);
        if (_cipher == null) {
          return reader.read();
        } else {
          return reader.readEncrypted(_cipher);
        }
      } else {
        return bytes;
      }
    } else {
      return value.dartify();
    }
  }

This sounds bad as well, why would dartify be different in JS build and WASM build? Sound like a recipe for disaster.

I need to do some more digging, I have not debugged properly what is going on in hive_ce.

@rydmike
Copy link
Author

rydmike commented Nov 25, 2024

If I run these tests

import 'dart:js_interop';

import 'package:test/test.dart';

void main() {
  group('dartify tests with no casts', () {
    // Int value tests
    test('dartify JSAny? value 1 and test if int?', () {
      JSAny? jsValue = 1 as JSAny?;
      Object? dartValue = jsValue.dartify();
      int? typedValue = 1;
      print('dartValue: $dartValue');
      expect(dartValue, equals(typedValue));
    });
    test('dartify JSAny? value 1 and test if int', () {
      JSAny? jsValue = 1 as JSAny?;
      Object? dartValue = jsValue.dartify();
      int typedValue = 1;
      print('dartValue: $dartValue');
      expect(dartValue, equals(typedValue));
    });
    test('dartify JSAny? value null and test if int? null', () {
      JSAny? jsValue = null as JSAny?;
      Object? dartValue = jsValue.dartify();
      int? typedValue = null;
      print('dartValue: $dartValue');
      expect(dartValue, equals(typedValue));
    });

    // Double value tests
    test('dartify JSAny? value 1.0 and test if double?', () {
      JSAny? jsValue = 1.0 as JSAny?;
      Object? dartValue = jsValue.dartify();
      double? typedValue = 1;
      print('dartValue: $dartValue');
      expect(dartValue, equals(typedValue));
    });
    test('dartify JSAny? value 1.0 and test if double', () {
      JSAny? jsValue = 1.0 as JSAny?;
      Object? dartValue = jsValue.dartify();
      double typedValue = 1.0;
      print('dartValue: $dartValue');
      expect(dartValue, equals(typedValue));
    });
    test('dartify JSAny? value null and test if double? null', () {
      JSAny? jsValue = null as JSAny?;
      Object? dartValue = jsValue.dartify();
      double? typedValue = null;
      print('dartValue: $dartValue');
      expect(dartValue, equals(typedValue));
    });
  });

  group('dartify tests with casts', () {
    // Int value tests
    test('dartify JSAny? value 1 and test if int?', () {
      JSAny? jsValue = 1 as JSAny?;
      Object? dartValue = jsValue.dartify();
      int? typedValue = 1;
      print('dartValue: $dartValue');
      expect(dartValue as int?, equals(typedValue));
    });
    test('dartify JSAny? value 1 and test if int', () {
      JSAny? jsValue = 1 as JSAny?;
      Object? dartValue = jsValue.dartify();
      int typedValue = 1;
      print('dartValue: $dartValue');
      expect(dartValue as int, equals(typedValue));
    });
    test('dartify JSAny? value null and test if int? null', () {
      JSAny? jsValue = null as JSAny?;
      Object? dartValue = jsValue.dartify();
      int? typedValue = null;
      print('dartValue: $dartValue');
      expect(dartValue as int?, equals(typedValue));
    });

    // Double value tests
    test('dartify JSAny? value 1.0 and test if double?', () {
      JSAny? jsValue = 1.0 as JSAny?;
      Object? dartValue = jsValue.dartify();
      double? typedValue = 1;
      print('dartValue: $dartValue');
      expect(dartValue as double?, equals(typedValue));
    });
    test('dartify JSAny? value 1.0 and test if double', () {
      JSAny? jsValue = 1 as JSAny?;
      Object? dartValue = jsValue.dartify();
      double typedValue = 1.0;
      print('dartValue: $dartValue');
      expect(dartValue as double, equals(typedValue));
    });
    test('dartify JSAny? value null and test if double? null', () {
      JSAny? jsValue = null as JSAny?;
      Object? dartValue = jsValue.dartify();
      double? typedValue = null;
      print('dartValue: $dartValue');
      expect(dartValue as double?, equals(typedValue));
    });
  });
}

In Chrome as WEB JS build, ALL TESTS pass:

00:00  0: dartify tests with no casts dartify JSAny? value 1 and test if int?

dartValue: 1
00:00  1: dartify tests with no casts dartify JSAny? value 1 and test if int

dartValue: 1
00:00  2: dartify tests with no casts dartify JSAny? value null and test if int? null

dartValue: null
00:00  3: dartify tests with no casts dartify JSAny? value 1.0 and test if double?

dartValue: 1
00:00  4: dartify tests with no casts dartify JSAny? value 1.0 and test if double

dartValue: 1
00:00  5: dartify tests with no casts dartify JSAny? value null and test if double? null

dartValue: null
00:00  6: dartify tests with casts dartify JSAny? value 1 and test if int?

dartValue: 1
00:00  7: dartify tests with casts dartify JSAny? value 1 and test if int

dartValue: 1
00:00  8: dartify tests with casts dartify JSAny? value null and test if int? null

dartValue: null
00:00  9: dartify tests with casts dartify JSAny? value 1.0 and test if double?

dartValue: 1
00:00  10: dartify tests with casts dartify JSAny? value 1.0 and test if double

dartValue: 1
00:00  11: dartify tests with casts dartify JSAny? value null and test if double? null

dartValue: null
00:00  12: All tests passed!

But if run in Chrome, as a WEB WASM build:

00:00  0: dartify tests with no casts dartify JSAny? value 1 and test if int?

main.dart.mjs:72 00:00  0 -1: dartify tests with no casts dartify JSAny? value 1 and test if int? [E]

main.dart.mjs:72   Type 'int' is not a subtype of type 'JSValue?' in type cast

main.dart.mjs:72   main.dart.wasm 1:361378  module0._asSubtype
  main.dart.wasm 1:409945  module0.main closure at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:8:53
  main.dart.wasm 1:699587  module0.closure wrapper at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:8:53 trampoline
  main.dart.wasm 1:477026  module0.Declarer.test closure at file:///Users/rydmike/.pub-cache/hosted/pub.dev/test_api-0.7.3/lib/src/backend/declarer.dart:213:22 inner
  main.dart.wasm 1:479129  module0._awaitHelper closure at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5
  main.dart.wasm 1:479283  module0.closure wrapper at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5 trampoline
  main.dart.wasm 1:462521  module0._rootRunUnary
  

main.dart.mjs:72 00:00  0 -1: dartify tests with no casts dartify JSAny? value 1 and test if int

main.dart.mjs:72 00:00  0 -2: dartify tests with no casts dartify JSAny? value 1 and test if int [E]

main.dart.mjs:72   Type 'int' is not a subtype of type 'JSValue?' in type cast

main.dart.mjs:72   main.dart.wasm 1:361378  module0._asSubtype
  main.dart.wasm 1:409988  module0.main closure at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:15:52
  main.dart.wasm 1:699612  module0.closure wrapper at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:15:52 trampoline
  main.dart.wasm 1:477026  module0.Declarer.test closure at file:///Users/rydmike/.pub-cache/hosted/pub.dev/test_api-0.7.3/lib/src/backend/declarer.dart:213:22 inner
  main.dart.wasm 1:479129  module0._awaitHelper closure at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5
  main.dart.wasm 1:479283  module0.closure wrapper at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5 trampoline
  main.dart.wasm 1:462521  module0._rootRunUnary
  

main.dart.mjs:72 00:00  0 -2: dartify tests with no casts dartify JSAny? value null and test if int? null

main.dart.mjs:72 dartValue: null

main.dart.mjs:72 00:00  1 -2: dartify tests with no casts dartify JSAny? value 1.0 and test if double?

main.dart.mjs:72 00:00  1 -3: dartify tests with no casts dartify JSAny? value 1.0 and test if double? [E]

main.dart.mjs:72   Type 'double' is not a subtype of type 'JSValue?' in type cast

main.dart.mjs:72   main.dart.wasm 1:361378  module0._asSubtype
  main.dart.wasm 1:410086  module0.main closure at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:31:58
  main.dart.wasm 1:699662  module0.closure wrapper at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:31:58 trampoline
  main.dart.wasm 1:477026  module0.Declarer.test closure at file:///Users/rydmike/.pub-cache/hosted/pub.dev/test_api-0.7.3/lib/src/backend/declarer.dart:213:22 inner
  main.dart.wasm 1:479129  module0._awaitHelper closure at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5
  main.dart.wasm 1:479283  module0.closure wrapper at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5 trampoline
  main.dart.wasm 1:462521  module0._rootRunUnary
  

main.dart.mjs:72 00:00  1 -3: dartify tests with no casts dartify JSAny? value 1.0 and test if double

main.dart.mjs:72 00:00  1 -4: dartify tests with no casts dartify JSAny? value 1.0 and test if double [E]

main.dart.mjs:72   Type 'double' is not a subtype of type 'JSValue?' in type cast

main.dart.mjs:72   main.dart.wasm 1:361378  module0._asSubtype
  main.dart.wasm 1:410130  module0.main closure at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:38:57
  main.dart.wasm 1:699687  module0.closure wrapper at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:38:57 trampoline
  main.dart.wasm 1:477026  module0.Declarer.test closure at file:///Users/rydmike/.pub-cache/hosted/pub.dev/test_api-0.7.3/lib/src/backend/declarer.dart:213:22 inner
  main.dart.wasm 1:479129  module0._awaitHelper closure at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5
  main.dart.wasm 1:479283  module0.closure wrapper at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5 trampoline
  main.dart.wasm 1:462521  module0._rootRunUnary
  

main.dart.mjs:72 00:00  1 -4: dartify tests with no casts dartify JSAny? value null and test if double? null

main.dart.mjs:72 dartValue: null

main.dart.mjs:72 00:00  2 -4: dartify tests with casts dartify JSAny? value 1 and test if int?

main.dart.mjs:72 00:00  2 -5: dartify tests with casts dartify JSAny? value 1 and test if int? [E]

main.dart.mjs:72   Type 'int' is not a subtype of type 'JSValue?' in type cast

main.dart.mjs:72   main.dart.wasm 1:361378  module0._asSubtype
  main.dart.wasm 1:410383  module0.main closure at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:56:53
  main.dart.wasm 1:475102  module0.closure wrapper at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:56:53 trampoline
  main.dart.wasm 1:477026  module0.Declarer.test closure at file:///Users/rydmike/.pub-cache/hosted/pub.dev/test_api-0.7.3/lib/src/backend/declarer.dart:213:22 inner
  main.dart.wasm 1:479129  module0._awaitHelper closure at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5
  main.dart.wasm 1:479283  module0.closure wrapper at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5 trampoline
  main.dart.wasm 1:462521  module0._rootRunUnary
  

main.dart.mjs:72 00:00  2 -5: dartify tests with casts dartify JSAny? value 1 and test if int

main.dart.mjs:72 00:00  2 -6: dartify tests with casts dartify JSAny? value 1 and test if int [E]

main.dart.mjs:72   Type 'int' is not a subtype of type 'JSValue?' in type cast

main.dart.mjs:72   main.dart.wasm 1:361378  module0._asSubtype
  main.dart.wasm 1:410426  module0.main closure at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:63:52
  main.dart.wasm 1:475181  module0.closure wrapper at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:63:52 trampoline
  main.dart.wasm 1:477026  module0.Declarer.test closure at file:///Users/rydmike/.pub-cache/hosted/pub.dev/test_api-0.7.3/lib/src/backend/declarer.dart:213:22 inner
  main.dart.wasm 1:479129  module0._awaitHelper closure at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5
  main.dart.wasm 1:479283  module0.closure wrapper at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5 trampoline
  main.dart.wasm 1:462521  module0._rootRunUnary
  

main.dart.mjs:72 00:00  2 -6: dartify tests with casts dartify JSAny? value null and test if int? null

main.dart.mjs:72 dartValue: null

main.dart.mjs:72 00:00  3 -6: dartify tests with casts dartify JSAny? value 1.0 and test if double?

main.dart.mjs:72 00:00  3 -7: dartify tests with casts dartify JSAny? value 1.0 and test if double? [E]

main.dart.mjs:72   Type 'double' is not a subtype of type 'JSValue?' in type cast

main.dart.mjs:72   main.dart.wasm 1:361378  module0._asSubtype
  main.dart.wasm 1:410527  module0.main closure at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:79:58
  main.dart.wasm 1:475231  module0.closure wrapper at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:79:58 trampoline
  main.dart.wasm 1:477026  module0.Declarer.test closure at file:///Users/rydmike/.pub-cache/hosted/pub.dev/test_api-0.7.3/lib/src/backend/declarer.dart:213:22 inner
  main.dart.wasm 1:479129  module0._awaitHelper closure at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5
  main.dart.wasm 1:479283  module0.closure wrapper at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5 trampoline
  main.dart.wasm 1:462521  module0._rootRunUnary
  

main.dart.mjs:72 00:00  3 -7: dartify tests with casts dartify JSAny? value 1.0 and test if double

main.dart.mjs:72 00:00  3 -8: dartify tests with casts dartify JSAny? value 1.0 and test if double [E]

main.dart.mjs:72   Type 'int' is not a subtype of type 'JSValue?' in type cast

main.dart.mjs:72   main.dart.wasm 1:361378  module0._asSubtype
  main.dart.wasm 1:410570  module0.main closure at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:86:57
  main.dart.wasm 1:475256  module0.closure wrapper at file:///Users/rydmike/dev/code/hive_wasm_case/test/dartify_test.dart:86:57 trampoline
  main.dart.wasm 1:477026  module0.Declarer.test closure at file:///Users/rydmike/.pub-cache/hosted/pub.dev/test_api-0.7.3/lib/src/backend/declarer.dart:213:22 inner
  main.dart.wasm 1:479129  module0._awaitHelper closure at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5
  main.dart.wasm 1:479283  module0.closure wrapper at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:95:5 trampoline
  main.dart.wasm 1:462521  module0._rootRunUnary
  

main.dart.mjs:72 00:00  3 -8: dartify tests with casts dartify JSAny? value null and test if double? null

main.dart.mjs:72 dartValue: null

main.dart.mjs:72 00:00  4 -8: Some tests failed.

Only 4 of the 12 test pass. I honestly have no idea how this is supposed to work, but that we are not getting usable results in the WEB assembly build when used like this is clear. The JS build works great.

@rydmike
Copy link
Author

rydmike commented Nov 25, 2024

This is also interesting, in a WEB WASM build on Chrome it does this:

import 'dart:js_interop';

void main() {
  int i = 1;
  print(i.toJS.dartify()); // 1.0
  print(i.toJS.dartify().runtimeType); // double
  print(i.toJS.dartify() is int); // false
  print(i.toJS.dartify() is double); // true
}

Screenshot 2024-11-25 at 23 19 24

But in a WEB JS build in Chrome, it does this:

import 'dart:js_interop';

void main() {
  int i = 1;
  print(i.toJS.dartify()); // 1
  print(i.toJS.dartify().runtimeType); // int
  print(i.toJS.dartify() is int); // true
  print(i.toJS.dartify() is double); // true
}

Screenshot 2024-11-25 at 23 20 53

And yes I fully expect the JS Web build behavior, that is fine, but there is no way I can reconcile that with the WASM web build behavior and get the app to work as it does in VM and and current JS builds.

@rydmike
Copy link
Author

rydmike commented Nov 26, 2024

Added some more analysis here: flutter/flutter#186300 (comment)

Have not been able to figure out what triggers issue-1), but issue-2) is very clear now, just don't know how to fix it in hive_ce yet. I tried a few things that did not work. There is only so much you can do with JSAny?.

Still issue-2) I can work around with the "hack" I already have in the issue demo app, it sucks that it is needed, since it means hive WASM build is broken compared to JS and VM build, you must know you are on WASM and add the hack, but it works at least.

But for Issue-1) I do not yet have a working fix or hacky workaround. Could there be something odd in the cache?

@Rexios80
Copy link
Member

Can you simplify your test case at all and still reproduce the issue? You don't need an abstraction layer for the storage unless that is what's causing the issue.

@Rexios80
Copy link
Member

It is definitely the abstraction causing the issue. When I access the Hive box directly instead of through the StorageService in your example, the issue goes away. This again seems like an issue with the WASM compiler.

@rydmike
Copy link
Author

rydmike commented Nov 27, 2024

Thanks, if that is the case (the abstraction) it is a very good find. I will investigate it tonight. Maybe from it we can make a test case without Hive at all.

I left the abstraction in the repro sample because it is used in the original case where I ran into the issues. There are 3 implementations in the orig source, mostly as examples for educational purposes. Mem, shared prefs, hive. Only Hive has support for all needed features in the Playground.

When I stripped out a reproduction sample I just left the abstraction in there, it did not even occur to me that it might be triggering issue-1) in this case.

Issue-2) is clear why it happens, it is bad and unfortunate. I also think that this diff in js_interop with WASM and JS is bound to cause a lot of other issues and confusion going forward. But I doubt there is anything anybody can do to change their views on that. Wish there was a "legacy" interop mode.

For hive it is bad since it means we get different data type back in JS and WASM builds for int values. I think JS way is better and safer, it is fine if we get an int back when saving a double that looks like an int, we can still cast that to double, but it not ok that we get a double back when saving an int, we cannot cast that to int. Somehow this change and issue should be dealt with internally in hive, so that consumers do not have to be aware if it and work around it. I played with some options, but could not do much with that JSAny? type.

Anyway, I'll try a version without the abstraction too, for issue-1).

@rydmike
Copy link
Author

rydmike commented Nov 27, 2024

Did a simple tweak for issue-2) to try working around the daritfy() getting returned as double on WASM build and int on JS (and VM) builds. I added this woraround:

/// The [dartifyJS] is an extension on the JSAny to convert JSAny
/// to a Dart object. It differs from [JSAny.dartify] in js_interop.dart
/// in that it returns values that look like int, as int and not double.
extension _JSAnyDartifyJS on JSAny? {
  /// Converts a [JSAny] to a Dart object.
  Object? dartifyJS() {
    if (this == null) return null;
    if (isA<JSNumber>()) {
      final JSNumber number = this as JSNumber;
      final double value = number.toDartDouble;
      if (value % 1 == 0) {
        return value.toInt();
      } else {
        return value.toDouble();
      }
    } else {
      return dartify();
    }
  }
}

And anywhere in storage_backend_js.dart where .dartify() was used, I used .dartifyJS() instead.

https://github.com/rydmike/hive_ce/blob/main/hive/lib/src/backend/js/native/storage_backend_js.dart

I have not tested this extensively, just wanted to see if it would work for my use cases. At least with this in place, it works and I no longer get doubles when expecting ints. Worked on both JS and WASM build. Have not run your extensive test suite.

I'm not sure how the .dartify() was used and impacted keys in WASM builds, if at all, but it potentially could impact them too, since it would also be different in WASM and JS builds.

Are you by the way testing the WEB things as WASM builds too, or only JS builds? If only JS, then probably need to run the same tests as WASM builds too.

Going to look at the other issue later tonight.

@rydmike
Copy link
Author

rydmike commented Nov 27, 2024

For info, the issue in Flutter repo got closed. For issue-2) part I agree, it is fine, need to fix it here in hive_ce to provide an API that makes WASM builds consistent (compatible) with current results in JS and VM builds.

However, nothing written in the closing of the case explains issue-1), as I pointed out here: flutter/flutter#186300 (comment)

Need to figure out a simpler repro case for it and see where it leads. If it leads back to Dart/Flutter, then maybe open a new issue for it, if the original issue does not get re-opened.

rydmike added a commit to rydmike/flex_color_scheme that referenced this issue Nov 27, 2024
@Rexios80
Copy link
Member

Rexios80 commented Dec 3, 2024

What is the status of this? Can you do (value as num).toInt()? That's how packages such as json_serializable deal with this.

@rydmike
Copy link
Author

rydmike commented Dec 3, 2024

As far as I could see it has to be dealt with already in hive_ce, when the value is still a JSAny and there do the conversion to same types it gets by default in a JS build. Then I could got everything to work. It is when I try to do the type checks for issue-2) that things blow up, with issue-1) appearing too.

If I add the handling to hive_ce when it is still JSAny and return the correct, well let's call it same types as it has in JS builds anyway, then all works as before.

So as mentioned before, adding this:

https://github.com/rydmike/hive_ce/blob/f7472d99c1504a122f394024c0a09a04b91c54dc/hive/lib/src/backend/js/native/storage_backend_js.dart#L20C1-L37C1

/// The [dartifyJS] is an extension on the JSAny to convert JSAny
/// to a Dart object. It differs from [JSAny.dartify] in js_interop.dart
/// in that it returns values that look like int, as int and not double.
extension _JSAnyDartifyJS on JSAny? {
  /// Converts a [JSAny] to a Dart object.
  Object? dartifyJS() {
    if (this == null) return null;
    if (isA<JSNumber>()) {
      final JSNumber number = this as JSNumber;
      final double value = number.toDartDouble;
      if (value % 1 == 0) {
        return value.toInt();
      } else {
        return value.toDouble();
      }
    } else {
      return dartify();
    }
  }
}

And then in hive_ce using the dartifyJS() instead of dartify(), with that I could remove my work arounds and everything works OK, both issue-1) and issue-2), like it does in JS mode builds.

This fix keeps the returned type in WASM builds, as they are in JS and VM builds, something I must have for my WASM build and use case. I cannot suddenly have it change type of ints to doubles, just because that is what actually as in JS, when it in the past returned an int (well number without decimal point anyway), if it looked like an int.

I am currently using the above fork with the change in place via GitHub, instead of from pub. Have not noticed any issues with it, so far.

But as mentioned, I have not run your test suite. Also if you are not running your test suite in wasm build mode, maybe considering adding that to it.

I can submit a PR with the fix if you want to try it out, but I don't have any tests for it. If you want to try it, let me know if you want the PR in some test branch or similar, instead of main.

@Rexios80
Copy link
Member

Rexios80 commented Dec 3, 2024

I don't think you answered my question about why (value as num).toInt() won't work if you use it on the value you get from Hive that you expect to be an int.

The issue with the dartifyJS extension you made is that values that are actually doubles (such as 2.0, 3.0, etc) will always get returned as ints.

@Rexios80
Copy link
Member

Rexios80 commented Dec 3, 2024

Also if you are not running your test suite in wasm build mode, maybe considering adding that to it.

The tests do run compiled to WASM

See here

@rydmike
Copy link
Author

rydmike commented Dec 3, 2024

I was doing this to handle it in the service:

      if (App.isRunningWithWasm &&
          storedValue != null &&
          (storedValue is double) &&
          (defaultValue is int || defaultValue is int?)) {
        debugPrint('  ** WASM Error : Expected int but got double, '
            'returning as int: $storedValue');
        final T loaded = storedValue.round() as T;        
        return loaded;
      } else {
        final T loaded = storedValue as T;
        return loaded;
      }
    } catch (e, stackTrace) {

At the point when we are inside the if (App.isRunningWithWasm... we know we got a double and can convert it to an int, I was just doing it with round here:

final T loaded = storedValue.round() as T;

Yes, we can do this with this to:

final T loaded = (storedValue as num).toInt();

Although that does not compile with strict types:

error: A value of type 'int' can't be assigned to a variable of type 'T'. (invalid_assignment at [hive_wasm_case] lib/services/storage_service_hive.dart:114)

So we have to type cast it to T:

final T loaded = (storedValue as num).toInt() as T;

Which then works the same as the version with .round() for all relevant values that would be considered int. Both fixes the previously discussed issue-2) just fine.

BUT this then gets us the same error, issue-1) that we were both confused about above earlier. We get this error regardless of used above type conversion approach.

However, armed with Slava's keen observation and comment about how confusing the throw location was, I can in fact now rewrite the if condition in a way that works, if I use my sameTypes() function:

/// Check whether two types are the same type in Dart when working with
/// generic types.
///
/// Uses the same definition as the language specification for when two
/// types are the same. Currently the same as mutual sub-typing.
bool sameTypes<S, V>() {
  void func<X extends S>() {}
  // Dart spec says this is only true if S and V are "the same type".
  return func is void Function<X extends V>();
}

I can instead do this:

      final bool isNullableIntT = sameTypes<T, int?>();
      final bool isIntT = sameTypes<T, int>();
      if (App.isRunningWithWasm &&
          storedValue != null &&
          (storedValue is double) &&
          (isNullableIntT || isIntT)) {
        debugPrint('  ** WASM Error : Expected int but got double, '
            'returning as int: $storedValue');
        // Either type conversion variant works.
        // final T loaded = storedValue.round() as T;
        final T loaded = (storedValue as num).toInt() as T;
        return loaded;
      } else {
        final T loaded = storedValue as T;
        return loaded;
      }
    } catch (e, stackTrace) {

Then it works without issue-1) and it catches and fixes issue-2) when needed.

It does not really matter if I use the final T loaded = storedValue.round() as T; or final T loaded = (storedValue as num).toInt() as T; version.

So yes with a fairly complex special WASM case handling involving a quite exotic sameTypes function, I can in fact work around this issue, with my original idea for catching it, then converting to int with either of the issues.

I updated the issue sample with the above changes to demonstrate that fixed work-around I originally aimed for, is in fact now working with the above changes:

https://github.com/rydmike/hive_wasm_case

BUT the fact remains, that the user of hive_ce still have to be aware that an int saved in a VM or JS build comes back as an int looking value and can safely be assigned directly to an int variable type, this has always worked in Hive*, but same value saved in a WASM build, now comes back as double looking value and cannot be assigned to an int variable type, without extra type checks and conversions.

Thus it unexpectedly to users behaves quite differently in WASM builds. One of my joys with Hive was that it did behave consistently with types across platforms, it no longer does so.

I think we should catch this WASM diff with a failing test, then add a fix for it, and verify that the test passes and does not break anything else.

That is just my opinion of course 😃

I might have time to make suggestion later this week and submit a PR, if it is of interest.

@Rexios80
Copy link
Member

Rexios80 commented Dec 3, 2024

Maybe I can fix this by adding a type parameter to the get method

@rydmike
Copy link
Author

rydmike commented Dec 3, 2024

I experimented with the fix I have and put it through the test suite.

First I ran the tests without any changes on my fork, to see how they worked. I disabled the beta channel tests to save some time running the tests.

For some reason, the ensure-codegen fails for me in the no code changes baseline, but OK I know that is my baseline then:

Screenshot 2024-12-03 at 23 50 13

Then I added a very simple test to the integration test, that I knew will fail on the WASM build. I made it continue even if tests fails, because otherwise it would cancel the JS test, when WASM had failed, and I could not see that both VM and JS were still OK, they were, while we get the expected failures in the WASM build, but all OK on JS and VM:

Screenshot 2024-12-03 at 23 49 02

Added my fix, and removed the continue on error:

Screenshot 2024-12-04 at 0 06 48

New test that failed on WASM now passes and no other failures, except the ensure-codegen that failed in my baseline.

So my customized dartifyJS() version works OK. I called it dartifyJS() because it is dartify() version that returns the JS like result, also in WASM build, that is needed here, can call it whetever is preferred of course, it is only an internal extension.

It looks like this:

// The [dartifyJS] is an extension on the JSAny to convert JSAny
// to a Dart object. It differs from [JSAny.dartify] in js_interop.dart
// in that it returns values that look like int, as int and not double.
extension _JSAnyDartifyJS on JSAny? {
  /// Converts a [JSAny] to a Dart object.
  Object? dartifyJS() {
    if (this == null) return null;
    if (isA<JSNumber>()) {
      final number = this as JSNumber;
      final value = number.toDartDouble;
      if (value % 1 == 0) {
        return value.toInt();
      } else {
        return value.toDouble();
      }
    } else {
      return dartify();
    }
  }
}

It is used in 3 places in storage_backend_js.dart where dartify() was used, dartifyJS() is used instead.

Based on this, this fix seems to allow hive to work in identical manner with int data types as it have in the past done in Web JS and VM builds, without using any extra type parameters.

If you are interested in trying it out I can submit a PR. You can of course improve the style and test to your liking after that.

@Rexios80
Copy link
Member

Rexios80 commented Dec 3, 2024

I appreciate the effort but your solution isn't robust enough to merge. I'll work on it when I get a chance.

@rydmike
Copy link
Author

rydmike commented Dec 3, 2024

Ok, that's fine, it passes all the tests you have though 🤷 😄 Which is good enough for me for now, to at least move on with some degree of confidence that it works well enough for my current use case.

Looking forward to seeing what you come up with that is not a breaking change nor requires any changes on hive user side when using it it with WASM.

@Rexios80
Copy link
Member

Rexios80 commented Dec 4, 2024

Try this PR: #56

@Rexios80 Rexios80 mentioned this issue Dec 4, 2024
@rydmike
Copy link
Author

rydmike commented Dec 4, 2024

Tried it, it still fails, at least when I try it with the repro sample.

I updated the repro sample to use the PR: https://github.com/rydmike/hive_wasm_case

As before, when you actually (re)load data from the hive box, e.g. at startup, int values come back as looking like double and their type conversion fails.

Screenshot 2024-12-04 at 11 10 45

If you add the simple integration test line I used in my fork (or some similar separate integration test for this case) you should see it with this fix added too. Although I did not try to pull in these changes to my fork and try it. You current test does not test the PR fix all the way from get.

I do see what you did, and it was also a good idea to borrow the sameTypes as a helper. When I looked at it at first I thought this looks good and might work, but sadly it does not.

I suspect the issue is because a hive box is typically opened with dynamic or maybe Object?, as it needs to be to support multiple types.

The box get will not return the type of the value behind a key via the optional default value of a known type T (Or E in the LazyBoxImpl), even when the optional default value is given. Nor can we use a get with the type of a known default value.

This could be fixable by introducing type on e.g. get<T>, but that would be very breaking.

Meaning, in the repro sample where it does this:

  @override
  Future<T> load<T>(String key, T defaultValue) async {
    // A dynamic to hold the value we get from the Hive storage.
    dynamic storedValue;
    try {
      storedValue = await _hiveBox.get(key, defaultValue: defaultValue);

      final bool isNullableDoubleT = sameTypes<T, double?>();
      debugPrint('Store LOAD ______________');
      debugPrint('  Store key     : $key');
      debugPrint('  Type expected : $T');
      debugPrint('  Stored value  : $storedValue');
      debugPrint('  Stored type   : ${storedValue.runtimeType}');
      debugPrint('  Default value : $defaultValue');
      debugPrint('  Default type  : ${defaultValue.runtimeType}');
      debugPrint('  T is double?  : $isNullableDoubleT');
      debugPrint('  Using WASM    : ${App.isRunningWithWasm}');
      return storedValue as T;
    }

We cannot do this:

  @override
  Future<T> load<T>(String key, T defaultValue) async {
    // A dynamic to hold the value we get from the Hive storage.
    T storedValue;
    try {
      storedValue = await _hiveBox.get(key, defaultValue: defaultValue);

Because that is a compile time type error.

error: A value of type 'dynamic' can't be assigned to a variable of type 'T'. (invalid_assignment at [hive_wasm_case]

Because the actual box contains many types, and must be opened with dynamic, which still defines the return type of get.

It is doable if get for a key requires a type, but that would as mentioned be very breaking, or even a totally new API, e.g.:

  @override
  Future<T> load<T>(String key, T defaultValue) async {
    // A dynamic to hold the value we get from the Hive storage.
    T storedValue;
    try {
      storedValue = await _hiveBox.get<T>(key, defaultValue: defaultValue);

Might work if introduced, but it would be breaking or at minimum a new API interface needed (for something that should work with current API), plus it would be kind of annoying to use for most use cases 😄

Maybe there is some other way around it, looking forward to see what you come up with.

My solution still works 😏, might not be pretty, but it worked with all test cases 🤷🏻‍♂️ but I do agree that if there is a better way, that would be excellent 👍🏻

@Rexios80
Copy link
Member

Rexios80 commented Dec 4, 2024

If your box has a dynamic type parameter there is no way for Hive to know what type you are trying to pull out of it. Again, why can't you use (value as num).toInt() on the value you read from the Hive box? You shouldn't need an internal workaround. Your code does know it's trying to pull out an int value right?

@rydmike
Copy link
Author

rydmike commented Dec 5, 2024

The original use case where I discovered this issue has many different types, not just int and double like the repro sample, but still mostly simple types like Color and a lot of enums. I suspect that typically most users of Hive use a dynamic box (or maybe Object?), to be able to store values with different types.

As shown earlier above, yes by using the sameTypes() function and checking if it is a wasm build, I can correctly intercept the issue in a Wasm build and correct it.

However, as a user of Hive CE, this is not what I expect to have to do. I expect to get the same results back in VM, JS and WASM builds from a multi-platform storage solution like Hive CE.

You do not need this extra type check in Hive CE in VM or JS builds, it returns the expected and correctly looking "int" values that converts correctly to int types. It is a really big foot-gun that it is different in a Wasm build. The internal differences between JS and Wasm are understandable, but a storage package that abstracts away the platform diffs, should preferably not suffer from this, it should work the same regardless of used platform.

Users of Hive CE will need a BIG warning about this issue/difference if it is not fixed. I suspect the only reason it was not discovered earlier, is because usage of Hive CE is still fairly low, and using it in an actual Wasm build is even less common.

Consider putting a test case for this use case in your integration tests, it will fail (throw) in the Wasm build and pass in other platforms.

@Rexios80
Copy link
Member

Rexios80 commented Dec 5, 2024

How many people actually use a dynamic box? Using dynamic types is asking for trouble, and you found it. Can you use a type adapter and a structured object instead?

It seems like your use case would be better suited for shared preferences.

@rydmike
Copy link
Author

rydmike commented Dec 5, 2024

Agreed, Hive is indeed used as kind of a replacement for Shared Preferences in this case.
This was some of its first use cases when it was launched several years ago.

In this case it was back then also selected because long before Shared Preferences supported all platforms, Hive already supported all, a huge win for it back then. Plus, Hive was also a bit faster than SharedPrefs, to the degree that in this use case it removed the need to even throttle fast UI interactions that needed to be saved.

Later, Shared Preferences finally added support for all platforms. However, Hive also conveniently supported saving null values. SharedPrefs does not, you have to encode a null value yourself some way. Which I actually did as well, in an implementation that uses it.

In this use case, the absence of a key is not necessarily null, but the default value (which may or may not be null) defined for the key, null is also a valid persisted choice, for keys with nullable values. This has to do with theming in Flutter, were nulls represent the SDK widget theme defaults.

A tutorial section for this use case, discusses and presents the diffs between SharedPrefs and Hive, when using them for simple key-value pairs.

Hive also has a number of convenience features that SharedPrefs lack, making SharedPrefs far less convenient for this use case. Not impossible to use, just not a migration I currently plan to do.

For storing anything more advanced than just simple key-value pairs, e.g. structured objects, or DB like data, I generally do not use Hive at all. I have always just used it as an option for SharedPrefs, with a bit more speed, plus some convenience features and having an easy way to encode simple data like Colors and enums. Sure I have implementations that encode them into SharedPrefs too.

BTW, not using hive_ce_codegen nor the hive_ce_flutter packages, just vanilla hive_ce.


Still regardless of what Hive is used for, the relevant part here is that while Hive CE technically sort of works in WASM builds too, it has an API response that is broken in WASM compared to its past response in VM and JS builds.

WASM build has a different result for int values stored in a dynamic box. For a storage solution that at least originally was supposed to offer same API and result regardless of platform used, this imo is an unfortunate regression of its usability.

Sure, you can work around it, but you have to be aware that you have to work around it, especially since no such workarounds were needed before.

As also noted, Hive CE currently lacks the integration test to catch this issue, so not surprising it was not caught earlier from that viewpoint as well.

@Rexios80
Copy link
Member

Rexios80 commented Dec 5, 2024

Maybe I can move the fix to the box layer. That isn't ideal, but refactoring the code to not use dynamic everywhere is not trivial.

@rydmike
Copy link
Author

rydmike commented Dec 6, 2024

Thanks and sure, maybe that can be done.

Yes, I can see that refactoring the code to not use dynamic is not trivial.

Also, my original workaround is not ideal, as you mentioned too. All works in JS builds with it, but WASM is still not playing well with it in all cases (none of them were caught in Hive CE test suite though). I tried a number of things to fix the gap I found with it, but the way WASM treats JS numbers, makes it fairly impossible to deal with the numbers in a safe way at the <JSAny?> and level.

Therefore I have resorted back to checking for the "special needs case for WASM builds", when I get data from Hive for this use case. Using this for now as the safest work-around found so far:

      final dynamic value = await _hiveBox.get(key, defaultValue: defaultValue);
      // Add workaround for Hive WASM returning double instead of int, when
      // values saved to IndexedDb were int.
      // See issue: https://github.com/IO-Design-Team/hive_ce/issues/46
      if (App.isRunningWithWasm &&
          value != null &&
          (value is double) &&
          (sameTypes<T, int?>() || sameTypes<T, int>())) {
        final T loaded = value.round() as T;
        if (_debug) {
          debugPrint(' ** WASM Issue : Expected int but got double, '
              'returning as int: $loaded');
        }
        return loaded;
      } else {
        return value as T;
      }

Really specific way to handle this case, and only in WASM builds. Feels a bit too much like a hack for my taste, but for now the safest way around the issue that I have found.

The App.isRunningWithWasm is of course defined as the compile time const bool.fromEnvironment('dart.tool.dart2wasm').


Not sure if there is a way around this WASM specific issue with Hive CE, without major refactoring and maybe even some breaking change.

Perhaps the easiest way to resolve it, is indeed to just document it with a very visibly WARNING in Hive CE docs and inform users that if they use a dynamic Hive box in WASM, that they should expect persisted int values coming back as double values and to handle the conversion back to int themselves in WASM builds, even if this is not needed in VM and JS builds.

@rydmike
Copy link
Author

rydmike commented Jan 6, 2025

Thanks and yes I agree, this is probably the only and best way of handling this issue. As I mentioned I resorted to accepting the extra type check and conversion in WASM builds as well.

This debug print will help others facing the same issue. I am sure more users will run into it eventually.

Thankful that you made and keep this Hive classic fork alive and went through the trouble of migrating it to Wasm as well 👍💙

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants