ekibun/flutter_qjs

Give more control over `jsExecutePendingJob` / `_executePendingJob`

Matthiee opened this issue · 1 comments

We are using Zone's to control some aspects of how the eval code is executed.

If the eval is executed in CustomZone, all the JS code that is behind an async/await will be run in the RootZone where the QuickJS.dispatch() is executed. This is incorrect, we would expect all the code in the eval to be run in the CustomZone.

We can't change this behaviour as there is no way to manually execute pending jobs.

Making the _executePendingJob public would be sufficient for us.


Closing the QuickJS and running the Dispatch again inside the correct zone is too expensive for us.

@ekibun here is an example test as to why this is needed.

The only difference between the working test and the failing test is the https://api.flutter.dev/flutter/dart-async/runZoned.html being used.

Our application is using custom zones heavily and losing the zone values after the first async/await call is something preventing us from using this library.

In a patched version using #32 we can run the event loop in the custom zone.

Example unit test code:

import 'dart:async';

import 'package:flutter_qjs/flutter_qjs.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  Future<String?> fetch(String url) async {
    await Future.delayed(const Duration(milliseconds: 100));

    return Zone.current['custom'] as String?;
  }

  late FlutterQjs qjs;

  setUp(() async {
    qjs = FlutterQjs();

    final setToGlobalFunc =
        qjs.evaluate('(key, val) => { this[key] = val; }') as JSInvokable;

    setToGlobalFunc.invoke(['fetch', fetch]);
    setToGlobalFunc.invoke(['print', print]);

    setToGlobalFunc.destroy();
    qjs.dispatch();

    await qjs.evaluate('''
     class MyClass {
      async download() {
        const result1 = await fetch("http://google.com");
        const result2 = await fetch("https://example.com/");
        return { a: result1, b: result2 };
      }
    }
''');
  });

  test('async fetch (works)', () async {
    final result = await qjs.evaluate('(new MyClass()).download()');

    print('result here is $result');

    expect(result['a'], isNull, reason: 'no zone data available');
    expect(result['b'], isNull, reason: 'no zone data available');
  });

  test('async fetch (broken)', () async {
    final result = await runZoned(
      () async => await qjs.evaluate('(new MyClass()).download()'),
      zoneValues: {
        'custom': 'some value',
      },
    );

    print('result here is $result');

    expect(result['a'], 'some value', reason: '1st fetch executed in runZoned');

    // This contains null, while it should contain the zone value. 
    // This indicates it has ran outside of the zone.
    expect(result['b'], 'some value', reason: '2nd fetch executed in runZoned');
  });
}