-
Notifications
You must be signed in to change notification settings - Fork 4
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
Comments
I believe this needs to be |
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
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 But in the WASM builds, while it looks like an I added an epic and horrific work-around hack as shown below, that is only used on the WASM build that converts the returned /// 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 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 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 |
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. |
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. |
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 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 Since I'm not familiar enough with the I should be able to finish writing up my findings tomorrow and clean up the repro sample. |
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 The report below is the same as the one I used in the Flutter repo. Type conversion issues in WASM buildsWhen saving key-value pair to a Hive box in a Flutter WASM web build, the of types 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 codeThe reproduction sample is provided in a separate repository here: https://github.com/rydmike/hive_wasm_case The sample uses the The package works as expected when building for Web JS target with no Expected resultsExpect retrieving Build and run the sample code using WEB target in Chrome browser. Hit the (Increase values and save to DB) button to add some key-value pairs to the IndexedDB for local storage. We can verify that the values have been persisted as expected in comparing console log debug prints and values persisted in browser IndexedDB. 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. 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 resultsLet's repeat this process again, but this time we build for web target with 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. We can again verify that the values have been persisted as expected by checking the console. Issue 1) double? type conversion issueNext clear the console and then hit (Load values from DB) button. We can see that the saved value that had type nullable 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. 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 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. If we hit (Increase values and save to DB) button again and update the Again if we restart the app with a page refresh (or Flutter hot restart), the It is worth noticing that issue 1) is present all the time, but the second issue 2) with Flutter versionUsed Flutter version: Channel master It reproduces on beta and stable 3.24.5 too. Flutter doctor
|
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. |
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 The odd part is that in a WEB JS build we NEVER get "1.0" from Hive, when we have an But with WEB WASM build, we get "1.0" if we after saving value 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
/// 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 I need to do some more digging, I have not debugged properly what is going on in |
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. |
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
} 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
} 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. |
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 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 But for Issue-1) I do not yet have a working fix or hacky workaround. Could there be something odd in the cache? |
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. |
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. |
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 Anyway, I'll try a version without the abstraction too, for issue-1). |
Did a simple tweak for issue-2) to try working around the /// 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 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 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. |
For info, the issue in Flutter repo got closed. For issue-2) part I agree, it is fine, need to fix it here in 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. |
What is the status of this? Can you do |
As far as I could see it has to be dealt with already in If I add the handling to So as mentioned before, adding 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 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 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. |
I don't think you answered my question about why The issue with the |
The tests do run compiled to WASM |
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 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:
So we have to type cast it to final T loaded = (storedValue as num).toInt() as T; Which then works the same as the version with 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 So yes with a fairly complex special WASM case handling involving a quite exotic 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 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. |
Maybe I can fix this by adding a type parameter to the get method |
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: 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: Added my fix, and removed the continue on error: New test that failed on WASM now passes and no other failures, except the ensure-codegen that failed in my baseline. So my customized 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 Based on this, this fix seems to allow hive to work in identical manner with 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. |
I appreciate the effort but your solution isn't robust enough to merge. I'll work on it when I get a chance. |
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. |
Try this PR: #56 |
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, 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 I do see what you did, and it was also a good idea to borrow the I suspect the issue is because a hive box is typically opened with The box This could be fixable by introducing type on e.g. 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.
Because the actual It is doable if @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 👍🏻 |
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 |
The original use case where I discovered this issue has many different types, not just As shown earlier above, yes by using the 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 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. |
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. |
Agreed, Hive is indeed used as kind of a replacement for Shared Preferences in this case. 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 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. |
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. |
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 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 |
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 👍💙 |
Odd issue in Web WASM build with
int
typeFirst 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 ofhtml
.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 asint
, so all is fine:However, building the same app as Wasm, Hive consistently returns values stored as
int
asdouble
.The stored
int
values certainly visually look like ints in the IndexedDB, same as they do when using the Web JS build.Have you seen this issue before?
When storing
double
types in Hive, withint
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 expecteddouble
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 defaultint
value for thekey
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 thehive
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
The text was updated successfully, but these errors were encountered: