c8f8190444
This was difficult to observe in practice, as it required a combination of two unlikely things: - an `async` function with no awaits - a custom Zone with side-effecting scheduleMicrotask callback The new implementation matches dart2js and the Dart VM's behavior. Change-Id: I4ff275f37aa93d23643481f00c33aeacb7138b01 Reviewed-on: https://dart-review.googlesource.com/58980 Commit-Queue: Jenny Messerly <jmesserly@google.com> Reviewed-by: Vijay Menon <vsm@google.com>
519 lines
16 KiB
Dart
519 lines
16 KiB
Dart
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
// Patch file for the dart:async library.
|
|
|
|
import 'dart:_js_helper'
|
|
show notNull, patch, setTraceForException, ReifyFunctionTypes;
|
|
import 'dart:_isolate_helper'
|
|
show TimerImpl, global, leaveJsAsync, enterJsAsync;
|
|
import 'dart:_foreign_helper' show JS, JSExportName;
|
|
import 'dart:_runtime' as dart;
|
|
|
|
typedef void _Callback();
|
|
typedef void _TakeCallback(_Callback callback);
|
|
|
|
/// This function adapts ES6 generators to implement Dart's async/await.
|
|
///
|
|
/// It's designed to interact with Dart's Future and follow Dart async/await
|
|
/// semantics.
|
|
///
|
|
/// See https://github.com/dart-lang/sdk/issues/27315 for ideas on reconciling
|
|
/// Dart's Future and ES6 Promise. At that point we should use native JS
|
|
/// async/await.
|
|
///
|
|
/// Inspired by `co`: https://github.com/tj/co/blob/master/index.js, which is a
|
|
/// stepping stone for ES async/await.
|
|
@JSExportName('async')
|
|
@ReifyFunctionTypes(false)
|
|
_async<T>(Function() initGenerator) {
|
|
var iter;
|
|
Object Function(Object) onValue;
|
|
Object Function(Object) onError;
|
|
|
|
onAwait(Object value) {
|
|
_Future f;
|
|
if (value is _Future) {
|
|
f = value;
|
|
} else if (value is Future) {
|
|
f = _Future();
|
|
_Future._chainForeignFuture(value, f);
|
|
} else {
|
|
f = _Future.value(value);
|
|
}
|
|
f = JS('', '#', f._thenNoZoneRegistration(onValue, onError));
|
|
return f;
|
|
}
|
|
|
|
onValue = (value) {
|
|
var iteratorResult = JS('', '#.next(#)', iter, value);
|
|
value = JS('', '#.value', iteratorResult);
|
|
return JS('bool', '#.done', iteratorResult) ? value : onAwait(value);
|
|
};
|
|
|
|
// If the awaited Future throws, we want to convert this to an exception
|
|
// thrown from the `yield` point, as if it was thrown there.
|
|
//
|
|
// If the exception is not caught inside `gen`, it will emerge here, which
|
|
// will send it to anyone listening on this async function's Future<T>.
|
|
//
|
|
// In essence, we are giving the code inside the generator a chance to
|
|
// use try-catch-finally.
|
|
onError = (value) {
|
|
var iteratorResult = JS('', '#.throw(#)', iter, value);
|
|
value = JS('', '#.value', iteratorResult);
|
|
return JS('bool', '#.done', iteratorResult) ? value : onAwait(value);
|
|
};
|
|
|
|
var zone = Zone.current;
|
|
if (!identical(zone, _rootZone)) {
|
|
onValue = zone.registerUnaryCallback(onValue);
|
|
onError = zone.registerUnaryCallback(onError);
|
|
}
|
|
|
|
var asyncFuture = _Future<T>();
|
|
|
|
// This will be set to true once we've yielded to the event loop.
|
|
//
|
|
// Before we've done that, we need to complete the future asynchronously to
|
|
// match dart2js/VM. See https://github.com/dart-lang/sdk/issues/33330
|
|
//
|
|
// Once we've yielded to the event loop we can complete synchronously.
|
|
// Other implementations call this `isSync` to indicate that.
|
|
bool isRunningAsEvent = false;
|
|
runBody() {
|
|
try {
|
|
iter = JS('', '#[Symbol.iterator]()', initGenerator());
|
|
var iteratorValue = JS('', '#.next(null)', iter);
|
|
var value = JS('', '#.value', iteratorValue);
|
|
if (JS('bool', '#.done', iteratorValue)) {
|
|
// TODO(jmesserly): this is a workaround for ignored cast failures.
|
|
// Remove it once we've fixed those. We should be able to call:
|
|
//
|
|
// if (isRunningAsEvent) {
|
|
// asyncFuture._complete(value);
|
|
// } else {
|
|
// asyncFuture._asyncComplete(value);
|
|
// }
|
|
//
|
|
// But if the user code returns `Future<dynamic>` instead of
|
|
// `Future<T>`, that function won't recognize it as a future and will
|
|
// instead treat it as a completed value.
|
|
if (value is Future) {
|
|
if (value is _Future) {
|
|
_Future._chainCoreFuture(value, asyncFuture);
|
|
} else {
|
|
_Future._chainForeignFuture(value, asyncFuture);
|
|
}
|
|
} else if (isRunningAsEvent) {
|
|
asyncFuture._completeWithValue(JS('', '#', value));
|
|
} else {
|
|
asyncFuture._asyncComplete(JS('', '#', value));
|
|
}
|
|
} else {
|
|
_Future._chainCoreFuture(onAwait(value), asyncFuture);
|
|
}
|
|
} catch (e, s) {
|
|
if (isRunningAsEvent) {
|
|
_completeWithErrorCallback(asyncFuture, e, s);
|
|
} else {
|
|
_asyncCompleteWithErrorCallback(asyncFuture, e, s);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dart.startAsyncSynchronously) {
|
|
runBody();
|
|
isRunningAsEvent = true;
|
|
} else {
|
|
isRunningAsEvent = true;
|
|
scheduleMicrotask(runBody);
|
|
}
|
|
return asyncFuture;
|
|
}
|
|
|
|
@patch
|
|
class _AsyncRun {
|
|
@patch
|
|
static void _scheduleImmediate(void callback()) {
|
|
_scheduleImmediateClosure(callback);
|
|
}
|
|
|
|
// Lazily initialized.
|
|
static final _TakeCallback _scheduleImmediateClosure =
|
|
_initializeScheduleImmediate();
|
|
|
|
static _TakeCallback _initializeScheduleImmediate() {
|
|
// TODO(rnystrom): Not needed by dev_compiler.
|
|
// requiresPreamble();
|
|
if (JS('', '#.scheduleImmediate', global) != null) {
|
|
return _scheduleImmediateJsOverride;
|
|
}
|
|
if (JS('', '#.MutationObserver', global) != null &&
|
|
JS('', '#.document', global) != null) {
|
|
// Use mutationObservers.
|
|
var div = JS('', '#.document.createElement("div")', global);
|
|
var span = JS('', '#.document.createElement("span")', global);
|
|
_Callback storedCallback;
|
|
|
|
internalCallback(_) {
|
|
leaveJsAsync();
|
|
var f = storedCallback;
|
|
storedCallback = null;
|
|
f();
|
|
}
|
|
|
|
;
|
|
|
|
var observer =
|
|
JS('', 'new #.MutationObserver(#)', global, internalCallback);
|
|
JS('', '#.observe(#, { childList: true })', observer, div);
|
|
|
|
return (void callback()) {
|
|
assert(storedCallback == null);
|
|
enterJsAsync();
|
|
storedCallback = callback;
|
|
// Because of a broken shadow-dom polyfill we have to change the
|
|
// children instead a cheap property.
|
|
// See https://github.com/Polymer/ShadowDOM/issues/468
|
|
JS('', '#.firstChild ? #.removeChild(#): #.appendChild(#)', div, div,
|
|
span, div, span);
|
|
};
|
|
} else if (JS('', '#.setImmediate', global) != null) {
|
|
return _scheduleImmediateWithSetImmediate;
|
|
}
|
|
// TODO(20055): We should use DOM promises when available.
|
|
return _scheduleImmediateWithTimer;
|
|
}
|
|
|
|
static void _scheduleImmediateJsOverride(void callback()) {
|
|
internalCallback() {
|
|
leaveJsAsync();
|
|
callback();
|
|
}
|
|
|
|
;
|
|
enterJsAsync();
|
|
JS('void', '#.scheduleImmediate(#)', global, internalCallback);
|
|
}
|
|
|
|
static void _scheduleImmediateWithSetImmediate(void callback()) {
|
|
internalCallback() {
|
|
leaveJsAsync();
|
|
callback();
|
|
}
|
|
|
|
;
|
|
enterJsAsync();
|
|
JS('void', '#.setImmediate(#)', global, internalCallback);
|
|
}
|
|
|
|
static void _scheduleImmediateWithTimer(void callback()) {
|
|
Timer._createTimer(Duration.zero, callback);
|
|
}
|
|
}
|
|
|
|
@patch
|
|
class DeferredLibrary {
|
|
@patch
|
|
Future<Null> load() {
|
|
throw 'DeferredLibrary not supported. '
|
|
'please use the `import "lib.dart" deferred as lib` syntax.';
|
|
}
|
|
}
|
|
|
|
@patch
|
|
class Timer {
|
|
@patch
|
|
static Timer _createTimer(Duration duration, void callback()) {
|
|
int milliseconds = duration.inMilliseconds;
|
|
if (milliseconds < 0) milliseconds = 0;
|
|
return TimerImpl(milliseconds, callback);
|
|
}
|
|
|
|
@patch
|
|
static Timer _createPeriodicTimer(
|
|
Duration duration, void callback(Timer timer)) {
|
|
int milliseconds = duration.inMilliseconds;
|
|
if (milliseconds < 0) milliseconds = 0;
|
|
return TimerImpl.periodic(milliseconds, callback);
|
|
}
|
|
}
|
|
|
|
@patch
|
|
void _rethrow(Object error, StackTrace stackTrace) {
|
|
setTraceForException(error, stackTrace);
|
|
dart.throw_(error);
|
|
}
|
|
|
|
/// Used by the compiler to implement `async*` functions.
|
|
///
|
|
/// This is inspired by _AsyncStarStreamController in dart-lang/sdk's
|
|
/// runtime/lib/core_patch.dart
|
|
///
|
|
/// Given input like:
|
|
///
|
|
/// foo() async* {
|
|
/// yield 1;
|
|
/// yield* bar();
|
|
/// print(await baz());
|
|
/// }
|
|
///
|
|
/// This compiles to:
|
|
///
|
|
/// function foo() {
|
|
/// return new (AsyncStarImplOfT()).new(function*(stream) {
|
|
/// if (stream.add(1)) return;
|
|
/// yield;
|
|
/// if (stream.addStream(bar()) return;
|
|
/// yield;
|
|
/// print(yield baz());
|
|
/// });
|
|
/// }
|
|
///
|
|
class _AsyncStarImpl<T> {
|
|
StreamController<T> controller;
|
|
Object Function(_AsyncStarImpl<T>) initGenerator;
|
|
@notNull
|
|
bool isSuspendedAtYieldStar = false;
|
|
@notNull
|
|
bool onListenReceived = false;
|
|
@notNull
|
|
bool isScheduled = false;
|
|
@notNull
|
|
bool isSuspendedAtYield = false;
|
|
|
|
/// Whether we're suspended at an `await`.
|
|
@notNull
|
|
bool isSuspendedAtAwait = false;
|
|
|
|
Completer cancellationCompleter;
|
|
Object jsIterator;
|
|
|
|
Null Function(Object, StackTrace) _handleErrorCallback;
|
|
void Function([Object]) _runBodyCallback;
|
|
|
|
_AsyncStarImpl(this.initGenerator) {
|
|
controller = StreamController(
|
|
onListen: JS('!', 'this.onListen.bind(this)'),
|
|
onResume: JS('!', 'this.onResume.bind(this)'),
|
|
onCancel: JS('!', 'this.onCancel.bind(this)'));
|
|
jsIterator = JS('!', '#[Symbol.iterator]()', initGenerator(this));
|
|
}
|
|
|
|
/// The stream produced by this `async*` function.
|
|
Stream<T> get stream => controller.stream;
|
|
|
|
/// Returns the callback used for error handling.
|
|
///
|
|
/// This callback throws the error back into the user code, at the appropriate
|
|
/// location (e.g. `await` `yield` or `yield*`). This gives user code a chance
|
|
/// to handle it try-catch. If they do not handle, the error gets routed to
|
|
/// the [stream] as an error via [addError].
|
|
///
|
|
/// As a performance optimization, this callback is only bound once to the
|
|
/// current [Zone]. This works because a single subscription stream should
|
|
/// always be running in its original zone. An `async*` method will always
|
|
/// save/restore the zone that was active when `listen()` was first called,
|
|
/// similar to a stream. This follows from section 16.14 of the Dart 4th
|
|
/// edition spec:
|
|
///
|
|
/// > If `f` is marked `async*` (9), then a fresh instance `s` implementing
|
|
/// > the built-in class `Stream` is associated with the invocation and
|
|
/// > immediately returned. When `s` is listened to, execution of the body of
|
|
/// > `f` will begin.
|
|
///
|
|
Null Function(Object, StackTrace) get handleError {
|
|
if (_handleErrorCallback == null) {
|
|
_handleErrorCallback = (error, StackTrace stackTrace) {
|
|
try {
|
|
JS('', '#.throw(#)', jsIterator, error);
|
|
} catch (e) {
|
|
addError(e, stackTrace);
|
|
}
|
|
};
|
|
var zone = Zone.current;
|
|
if (!identical(zone, Zone.root)) {
|
|
_handleErrorCallback = zone.bindBinaryCallback(_handleErrorCallback);
|
|
}
|
|
}
|
|
return _handleErrorCallback;
|
|
}
|
|
|
|
void scheduleGenerator() {
|
|
// TODO(jmesserly): is this isPaused check in the right place? Assuming the
|
|
// async* Stream yields, then is paused (by other code), the body will
|
|
// already be scheduled. This will cause at least one more iteration to
|
|
// run (adding another data item to the Stream) before actually pausing.
|
|
// It could be fixed by moving the `isPaused` check inside `runBody`.
|
|
if (isScheduled ||
|
|
controller.isPaused ||
|
|
isSuspendedAtYieldStar ||
|
|
isSuspendedAtAwait) {
|
|
return;
|
|
}
|
|
isScheduled = true;
|
|
// Capture the current zone. See comment on [handleError] for more
|
|
// information about this optimization.
|
|
var zone = Zone.current;
|
|
if (_runBodyCallback == null) {
|
|
_runBodyCallback = JS('!', '#.bind(this)', runBody);
|
|
if (!identical(zone, Zone.root)) {
|
|
var registered = zone.registerUnaryCallback(_runBodyCallback);
|
|
_runBodyCallback = ([arg]) => zone.runUnaryGuarded(registered, arg);
|
|
}
|
|
}
|
|
zone.scheduleMicrotask(_runBodyCallback);
|
|
}
|
|
|
|
void runBody(awaitValue) {
|
|
isScheduled = false;
|
|
isSuspendedAtYield = false;
|
|
isSuspendedAtAwait = false;
|
|
|
|
Object iterResult;
|
|
try {
|
|
iterResult = JS('', '#.next(#)', jsIterator, awaitValue);
|
|
} catch (e, s) {
|
|
addError(e, s);
|
|
close();
|
|
return null;
|
|
}
|
|
|
|
if (JS('!', '#.done', iterResult)) {
|
|
close();
|
|
return null;
|
|
}
|
|
|
|
// If we're suspended at a yield/yield*, we're done for now.
|
|
if (isSuspendedAtYield || isSuspendedAtYieldStar) return null;
|
|
|
|
// Handle `await`: if we get a value passed to `yield` it means we are
|
|
// waiting on this Future. Make sure to prevent scheduling, and pass the
|
|
// value back as the result of the `yield`.
|
|
//
|
|
// TODO(jmesserly): is the timing here correct? The assumption here is
|
|
// that we should schedule `await` in `async*` the same as in `async`.
|
|
isSuspendedAtAwait = true;
|
|
FutureOr<Object> value = JS('', '#.value', iterResult);
|
|
|
|
// TODO(jmesserly): this logic was copied from `async` function impl.
|
|
_Future f;
|
|
if (value is _Future) {
|
|
f = value;
|
|
} else if (value is Future) {
|
|
f = _Future();
|
|
_Future._chainForeignFuture(value, f);
|
|
} else {
|
|
f = _Future.value(value);
|
|
}
|
|
f._thenNoZoneRegistration(_runBodyCallback, handleError);
|
|
}
|
|
|
|
/// Adds element to [stream] and returns true if the caller should terminate
|
|
/// execution of the generator.
|
|
///
|
|
/// This is called from generated code like this:
|
|
///
|
|
/// if (controller.add(1)) return;
|
|
/// yield;
|
|
//
|
|
// TODO(hausner): Per spec, the generator should be suspended before exiting
|
|
// when the stream is closed. We could add a getter like this:
|
|
//
|
|
// get isCancelled => controller.hasListener;
|
|
//
|
|
// The generator would translate a 'yield e' statement to
|
|
//
|
|
// controller.add(1);
|
|
// suspend; // this is `yield` in JS.
|
|
// if (controller.isCancelled) return;
|
|
bool add(T event) {
|
|
if (!onListenReceived) _fatal("yield before stream is listened to");
|
|
if (isSuspendedAtYield) _fatal("unexpected yield");
|
|
// If stream is cancelled, tell caller to exit the async generator.
|
|
if (!controller.hasListener) {
|
|
return true;
|
|
}
|
|
controller.add(event);
|
|
scheduleGenerator();
|
|
isSuspendedAtYield = true;
|
|
return false;
|
|
}
|
|
|
|
/// Adds the elements of [stream] into this [controller]'s stream, and returns
|
|
/// true if the caller should terminate execution of the generator.
|
|
///
|
|
/// The generator will be scheduled again when all of the elements of the
|
|
/// added stream have been consumed.
|
|
bool addStream(Stream<T> stream) {
|
|
if (!onListenReceived) _fatal("yield* before stream is listened to");
|
|
// If stream is cancelled, tell caller to exit the async generator.
|
|
if (!controller.hasListener) return true;
|
|
isSuspendedAtYieldStar = true;
|
|
var whenDoneAdding = controller.addStream(stream, cancelOnError: false);
|
|
whenDoneAdding.then((_) {
|
|
isSuspendedAtYieldStar = false;
|
|
scheduleGenerator();
|
|
if (!isScheduled) isSuspendedAtYield = true;
|
|
}, onError: handleError);
|
|
return false;
|
|
}
|
|
|
|
void addError(Object error, StackTrace stackTrace) {
|
|
if (cancellationCompleter != null && !cancellationCompleter.isCompleted) {
|
|
// If the stream has been cancelled, complete the cancellation future
|
|
// with the error.
|
|
cancellationCompleter.completeError(error, stackTrace);
|
|
return;
|
|
}
|
|
// If stream is cancelled, tell caller to exit the async generator.
|
|
if (!controller.hasListener) return;
|
|
controller.addError(error, stackTrace);
|
|
// No need to schedule the generator body here. This code is only
|
|
// called from the catch clause of the implicit try-catch-finally
|
|
// around the generator body. That is, we are on the error path out
|
|
// of the generator and do not need to run the generator again.
|
|
}
|
|
|
|
close() {
|
|
if (cancellationCompleter != null && !cancellationCompleter.isCompleted) {
|
|
// If the stream has been cancelled, complete the cancellation future
|
|
// with the error.
|
|
cancellationCompleter.complete();
|
|
}
|
|
controller.close();
|
|
}
|
|
|
|
onListen() {
|
|
assert(!onListenReceived);
|
|
onListenReceived = true;
|
|
scheduleGenerator();
|
|
}
|
|
|
|
onResume() {
|
|
if (isSuspendedAtYield) {
|
|
scheduleGenerator();
|
|
}
|
|
}
|
|
|
|
onCancel() {
|
|
if (controller.isClosed) {
|
|
return null;
|
|
}
|
|
if (cancellationCompleter == null) {
|
|
cancellationCompleter = Completer();
|
|
// Only resume the generator if it is suspended at a yield.
|
|
// Cancellation does not affect an async generator that is
|
|
// suspended at an await.
|
|
if (isSuspendedAtYield) {
|
|
scheduleGenerator();
|
|
}
|
|
}
|
|
return cancellationCompleter.future;
|
|
}
|
|
|
|
_fatal(String message) => throw StateError(message);
|
|
}
|