simolus3 / drift

Drift is an easy to use, reactive, typesafe persistence library for Dart & Flutter.
https://drift.simonbinder.eu/
MIT License
2.54k stars 360 forks source link

Web WASM: "RuntimeError: illegal cast at DriftCommunication._handleMessage" #3113

Closed OlegShNayax closed 1 month ago

OlegShNayax commented 1 month ago

Steps to reproduce

  1. Clone repository https://github.com/OlegShNayax/drift_wasm_example
  2. Build project using flutter build web --wasm --no-strip-wasm
  3. Run project using dhttpd '--headers=Cross-Origin-Embedder-Policy=credentialless;Cross-Origin-Opener-Policy=same-origin' --path=build/web --port=8085
  4. Open project in Chrome http://localhost:8085/
  5. Open console Developer Tools > Console

Expected results

We see log "initialize drift database" end everything works fine.

Actual results

We see log "initialize drift database" and error:

main.dart.wasm:0x149921 Uncaught 
RuntimeError: illegal cast
    at DriftCommunication._handleMessage (main.dart.wasm:0x149921)
    at DriftCommunication._handleMessage tear-off trampoline (main.dart.wasm:0x149b3c)
    at _RootZone.runUnaryGuarded (main.dart.wasm:0xd475e)
    at _BufferingStreamSubscription._sendData (main.dart.wasm:0xd87a6)
    at _BufferingStreamSubscription._add (main.dart.wasm:0xd8974)
    at _SyncStreamController._sendData (main.dart.wasm:0x14fe16)
    at _StreamController._add (main.dart.wasm:0x146cc2)
    at _StreamController.add (main.dart.wasm:0x146c76)
    at _StreamController.add tear-off trampoline (main.dart.wasm:0x150957)
    at _RootZone.runUnaryGuarded (main.dart.wasm:0xd475e)

Code sample

Code sample
**main.dart** ```dart import 'package:drift_wasm_example/database/dao/drift_actor_dao.dart'; import 'package:drift_wasm_example/database/drift_database.dart'; import 'package:drift_wasm_example/database/entities/drift_actor_entity.dart'; import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { int _counter = 0; late final DriftDatabaseImpl _database; late final DriftActorDao _actorDao; @override void initState() { super.initState(); _initializeDatabase(); } Future _initializeDatabase() async { print("initialize drift database"); _database = DriftDatabaseImpl(); _actorDao = _database.driftActorDao; } Future _insertActors() async { final generatedActors = List.generate( 100, (index) => DriftActorEntity( actorID: "$index", parentActorID: "$index", actorDescription: "Actor #$index description", actorDistributorId: "$index", actorTypeID: index, actorStatus: index, actorMachinesCount: index * 10, ), ); await _actorDao.insertActors(generatedActors); } Future _printActors() async { final savedActors = await _actorDao.getActors(); for(final actor in savedActors) { print(actor); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headlineMedium, ), ], ), ), persistentFooterButtons: [ FloatingActionButton( onPressed: _insertActors, tooltip: 'Insert 100 actors', child: const Icon(Icons.add), ), const SizedBox(width: 20.0), FloatingActionButton( onPressed: _printActors, tooltip: 'Print actors', child: const Icon(Icons.print), ), ], // This trailing comma makes auto-formatting nicer for build methods. ); } } ``` **drift_database.dart** ```dart DatabaseConnection connectOnWeb() { return DatabaseConnection.delayed(Future(() async { final result = await WasmDatabase.open( databaseName: 'my_app_db', // prefer to only use valid identifiers here sqlite3Uri: Uri.parse('sqlite3.wasm'), driftWorkerUri: Uri.parse('drift_worker.js'), ); if (result.missingFeatures.isNotEmpty) { // Depending how central local persistence is to your app, you may want // to show a warning to the user if only unrealiable implemetentations // are available. print('Using ${result.chosenImplementation} due to missing browser ' 'features: ${result.missingFeatures}'); } return result.resolvedExecutor; })); } @DriftDatabase(tables: [ DriftActorTable, ], daos: [ DriftActorDao ]) class DriftDatabaseImpl extends _$DriftDatabaseImpl { DriftDatabaseImpl._(super.e); factory DriftDatabaseImpl() => DriftDatabaseImpl._(connectOnWeb()); @override int get schemaVersion => 1; } ``` **drift_actor_entity.dart** ```dart class DriftActorEntity implements Insertable { String? actorID; String? parentActorID; String? actorDescription; String? actorDistributorId; int? actorTypeID; int? actorStatus; int? actorMachinesCount; DriftActorEntity( {this.parentActorID, this.actorID, this.actorDescription, this.actorDistributorId, this.actorTypeID, this.actorStatus, this.actorMachinesCount}); @override Map toColumns(bool nullToAbsent) { return DriftActorTableCompanion( actorID: Value(actorID), parentActorID: Value(parentActorID), actorDescription: Value(actorDescription), actorDistributorId: Value(actorDistributorId), actorTypeID: Value(actorTypeID), actorStatus: Value(actorStatus), actorMachinesCount: Value(actorMachinesCount), ).toColumns(nullToAbsent); } @override String toString() { return 'DriftActorEntity{actorID: $actorID, parentActorID: $parentActorID, actorDescription: $actorDescription, actorDistributorId: $actorDistributorId, actorTypeID: $actorTypeID, actorStatus: $actorStatus, actorMachinesCount: $actorMachinesCount}'; } } ``` **DriftActorTable** ```dart @UseRowClass(DriftActorEntity) class DriftActorTable extends Table { @override String get tableName => 'actor'; TextColumn get actorID => text().named("actorID").nullable()(); TextColumn get parentActorID => text().named("parentActorID").nullable()(); TextColumn get actorDescription => text().named("actorDescription").nullable()(); TextColumn get actorDistributorId => text().named("actorDistributorId").nullable()(); IntColumn get actorTypeID => integer().named("actorTypeID").nullable()(); IntColumn get actorStatus => integer().named("actorStatus").nullable()(); IntColumn get actorMachinesCount => integer().named("actorMachinesCount").nullable()(); @override Set? get primaryKey => {actorID}; } ``` **DriftActorDao** ```dart @DriftAccessor(tables: [DriftActorTable]) class DriftActorDao extends DatabaseAccessor with _$DriftActorDaoMixin { DriftActorDao(DriftDatabaseImpl db) : super(db); @override Future insertActors(List actors) { return batch((batch) { batch.insertAll(driftActorTable, actors, mode: InsertMode.insertOrReplace); }); } @override Future> getActors() { return (select(driftActorTable)).get(); } } ```

Flutter Doctor output

Flutter Doctor output
``` % flutter doctor -v [✓] Flutter (Channel stable, 3.22.3, on macOS 13.6.7 22G720 darwin-x64, locale en-IL) • Flutter version 3.22.3 on channel stable at /Users/olegs/Documents/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision b0850beeb2 (8 days ago), 2024-07-16 21:43:41 -0700 • Engine revision 235db911ba • Dart version 3.4.4 • DevTools version 2.34.3 [✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0-rc5) • Android SDK at /Users/olegs/Library/Android/sdk • Platform android-34, build-tools 31.0.0-rc5 • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b829.9-10027231) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 15.0) • Xcode at /Users/olegs/Downloads/Xcode.app/Contents/Developer • Build 15A240d • CocoaPods version 1.15.2 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2022.3) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b829.9-10027231) [✓] VS Code (version 1.58.2) • VS Code at /Users/olegs/Downloads/Visual Studio Code.app/Contents • Flutter extension can be installed from: 🔨 https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter [✓] VS Code (version 1.91.1) • VS Code at /Applications/Visual Studio Code 2.app/Contents • Flutter extension can be installed from: 🔨 https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter [✓] Connected device (2 available) • macOS (desktop) • macos • darwin-x64 • macOS 13.6.7 22G720 darwin-x64 • Chrome (web) • chrome • web-javascript • Google Chrome 126.0.6478.183 [✓] Network resources • All expected network resources are available. ```
SleepySquash commented 1 month ago

I'll write down some notes I've gathered while investigating the same problem. The issue is reproducible for example app hosted under drift/examples/app here in the repository as well.

This is caused by DriftProtocol.deserialize() method doing as int conversion of the message[0] and message[1] arguments and receiving doubles instead - trying to cast double as int fails and the exception thus arises.

You may replace the following lines:

final tag = message[0];
final id = message[1] as int;

with:

final tag = message[0] is double ? (message[0] as double).toInt() : message[0] as int;
final id = message[1] is double ? (message[1] as double).toInt() : message[1] as int;

And also do the same thing for decodePayload() function, which does some as int as well. This will fix the initial RuntimeError: illegal cast issue, and drift will connect to the WasmDatabase. However, there seems to be more work needed to be done, as SELECT/INSERT/DELETE statements don't seem to work, they all throw the same exceptions with illegal cast (perhaps due to those queries expecting last ROW_ID to be int and converting it as such, despite being double?):

Screenshot 2024-07-25 at 11 05 20

Running customStatement for INSERTs indicate that items are indeed being added. Then customSelect may be executed to further ensure that items are indeed added, yet due to conversion issues it still fails, you can see at the attached screenshot that items are indeed there, yet Invalid radix-10 number is thrown at DateTime being stored as the microseconds (double instead of int, I guess, as drift expects?)

Screenshot 2024-07-25 at 11 21 57
OlegShNayax commented 1 month ago

@SleepySquash Thank you for really helpful notes.

After modification that you mentioned. I getting another error

main.dart.wasm:0x5161fd Uncaught RuntimeError: illegal cast
    at DeleteStatement.go closure at file:///Users/olegs/.pub-cache/hosted/pub.dev/drift-2.19.1/lib/src/runtime/query_builder/statements/delete.dart:52:41 inner (main.dart.wasm:0x5161fd)
    at _awaitHelperWithTypeCheck closure at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:97:16 (main.dart.wasm:0x481ddb)
    at closure wrapper at org-dartlang-sdk:///dart-sdk/lib/_internal/wasm/lib/async_patch.dart:97:16 trampoline (main.dart.wasm:0x481ecd)
    at _RootZone.runUnary (main.dart.wasm:0x482a28)
    at _Future._propagateToListeners (main.dart.wasm:0x48260f)
    at _Future._completeWithValue (main.dart.wasm:0x482cc9)
    at _Future._asyncCompleteWithValue closure at org-dartlang-sdk:///dart-sdk/lib/async/future_impl.dart:721:29 (main.dart.wasm:0x49626f)
    at closure wrapper at org-dartlang-sdk:///dart-sdk/lib/async/future_impl.dart:721:29 trampoline (main.dart.wasm:0x496286)
    at _startMicrotaskLoop (main.dart.wasm:0x481190)
    at _startMicrotaskLoop tear-off trampoline (main.dart.wasm:0x4811fb)
SleepySquash commented 1 month ago

@OlegShNayax, yep, that's what I'm stuck at with too. Seems like the as int conversion story gets deeper into drift and even perhaps sqlite3 packages and the DriftProtocol fixes I've provided earlier do not suffice. If there's anything more you'll discover, please, write down your thoughts here - it'll surely help the maintainer to investigate the problem and fix it.

simolus3 commented 1 month ago

This is pretty hard to test at the moment because there is no good way to write integration tests for dart2wasm outside of Flutter yet. Nothing in drift is aware of dart2wasm at all, and we're not running any tests compiled to wasm. package:sqlite3 has experimental support for dart2wasm (https://github.com/simolus3/sqlite3.dart/issues/230 is the issue to follow). One of the issues here appears to be a compiler bug (https://github.com/dart-lang/sdk/issues/56321), I'll fix the casts as well.

As a short-term solution I'll look into running unit tests with dart2wasm as well, but that doesn't say too much because most of the subtle bugs are usually around the dart:js_interop layer which we can only test with integration tests. I've opened PRs to the SDK and build_web_compilers to support the test architecture drift is using for that, but it will definitely take a while for drift to have well-tested wasm support.

simolus3 commented 1 month ago

The problems are fixed on develop.

I've added CI steps running unit tests for drift with both dartdevc and dart2wasm (dart2js unfortunately is way too slow on the runners, even with shards enabled). We're also running integration tests with dart2js as part of the CI now. Running them with dart2wasm is blocked on https://github.com/dart-lang/build/issues/3621 (which I am also working on).

definev commented 1 month ago

How to use develop branch? I can't start my app using wasm with current pub version

simolus3 commented 1 month ago

There are some notes on that here