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

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

Closed
OlegShNayax opened this issue Jul 25, 2024 · 7 comments
Closed

Comments

@OlegShNayax
Copy link

OlegShNayax commented Jul 25, 2024

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

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<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  late final DriftDatabaseImpl _database;
  late final DriftActorDao _actorDao;

  @override
  void initState() {
    super.initState();
    _initializeDatabase();
  }

  Future<void> _initializeDatabase() async {
    print("initialize drift database");
    _database = DriftDatabaseImpl();
    _actorDao = _database.driftActorDao;
  }

  Future<void> _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<void> _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: <Widget>[
            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

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

class DriftActorEntity implements Insertable<DriftActorEntity> {
  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<String, Expression> 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

@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<Column>? get primaryKey => {actorID};
}

DriftActorDao

@DriftAccessor(tables: [DriftActorTable])
class DriftActorDao extends DatabaseAccessor<DriftDatabaseImpl>
    with _$DriftActorDaoMixin {
  DriftActorDao(DriftDatabaseImpl db) : super(db);

  @override
  Future<void> insertActors(List<DriftActorEntity> actors) {
    return batch((batch) {
      batch.insertAll(driftActorTable, actors,
          mode: InsertMode.insertOrReplace);
    });
  }

  @override
  Future<List<DriftActorEntity>> 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.

I think there is issue with drift_worker.js

@SleepySquash
Copy link

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
Copy link
Author

@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
Copy link

@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
Copy link
Owner

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 (simolus3/sqlite3.dart#230 is the issue to follow).
One of the issues here appears to be a compiler bug (dart-lang/sdk#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
Copy link
Owner

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 dart-lang/build#3621 (which I am also working on).

@definev
Copy link

definev commented Jul 29, 2024

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

@simolus3
Copy link
Owner

There are some notes on that here

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

No branches or pull requests

4 participants