asyncEval method

Future<InstanceRef?> asyncEval(
  1. String expression, {
  2. required Disposable? isAlive,
  3. Map<String, String>? scope,
})

A safeEval variant that can use await.

This is useful to obtain the value emitted by a future, by potentially doing:

final result = await asyncEval('await Future.value(42)');

where result will be an InstanceRef that points to 42.

If the FutureOr awaited threw, asyncEval will throw a FutureFailedException, which can be caught to access the StackTrace and error.

Implementation

Future<InstanceRef?> asyncEval(
  String expression, {
  required Disposable? isAlive,
  Map<String, String>? scope,
}) async {
  final futureId = _nextAsyncEvalId++;

  // start awaiting the event before starting the evaluation, in case the
  // event is received before the eval function completes.
  final future = serviceManager.service!.onExtensionEvent.firstWhere((event) {
    return event.extensionKind == 'future_completed' &&
        event.extensionData!.data['future_id'] == futureId &&
        // Using `_clientId` here as if two chrome tabs open the devtool, it is
        // possible to have conflicts on `future_id`
        event.extensionData!.data['client_id'] == _clientId;
  });

  final readerGroup = 'asyncEval-$futureId';

  /// Workaround to not being able to import libraries directly from an evaluation
  final postEventRef = await _dartDeveloperEval.safeEval(
    'postEvent',
    isAlive: isAlive,
  );
  final widgetInspectorServiceRef = await _widgetInspectorEval.safeEval(
    'WidgetInspectorService.instance',
    isAlive: isAlive,
  );

  final readerId = await safeEval(
    // since we are awaiting the Future, we need to make sure that during the awaiting,
    // the "reader" is not GCed
    'widgetInspectorService.toId(<dynamic>[], "$readerGroup")',
    isAlive: isAlive,
    scope: {'widgetInspectorService': widgetInspectorServiceRef.id!},
  ).then((ref) => ref.valueAsString!);

  await safeEval(
    '() async {'
    '  final reader = widgetInspectorService.toObject("$readerId", "$readerGroup") as List;'
    '  try {'
    // Cast as dynamic so that it is possible to await Future<void>
    '    dynamic result = ($expression) as dynamic;'
    '    reader.add(result);'
    '  } catch (err, stack) {'
    '    reader.add(err);'
    '    reader.add(stack);'
    '  } finally {'
    '    postEvent("future_completed", {"future_id": $futureId, "client_id": $_clientId});'
    '  }'
    '}()',
    isAlive: isAlive,
    scope: {
      ...?scope,
      'postEvent': postEventRef.id!,
      'widgetInspectorService': widgetInspectorServiceRef.id!,
    },
  );

  await future;

  final resultRef = await evalInstance(
    '() {'
    '  final result = widgetInspectorService.toObject("$readerId", "$readerGroup") as List;'
    '  widgetInspectorService.disposeGroup("$readerGroup");'
    '  return result;'
    '}()',
    isAlive: isAlive,
    scope: {'widgetInspectorService': widgetInspectorServiceRef.id!},
  );

  assert(resultRef.length == 1 || resultRef.length == 2);
  if (resultRef.length == 2) {
    throw FutureFailedException(
      expression,
      resultRef.elements![0],
      resultRef.elements![1],
    );
  }

  return resultRef.elements![0];
}