Reapply zone tasks.
This reverts commit 34d3c37233f3beb6d1dfb5b23d23c0a674419661.
Committed: https://github.com/dart-lang/sdk/commit/f746f8f77e93fd93cb4ff3b4ed644c96a432947a
[email protected]
Review URL: https://codereview.chromium.org/2119243002 .
Reverted: https://github.com/dart-lang/sdk/commit/dae392291595fa394d601d2d0baa77b66d85533f
diff --git a/sdk/lib/async/timer.dart b/sdk/lib/async/timer.dart
index 1bbb65b..135c868 100644
--- a/sdk/lib/async/timer.dart
+++ b/sdk/lib/async/timer.dart
@@ -4,6 +4,84 @@
part of dart.async;
+abstract class _TimerTask implements Timer {
+ final Zone _zone;
+ final Timer _nativeTimer;
+
+ _TimerTask(this._nativeTimer, this._zone);
+
+ void cancel() {
+ _nativeTimer.cancel();
+ }
+
+ bool get isActive => _nativeTimer.isActive;
+}
+
+class _SingleShotTimerTask extends _TimerTask {
+ // TODO(floitsch): the generic argument should be 'void'.
+ final ZoneCallback<dynamic> _callback;
+
+ _SingleShotTimerTask(Timer timer, this._callback, Zone zone)
+ : super(timer, zone);
+}
+
+class _PeriodicTimerTask extends _TimerTask {
+ // TODO(floitsch): the first generic argument should be 'void'.
+ final ZoneUnaryCallback<dynamic, Timer> _callback;
+
+ _PeriodicTimerTask(Timer timer, this._callback, Zone zone)
+ : super(timer, zone);
+}
+
+/**
+ * A task specification for a single-shot timer.
+ *
+ * *Experimental*. Might disappear without notice.
+ */
+class SingleShotTimerTaskSpecification implements TaskSpecification {
+ static const String specificationName = "dart.async.timer";
+
+ /** The duration after which the timer should invoke the [callback]. */
+ final Duration duration;
+
+ /** The callback that should be run when the timer triggers. */
+ // TODO(floitsch): the generic argument should be void.
+ final ZoneCallback<dynamic> callback;
+
+ SingleShotTimerTaskSpecification(this.duration, void this.callback());
+
+ @override
+ String get name => specificationName;
+
+ @override
+ bool get isOneShot => true;
+}
+
+/**
+ * A task specification for a periodic timer.
+ *
+ * *Experimental*. Might disappear without notice.
+ */
+class PeriodicTimerTaskSpecification implements TaskSpecification {
+ static const String specificationName = "dart.async.periodic-timer";
+
+ /** The interval at which the periodic timer should invoke the [callback]. */
+ final Duration duration;
+
+ /** The callback that should be run when the timer triggers. */
+ // TODO(floitsch): the first generic argument should be void.
+ final ZoneUnaryCallback<dynamic, Timer> callback;
+
+ PeriodicTimerTaskSpecification(
+ this.duration, void this.callback(Timer timer));
+
+ @override
+ String get name => specificationName;
+
+ @override
+ bool get isOneShot => false;
+}
+
/**
* A count-down timer that can be configured to fire once or repeatedly.
*
@@ -47,10 +125,15 @@
if (Zone.current == Zone.ROOT) {
// No need to bind the callback. We know that the root's timer will
// be invoked in the root zone.
- return Zone.current.createTimer(duration, callback);
+ return Timer._createTimer(duration, callback);
}
- return Zone.current.createTimer(
- duration, Zone.current.bindCallback(callback, runGuarded: true));
+ return Zone.current.createTimer(duration, callback);
+ }
+
+ factory Timer._task(Zone zone, Duration duration, void callback()) {
+ SingleShotTimerTaskSpecification specification =
+ new SingleShotTimerTaskSpecification(duration, callback);
+ return zone.createTask(_createSingleShotTimerTask, specification);
}
/**
@@ -70,17 +153,65 @@
* scheduled for - even if the actual callback was delayed.
*/
factory Timer.periodic(Duration duration,
- void callback(Timer timer)) {
+ void callback(Timer timer)) {
if (Zone.current == Zone.ROOT) {
// No need to bind the callback. We know that the root's timer will
// be invoked in the root zone.
- return Zone.current.createPeriodicTimer(duration, callback);
+ return Timer._createPeriodicTimer(duration, callback);
}
+ return Zone.current.createPeriodicTimer(duration, callback);
+ }
+
+ factory Timer._periodicTask(Zone zone, Duration duration,
+ void callback(Timer timer)) {
+ PeriodicTimerTaskSpecification specification =
+ new PeriodicTimerTaskSpecification(duration, callback);
+ return zone.createTask(_createPeriodicTimerTask, specification);
+ }
+
+ static Timer _createSingleShotTimerTask(
+ SingleShotTimerTaskSpecification specification, Zone zone) {
+ ZoneCallback registeredCallback = identical(_ROOT_ZONE, zone)
+ ? specification.callback
+ : zone.registerCallback(specification.callback);
+
+ _TimerTask timerTask;
+
+ Timer nativeTimer = Timer._createTimer(specification.duration, () {
+ timerTask._zone.runTask(_runSingleShotCallback, timerTask, null);
+ });
+
+ timerTask = new _SingleShotTimerTask(nativeTimer, registeredCallback, zone);
+ return timerTask;
+ }
+
+ static void _runSingleShotCallback(_SingleShotTimerTask timerTask, Object _) {
+ timerTask._callback();
+ }
+
+ static Timer _createPeriodicTimerTask(
+ PeriodicTimerTaskSpecification specification, Zone zone) {
// TODO(floitsch): the return type should be 'void', and the type
// should be inferred.
- var boundCallback = Zone.current.bindUnaryCallback/*<dynamic, Timer>*/(
- callback, runGuarded: true);
- return Zone.current.createPeriodicTimer(duration, boundCallback);
+ ZoneUnaryCallback<dynamic, Timer> registeredCallback =
+ identical(_ROOT_ZONE, zone)
+ ? specification.callback
+ : zone.registerUnaryCallback/*<dynamic, Timer>*/(
+ specification.callback);
+
+ _TimerTask timerTask;
+
+ Timer nativeTimer =
+ Timer._createPeriodicTimer(specification.duration, (Timer _) {
+ timerTask._zone.runTask(_runPeriodicCallback, timerTask, null);
+ });
+
+ timerTask = new _PeriodicTimerTask(nativeTimer, registeredCallback, zone);
+ return timerTask;
+ }
+
+ static void _runPeriodicCallback(_PeriodicTimerTask timerTask, Object _) {
+ timerTask._callback(timerTask);
}
/**
diff --git a/sdk/lib/async/zone.dart b/sdk/lib/async/zone.dart
index 24f83f8..e0c3e4b 100644
--- a/sdk/lib/async/zone.dart
+++ b/sdk/lib/async/zone.dart
@@ -8,6 +8,13 @@
typedef R ZoneUnaryCallback<R, T>(T arg);
typedef R ZoneBinaryCallback<R, T1, T2>(T1 arg1, T2 arg2);
+/// *Experimental*. Might disappear without warning.
+typedef T TaskCreate<T, S extends TaskSpecification>(
+ S specification, Zone zone);
+/// *Experimental*. Might disappear without warning.
+typedef void TaskRun<T, A>(T task, A arg);
+
+
// TODO(floitsch): we are abusing generic typedefs as typedefs for generic
// functions.
/*ABUSE*/
@@ -33,19 +40,31 @@
Zone self, ZoneDelegate parent, Zone zone, R f(T1 arg1, T2 arg2));
typedef AsyncError ErrorCallbackHandler(Zone self, ZoneDelegate parent,
Zone zone, Object error, StackTrace stackTrace);
+/// *Experimental*. Might disappear without warning.
+/*ABUSE*/
+typedef T CreateTaskHandler<T, S extends TaskSpecification>(
+ Zone self, ZoneDelegate parent, Zone zone,
+ TaskCreate<T, S> create, S taskSpecification);
+/// *Experimental*. Might disappear without warning.
+/*ABUSE*/
+typedef void RunTaskHandler<T, A>(Zone self, ZoneDelegate parent, Zone zone,
+ TaskRun<T, A> run, T task, A arg);
typedef void ScheduleMicrotaskHandler(
Zone self, ZoneDelegate parent, Zone zone, void f());
-typedef Timer CreateTimerHandler(
- Zone self, ZoneDelegate parent, Zone zone, Duration duration, void f());
-typedef Timer CreatePeriodicTimerHandler(
- Zone self, ZoneDelegate parent, Zone zone,
- Duration period, void f(Timer timer));
typedef void PrintHandler(
Zone self, ZoneDelegate parent, Zone zone, String line);
typedef Zone ForkHandler(Zone self, ZoneDelegate parent, Zone zone,
ZoneSpecification specification,
Map zoneValues);
+// The following typedef declarations are used by functionality which
+// will be removed and replaced by tasksif the task experiment is successful.
+typedef Timer CreateTimerHandler(
+ Zone self, ZoneDelegate parent, Zone zone, Duration duration, void f());
+typedef Timer CreatePeriodicTimerHandler(
+ Zone self, ZoneDelegate parent, Zone zone,
+ Duration period, void f(Timer timer));
+
/** Pair of error and stack trace. Returned by [Zone.errorCallback]. */
class AsyncError implements Error {
final Object error;
@@ -56,10 +75,41 @@
String toString() => '$error';
}
+/**
+ * A task specification contains the necessary information to create a task.
+ *
+ * See [Zone.createTask] for how a specification is used to create a task.
+ *
+ * Task specifications should be public and it should be possible to create
+ * new instances as a user. That is, custom zones should be able to replace
+ * an existing specification with a modified one.
+ *
+ * *Experimental*. This class might disappear without warning.
+ */
+abstract class TaskSpecification {
+ /**
+ * Description of the task.
+ *
+ * This string is unused by the root-zone, but might be used for debugging,
+ * and testing. As such, it should be relatively unique in its category.
+ *
+ * As a general guideline we recommend: "package-name.library.action".
+ */
+ String get name;
+
+ /**
+ * Whether the scheduled task triggers at most once.
+ *
+ * If the task is not a one-shot task, it may need to be canceled to prevent
+ * further iterations of the task.
+ */
+ bool get isOneShot;
+}
class _ZoneFunction<T extends Function> {
final _Zone zone;
final T function;
+
const _ZoneFunction(this.zone, this.function);
}
@@ -85,6 +135,9 @@
abstract class ZoneSpecification {
/**
* Creates a specification with the provided handlers.
+ *
+ * The task-related parameters ([createTask] and [runTask]) are experimental
+ * and might be removed without warning.
*/
const factory ZoneSpecification({
HandleUncaughtErrorHandler handleUncaughtError,
@@ -96,7 +149,11 @@
RegisterBinaryCallbackHandler registerBinaryCallback,
ErrorCallbackHandler errorCallback,
ScheduleMicrotaskHandler scheduleMicrotask,
+ CreateTaskHandler createTask,
+ RunTaskHandler runTask,
+ // TODO(floitsch): mark as deprecated once tasks are non-experimental.
CreateTimerHandler createTimer,
+ // TODO(floitsch): mark as deprecated once tasks are non-experimental.
CreatePeriodicTimerHandler createPeriodicTimer,
PrintHandler print,
ForkHandler fork
@@ -105,6 +162,9 @@
/**
* Creates a specification from [other] with the provided handlers overriding
* the ones in [other].
+ *
+ * The task-related parameters ([createTask] and [runTask]) are experimental
+ * and might be removed without warning.
*/
factory ZoneSpecification.from(ZoneSpecification other, {
HandleUncaughtErrorHandler handleUncaughtError: null,
@@ -116,7 +176,11 @@
RegisterBinaryCallbackHandler registerBinaryCallback: null,
ErrorCallbackHandler errorCallback: null,
ScheduleMicrotaskHandler scheduleMicrotask: null,
+ CreateTaskHandler createTask: null,
+ RunTaskHandler runTask: null,
+ // TODO(floitsch): mark as deprecated once tasks are non-experimental.
CreateTimerHandler createTimer: null,
+ // TODO(floitsch): mark as deprecated once tasks are non-experimental.
CreatePeriodicTimerHandler createPeriodicTimer: null,
PrintHandler print: null,
ForkHandler fork: null
@@ -132,11 +196,14 @@
registerBinaryCallback: registerBinaryCallback ??
other.registerBinaryCallback,
errorCallback: errorCallback ?? other.errorCallback,
+
+ createTask: createTask ?? other.createTask,
+ runTask: runTask ?? other.runTask,
+ print : print ?? other.print,
+ fork: fork ?? other.fork,
scheduleMicrotask: scheduleMicrotask ?? other.scheduleMicrotask,
createTimer : createTimer ?? other.createTimer,
- createPeriodicTimer: createPeriodicTimer ?? other.createPeriodicTimer,
- print : print ?? other.print,
- fork: fork ?? other.fork);
+ createPeriodicTimer: createPeriodicTimer ?? other.createPeriodicTimer);
}
HandleUncaughtErrorHandler get handleUncaughtError;
@@ -148,10 +215,17 @@
RegisterBinaryCallbackHandler get registerBinaryCallback;
ErrorCallbackHandler get errorCallback;
ScheduleMicrotaskHandler get scheduleMicrotask;
- CreateTimerHandler get createTimer;
- CreatePeriodicTimerHandler get createPeriodicTimer;
+ /// *Experimental*. Might disappear without warning.
+ CreateTaskHandler get createTask;
+ /// *Experimental*. Might disappear without warning.
+ RunTaskHandler get runTask;
PrintHandler get print;
ForkHandler get fork;
+
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ CreateTimerHandler get createTimer;
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ CreatePeriodicTimerHandler get createPeriodicTimer;
}
/**
@@ -172,10 +246,14 @@
this.registerBinaryCallback: null,
this.errorCallback: null,
this.scheduleMicrotask: null,
- this.createTimer: null,
- this.createPeriodicTimer: null,
+ this.createTask: null,
+ this.runTask: null,
this.print: null,
- this.fork: null
+ this.fork: null,
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ this.createTimer: null,
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ this.createPeriodicTimer: null
});
final HandleUncaughtErrorHandler handleUncaughtError;
@@ -187,10 +265,15 @@
final RegisterBinaryCallbackHandler registerBinaryCallback;
final ErrorCallbackHandler errorCallback;
final ScheduleMicrotaskHandler scheduleMicrotask;
- final CreateTimerHandler createTimer;
- final CreatePeriodicTimerHandler createPeriodicTimer;
+ final CreateTaskHandler createTask;
+ final RunTaskHandler runTask;
final PrintHandler print;
final ForkHandler fork;
+
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ final CreateTimerHandler createTimer;
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ final CreatePeriodicTimerHandler createPeriodicTimer;
}
/**
@@ -217,10 +300,23 @@
Zone zone, /*=R*/ f(/*=T1*/ arg1, /*=T2*/ arg2));
AsyncError errorCallback(Zone zone, Object error, StackTrace stackTrace);
void scheduleMicrotask(Zone zone, void f());
- Timer createTimer(Zone zone, Duration duration, void f());
- Timer createPeriodicTimer(Zone zone, Duration period, void f(Timer timer));
+
+ /// *Experimental*. Might disappear without notice.
+ Object/*=T*/ createTask/*<T, S extends TaskSpecification>*/(
+ Zone zone, TaskCreate/*<T, S>*/ create,
+ TaskSpecification/*=S*/ specification);
+ /// *Experimental*. Might disappear without notice.
+ void runTask/*<T, A>*/(
+ Zone zone, TaskRun/*<T, A>*/ run, Object/*=T*/ task,
+ Object/*=A*/ argument);
+
void print(Zone zone, String line);
Zone fork(Zone zone, ZoneSpecification specification, Map zoneValues);
+
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ Timer createTimer(Zone zone, Duration duration, void f());
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ Timer createPeriodicTimer(Zone zone, Duration period, void f(Timer timer));
}
/**
@@ -411,13 +507,102 @@
void scheduleMicrotask(void f());
/**
+ * Creates a task in the current zone.
+ *
+ * A task represents an asynchronous operation or process that reports back
+ * through the event loop.
+ *
+ * This function allows the zone to intercept the initialization of the
+ * task while the [runTask] function is invoked when the task reports back.
+ *
+ * By default, in the root zone, the [create] function is invoked with the
+ * [specification] as argument. It returns a task object which is used for all
+ * future interactions between the zone and the task. The object is
+ * a unique instance representing the task. It is generally returned to
+ * whoever initiated the task.
+ * For example, the HTML library uses the returned [StreamSubscription] as
+ * task object when users register an event listener.
+ *
+ * Tasks are created when the program starts an operation that reports back
+ * through the event loop. For example, a timer or an HTTP request both
+ * return through the event loop and are therefore tasks.
+ *
+ * If the [create] function is not invoked (because a custom zone has
+ * replaced or intercepted it), then the operation is *not* started. This
+ * means that a custom zone can intercept tasks, like HTTP requests.
+ *
+ * A task goes through the following steps:
+ * - a user invokes a library function that should eventually return through
+ * the event loop.
+ * - the library function creates a [TaskSpecification] that contains the
+ * necessary information to start the operation, and invokes
+ * `Zone.current.createTask` with the specification and a [create] closure.
+ * The closure, when invoked, uses the specification to start the operation
+ * (usually by interacting with the underlying system, or as a native
+ * extension), and returns a task object that identifies the running task.
+ * - custom zones handle the request and (unless completely intercepted and
+ * aborted), end up calling the root zone's [createTask] which runs the
+ * provided `create` closure, which may have been replaced at this point.
+ * - later, the asynchronous operation returns through the event loop.
+ * It invokes [Zone.runTask] on the zone in which the task should run
+ * (and which was originally passed to the `create` function by
+ * `createTask`). The [runTask] function receives the
+ * task object, a `run` function and an argument. As before, custom zones
+ * may intercept this call. Eventually (unless aborted), the `run` function
+ * is invoked. This last step may happen multiple times for tasks that are
+ * not oneshot tasks (see [ZoneSpecification.isOneShot]).
+ *
+ * Custom zones may replace the [specification] with a different one, thus
+ * modifying the task parameters. An operation that wishes to be an
+ * interceptable task must publicly specify the types that intercepting code
+ * sees:
+ * - The specification type (extending [TaskSpecification]) which holds the
+ * information available when intercepting the `createTask` call.
+ * - The task object type, returned by `createTask` and [create]. This object
+ * may simply be typed as [Object].
+ * - The argument type, if [runTask] takes a meaningful argument.
+ *
+ * *Experimental*. Might disappear without notice.
+ */
+ Object/*=T*/ createTask/*<T, S extends TaskSpecification>*/(
+ /*=T*/ create(TaskSpecification/*=S*/ specification, Zone zone),
+ TaskSpecification/*=S*/ specification);
+
+ /**
+ * Runs a task callback.
+ *
+ * This function is invoked when an operation, started through [createTask],
+ * generates an event.
+ *
+ * Generally, tasks schedule Dart code in the global event loop when the
+ * [createTask] function is invoked. Since the
+ * event loop does not expect any return value from the code it runs, the
+ * [runTask] function is a void function.
+ *
+ * The [task] object must be the same as the one created with [createTask].
+ *
+ * It is good practice that task operations provide a meaningful [argument],
+ * so that custom zones can interact with it. They might want to log or
+ * replace the argument before calling the [run] function.
+ *
+ * See [createTask].
+ *
+ * *Experimental*. Might disappear without notice.
+ */
+ void runTask/*<T, A>*/(
+ /*=T*/ run(/*=T*/ task, /*=A*/ argument), Object/*=T*/ task,
+ Object/*=A*/ argument);
+
+ /**
* Creates a Timer where the callback is executed in this zone.
*/
+ // TODO(floitsch): deprecate once tasks are non-experimental.
Timer createTimer(Duration duration, void callback());
/**
* Creates a periodic Timer where the callback is executed in this zone.
*/
+ // TODO(floitsch): deprecate once tasks are non-experimental.
Timer createPeriodicTimer(Duration period, void callback(Timer timer));
/**
@@ -523,7 +708,7 @@
// TODO(floitsch): make this a generic method call on '<R>' once it's
// supported. Remove the unnecessary cast.
return handler(implZone, _parentDelegate(implZone), zone, f)
- as Object/*=ZoneCallback<R>*/;
+ as dynamic/*=ZoneCallback<R>*/;
}
ZoneUnaryCallback/*<R, T>*/ registerUnaryCallback/*<R, T>*/(
@@ -534,7 +719,7 @@
// TODO(floitsch): make this a generic method call on '<R, T>' once it's
// supported. Remove the unnecessary cast.
return handler(implZone, _parentDelegate(implZone), zone, f)
- as Object/*=ZoneUnaryCallback<R, T>*/;
+ as dynamic/*=ZoneUnaryCallback<R, T>*/;
}
ZoneBinaryCallback/*<R, T1, T2>*/ registerBinaryCallback/*<R, T1, T2>*/(
@@ -545,7 +730,7 @@
// TODO(floitsch): make this a generic method call on '<R, T1, T2>' once
// it's supported. Remove the unnecessary cast.
return handler(implZone, _parentDelegate(implZone), zone, f)
- as Object/*=ZoneBinaryCallback<R, T1, T2>*/;
+ as dynamic/*=ZoneBinaryCallback<R, T1, T2>*/;
}
AsyncError errorCallback(Zone zone, Object error, StackTrace stackTrace) {
@@ -564,18 +749,25 @@
handler(implZone, _parentDelegate(implZone), zone, f);
}
- Timer createTimer(Zone zone, Duration duration, void f()) {
- var implementation = _delegationTarget._createTimer;
+ Object/*=T*/ createTask/*<T, S extends TaskSpecification>*/(
+ Zone zone, TaskCreate/*<T, S>*/ create, TaskSpecification/*=S*/ specification) {
+ var implementation = _delegationTarget._createTask;
_Zone implZone = implementation.zone;
- CreateTimerHandler handler = implementation.function;
- return handler(implZone, _parentDelegate(implZone), zone, duration, f);
+ // TODO(floitsch): make the handler call a generic method call on '<T, S>'
+ // once it's supported. Remove the unnecessary cast.
+ var handler =
+ implementation.function as CreateTaskHandler/*<T, S>*/;
+ return handler(
+ implZone, _parentDelegate(implZone), zone, create, specification);
}
- Timer createPeriodicTimer(Zone zone, Duration period, void f(Timer timer)) {
- var implementation = _delegationTarget._createPeriodicTimer;
+ void runTask/*<T, A>*/(Zone zone, TaskRun run, Object /*=T*/ task,
+ Object /*=A*/ argument) {
+ var implementation = _delegationTarget._runTask;
_Zone implZone = implementation.zone;
- CreatePeriodicTimerHandler handler = implementation.function;
- return handler(implZone, _parentDelegate(implZone), zone, period, f);
+ RunTaskHandler handler = implementation.function;
+ // TODO(floitsch): make this a generic call on '<T, A>'.
+ handler(implZone, _parentDelegate(implZone), zone, run, task, argument);
}
void print(Zone zone, String line) {
@@ -593,6 +785,22 @@
return handler(
implZone, _parentDelegate(implZone), zone, specification, zoneValues);
}
+
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ Timer createTimer(Zone zone, Duration duration, void f()) {
+ var implementation = _delegationTarget._createTimer;
+ _Zone implZone = implementation.zone;
+ CreateTimerHandler handler = implementation.function;
+ return handler(implZone, _parentDelegate(implZone), zone, duration, f);
+ }
+
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ Timer createPeriodicTimer(Zone zone, Duration period, void f(Timer timer)) {
+ var implementation = _delegationTarget._createPeriodicTimer;
+ _Zone implZone = implementation.zone;
+ CreatePeriodicTimerHandler handler = implementation.function;
+ return handler(implZone, _parentDelegate(implZone), zone, period, f);
+ }
}
@@ -610,11 +818,17 @@
_ZoneFunction<RegisterBinaryCallbackHandler> get _registerBinaryCallback;
_ZoneFunction<ErrorCallbackHandler> get _errorCallback;
_ZoneFunction<ScheduleMicrotaskHandler> get _scheduleMicrotask;
- _ZoneFunction<CreateTimerHandler> get _createTimer;
- _ZoneFunction<CreatePeriodicTimerHandler> get _createPeriodicTimer;
+ _ZoneFunction<CreateTaskHandler> get _createTask;
+ _ZoneFunction<RunTaskHandler> get _runTask;
_ZoneFunction<PrintHandler> get _print;
_ZoneFunction<ForkHandler> get _fork;
_ZoneFunction<HandleUncaughtErrorHandler> get _handleUncaughtError;
+
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ _ZoneFunction<CreateTimerHandler> get _createTimer;
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ _ZoneFunction<CreatePeriodicTimerHandler> get _createPeriodicTimer;
+
_Zone get parent;
ZoneDelegate get _delegate;
Map get _map;
@@ -636,12 +850,17 @@
_ZoneFunction<RegisterBinaryCallbackHandler> _registerBinaryCallback;
_ZoneFunction<ErrorCallbackHandler> _errorCallback;
_ZoneFunction<ScheduleMicrotaskHandler> _scheduleMicrotask;
- _ZoneFunction<CreateTimerHandler> _createTimer;
- _ZoneFunction<CreatePeriodicTimerHandler> _createPeriodicTimer;
+ _ZoneFunction<CreateTaskHandler> _createTask;
+ _ZoneFunction<RunTaskHandler> _runTask;
_ZoneFunction<PrintHandler> _print;
_ZoneFunction<ForkHandler> _fork;
_ZoneFunction<HandleUncaughtErrorHandler> _handleUncaughtError;
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ _ZoneFunction<CreateTimerHandler> _createTimer;
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ _ZoneFunction<CreatePeriodicTimerHandler> _createPeriodicTimer;
+
// A cached delegate to this zone.
ZoneDelegate _delegateCache;
@@ -692,13 +911,14 @@
? new _ZoneFunction<ScheduleMicrotaskHandler>(
this, specification.scheduleMicrotask)
: parent._scheduleMicrotask;
- _createTimer = (specification.createTimer != null)
- ? new _ZoneFunction<CreateTimerHandler>(this, specification.createTimer)
- : parent._createTimer;
- _createPeriodicTimer = (specification.createPeriodicTimer != null)
- ? new _ZoneFunction<CreatePeriodicTimerHandler>(
- this, specification.createPeriodicTimer)
- : parent._createPeriodicTimer;
+ _createTask = (specification.createTask != null)
+ ? new _ZoneFunction<CreateTaskHandler>(
+ this, specification.createTask)
+ : parent._createTask;
+ _runTask = (specification.runTask != null)
+ ? new _ZoneFunction<RunTaskHandler>(
+ this, specification.runTask)
+ : parent._runTask;
_print = (specification.print != null)
? new _ZoneFunction<PrintHandler>(this, specification.print)
: parent._print;
@@ -709,6 +929,16 @@
? new _ZoneFunction<HandleUncaughtErrorHandler>(
this, specification.handleUncaughtError)
: parent._handleUncaughtError;
+
+ // Deprecated fields, once tasks are non-experimental.
+ _createTimer = (specification.createTimer != null)
+ ? new _ZoneFunction<CreateTimerHandler>(
+ this, specification.createTimer)
+ : parent._createTimer;
+ _createPeriodicTimer = (specification.createPeriodicTimer != null)
+ ? new _ZoneFunction<CreatePeriodicTimerHandler>(
+ this, specification.createPeriodicTimer)
+ : parent._createPeriodicTimer;
}
/**
@@ -859,7 +1089,7 @@
// TODO(floitsch): make this a generic method call on '<R>' once it's
// supported. Remove the unnecessary cast.
return handler(implementation.zone, parentDelegate, this, callback)
- as Object/*=ZoneCallback<R>*/;
+ as dynamic/*=ZoneCallback<R>*/;
}
ZoneUnaryCallback/*<R, T>*/ registerUnaryCallback/*<R, T>*/(
@@ -871,7 +1101,7 @@
// TODO(floitsch): make this a generic method call on '<R, T>' once it's
// supported. Remove the unnecessary cast.
return handler(implementation.zone, parentDelegate, this, callback)
- as Object/*=ZoneUnaryCallback<R, T>*/;
+ as dynamic/*=ZoneUnaryCallback<R, T>*/;
}
ZoneBinaryCallback/*<R, T1, T2>*/ registerBinaryCallback/*<R, T1, T2>*/(
@@ -883,7 +1113,7 @@
// TODO(floitsch): make this a generic method call on '<R, T1, T2>' once
// it's supported. Remove the unnecessary cast.
return handler(implementation.zone, parentDelegate, this, callback)
- as Object/*=ZoneBinaryCallback<R, T1, T2>*/;
+ as dynamic/*=ZoneBinaryCallback<R, T1, T2>*/;
}
AsyncError errorCallback(Object error, StackTrace stackTrace) {
@@ -902,24 +1132,29 @@
assert(implementation != null);
ZoneDelegate parentDelegate = _parentDelegate(implementation.zone);
ScheduleMicrotaskHandler handler = implementation.function;
- return handler(implementation.zone, parentDelegate, this, f);
+ handler(implementation.zone, parentDelegate, this, f);
}
- Timer createTimer(Duration duration, void f()) {
- var implementation = this._createTimer;
- assert(implementation != null);
+ Object/*=T*/ createTask/*<T, S extends TaskSpecification>*/(
+ TaskCreate/*<T, S>*/ create, TaskSpecification/*=S*/ specification) {
+ var implementation = this._createTask;
ZoneDelegate parentDelegate = _parentDelegate(implementation.zone);
- CreateTimerHandler handler = implementation.function;
- return handler(implementation.zone, parentDelegate, this, duration, f);
- }
-
- Timer createPeriodicTimer(Duration duration, void f(Timer timer)) {
- var implementation = this._createPeriodicTimer;
- assert(implementation != null);
- ZoneDelegate parentDelegate = _parentDelegate(implementation.zone);
- CreatePeriodicTimerHandler handler = implementation.function;
+ // TODO(floitsch): make the handler call a generic method call on '<T, S>'
+ // once it's supported. Remove the unnecessary cast.
+ var handler =
+ implementation.function as CreateTaskHandler/*<T, S>*/;
return handler(
- implementation.zone, parentDelegate, this, duration, f);
+ implementation.zone, parentDelegate, this, create, specification);
+ }
+
+ void runTask/*<T, A>*/(
+ TaskRun/*<T, A>*/ run, Object/*=T*/ task, Object/*=A*/ arg1) {
+ var implementation = this._runTask;
+ ZoneDelegate parentDelegate = _parentDelegate(implementation.zone);
+ RunTaskHandler handler = implementation.function;
+ // TODO(floitsch): make this a generic method call on '<T, A>' once it's
+ // supported.
+ handler(implementation.zone, parentDelegate, this, run, task, arg1);
}
void print(String line) {
@@ -929,6 +1164,25 @@
PrintHandler handler = implementation.function;
return handler(implementation.zone, parentDelegate, this, line);
}
+
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ Timer createTimer(Duration duration, void f()) {
+ var implementation = this._createTimer;
+ assert(implementation != null);
+ ZoneDelegate parentDelegate = _parentDelegate(implementation.zone);
+ CreateTimerHandler handler = implementation.function;
+ return handler(implementation.zone, parentDelegate, this, duration, f);
+ }
+
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ Timer createPeriodicTimer(Duration duration, void f(Timer timer)) {
+ var implementation = this._createPeriodicTimer;
+ assert(implementation != null);
+ ZoneDelegate parentDelegate = _parentDelegate(implementation.zone);
+ CreatePeriodicTimerHandler handler = implementation.function;
+ return handler(
+ implementation.zone, parentDelegate, this, duration, f);
+ }
}
/*=R*/ _rootHandleUncaughtError/*<R>*/(
@@ -1006,22 +1260,39 @@
_scheduleAsyncCallback(f);
}
+Object/*=T*/ _rootCreateTask/*<T, S extends TaskSpecification>*/(
+ Zone self, ZoneDelegate parent, Zone zone,
+ TaskCreate/*<T, S>*/ create, TaskSpecification/*=S*/ specification) {
+ return create(specification, zone);
+}
+
+void _rootRunTask/*<T, A>*/(
+ Zone self, ZoneDelegate parent, Zone zone, TaskRun run/*<T, A>*/,
+ Object/*=T*/ task, Object/*=A*/ arg) {
+ if (Zone._current == zone) {
+ run(task, arg);
+ return;
+ }
+
+ Zone old = Zone._enter(zone);
+ try {
+ run(task, arg);
+ } catch (e, s) {
+ zone.handleUncaughtError/*<dynamic>*/(e, s);
+ } finally {
+ Zone._leave(old);
+ }
+}
+
Timer _rootCreateTimer(Zone self, ZoneDelegate parent, Zone zone,
Duration duration, void callback()) {
- if (!identical(_ROOT_ZONE, zone)) {
- callback = zone.bindCallback(callback);
- }
- return Timer._createTimer(duration, callback);
+ return new Timer._task(zone, duration, callback);
}
Timer _rootCreatePeriodicTimer(
Zone self, ZoneDelegate parent, Zone zone,
Duration duration, void callback(Timer timer)) {
- if (!identical(_ROOT_ZONE, zone)) {
- // TODO(floitsch): the return type should be 'void'.
- callback = zone.bindUnaryCallback/*<dynamic, Timer>*/(callback);
- }
- return Timer._createPeriodicTimer(duration, callback);
+ return new Timer._periodicTask(zone, duration, callback);
}
void _rootPrint(Zone self, ZoneDelegate parent, Zone zone, String line) {
@@ -1082,10 +1353,10 @@
_ZoneFunction<ScheduleMicrotaskHandler> get _scheduleMicrotask =>
const _ZoneFunction<ScheduleMicrotaskHandler>(
_ROOT_ZONE, _rootScheduleMicrotask);
- _ZoneFunction<CreateTimerHandler> get _createTimer =>
- const _ZoneFunction<CreateTimerHandler>(_ROOT_ZONE, _rootCreateTimer);
- _ZoneFunction<CreatePeriodicTimerHandler> get _createPeriodicTimer =>
- const _ZoneFunction<CreatePeriodicTimerHandler>(_ROOT_ZONE, _rootCreatePeriodicTimer);
+ _ZoneFunction<CreateTaskHandler> get _createTask =>
+ const _ZoneFunction<CreateTaskHandler>(_ROOT_ZONE, _rootCreateTask);
+ _ZoneFunction<RunTaskHandler> get _runTask =>
+ const _ZoneFunction<RunTaskHandler>(_ROOT_ZONE, _rootRunTask);
_ZoneFunction<PrintHandler> get _print =>
const _ZoneFunction<PrintHandler>(_ROOT_ZONE, _rootPrint);
_ZoneFunction<ForkHandler> get _fork =>
@@ -1094,6 +1365,14 @@
const _ZoneFunction<HandleUncaughtErrorHandler>(
_ROOT_ZONE, _rootHandleUncaughtError);
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ _ZoneFunction<CreateTimerHandler> get _createTimer =>
+ const _ZoneFunction<CreateTimerHandler>(_ROOT_ZONE, _rootCreateTimer);
+ // TODO(floitsch): deprecate once tasks are non-experimental.
+ _ZoneFunction<CreatePeriodicTimerHandler> get _createPeriodicTimer =>
+ const _ZoneFunction<CreatePeriodicTimerHandler>(
+ _ROOT_ZONE, _rootCreatePeriodicTimer);
+
// The parent zone.
_Zone get parent => null;
@@ -1225,6 +1504,16 @@
_rootScheduleMicrotask(null, null, this, f);
}
+ Object/*=T*/ createTask/*<T, S extends TaskSpecification>*/(
+ TaskCreate/*<T, S>*/ create, TaskSpecification/*=S*/ specification) {
+ return _rootCreateTask/*<T, S>*/(null, null, this, create, specification);
+ }
+
+ void runTask/*<T, A>*/(
+ TaskRun/*<T, A>*/ run, Object/*=T*/ task, Object/*=A*/ arg) {
+ _rootRunTask/*<T, A>*/(null, null, this, run, task, arg);
+ }
+
Timer createTimer(Duration duration, void f()) {
return Timer._createTimer(duration, f);
}
diff --git a/sdk/lib/html/dart2js/html_dart2js.dart b/sdk/lib/html/dart2js/html_dart2js.dart
index 0bc3749..e5eae90 100644
--- a/sdk/lib/html/dart2js/html_dart2js.dart
+++ b/sdk/lib/html/dart2js/html_dart2js.dart
@@ -19240,6 +19240,109 @@
// BSD-style license that can be found in the LICENSE file.
+/**
+ * A task specification for HTTP requests.
+ *
+ * This specification is not available when an HTTP request is sent through
+ * direct use of [HttpRequest.send]. See [HttpRequestSendTaskSpecification].
+ *
+ * A task created from this specification is a `Future<HttpRequest>`.
+ *
+ * *Experimental*. This class may disappear without notice.
+ */
+class HttpRequestTaskSpecification extends TaskSpecification {
+ /// The URL of the request.
+ final String url;
+
+ /// The HTTP request method.
+ ///
+ /// By default (when `null`) this is a `"GET"` request. Alternatively, the
+ /// method can be `"POST"`, `"PUT"`, `"DELETE"`, etc.
+ final String method;
+
+ /// Whether the request should send credentials. Credentials are only useful
+ /// for cross-origin requests.
+ ///
+ /// See [HttpRequest.request] for more information.
+ final bool withCredentials;
+
+ /// The desired response format.
+ ///
+ /// Supported types are:
+ /// - `""`: (same as `"text"`),
+ /// - `"arraybuffer"`,
+ /// - `"blob"`,
+ /// - `"document"`,
+ /// - `"json"`,
+ /// - `"text"`
+ ///
+ /// When no value is provided (when equal to `null`) defaults to `""`.
+ final String responseType;
+
+ /// The desired MIME type.
+ ///
+ /// This overrides the default MIME type which is set up to transfer textual
+ /// data.
+ final String mimeType;
+
+ /// The request headers that should be sent with the request.
+ final Map<String, String> requestHeaders;
+
+ /// The data that is sent with the request.
+ ///
+ /// When data is provided (the value is not `null`), it must be a
+ /// [ByteBuffer], [Blob], [Document], [String], or [FormData].
+ final dynamic sendData;
+
+ /// The function that is invoked on progress updates. This function is
+ /// registered as an event listener on the created [HttpRequest] object, and
+ /// thus has its own task. Further invocations of the progress function do
+ /// *not* use the HTTP request task as task object.
+ ///
+ /// Creating an HTTP request automatically registers the on-progress listener.
+ final ZoneUnaryCallback<dynamic, ProgressEvent> onProgress;
+
+ HttpRequestTaskSpecification(this.url,
+ {String this.method, bool this.withCredentials, String this.responseType,
+ String this.mimeType, Map<String, String> this.requestHeaders,
+ this.sendData,
+ void this.onProgress(ProgressEvent e)});
+
+ String get name => "dart.html.http-request";
+ bool get isOneShot => true;
+}
+
+/**
+ * A task specification for HTTP requests that are initiated through a direct
+ * invocation of [HttpRequest.send].
+ *
+ * This specification serves as signal to zones that an HTTP request has been
+ * initiated. The created task is the [request] object itself, and
+ * no callback is ever executed in this task.
+ *
+ * Note that event listeners on the HTTP request are also registered in the
+ * zone (although with their own task creations), and that a zone can thus
+ * detect when the HTTP request returns.
+ *
+ * HTTP requests that are initiated through `request` methods don't use
+ * this class but use [HttpRequestTaskSpecification].
+ *
+ * *Experimental*. This class may disappear without notice.
+ */
+class HttpRequestSendTaskSpecification extends TaskSpecification {
+ final HttpRequest request;
+ final dynamic sendData;
+
+ HttpRequestSendTaskSpecification(this.request, this.sendData);
+
+ String get name => "dart.html.http-request-send";
+
+ /**
+ * No callback is ever executed in an HTTP request send task.
+ */
+ bool get isOneShot => false;
+}
+
/**
* A client-side XHR request for getting data from a URL,
* formally known as XMLHttpRequest.
@@ -19428,7 +19531,34 @@
{String method, bool withCredentials, String responseType,
String mimeType, Map<String, String> requestHeaders, sendData,
void onProgress(ProgressEvent e)}) {
+ var spec = new HttpRequestTaskSpecification(
+ url, method: method,
+ withCredentials: withCredentials,
+ responseType: responseType,
+ mimeType: mimeType,
+ requestHeaders: requestHeaders,
+ sendData: sendData,
+ onProgress: onProgress);
+
+ if (identical(Zone.current, Zone.ROOT)) {
+ return _createHttpRequestTask(spec, null);
+ }
+ return Zone.current.createTask(_createHttpRequestTask, spec);
+ }
+
+ static Future<HttpRequest> _createHttpRequestTask(
+ HttpRequestTaskSpecification spec, Zone zone) {
+ String url = spec.url;
+ String method = spec.method;
+ bool withCredentials = spec.withCredentials;
+ String responseType = spec.responseType;
+ String mimeType = spec.mimeType;
+ Map<String, String> requestHeaders = spec.requestHeaders;
+ var sendData = spec.sendData;
+ var onProgress = spec.onProgress;
+
var completer = new Completer<HttpRequest>();
+ var task = completer.future;
var xhr = new HttpRequest();
if (method == null) {
@@ -19468,23 +19598,42 @@
// redirect case will be handled by the browser before it gets to us,
// so if we see it we should pass it through to the user.
var unknownRedirect = xhr.status > 307 && xhr.status < 400;
-
- if (accepted || fileUri || notModified || unknownRedirect) {
+
+ var isSuccessful = accepted || fileUri || notModified || unknownRedirect;
+
+ if (zone == null && isSuccessful) {
completer.complete(xhr);
- } else {
+ } else if (zone == null) {
completer.completeError(e);
+ } else if (isSuccessful) {
+ zone.runTask((task, value) {
+ completer.complete(value);
+ }, task, xhr);
+ } else {
+ zone.runTask((task, error) {
+ completer.completeError(error);
+ }, task, e);
}
});
- xhr.onError.listen(completer.completeError);
-
- if (sendData != null) {
- xhr.send(sendData);
+ if (zone == null) {
+ xhr.onError.listen(completer.completeError);
} else {
- xhr.send();
+ xhr.onError.listen((error) {
+ zone.runTask((task, error) {
+ completer.completeError(error);
+ }, task, error);
+ });
}
- return completer.future;
+ if (sendData != null) {
+ // TODO(floitsch): should we go through 'send()' and have nested tasks?
+ xhr._send(sendData);
+ } else {
+ xhr._send();
+ }
+
+ return task;
}
/**
@@ -19538,6 +19687,9 @@
return xhr.responseText;
});
}
+ // TODO(floitsch): the following code doesn't go through task zones.
+ // Since 'XDomainRequest' is an IE9 feature we should probably just remove
+ // it.
var completer = new Completer<String>();
if (method == null) {
method = 'GET';
@@ -19616,13 +19768,43 @@
*
* Note: Most simple HTTP requests can be accomplished using the [getString],
* [request], [requestCrossOrigin], or [postFormData] methods. Use of this
- * `open` method is intended only for more complext HTTP requests where
+ * `open` method is intended only for more complex HTTP requests where
* finer-grained control is needed.
*/
@DomName('XMLHttpRequest.open')
@DocsEditable()
void open(String method, String url, {bool async, String user, String password}) native;
+ /**
+ * Sends the request with any given `data`.
+ *
+ * Note: Most simple HTTP requests can be accomplished using the [getString],
+ * [request], [requestCrossOrigin], or [postFormData] methods. Use of this
+ * `send` method is intended only for more complex HTTP requests where
+ * finer-grained control is needed.
+ *
+ * ## Other resources
+ *
+ * * [XMLHttpRequest.send](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#send%28%29)
+ * from MDN.
+ */
+ @DomName('XMLHttpRequest.send')
+ @DocsEditable()
+ void send([body_OR_data]) {
+ if (identical(Zone.current, Zone.ROOT)) {
+ _send(body_OR_data);
+ } else {
+ Zone.current.createTask(_createHttpRequestSendTask,
+ new HttpRequestSendTaskSpecification(this, body_OR_data));
+ }
+ }
+
+ static HttpRequest _createHttpRequestSendTask(
+ HttpRequestSendTaskSpecification spec, Zone zone) {
+ spec.request._send(spec.sendData);
+ return spec.request;
+ }
+
// To suppress missing implicit constructor warnings.
factory HttpRequest._() { throw new UnsupportedError("Not supported"); }
@@ -19893,12 +20075,13 @@
@SupportedBrowser(SupportedBrowser.SAFARI)
void overrideMimeType(String mime) native;
+ @JSName('send')
/**
* Send the request with any given `data`.
*
* Note: Most simple HTTP requests can be accomplished using the [getString],
* [request], [requestCrossOrigin], or [postFormData] methods. Use of this
- * `send` method is intended only for more complext HTTP requests where
+ * `send` method is intended only for more complex HTTP requests where
* finer-grained control is needed.
*
* ## Other resources
@@ -19908,7 +20091,7 @@
*/
@DomName('XMLHttpRequest.send')
@DocsEditable()
- void send([body_OR_data]) native;
+ void _send([body_OR_data]) native;
/**
* Sets the value of an HTTP requst header.
@@ -34485,6 +34668,99 @@
// BSD-style license that can be found in the LICENSE file.
+typedef void RemoveFrameRequestMapping(int id);
+
+/**
+ * The task object representing animation-frame requests.
+ *
+ * For historical reasons, [Window.requestAnimationFrame] returns an integer
+ * to users. However, zone tasks must be unique objects, and an integer can
+ * therefore not be used as task object. The [Window] class thus keeps a mapping
+ * from the integer ID to the corresponding task object. All zone related
+ * operations work on this task object, whereas users of
+ * [Window.requestAnimationFrame] only see the integer ID.
+ *
+ * Since this mapping takes up space, it must be removed when the
+ * animation-frame task has triggered. The default implementation does this
+ * automatically, but intercepting implementations of `requestAnimationFrame`
+ * must make sure to call the [AnimationFrameTask.removeMapping]
+ * function that is provided in the task specification.
+ *
+ * *Experimental*. This class may disappear without notice.
+ */
+abstract class AnimationFrameTask {
+ /** The ID that is returned to users. */
+ int get id;
+
+ /** The zone in which the task will run. */
+ Zone get zone;
+
+ /**
+ * Cancels the animation-frame request.
+ *
+ * A call to [Window.cancelAnimationFrame] with an `id` argument equal to [id]
+ * forwards the request to this function.
+ *
+ * Zones that intercept animation-frame requests implement this method so
+ * that they can react to cancelation requests.
+ */
+ void cancel(Window window);
+
+ /**
+ * Maps animation-frame request IDs to their task objects.
+ */
+ static final Map<int, _AnimationFrameTask> _tasks = {};
+
+ /**
+ * Removes the mapping from [id] to [AnimationFrameTask].
+ *
+ * This function must be invoked by user-implemented animation-frame
+ * tasks, before running [callback].
+ *
+ * See [AnimationFrameTask].
+ */
+ static void removeMapping(int id) {
+ _tasks.remove(id);
+ }
+}
+
+class _AnimationFrameTask implements AnimationFrameTask {
+ final int id;
+ final Zone zone;
+ final FrameRequestCallback _callback;
+
+ _AnimationFrameTask(this.id, this.zone, this._callback);
+
+ void cancel(Window window) {
+ window._cancelAnimationFrame(this.id);
+ }
+}
+
+/**
+ * The task specification for an animation-frame request.
+ *
+ * *Experimental*. This class may disappear without notice.
+ */
+class AnimationFrameRequestSpecification implements TaskSpecification {
+ /**
+ * The window on which [Window.requestAnimationFrame] was invoked.
+ */
+ final Window window;
+
+ /**
+ * The callback that is executed when the animation-frame is ready.
+ *
+ * Note that the callback hasn't been registered in any zone when the `create`
+ * function (passed to [Zone.createTask]) is invoked.
+ */
+ final FrameRequestCallback callback;
+
+ AnimationFrameRequestSpecification(this.window, this.callback);
+
+ String get name => "dart.html.request-animation-frame";
+ bool get isOneShot => true;
+}
+
@DocsEditable()
/**
* Top-level container for the current browser tab or window.
@@ -34540,9 +34816,7 @@
*/
Future<num> get animationFrame {
var completer = new Completer<num>.sync();
- requestAnimationFrame((time) {
- completer.complete(time);
- });
+ requestAnimationFrame(completer.complete);
return completer.future;
}
@@ -34625,7 +34899,30 @@
@DomName('Window.requestAnimationFrame')
int requestAnimationFrame(FrameRequestCallback callback) {
_ensureRequestAnimationFrame();
- return _requestAnimationFrame(_wrapZone/*<num, dynamic>*/(callback));
+ if (identical(Zone.current, Zone.ROOT)) {
+ return _requestAnimationFrame(callback);
+ }
+ var spec = new AnimationFrameRequestSpecification(this, callback);
+ var task = Zone.current.createTask/*<AnimationFrameTask>*/(
+ _createAnimationFrameTask, spec);
+ AnimationFrameTask._tasks[task.id] = task;
+ return task.id;
+ }
+
+ static _AnimationFrameTask _createAnimationFrameTask(
+ AnimationFrameRequestSpecification spec, Zone zone) {
+ var task;
+ var id = spec.window._requestAnimationFrame((num time) {
+ AnimationFrameTask.removeMapping(task.id);
+ zone.runTask(_runAnimationFrame, task, time);
+ });
+ var callback = zone.registerUnaryCallback(spec.callback);
+ task = new _AnimationFrameTask(id, zone, callback);
+ return task;
+ }
+
+ static void _runAnimationFrame(_AnimationFrameTask task, num time) {
+ task._callback(time);
}
/**
@@ -34638,7 +34935,13 @@
*/
void cancelAnimationFrame(int id) {
_ensureRequestAnimationFrame();
- _cancelAnimationFrame(id);
+ var task = AnimationFrameTask._tasks.remove(id);
+ if (task == null) {
+ // Assume that the animation frame request wasn't intercepted by a zone.
+ _cancelAnimationFrame(id);
+ return;
+ }
+ task.cancel(this);
}
@JSName('requestAnimationFrame')
@@ -39964,6 +40267,41 @@
StreamSubscription<T> capture(void onData(T event));
}
+/// Task specification for DOM Events.
+///
+/// *Experimental*. May disappear without notice.
+class EventSubscriptionSpecification<T extends Event>
+ implements TaskSpecification {
+ @override
+ final String name;
+ @override
+ final bool isOneShot;
+
+ final EventTarget target;
+ /// The event-type of the event. For example 'click' for click events.
+ final String eventType;
+ // TODO(floitsch): the first generic argument should be 'void'.
+ final ZoneUnaryCallback<dynamic, T> onData;
+ final bool useCapture;
+
+ EventSubscriptionSpecification({this.name, this.isOneShot, this.target,
+ this.eventType, void this.onData(T event), this.useCapture});
+
+ /// Returns a copy of this instance, with every non-null argument replaced
+ /// by the given value.
+ EventSubscriptionSpecification<T> replace(
+ {String name, bool isOneShot, EventTarget target,
+ String eventType, void onData(T event), bool useCapture}) {
+ return new EventSubscriptionSpecification<T>(
+ name: name ?? this.name,
+ isOneShot: isOneShot ?? this.isOneShot,
+ target: target ?? this.target,
+ eventType: eventType ?? this.eventType,
+ onData: onData ?? this.onData,
+ useCapture: useCapture ?? this.useCapture);
+ }
+}
+
/**
* Adapter for exposing DOM events as Dart streams.
*/
@@ -39971,8 +40309,16 @@
final EventTarget _target;
final String _eventType;
final bool _useCapture;
+ /// The name that is used in the task specification.
+ final String _name;
+ /// Whether the stream can trigger multiple times.
+ final bool _isOneShot;
- _EventStream(this._target, this._eventType, this._useCapture);
+ _EventStream(this._target, String eventType, this._useCapture,
+ {String name, bool isOneShot: false})
+ : _eventType = eventType,
+ _isOneShot = isOneShot,
+ _name = name ?? "dart.html.event.$eventType";
// DOM events are inherently multi-subscribers.
Stream<T> asBroadcastStream({void onListen(StreamSubscription<T> subscription),
@@ -39980,13 +40326,31 @@
=> this;
bool get isBroadcast => true;
+ StreamSubscription<T> _listen(
+ void onData(T event), {bool useCapture}) {
+
+ if (identical(Zone.current, Zone.ROOT)) {
+ return new _EventStreamSubscription<T>(
+ this._target, this._eventType, onData, this._useCapture,
+ Zone.current);
+ }
+
+ var specification = new EventSubscriptionSpecification<T>(
+ name: this._name, isOneShot: this._isOneShot,
+ target: this._target, eventType: this._eventType,
+ onData: onData, useCapture: useCapture);
+ // We need to wrap the _createStreamSubscription call, since a tear-off
+ // would not bind the generic type 'T'.
+ return Zone.current.createTask((spec, Zone zone) {
+ return _createStreamSubscription/*<T>*/(spec, zone);
+ }, specification);
+ }
+
StreamSubscription<T> listen(void onData(T event),
{ Function onError,
void onDone(),
bool cancelOnError}) {
-
- return new _EventStreamSubscription<T>(
- this._target, this._eventType, onData, this._useCapture);
+ return _listen(onData, useCapture: this._useCapture);
}
}
@@ -40001,8 +40365,9 @@
*/
class _ElementEventStreamImpl<T extends Event> extends _EventStream<T>
implements ElementStream<T> {
- _ElementEventStreamImpl(target, eventType, useCapture) :
- super(target, eventType, useCapture);
+ _ElementEventStreamImpl(target, eventType, useCapture,
+ {String name, bool isOneShot: false}) :
+ super(target, eventType, useCapture, name: name, isOneShot: isOneShot);
Stream<T> matches(String selector) => this.where(
(event) => _matchesWithAncestors(event, selector)).map((e) {
@@ -40010,9 +40375,9 @@
return e;
});
- StreamSubscription<T> capture(void onData(T event)) =>
- new _EventStreamSubscription<T>(
- this._target, this._eventType, onData, true);
+ StreamSubscription<T> capture(void onData(T event)) {
+ return _listen(onData, useCapture: true);
+ }
}
/**
@@ -40061,7 +40426,13 @@
bool get isBroadcast => true;
}
-// We would like this to just be EventListener<T> but that typdef cannot
+StreamSubscription/*<T>*/ _createStreamSubscription/*<T>*/(
+ EventSubscriptionSpecification/*<T>*/ spec, Zone zone) {
+ return new _EventStreamSubscription/*<T>*/(spec.target, spec.eventType,
+ spec.onData, spec.useCapture, zone);
+}
+
+// We would like this to just be EventListener<T> but that typedef cannot
// use generics until dartbug/26276 is fixed.
typedef _EventListener<T extends Event>(T event);
@@ -40070,15 +40441,19 @@
EventTarget _target;
final String _eventType;
EventListener _onData;
+ EventListener _domCallback;
final bool _useCapture;
+ final Zone _zone;
// TODO(jacobr): for full strong mode correctness we should write
- // _onData = onData == null ? null : _wrapZone/*<Event, dynamic>*/((e) => onData(e as T))
+ // _onData = onData == null ? null : _wrapZone/*<dynamic, Event>*/((e) => onData(e as T))
// but that breaks 114 co19 tests as well as multiple html tests as it is reasonable
// to pass the wrong type of event object to an event listener as part of a
// test.
_EventStreamSubscription(this._target, this._eventType, void onData(T event),
- this._useCapture) : _onData = _wrapZone/*<Event, dynamic>*/(onData) {
+ this._useCapture, Zone zone)
+ : _zone = zone,
+ _onData = _registerZone/*<dynamic, Event>*/(zone, onData) {
_tryResume();
}
@@ -40100,7 +40475,7 @@
}
// Remove current event listener.
_unlisten();
- _onData = _wrapZone/*<Event, dynamic>*/(handleData);
+ _onData = _registerZone/*<dynamic, Event>*/(_zone, handleData);
_tryResume();
}
@@ -40129,14 +40504,25 @@
}
void _tryResume() {
- if (_onData != null && !isPaused) {
- _target.addEventListener(_eventType, _onData, _useCapture);
+ if (_onData == null || isPaused) return;
+ if (identical(_zone, Zone.ROOT)) {
+ _domCallback = _onData;
+ } else {
+ _domCallback = (event) {
+ _zone.runTask(_runEventNotification, this, event);
+ };
}
+ _target.addEventListener(_eventType, _domCallback, _useCapture);
+ }
+
+ static void _runEventNotification/*<T>*/(
+ _EventStreamSubscription/*<T>*/ subscription, /*=T*/ event) {
+ subscription._onData(event);
}
void _unlisten() {
if (_onData != null) {
- _target.removeEventListener(_eventType, _onData, _useCapture);
+ _target.removeEventListener(_eventType, _domCallback, _useCapture);
}
}
@@ -43370,31 +43756,26 @@
// BSD-style license that can be found in the LICENSE file.
-// TODO(jacobr): remove these typedefs when dart:async supports generic types.
-typedef R _wrapZoneCallback<A, R>(A a);
-typedef R _wrapZoneBinaryCallback<A, B, R>(A a, B b);
-
-_wrapZoneCallback/*<A, R>*/ _wrapZone/*<A, R>*/(_wrapZoneCallback/*<A, R>*/ callback) {
- // For performance reasons avoid wrapping if we are in the root zone.
- if (Zone.current == Zone.ROOT) return callback;
+ZoneUnaryCallback/*<R, T>*/ _registerZone/*<R, T>*/(Zone zone,
+ ZoneUnaryCallback/*<R, T>*/ callback) {
+ // For performance reasons avoid registering if we are in the root zone.
+ if (identical(zone, Zone.ROOT)) return callback;
if (callback == null) return null;
- // TODO(jacobr): we cast to _wrapZoneCallback/*<A, R>*/ to hack around missing
- // generic method support in zones.
- // ignore: STRONG_MODE_DOWN_CAST_COMPOSITE
- _wrapZoneCallback/*<A, R>*/ wrapped =
- Zone.current.bindUnaryCallback(callback, runGuarded: true);
- return wrapped;
+ return zone.registerUnaryCallback(callback);
}
-_wrapZoneBinaryCallback/*<A, B, R>*/ _wrapBinaryZone/*<A, B, R>*/(_wrapZoneBinaryCallback/*<A, B, R>*/ callback) {
- if (Zone.current == Zone.ROOT) return callback;
+ZoneUnaryCallback/*<R, T>*/ _wrapZone/*<R, T>*/(ZoneUnaryCallback/*<R, T>*/ callback) {
+ // For performance reasons avoid wrapping if we are in the root zone.
+ if (identical(Zone.current, Zone.ROOT)) return callback;
if (callback == null) return null;
- // We cast to _wrapZoneBinaryCallback/*<A, B, R>*/ to hack around missing
- // generic method support in zones.
- // ignore: STRONG_MODE_DOWN_CAST_COMPOSITE
- _wrapZoneBinaryCallback/*<A, B, R>*/ wrapped =
- Zone.current.bindBinaryCallback(callback, runGuarded: true);
- return wrapped;
+ return Zone.current.bindUnaryCallback(callback, runGuarded: true);
+}
+
+ZoneBinaryCallback/*<R, A, B>*/ _wrapBinaryZone/*<R, A, B>*/(
+ ZoneBinaryCallback/*<R, A, B>*/ callback) {
+ if (identical(Zone.current, Zone.ROOT)) return callback;
+ if (callback == null) return null;
+ return Zone.current.bindBinaryCallback(callback, runGuarded: true);
}
/**
diff --git a/sdk/lib/html/dartium/html_dartium.dart b/sdk/lib/html/dartium/html_dartium.dart
index bc92d6d..35b620a 100644
--- a/sdk/lib/html/dartium/html_dartium.dart
+++ b/sdk/lib/html/dartium/html_dartium.dart
@@ -20997,6 +20997,109 @@
// BSD-style license that can be found in the LICENSE file.
+/**
+ * A task specification for HTTP requests.
+ *
+ * This specification is not available when an HTTP request is sent through
+ * direct use of [HttpRequest.send]. See [HttpRequestSendTaskSpecification].
+ *
+ * A task created from this specification is a `Future<HttpRequest>`.
+ *
+ * *Experimental*. This class may disappear without notice.
+ */
+class HttpRequestTaskSpecification extends TaskSpecification {
+ /// The URL of the request.
+ final String url;
+
+ /// The HTTP request method.
+ ///
+ /// By default (when `null`) this is a `"GET"` request. Alternatively, the
+ /// method can be `"POST"`, `"PUT"`, `"DELETE"`, etc.
+ final String method;
+
+ /// Whether the request should send credentials. Credentials are only useful
+ /// for cross-origin requests.
+ ///
+ /// See [HttpRequest.request] for more information.
+ final bool withCredentials;
+
+ /// The desired response format.
+ ///
+ /// Supported types are:
+ /// - `""`: (same as `"text"`),
+ /// - `"arraybuffer"`,
+ /// - `"blob"`,
+ /// - `"document"`,
+ /// - `"json"`,
+ /// - `"text"`
+ ///
+ /// When no value is provided (when equal to `null`) defaults to `""`.
+ final String responseType;
+
+ /// The desired MIME type.
+ ///
+ /// This overrides the default MIME type which is set up to transfer textual
+ /// data.
+ final String mimeType;
+
+ /// The request headers that should be sent with the request.
+ final Map<String, String> requestHeaders;
+
+ /// The data that is sent with the request.
+ ///
+ /// When data is provided (the value is not `null`), it must be a
+ /// [ByteBuffer], [Blob], [Document], [String], or [FormData].
+ final dynamic sendData;
+
+ /// The function that is invoked on progress updates. This function is
+ /// registered as an event listener on the created [HttpRequest] object, and
+ /// thus has its own task. Further invocations of the progress function do
+ /// *not* use the HTTP request task as task object.
+ ///
+ /// Creating an HTTP request automatically registers the on-progress listener.
+ final ZoneUnaryCallback<dynamic, ProgressEvent> onProgress;
+
+ HttpRequestTaskSpecification(this.url,
+ {String this.method, bool this.withCredentials, String this.responseType,
+ String this.mimeType, Map<String, String> this.requestHeaders,
+ this.sendData,
+ void this.onProgress(ProgressEvent e)});
+
+ String get name => "dart.html.http-request";
+ bool get isOneShot => true;
+}
+
+/**
+ * A task specification for HTTP requests that are initiated through a direct
+ * invocation of [HttpRequest.send].
+ *
+ * This specification serves as signal to zones that an HTTP request has been
+ * initiated. The created task is the [request] object itself, and
+ * no callback is ever executed in this task.
+ *
+ * Note that event listeners on the HTTP request are also registered in the
+ * zone (although with their own task creations), and that a zone can thus
+ * detect when the HTTP request returns.
+ *
+ * HTTP requests that are initiated through `request` methods don't use
+ * this class but use [HttpRequestTaskSpecification].
+ *
+ * *Experimental*. This class may disappear without notice.
+ */
+class HttpRequestSendTaskSpecification extends TaskSpecification {
+ final HttpRequest request;
+ final dynamic sendData;
+
+ HttpRequestSendTaskSpecification(this.request, this.sendData);
+
+ String get name => "dart.html.http-request-send";
+
+ /**
+ * No callback is ever executed in an HTTP request send task.
+ */
+ bool get isOneShot => false;
+}
+
/**
* A client-side XHR request for getting data from a URL,
* formally known as XMLHttpRequest.
@@ -21184,7 +21287,34 @@
{String method, bool withCredentials, String responseType,
String mimeType, Map<String, String> requestHeaders, sendData,
void onProgress(ProgressEvent e)}) {
+ var spec = new HttpRequestTaskSpecification(
+ url, method: method,
+ withCredentials: withCredentials,
+ responseType: responseType,
+ mimeType: mimeType,
+ requestHeaders: requestHeaders,
+ sendData: sendData,
+ onProgress: onProgress);
+
+ if (identical(Zone.current, Zone.ROOT)) {
+ return _createHttpRequestTask(spec, null);
+ }
+ return Zone.current.createTask(_createHttpRequestTask, spec);
+ }
+
+ static Future<HttpRequest> _createHttpRequestTask(
+ HttpRequestTaskSpecification spec, Zone zone) {
+ String url = spec.url;
+ String method = spec.method;
+ bool withCredentials = spec.withCredentials;
+ String responseType = spec.responseType;
+ String mimeType = spec.mimeType;
+ Map<String, String> requestHeaders = spec.requestHeaders;
+ var sendData = spec.sendData;
+ var onProgress = spec.onProgress;
+
var completer = new Completer<HttpRequest>();
+ var task = completer.future;
var xhr = new HttpRequest();
if (method == null) {
@@ -21224,23 +21354,42 @@
// redirect case will be handled by the browser before it gets to us,
// so if we see it we should pass it through to the user.
var unknownRedirect = xhr.status > 307 && xhr.status < 400;
-
- if (accepted || fileUri || notModified || unknownRedirect) {
+
+ var isSuccessful = accepted || fileUri || notModified || unknownRedirect;
+
+ if (zone == null && isSuccessful) {
completer.complete(xhr);
- } else {
+ } else if (zone == null) {
completer.completeError(e);
+ } else if (isSuccessful) {
+ zone.runTask((task, value) {
+ completer.complete(value);
+ }, task, xhr);
+ } else {
+ zone.runTask((task, error) {
+ completer.completeError(error);
+ }, task, e);
}
});
- xhr.onError.listen(completer.completeError);
-
- if (sendData != null) {
- xhr.send(sendData);
+ if (zone == null) {
+ xhr.onError.listen(completer.completeError);
} else {
- xhr.send();
+ xhr.onError.listen((error) {
+ zone.runTask((task, error) {
+ completer.completeError(error);
+ }, task, error);
+ });
}
- return completer.future;
+ if (sendData != null) {
+ // TODO(floitsch): should we go through 'send()' and have nested tasks?
+ xhr._send(sendData);
+ } else {
+ xhr._send();
+ }
+
+ return task;
}
/**
@@ -21290,6 +21439,9 @@
return xhr.responseText;
});
}
+ // TODO(floitsch): the following code doesn't go through task zones.
+ // Since 'XDomainRequest' is an IE9 feature we should probably just remove
+ // it.
}
/**
@@ -21340,7 +21492,7 @@
*
* Note: Most simple HTTP requests can be accomplished using the [getString],
* [request], [requestCrossOrigin], or [postFormData] methods. Use of this
- * `open` method is intended only for more complext HTTP requests where
+ * `open` method is intended only for more complex HTTP requests where
* finer-grained control is needed.
*/
@DomName('XMLHttpRequest.open')
@@ -21353,6 +21505,36 @@
}
}
+ /**
+ * Sends the request with any given `data`.
+ *
+ * Note: Most simple HTTP requests can be accomplished using the [getString],
+ * [request], [requestCrossOrigin], or [postFormData] methods. Use of this
+ * `send` method is intended only for more complex HTTP requests where
+ * finer-grained control is needed.
+ *
+ * ## Other resources
+ *
+ * * [XMLHttpRequest.send](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#send%28%29)
+ * from MDN.
+ */
+ @DomName('XMLHttpRequest.send')
+ @DocsEditable()
+ void send([body_OR_data]) {
+ if (identical(Zone.current, Zone.ROOT)) {
+ _send(body_OR_data);
+ } else {
+ Zone.current.createTask(_createHttpRequestSendTask,
+ new HttpRequestSendTaskSpecification(this, body_OR_data));
+ }
+ }
+
+ static HttpRequest _createHttpRequestSendTask(
+ HttpRequestSendTaskSpecification spec, Zone zone) {
+ spec.request._send(spec.sendData);
+ return spec.request;
+ }
+
// To suppress missing implicit constructor warnings.
factory HttpRequest._() { throw new UnsupportedError("Not supported"); }
@@ -21656,7 +21838,7 @@
@SupportedBrowser(SupportedBrowser.SAFARI)
void overrideMimeType(String mime) => _blink.BlinkXMLHttpRequest.instance.overrideMimeType_Callback_1_(this, mime);
- void send([body_OR_data]) {
+ void _send([body_OR_data]) {
if (body_OR_data != null) {
_blink.BlinkXMLHttpRequest.instance.send_Callback_1_(this, body_OR_data);
return;
@@ -37662,10 +37844,10 @@
if ((blob_OR_source_OR_stream is Blob || blob_OR_source_OR_stream == null)) {
return _blink.BlinkURL.instance.createObjectURL_Callback_1_(blob_OR_source_OR_stream);
}
- if ((blob_OR_source_OR_stream is MediaStream)) {
+ if ((blob_OR_source_OR_stream is MediaSource)) {
return _blink.BlinkURL.instance.createObjectURL_Callback_1_(blob_OR_source_OR_stream);
}
- if ((blob_OR_source_OR_stream is MediaSource)) {
+ if ((blob_OR_source_OR_stream is MediaStream)) {
return _blink.BlinkURL.instance.createObjectURL_Callback_1_(blob_OR_source_OR_stream);
}
throw new ArgumentError("Incorrect number or type of arguments");
@@ -39131,6 +39313,99 @@
// BSD-style license that can be found in the LICENSE file.
+typedef void RemoveFrameRequestMapping(int id);
+
+/**
+ * The task object representing animation-frame requests.
+ *
+ * For historical reasons, [Window.requestAnimationFrame] returns an integer
+ * to users. However, zone tasks must be unique objects, and an integer can
+ * therefore not be used as task object. The [Window] class thus keeps a mapping
+ * from the integer ID to the corresponding task object. All zone related
+ * operations work on this task object, whereas users of
+ * [Window.requestAnimationFrame] only see the integer ID.
+ *
+ * Since this mapping takes up space, it must be removed when the
+ * animation-frame task has triggered. The default implementation does this
+ * automatically, but intercepting implementations of `requestAnimationFrame`
+ * must make sure to call the [AnimationFrameTask.removeMapping]
+ * function that is provided in the task specification.
+ *
+ * *Experimental*. This class may disappear without notice.
+ */
+abstract class AnimationFrameTask {
+ /** The ID that is returned to users. */
+ int get id;
+
+ /** The zone in which the task will run. */
+ Zone get zone;
+
+ /**
+ * Cancels the animation-frame request.
+ *
+ * A call to [Window.cancelAnimationFrame] with an `id` argument equal to [id]
+ * forwards the request to this function.
+ *
+ * Zones that intercept animation-frame requests implement this method so
+ * that they can react to cancelation requests.
+ */
+ void cancel(Window window);
+
+ /**
+ * Maps animation-frame request IDs to their task objects.
+ */
+ static final Map<int, _AnimationFrameTask> _tasks = {};
+
+ /**
+ * Removes the mapping from [id] to [AnimationFrameTask].
+ *
+ * This function must be invoked by user-implemented animation-frame
+ * tasks, before running [callback].
+ *
+ * See [AnimationFrameTask].
+ */
+ static void removeMapping(int id) {
+ _tasks.remove(id);
+ }
+}
+
+class _AnimationFrameTask implements AnimationFrameTask {
+ final int id;
+ final Zone zone;
+ final FrameRequestCallback _callback;
+
+ _AnimationFrameTask(this.id, this.zone, this._callback);
+
+ void cancel(Window window) {
+ window._cancelAnimationFrame(this.id);
+ }
+}
+
+/**
+ * The task specification for an animation-frame request.
+ *
+ * *Experimental*. This class may disappear without notice.
+ */
+class AnimationFrameRequestSpecification implements TaskSpecification {
+ /**
+ * The window on which [Window.requestAnimationFrame] was invoked.
+ */
+ final Window window;
+
+ /**
+ * The callback that is executed when the animation-frame is ready.
+ *
+ * Note that the callback hasn't been registered in any zone when the `create`
+ * function (passed to [Zone.createTask]) is invoked.
+ */
+ final FrameRequestCallback callback;
+
+ AnimationFrameRequestSpecification(this.window, this.callback);
+
+ String get name => "dart.html.request-animation-frame";
+ bool get isOneShot => true;
+}
+
@DocsEditable()
/**
* Top-level container for the current browser tab or window.
@@ -39185,9 +39460,7 @@
*/
Future<num> get animationFrame {
var completer = new Completer<num>.sync();
- requestAnimationFrame((time) {
- completer.complete(time);
- });
+ requestAnimationFrame(completer.complete);
return completer.future;
}
@@ -44492,6 +44765,41 @@
StreamSubscription<T> capture(void onData(T event));
}
+/// Task specification for DOM Events.
+///
+/// *Experimental*. May disappear without notice.
+class EventSubscriptionSpecification<T extends Event>
+ implements TaskSpecification {
+ @override
+ final String name;
+ @override
+ final bool isOneShot;
+
+ final EventTarget target;
+ /// The event-type of the event. For example 'click' for click events.
+ final String eventType;
+ // TODO(floitsch): the first generic argument should be 'void'.
+ final ZoneUnaryCallback<dynamic, T> onData;
+ final bool useCapture;
+
+ EventSubscriptionSpecification({this.name, this.isOneShot, this.target,
+ this.eventType, void this.onData(T event), this.useCapture});
+
+ /// Returns a copy of this instance, with every non-null argument replaced
+ /// by the given value.
+ EventSubscriptionSpecification<T> replace(
+ {String name, bool isOneShot, EventTarget target,
+ String eventType, void onData(T event), bool useCapture}) {
+ return new EventSubscriptionSpecification<T>(
+ name: name ?? this.name,
+ isOneShot: isOneShot ?? this.isOneShot,
+ target: target ?? this.target,
+ eventType: eventType ?? this.eventType,
+ onData: onData ?? this.onData,
+ useCapture: useCapture ?? this.useCapture);
+ }
+}
+
/**
* Adapter for exposing DOM events as Dart streams.
*/
@@ -44499,8 +44807,16 @@
final EventTarget _target;
final String _eventType;
final bool _useCapture;
+ /// The name that is used in the task specification.
+ final String _name;
+ /// Whether the stream can trigger multiple times.
+ final bool _isOneShot;
- _EventStream(this._target, this._eventType, this._useCapture);
+ _EventStream(this._target, String eventType, this._useCapture,
+ {String name, bool isOneShot: false})
+ : _eventType = eventType,
+ _isOneShot = isOneShot,
+ _name = name ?? "dart.html.event.$eventType";
// DOM events are inherently multi-subscribers.
Stream<T> asBroadcastStream({void onListen(StreamSubscription<T> subscription),
@@ -44508,13 +44824,31 @@
=> this;
bool get isBroadcast => true;
+ StreamSubscription<T> _listen(
+ void onData(T event), {bool useCapture}) {
+
+ if (identical(Zone.current, Zone.ROOT)) {
+ return new _EventStreamSubscription<T>(
+ this._target, this._eventType, onData, this._useCapture,
+ Zone.current);
+ }
+
+ var specification = new EventSubscriptionSpecification<T>(
+ name: this._name, isOneShot: this._isOneShot,
+ target: this._target, eventType: this._eventType,
+ onData: onData, useCapture: useCapture);
+ // We need to wrap the _createStreamSubscription call, since a tear-off
+ // would not bind the generic type 'T'.
+ return Zone.current.createTask((spec, Zone zone) {
+ return _createStreamSubscription/*<T>*/(spec, zone);
+ }, specification);
+ }
+
StreamSubscription<T> listen(void onData(T event),
{ Function onError,
void onDone(),
bool cancelOnError}) {
-
- return new _EventStreamSubscription<T>(
- this._target, this._eventType, onData, this._useCapture);
+ return _listen(onData, useCapture: this._useCapture);
}
}
@@ -44529,8 +44863,9 @@
*/
class _ElementEventStreamImpl<T extends Event> extends _EventStream<T>
implements ElementStream<T> {
- _ElementEventStreamImpl(target, eventType, useCapture) :
- super(target, eventType, useCapture);
+ _ElementEventStreamImpl(target, eventType, useCapture,
+ {String name, bool isOneShot: false}) :
+ super(target, eventType, useCapture, name: name, isOneShot: isOneShot);
Stream<T> matches(String selector) => this.where(
(event) => _matchesWithAncestors(event, selector)).map((e) {
@@ -44538,9 +44873,9 @@
return e;
});
- StreamSubscription<T> capture(void onData(T event)) =>
- new _EventStreamSubscription<T>(
- this._target, this._eventType, onData, true);
+ StreamSubscription<T> capture(void onData(T event)) {
+ return _listen(onData, useCapture: true);
+ }
}
/**
@@ -44589,7 +44924,13 @@
bool get isBroadcast => true;
}
-// We would like this to just be EventListener<T> but that typdef cannot
+StreamSubscription/*<T>*/ _createStreamSubscription/*<T>*/(
+ EventSubscriptionSpecification/*<T>*/ spec, Zone zone) {
+ return new _EventStreamSubscription/*<T>*/(spec.target, spec.eventType,
+ spec.onData, spec.useCapture, zone);
+}
+
+// We would like this to just be EventListener<T> but that typedef cannot
// use generics until dartbug/26276 is fixed.
typedef _EventListener<T extends Event>(T event);
@@ -44598,15 +44939,19 @@
EventTarget _target;
final String _eventType;
EventListener _onData;
+ EventListener _domCallback;
final bool _useCapture;
+ final Zone _zone;
// TODO(jacobr): for full strong mode correctness we should write
- // _onData = onData == null ? null : _wrapZone/*<Event, dynamic>*/((e) => onData(e as T))
+ // _onData = onData == null ? null : _wrapZone/*<dynamic, Event>*/((e) => onData(e as T))
// but that breaks 114 co19 tests as well as multiple html tests as it is reasonable
// to pass the wrong type of event object to an event listener as part of a
// test.
_EventStreamSubscription(this._target, this._eventType, void onData(T event),
- this._useCapture) : _onData = _wrapZone/*<Event, dynamic>*/(onData) {
+ this._useCapture, Zone zone)
+ : _zone = zone,
+ _onData = _registerZone/*<dynamic, Event>*/(zone, onData) {
_tryResume();
}
@@ -44628,7 +44973,7 @@
}
// Remove current event listener.
_unlisten();
- _onData = _wrapZone/*<Event, dynamic>*/(handleData);
+ _onData = _registerZone/*<dynamic, Event>*/(_zone, handleData);
_tryResume();
}
@@ -44657,14 +45002,25 @@
}
void _tryResume() {
- if (_onData != null && !isPaused) {
- _target.addEventListener(_eventType, _onData, _useCapture);
+ if (_onData == null || isPaused) return;
+ if (identical(_zone, Zone.ROOT)) {
+ _domCallback = _onData;
+ } else {
+ _domCallback = (event) {
+ _zone.runTask(_runEventNotification, this, event);
+ };
}
+ _target.addEventListener(_eventType, _domCallback, _useCapture);
+ }
+
+ static void _runEventNotification/*<T>*/(
+ _EventStreamSubscription/*<T>*/ subscription, /*=T*/ event) {
+ subscription._onData(event);
}
void _unlisten() {
if (_onData != null) {
- _target.removeEventListener(_eventType, _onData, _useCapture);
+ _target.removeEventListener(_eventType, _domCallback, _useCapture);
}
}
@@ -47856,31 +48212,26 @@
// BSD-style license that can be found in the LICENSE file.
-// TODO(jacobr): remove these typedefs when dart:async supports generic types.
-typedef R _wrapZoneCallback<A, R>(A a);
-typedef R _wrapZoneBinaryCallback<A, B, R>(A a, B b);
-
-_wrapZoneCallback/*<A, R>*/ _wrapZone/*<A, R>*/(_wrapZoneCallback/*<A, R>*/ callback) {
- // For performance reasons avoid wrapping if we are in the root zone.
- if (Zone.current == Zone.ROOT) return callback;
+ZoneUnaryCallback/*<R, T>*/ _registerZone/*<R, T>*/(Zone zone,
+ ZoneUnaryCallback/*<R, T>*/ callback) {
+ // For performance reasons avoid registering if we are in the root zone.
+ if (identical(zone, Zone.ROOT)) return callback;
if (callback == null) return null;
- // TODO(jacobr): we cast to _wrapZoneCallback/*<A, R>*/ to hack around missing
- // generic method support in zones.
- // ignore: STRONG_MODE_DOWN_CAST_COMPOSITE
- _wrapZoneCallback/*<A, R>*/ wrapped =
- Zone.current.bindUnaryCallback(callback, runGuarded: true);
- return wrapped;
+ return zone.registerUnaryCallback(callback);
}
-_wrapZoneBinaryCallback/*<A, B, R>*/ _wrapBinaryZone/*<A, B, R>*/(_wrapZoneBinaryCallback/*<A, B, R>*/ callback) {
- if (Zone.current == Zone.ROOT) return callback;
+ZoneUnaryCallback/*<R, T>*/ _wrapZone/*<R, T>*/(ZoneUnaryCallback/*<R, T>*/ callback) {
+ // For performance reasons avoid wrapping if we are in the root zone.
+ if (identical(Zone.current, Zone.ROOT)) return callback;
if (callback == null) return null;
- // We cast to _wrapZoneBinaryCallback/*<A, B, R>*/ to hack around missing
- // generic method support in zones.
- // ignore: STRONG_MODE_DOWN_CAST_COMPOSITE
- _wrapZoneBinaryCallback/*<A, B, R>*/ wrapped =
- Zone.current.bindBinaryCallback(callback, runGuarded: true);
- return wrapped;
+ return Zone.current.bindUnaryCallback(callback, runGuarded: true);
+}
+
+ZoneBinaryCallback/*<R, A, B>*/ _wrapBinaryZone/*<R, A, B>*/(
+ ZoneBinaryCallback/*<R, A, B>*/ callback) {
+ if (identical(Zone.current, Zone.ROOT)) return callback;
+ if (callback == null) return null;
+ return Zone.current.bindBinaryCallback(callback, runGuarded: true);
}
/**
diff --git a/tests/html/event_subscription_specification_test.dart b/tests/html/event_subscription_specification_test.dart
new file mode 100644
index 0000000..406ed37
--- /dev/null
+++ b/tests/html/event_subscription_specification_test.dart
@@ -0,0 +1,121 @@
+// Copyright (c) 2016, 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.
+
+library EventTaskZoneTest;
+
+import 'package:unittest/unittest.dart';
+import 'package:unittest/html_config.dart';
+import 'dart:async';
+import 'dart:html';
+
+// Tests event-subscription specifications.
+
+main() {
+ useHtmlConfiguration();
+
+ var defaultTarget = new Element.div();
+ var defaultOnData = (x) => null;
+
+ EventSubscriptionSpecification createSpec({useCapture, isOneShot}) {
+ return new EventSubscriptionSpecification(
+ name: "name",
+ target: defaultTarget,
+ useCapture: useCapture,
+ isOneShot: isOneShot,
+ onData: defaultOnData,
+ eventType: "eventType");
+ }
+
+ for (var useCapture in [true, false]) {
+ for (var isOneShot in [true, false]) {
+ var spec = createSpec(useCapture: useCapture, isOneShot: isOneShot);
+
+ test(
+ "EventSubscriptionSpecification - constructor "
+ "useCapture: $useCapture isOneShot: $isOneShot", () {
+ var replaced = spec.replace(eventType: 'replace-eventType');
+ expect(replaced.name, "name");
+ expect(replaced.target, defaultTarget);
+ expect(replaced.useCapture, useCapture);
+ expect(replaced.isOneShot, isOneShot);
+ expect(replaced.onData, equals(defaultOnData));
+ expect(replaced.eventType, "replace-eventType");
+ });
+
+ test(
+ "replace name "
+ "useCapture: $useCapture isOneShot: $isOneShot", () {
+ var replaced = spec.replace(name: 'replace-name');
+ expect(replaced.name, "replace-name");
+ expect(replaced.target, defaultTarget);
+ expect(replaced.useCapture, useCapture);
+ expect(replaced.isOneShot, isOneShot);
+ expect(replaced.onData, equals(defaultOnData));
+ expect(replaced.eventType, "eventType");
+ });
+
+ test(
+ "replace target "
+ "useCapture: $useCapture isOneShot: $isOneShot", () {
+ var replacementTarget = new Element.a();
+ var replaced = spec.replace(target: replacementTarget);
+ expect(replaced.name, "name");
+ expect(replaced.target, replacementTarget);
+ expect(replaced.useCapture, useCapture);
+ expect(replaced.isOneShot, isOneShot);
+ expect(replaced.onData, equals(defaultOnData));
+ expect(replaced.eventType, "eventType");
+ });
+
+ test(
+ "replace useCapture "
+ "useCapture: $useCapture isOneShot: $isOneShot", () {
+ var replaced = spec.replace(useCapture: !useCapture);
+ expect(replaced.name, "name");
+ expect(replaced.target, defaultTarget);
+ expect(replaced.useCapture, !useCapture);
+ expect(replaced.isOneShot, isOneShot);
+ expect(replaced.onData, equals(defaultOnData));
+ expect(replaced.eventType, "eventType");
+ });
+
+ test(
+ "replace isOneShot "
+ "useCapture: $useCapture isOneShot: $isOneShot", () {
+ var replaced = spec.replace(isOneShot: !isOneShot);
+ expect(replaced.name, "name");
+ expect(replaced.target, defaultTarget);
+ expect(replaced.useCapture, useCapture);
+ expect(replaced.isOneShot, !isOneShot);
+ expect(replaced.onData, equals(defaultOnData));
+ expect(replaced.eventType, "eventType");
+ });
+
+ test(
+ "replace onData "
+ "useCapture: $useCapture isOneShot: $isOneShot", () {
+ var replacementOnData = (x) {};
+ var replaced = spec.replace(onData: replacementOnData);
+ expect(replaced.name, "name");
+ expect(replaced.target, defaultTarget);
+ expect(replaced.useCapture, useCapture);
+ expect(replaced.isOneShot, isOneShot);
+ expect(replaced.onData, equals(replacementOnData));
+ expect(replaced.eventType, "eventType");
+ });
+
+ test(
+ "replace eventType "
+ "useCapture: $useCapture isOneShot: $isOneShot", () {
+ var replaced = spec.replace(eventType: 'replace-eventType');
+ expect(replaced.name, "name");
+ expect(replaced.target, defaultTarget);
+ expect(replaced.useCapture, useCapture);
+ expect(replaced.isOneShot, isOneShot);
+ expect(replaced.onData, equals(defaultOnData));
+ expect(replaced.eventType, "replace-eventType");
+ });
+ }
+ }
+}
diff --git a/tests/html/event_zone_task_test.dart b/tests/html/event_zone_task_test.dart
new file mode 100644
index 0000000..a0e85c8
--- /dev/null
+++ b/tests/html/event_zone_task_test.dart
@@ -0,0 +1,239 @@
+// Copyright (c) 2016, 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.
+
+library EventTaskZoneTest;
+
+import 'package:unittest/unittest.dart';
+import 'package:unittest/html_config.dart';
+import 'dart:async';
+import 'dart:html';
+
+// Tests zone tasks with DOM events.
+
+class AbortedEventStreamSubscription implements StreamSubscription<Event> {
+ final Zone zone;
+
+ AbortedEventStreamSubscription(this.zone);
+
+ @override
+ Future asFuture([futureValue]) {
+ throw new UnsupportedError("asFuture");
+ }
+
+ @override
+ Future cancel() {
+ return null;
+ }
+
+ @override
+ bool get isPaused => throw new UnsupportedError("pause");
+
+ @override
+ void onData(void handleData(Event data)) {
+ throw new UnsupportedError("cancel");
+ }
+
+ @override
+ void onDone(void handleDone()) {
+ throw new UnsupportedError("onDone");
+ }
+
+ @override
+ void onError(Function handleError) {
+ throw new UnsupportedError("onError");
+ }
+
+ @override
+ void pause([Future resumeSignal]) {
+ throw new UnsupportedError("pause");
+ }
+
+ @override
+ void resume() {
+ throw new UnsupportedError("resume");
+ }
+
+ static AbortedEventStreamSubscription _create(
+ EventSubscriptionSpecification spec, Zone zone) {
+ return new AbortedEventStreamSubscription(zone);
+ }
+}
+
+eventTest(String name, Event eventFn(), void validate(Event event),
+ void validateSpec(EventSubscriptionSpecification spec),
+ {String type: 'foo',
+ bool abortCreation: false,
+ EventSubscriptionSpecification modifySpec(
+ EventSubscriptionSpecification spec),
+ bool abortEvent: false,
+ Event modifyEvent(Event event)}) {
+ test(name, () {
+ var lastSpec;
+ var lastTask;
+ var lastEvent;
+
+ Object createTaskHandler(Zone self, ZoneDelegate parent, Zone zone,
+ TaskCreate create, TaskSpecification specification) {
+ if (specification is EventSubscriptionSpecification) {
+ if (abortCreation) {
+ create = AbortedEventStreamSubscription._create;
+ }
+ if (modifySpec != null) {
+ specification = modifySpec(specification);
+ }
+ lastSpec = specification;
+ return lastTask = parent.createTask(zone, create, specification);
+ }
+ return parent.createTask(zone, create, specification);
+ }
+
+ void runTaskHandler(Zone self, ZoneDelegate parent, Zone zone, TaskRun run,
+ Object task, Object arg) {
+ if (identical(task, lastTask)) {
+ if (abortEvent) return;
+ if (modifyEvent != null) {
+ arg = modifyEvent(arg);
+ }
+ parent.runTask(zone, run, task, arg);
+ return;
+ }
+ parent.runTask(zone, run, task, arg);
+ }
+
+ runZoned(() {
+ final el = new Element.tag('div');
+ var fired = false;
+ var sub = el.on[type].listen((ev) {
+ lastEvent = ev;
+ fired = true;
+ });
+ el.dispatchEvent(eventFn());
+
+ validateSpec(lastSpec);
+ validate(lastEvent);
+
+ if (abortEvent || abortCreation) {
+ expect(fired, isFalse, reason: 'Expected event to be intercepted.');
+ } else {
+ expect(fired, isTrue, reason: 'Expected event to be dispatched.');
+ }
+
+ sub.cancel();
+ },
+ zoneSpecification: new ZoneSpecification(
+ createTask: createTaskHandler,
+ runTask: runTaskHandler));
+ });
+}
+
+Function checkSpec(
+ [String expectedType = 'foo', bool expectedUseCapture = false]) {
+ return (EventSubscriptionSpecification spec) {
+ expect(spec.eventType, expectedType);
+ expect(spec.useCapture, expectedUseCapture);
+ };
+}
+
+main() {
+ useHtmlConfiguration();
+
+ eventTest('Event', () => new Event('foo'), (ev) {
+ expect(ev.type, equals('foo'));
+ }, checkSpec('foo'));
+
+ eventTest(
+ 'WheelEvent',
+ () => new WheelEvent("mousewheel",
+ deltaX: 1,
+ deltaY: 0,
+ detail: 4,
+ screenX: 3,
+ screenY: 4,
+ clientX: 5,
+ clientY: 6,
+ ctrlKey: true,
+ altKey: true,
+ shiftKey: true,
+ metaKey: true), (ev) {
+ expect(ev.deltaX, 1);
+ expect(ev.deltaY, 0);
+ expect(ev.screen.x, 3);
+ expect(ev.screen.y, 4);
+ expect(ev.client.x, 5);
+ expect(ev.client.y, 6);
+ expect(ev.ctrlKey, isTrue);
+ expect(ev.altKey, isTrue);
+ expect(ev.shiftKey, isTrue);
+ expect(ev.metaKey, isTrue);
+ }, checkSpec('mousewheel'), type: 'mousewheel');
+
+ eventTest('Event - no-create', () => new Event('foo'), (ev) {
+ expect(ev, isNull);
+ }, checkSpec('foo'), abortCreation: true);
+
+ eventTest(
+ 'WheelEvent - no-create',
+ () => new WheelEvent("mousewheel",
+ deltaX: 1,
+ deltaY: 0,
+ detail: 4,
+ screenX: 3,
+ screenY: 4,
+ clientX: 5,
+ clientY: 6,
+ ctrlKey: true,
+ altKey: true,
+ shiftKey: true,
+ metaKey: true), (ev) {
+ expect(ev, isNull);
+ }, checkSpec('mousewheel'), type: 'mousewheel', abortCreation: true);
+
+ eventTest('Event - no-run', () => new Event('foo'), (ev) {
+ expect(ev, isNull);
+ }, checkSpec('foo'), abortEvent: true);
+
+ eventTest(
+ 'WheelEvent - no-run',
+ () => new WheelEvent("mousewheel",
+ deltaX: 1,
+ deltaY: 0,
+ detail: 4,
+ screenX: 3,
+ screenY: 4,
+ clientX: 5,
+ clientY: 6,
+ ctrlKey: true,
+ altKey: true,
+ shiftKey: true,
+ metaKey: true), (ev) {
+ expect(ev, isNull);
+ }, checkSpec('mousewheel'), type: 'mousewheel', abortEvent: true);
+
+ // Register for 'foo', but receive a 'bar' event, because the specification
+ // is rewritten.
+ eventTest(
+ 'Event - replace eventType',
+ () => new Event('bar'),
+ (ev) {
+ expect(ev.type, equals('bar'));
+ },
+ checkSpec('bar'),
+ type: 'foo',
+ modifySpec: (EventSubscriptionSpecification spec) {
+ return spec.replace(eventType: 'bar');
+ });
+
+ // Intercept the 'foo' event and replace it with a 'bar' event.
+ eventTest(
+ 'Event - intercept result',
+ () => new Event('foo'),
+ (ev) {
+ expect(ev.type, equals('bar'));
+ },
+ checkSpec('foo'),
+ type: 'foo',
+ modifyEvent: (Event event) {
+ return new Event('bar');
+ });
+}
diff --git a/tests/html/html.status b/tests/html/html.status
index 4d57f42..65ff46f 100644
--- a/tests/html/html.status
+++ b/tests/html/html.status
@@ -142,6 +142,7 @@
event_test: RuntimeError # Issue 23437. Only three failures, but hard to break them out.
wheelevent_test: RuntimeError # Issue 23437
text_event_test: RuntimeError # Issue 23437
+event_zone_task_test: RuntimeError # Issue 23437
transition_event_test/functional: Skip # Times out. Issue 22167
request_animation_frame_test: Skip # Times out. Issue 22167
@@ -185,6 +186,7 @@
webgl_1_test/supported: Fail
websql_test/supported: Fail
xhr_test/json: Fail # IE10 returns string, not JSON object
+xhr_task_test/json: Fail # IE10 returns string, not JSON object
xhr_test/supported_overrideMimeType: Fail
xsltprocessor_test/supported: Fail
worker_test/functional: Fail # IE uses incorrect security context for Blob URIs.
@@ -242,7 +244,8 @@
touchevent_test/supported: Fail # IE does not support TouchEvents
webgl_1_test/functional: Fail
websql_test/supported: Fail
-xhr_test/json: Fail # IE10 returns string, not JSON object
+xhr_test/json: Fail # IE11 returns string, not JSON object
+xhr_task_test/json: Fail # IE11 returns string, not JSON object
xsltprocessor_test/supported: Fail
[ $runtime == ie10 ]
@@ -361,6 +364,7 @@
[ (($runtime == dartium || $runtime == drt) && $system == macos) || $system == windows ]
xhr_test/xhr: Skip # Times out. Issue 21527
+xhr_task_test/xhr: Skip # Times out. Issue 21527
[ $compiler == dart2analyzer ]
custom/document_register_basic_test: StaticWarning
diff --git a/tests/html/request_animation_task_test.dart b/tests/html/request_animation_task_test.dart
new file mode 100644
index 0000000..3d388dd
--- /dev/null
+++ b/tests/html/request_animation_task_test.dart
@@ -0,0 +1,170 @@
+// Copyright (c) 2016, 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.
+
+library EventTaskZoneTest;
+
+import 'package:unittest/unittest.dart';
+import 'package:unittest/html_config.dart';
+import 'dart:async';
+import 'dart:html';
+
+// Tests zone tasks with window.requestAnimationFrame.
+
+class MockAnimationFrameTask implements AnimationFrameTask {
+ static int taskId = 499;
+
+ final int id;
+ final Zone zone;
+ bool _isCanceled = false;
+ Function _callback;
+
+ MockAnimationFrameTask(
+ this.id, this.zone, this._callback);
+
+ void cancel(Window window) {
+ _isCanceled = true;
+ }
+
+ trigger(num stamp) {
+ zone.runTask(run, this, stamp);
+ }
+
+ static create(AnimationFrameRequestSpecification spec, Zone zone) {
+ var callback = zone.registerUnaryCallback(spec.callback);
+ return new MockAnimationFrameTask(
+ taskId++, zone, callback);
+ }
+
+ static run(MockAnimationFrameTask task, num arg) {
+ AnimationFrameTask.removeMapping(task.id);
+ task._callback(arg);
+ }
+}
+
+animationFrameTest() {
+ test("animationFrameTest - no intercept", () async {
+ AnimationFrameTask lastTask;
+ bool sawRequest = false;
+ int id;
+ num providedArg;
+
+ Object createTaskHandler(Zone self, ZoneDelegate parent, Zone zone,
+ TaskCreate create, TaskSpecification specification) {
+ if (specification is AnimationFrameRequestSpecification) {
+ sawRequest = true;
+ lastTask = parent.createTask(zone, create, specification);
+ id = lastTask.id;
+ return lastTask;
+ }
+ return parent.createTask(zone, create, specification);
+ }
+
+ void runTaskHandler(Zone self, ZoneDelegate parent, Zone zone, TaskRun run,
+ Object task, Object arg) {
+ if (identical(task, lastTask)) {
+ providedArg = arg;
+ }
+ parent.runTask(zone, run, task, arg);
+ }
+
+ var completer = new Completer();
+ var publicId;
+ runZoned(() {
+ publicId = window.requestAnimationFrame((num stamp) {
+ completer.complete(stamp);
+ });
+ },
+ zoneSpecification: new ZoneSpecification(
+ createTask: createTaskHandler, runTask: runTaskHandler));
+
+ var referenceCompleter = new Completer();
+ window.requestAnimationFrame((num stamp) {
+ referenceCompleter.complete(stamp);
+ });
+
+ var callbackStamp = await completer.future;
+ var referenceStamp = await referenceCompleter.future;
+
+ expect(callbackStamp, equals(referenceStamp));
+ expect(providedArg, equals(callbackStamp));
+ expect(sawRequest, isTrue);
+ expect(publicId, isNotNull);
+ expect(publicId, equals(id));
+ });
+}
+
+interceptedAnimationFrameTest() {
+ test("animationFrameTest - intercepted", () {
+ List<MockAnimationFrameTask> tasks = [];
+ List<num> loggedRuns = [];
+ int executedTaskId;
+ num executedStamp;
+
+ Object createTaskHandler(Zone self, ZoneDelegate parent, Zone zone,
+ TaskCreate create, TaskSpecification specification) {
+ if (specification is AnimationFrameRequestSpecification) {
+ var task = parent.createTask(
+ zone, MockAnimationFrameTask.create, specification);
+ tasks.add(task);
+ return task;
+ }
+ return parent.createTask(zone, create, specification);
+ }
+
+ void runTaskHandler(Zone self, ZoneDelegate parent, Zone zone, TaskRun run,
+ Object task, Object arg) {
+ if (tasks.contains(task)) {
+ loggedRuns.add(arg);
+ }
+ parent.runTask(zone, run, task, arg);
+ }
+
+ var id0, id1, id2;
+
+ runZoned(() {
+ id0 = window.requestAnimationFrame((num stamp) {
+ executedTaskId = id0;
+ executedStamp = stamp;
+ });
+ id1 = window.requestAnimationFrame((num stamp) {
+ executedTaskId = id1;
+ executedStamp = stamp;
+ });
+ id2 = window.requestAnimationFrame((num stamp) {
+ executedTaskId = id2;
+ executedStamp = stamp;
+ });
+ },
+ zoneSpecification: new ZoneSpecification(
+ createTask: createTaskHandler, runTask: runTaskHandler));
+
+ expect(tasks.length, 3);
+ expect(executedTaskId, isNull);
+ expect(executedStamp, isNull);
+ expect(loggedRuns.isEmpty, isTrue);
+
+ tasks[0].trigger(123.1);
+ expect(executedTaskId, id0);
+ expect(executedStamp, 123.1);
+
+ tasks[1].trigger(123.2);
+ expect(executedTaskId, id1);
+ expect(executedStamp, 123.2);
+
+ expect(loggedRuns, equals([123.1, 123.2]));
+
+ window.cancelAnimationFrame(id2);
+ expect(tasks[2]._isCanceled, isTrue);
+ // Cancel it a second time. Should not crash.
+ window.cancelAnimationFrame(id2);
+ expect(tasks[2]._isCanceled, isTrue);
+ });
+}
+
+main() {
+ useHtmlConfiguration();
+
+ animationFrameTest();
+ interceptedAnimationFrameTest();
+}
diff --git a/tests/html/xhr_task2_test.dart b/tests/html/xhr_task2_test.dart
new file mode 100644
index 0000000..6dee69e
--- /dev/null
+++ b/tests/html/xhr_task2_test.dart
@@ -0,0 +1,280 @@
+// Copyright (c) 2016, 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.
+
+library XHRTask2Test;
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:html';
+import 'dart:typed_data';
+import 'package:unittest/html_individual_config.dart';
+import 'package:unittest/unittest.dart';
+
+class MockProgressEvent implements ProgressEvent {
+ final target;
+ MockProgressEvent(this.target);
+
+ noSuchMethod(Invocation invocation) {
+ throw "missing function in MockProgressEvent";
+ }
+}
+
+class MockHttpRequestTask implements Future<HttpRequest> {
+ final Completer completer = new Completer<HttpRequest>();
+ final HttpRequestTaskSpecification spec;
+ final Zone zone;
+
+ MockHttpRequestTask(this.spec, this.zone);
+
+ void trigger(response) {
+ var xhr = new MockHttpRequest(spec, response);
+ var arg;
+ if (spec.url == "NonExistingFile") {
+ arg = new MockProgressEvent(xhr);
+ } else {
+ arg = xhr;
+ }
+ zone.runTask(run, this, arg);
+ }
+
+ then(onData, {onError}) => completer.future.then(onData, onError: onError);
+ catchError(f, {test}) => completer.future.catchError(f, test: test);
+ whenComplete(f) => completer.future.whenComplete(f);
+ asStream() => completer.future.asStream();
+ timeout(timeLimit, {onTimeout}) =>
+ completer.future.timeout(timeLimit, onTimeout: onTimeout);
+
+ static create(HttpRequestTaskSpecification spec, Zone zone) {
+ return new MockHttpRequestTask(spec, zone);
+ }
+
+ static run(MockHttpRequestTask task, value) {
+ if (value is HttpRequest) {
+ task.completer.complete(value);
+ } else {
+ task.completer.completeError(value);
+ }
+ }
+}
+
+class MockHttpRequest implements HttpRequest {
+ final HttpRequestTaskSpecification spec;
+ final response;
+
+ MockHttpRequest(this.spec, this.response);
+
+ noSuchMethod(Invocation invocation) {
+ print("isGetter: ${invocation.isGetter}");
+ print("isMethod: ${invocation.isMethod}");
+ print("memberName: ${invocation.memberName}");
+ }
+
+ int get status => spec.url == "NonExistingFile" ? 404 : 200;
+
+ get readyState => HttpRequest.DONE;
+ get responseText => "$response";
+
+ Map get responseHeaders => {'content-type': 'text/plain; charset=utf-8',};
+}
+
+main() {
+ useHtmlIndividualConfiguration();
+ unittestConfiguration.timeout = const Duration(milliseconds: 800);
+
+ var urlExpando = new Expando();
+
+ var url = 'some/url.html';
+
+ Function buildCreateTaskHandler(List log, List tasks) {
+ Object createTaskHandler(Zone self, ZoneDelegate parent, Zone zone,
+ TaskCreate create, TaskSpecification spec) {
+ if (spec is HttpRequestTaskSpecification) {
+ var url = spec.url;
+ var method = spec.method;
+ var withCredentials = spec.withCredentials;
+ var responseType = spec.responseType;
+ var mimeType = spec.mimeType;
+ var data = spec.sendData;
+
+ log.add("request $url");
+ var dataLog = data is List<int> ? "binary ${data.length}" : "$data";
+ log.add(" method: $method withCredentials: $withCredentials "
+ "responseType: $responseType mimeType: $mimeType data: $dataLog");
+ var task = parent.createTask(zone, MockHttpRequestTask.create, spec);
+ urlExpando[task] = url;
+ tasks.add(task);
+ return task;
+ }
+ if (spec is EventSubscriptionSpecification) {
+ EventSubscriptionSpecification eventSpec = spec;
+ if (eventSpec.target is HttpRequest) {
+ HttpRequest target = eventSpec.target;
+ log.add("event listener on http-request ${eventSpec.eventType}");
+ if (eventSpec.eventType == "readystatechange") {
+ var oldOnData = eventSpec.onData;
+ spec = eventSpec.replace(onData: (event) {
+ oldOnData(event);
+ if (target.readyState == HttpRequest.DONE) {
+ log.add("unknown request done");
+ }
+ });
+ }
+ }
+ }
+ return parent.createTask(zone, create, spec);
+ }
+
+ return createTaskHandler;
+ }
+
+ Function buildRunTaskHandler(List log, List tasks) {
+ void runTaskHandler(Zone self, ZoneDelegate parent, Zone zone, TaskRun run,
+ Object task, Object arg) {
+ if (tasks.contains(task)) {
+ var url = urlExpando[task];
+ if (arg is Error || arg is Exception) {
+ log.add("failed $url");
+ } else {
+ if (arg is ProgressEvent) {
+ log.add("success $url with progress-event");
+ } else if (arg is HttpRequest) {
+ log.add("success $url with http-request");
+ } else {
+ log.add("success $url (unknown arg)");
+ }
+ }
+ }
+ parent.runTask(zone, run, task, arg);
+ }
+
+ return runTaskHandler;
+ }
+
+ Future<List> runMocked(response, fun) async {
+ var log = [];
+ var tasks = [];
+ var future = runZoned(fun,
+ zoneSpecification: new ZoneSpecification(
+ createTask: buildCreateTaskHandler(log, tasks),
+ runTask: buildRunTaskHandler(log, tasks)));
+ // Wait a full cycle to make sure things settle.
+ await new Future(() {});
+ var beforeTriggerLog = log.toList();
+ log.clear();
+ expect(tasks.length, 1);
+ tasks.single.trigger(response);
+ await future;
+ return [beforeTriggerLog, log];
+ }
+
+ void validate200Response(xhr) {
+ expect(xhr.status, equals(200));
+ var data = JSON.decode(xhr.responseText);
+ expect(data, contains('feed'));
+ expect(data['feed'], contains('entry'));
+ expect(data, isMap);
+ }
+
+ void validate404(xhr) {
+ expect(xhr.status, equals(404));
+ // We cannot say much about xhr.responseText, most HTTP servers will
+ // include an HTML page explaining the error to a human.
+ String responseText = xhr.responseText;
+ expect(responseText, isNotNull);
+ }
+
+ group('xhr', () {
+ test('XHR.request No file', () async {
+ var log = await runMocked("404", () {
+ var completer = new Completer();
+ HttpRequest.request('NonExistingFile').then((_) {
+ fail('Request should not have succeeded.');
+ }, onError: expectAsync((error) {
+ var xhr = error.target;
+ expect(xhr.readyState, equals(HttpRequest.DONE));
+ validate404(xhr);
+ completer.complete('done');
+ }));
+ return completer.future;
+ });
+ expect(
+ log,
+ equals([
+ [
+ 'request NonExistingFile',
+ ' method: null withCredentials: null responseType: null '
+ 'mimeType: null data: null',
+ ],
+ ['success NonExistingFile with progress-event']
+ ]));
+ });
+
+ test('XHR.request file', () async {
+ var log = await runMocked('{"feed": {"entry": 499}}', () {
+ var completer = new Completer();
+ HttpRequest.request(url).then(expectAsync((xhr) {
+ expect(xhr.readyState, equals(HttpRequest.DONE));
+ validate200Response(xhr);
+ completer.complete('done');
+ }));
+ return completer.future;
+ });
+ expect(
+ log,
+ equals([
+ [
+ 'request $url',
+ ' method: null withCredentials: null responseType: null '
+ 'mimeType: null data: null'
+ ],
+ ['success $url with http-request']
+ ]));
+ });
+
+ test('XHR.getString file', () async {
+ var log = await runMocked("foo", () {
+ return HttpRequest.getString(url).then(expectAsync((str) {}));
+ });
+ expect(
+ log,
+ equals([
+ [
+ 'request $url',
+ ' method: null withCredentials: null responseType: null '
+ 'mimeType: null data: null'
+ ],
+ ['success $url with http-request']
+ ]));
+ });
+
+ test('XHR.request responseType arraybuffer', () async {
+ if (Platform.supportsTypedData) {
+ var data = new Uint8List(128);
+ var log = await runMocked(data.buffer, () {
+ return HttpRequest.request(url,
+ responseType: 'arraybuffer',
+ requestHeaders: {
+ 'Content-Type': 'text/xml'
+ }).then(expectAsync((xhr) {
+ expect(xhr.status, equals(200));
+ var byteBuffer = xhr.response;
+ expect(byteBuffer, new isInstanceOf<ByteBuffer>());
+ expect(byteBuffer, isNotNull);
+ }));
+ });
+ expect(
+ log,
+ equals([
+ [
+ 'request $url',
+ ' method: null withCredentials: null responseType: arraybuffer'
+ ' mimeType: null data: null'
+ ],
+ ['success $url with http-request']
+ ]));
+ }
+ ;
+ });
+ });
+}
diff --git a/tests/html/xhr_task_test.dart b/tests/html/xhr_task_test.dart
new file mode 100644
index 0000000..c315ae6
--- /dev/null
+++ b/tests/html/xhr_task_test.dart
@@ -0,0 +1,508 @@
+// Copyright (c) 2016, 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.
+
+library XHRTaskTest;
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:html';
+import 'dart:typed_data';
+import 'package:unittest/html_individual_config.dart';
+import 'package:unittest/unittest.dart';
+
+main() {
+ useHtmlIndividualConfiguration();
+
+ // Cache blocker is a workaround for:
+ // https://code.google.com/p/dart/issues/detail?id=11834
+ var cacheBlocker = new DateTime.now().millisecondsSinceEpoch;
+ var url = '/root_dart/tests/html/xhr_cross_origin_data.txt?'
+ 'cacheBlock=$cacheBlocker';
+
+ var urlExpando = new Expando();
+
+ Function buildCreateTaskHandler(List log, List tasks) {
+ Object createTaskHandler(Zone self, ZoneDelegate parent, Zone zone,
+ TaskCreate create, TaskSpecification spec) {
+ if (spec is HttpRequestTaskSpecification) {
+ var url = spec.url;
+ var method = spec.method;
+ var withCredentials = spec.withCredentials;
+ var responseType = spec.responseType;
+ var mimeType = spec.mimeType;
+ var data = spec.sendData;
+
+ log.add("request $url");
+ var dataLog = data is List<int> ? "binary ${data.length}" : "$data";
+ log.add(" method: $method withCredentials: $withCredentials "
+ "responseType: $responseType mimeType: $mimeType data: $dataLog");
+ var task = parent.createTask(zone, create, spec);
+ urlExpando[task] = url;
+ tasks.add(task);
+ return task;
+ }
+ if (spec is HttpRequestSendTaskSpecification) {
+ var data = spec.sendData;
+ var dataLog = data is List<int> ? "binary ${data.length}" : "$data";
+ log.add("http-request (no info), data: $dataLog");
+ var task = parent.createTask(zone, create, spec);
+ tasks.add(task);
+ urlExpando[task] = "unknown";
+ return task;
+ }
+ if (spec is EventSubscriptionSpecification) {
+ EventSubscriptionSpecification eventSpec = spec;
+ if (eventSpec.target is HttpRequest) {
+ HttpRequest target = eventSpec.target;
+ log.add("event listener on http-request ${eventSpec.eventType}");
+ if (eventSpec.eventType == "readystatechange") {
+ var oldOnData = eventSpec.onData;
+ spec = eventSpec.replace(onData: (event) {
+ oldOnData(event);
+ if (target.readyState == HttpRequest.DONE) {
+ log.add("unknown request done");
+ }
+ });
+ }
+ }
+ }
+ return parent.createTask(zone, create, spec);
+ }
+
+ return createTaskHandler;
+ }
+
+ Function buildRunTaskHandler(List log, List tasks) {
+ void runTaskHandler(Zone self, ZoneDelegate parent, Zone zone,
+ TaskRun run, Object task, Object arg) {
+ if (tasks.contains(task)) {
+ var url = urlExpando[task];
+ if (arg is Error || arg is Exception) {
+ log.add("failed $url");
+ } else {
+ if (arg is ProgressEvent) {
+ log.add("success $url with progress-event");
+ } else if (arg is HttpRequest){
+ log.add("success $url with http-request");
+ } else {
+ log.add("success $url (unknown arg)");
+ }
+ }
+ }
+ parent.runTask(zone, run, task, arg);
+ }
+
+ return runTaskHandler;
+ }
+
+ Future<List> runWithLogging(fun) async {
+ var log = [];
+ var tasks = [];
+ await runZoned(fun, zoneSpecification: new ZoneSpecification(
+ createTask: buildCreateTaskHandler(log, tasks),
+ runTask: buildRunTaskHandler(log, tasks)));
+ return log;
+ }
+
+ void validate200Response(xhr) {
+ expect(xhr.status, equals(200));
+ var data = JSON.decode(xhr.responseText);
+ expect(data, contains('feed'));
+ expect(data['feed'], contains('entry'));
+ expect(data, isMap);
+ }
+
+ void validate404(xhr) {
+ expect(xhr.status, equals(404));
+ // We cannot say much about xhr.responseText, most HTTP servers will
+ // include an HTML page explaining the error to a human.
+ String responseText = xhr.responseText;
+ expect(responseText, isNotNull);
+ }
+
+ group('xhr', () {
+ test('XHR No file', () async {
+ var log = await runWithLogging(() {
+ var completer = new Completer();
+ HttpRequest xhr = new HttpRequest();
+ xhr.open("GET", "NonExistingFile", async: true);
+ xhr.onReadyStateChange.listen(expectAsyncUntil((event) {
+ if (xhr.readyState == HttpRequest.DONE) {
+ validate404(xhr);
+ completer.complete("done");
+ }
+ }, () => xhr.readyState == HttpRequest.DONE));
+ xhr.send();
+ return completer.future;
+ });
+ expect(log, equals([
+ 'event listener on http-request readystatechange',
+ 'http-request (no info), data: null',
+ 'unknown request done'
+ ]));
+ });
+
+ test('XHR_file', () async {
+ var log = await runWithLogging(() {
+ var completer = new Completer();
+ var loadEndCalled = false;
+
+ var xhr = new HttpRequest();
+ xhr.open('GET', url, async: true);
+ xhr.onReadyStateChange.listen(expectAsyncUntil((e) {
+ if (xhr.readyState == HttpRequest.DONE) {
+ validate200Response(xhr);
+
+ Timer.run(expectAsync(() {
+ expect(loadEndCalled, HttpRequest.supportsLoadEndEvent);
+ completer.complete("done");
+ }));
+ }
+ }, () => xhr.readyState == HttpRequest.DONE));
+
+ xhr.onLoadEnd.listen((ProgressEvent e) {
+ loadEndCalled = true;
+ });
+ xhr.send();
+ return completer.future;
+ });
+ expect(log, equals([
+ 'event listener on http-request readystatechange',
+ 'event listener on http-request loadend',
+ 'http-request (no info), data: null',
+ 'unknown request done'
+ ]));
+ });
+
+ test('XHR.request No file', () async {
+ var log = await runWithLogging(() {
+ var completer = new Completer();
+ HttpRequest.request('NonExistingFile').then(
+ (_) { fail('Request should not have succeeded.'); },
+ onError: expectAsync((error) {
+ var xhr = error.target;
+ expect(xhr.readyState, equals(HttpRequest.DONE));
+ validate404(xhr);
+ completer.complete('done');
+ }));
+ return completer.future;
+ });
+ expect(log, equals([
+ 'request NonExistingFile',
+ ' method: null withCredentials: null responseType: null '
+ 'mimeType: null data: null',
+ 'event listener on http-request load',
+ 'event listener on http-request error',
+ 'success NonExistingFile with progress-event'
+ ]));
+ });
+
+ test('XHR.request file', () async {
+ var log = await runWithLogging(() {
+ var completer = new Completer();
+ HttpRequest.request(url).then(expectAsync((xhr) {
+ expect(xhr.readyState, equals(HttpRequest.DONE));
+ validate200Response(xhr);
+ completer.complete('done');
+ }));
+ return completer.future;
+ });
+ expect(log, equals([
+ 'request $url',
+ ' method: null withCredentials: null responseType: null '
+ 'mimeType: null data: null',
+ 'event listener on http-request load',
+ 'event listener on http-request error',
+ 'success $url with http-request'
+ ]));
+ });
+
+ test('XHR.request onProgress', () async {
+ var log = await runWithLogging(() {
+ var completer = new Completer();
+ var progressCalled = false;
+ HttpRequest.request(url,
+ onProgress: (_) {
+ progressCalled = true;
+ }).then(expectAsync(
+ (xhr) {
+ expect(xhr.readyState, equals(HttpRequest.DONE));
+ expect(progressCalled, HttpRequest.supportsProgressEvent);
+ validate200Response(xhr);
+ completer.complete("done");
+ }));
+ return completer.future;
+ });
+ expect(log, equals([
+ 'request $url',
+ ' method: null withCredentials: null responseType: null '
+ 'mimeType: null data: null',
+ 'event listener on http-request progress',
+ 'event listener on http-request load',
+ 'event listener on http-request error',
+ 'success $url with http-request'
+ ]));
+ });
+
+ test('XHR.request withCredentials No file', () async {
+ var log = await runWithLogging(() {
+ var completer = new Completer();
+ HttpRequest.request('NonExistingFile', withCredentials: true).then(
+ (_) { fail('Request should not have succeeded.'); },
+ onError: expectAsync((error) {
+ var xhr = error.target;
+ expect(xhr.readyState, equals(HttpRequest.DONE));
+ validate404(xhr);
+ completer.complete("done");
+ }));
+ return completer.future;
+ });
+ expect(log, equals([
+ 'request NonExistingFile',
+ ' method: null withCredentials: true responseType: null '
+ 'mimeType: null data: null',
+ 'event listener on http-request load',
+ 'event listener on http-request error',
+ 'success NonExistingFile with progress-event'
+ ]));
+ });
+
+
+ test('XHR.request withCredentials file', () async {
+ var log = await runWithLogging(() {
+ var completer = new Completer();
+ HttpRequest.request(url, withCredentials: true).then(expectAsync((xhr) {
+ expect(xhr.readyState, equals(HttpRequest.DONE));
+ validate200Response(xhr);
+ completer.complete("done");
+ }));
+ return completer.future;
+ });
+ expect(log, equals([
+ 'request $url',
+ ' method: null withCredentials: true responseType: null '
+ 'mimeType: null data: null',
+ 'event listener on http-request load',
+ 'event listener on http-request error',
+ 'success $url with http-request'
+ ]));
+ });
+
+ test('XHR.getString file', () async {
+ var log = await runWithLogging(() {
+ return HttpRequest.getString(url).then(expectAsync((str) {}));
+ });
+ expect(log, equals([
+ 'request $url',
+ ' method: null withCredentials: null responseType: null '
+ 'mimeType: null data: null',
+ 'event listener on http-request load',
+ 'event listener on http-request error',
+ 'success $url with http-request'
+ ]));
+ });
+
+ test('XHR.getString No file', () async {
+ var log = await runWithLogging(() {
+ return HttpRequest.getString('NonExistingFile').then(
+ (_) { fail('Succeeded for non-existing file.'); },
+ onError: expectAsync((error) {
+ validate404(error.target);
+ }));
+ });
+ expect(log, equals([
+ 'request NonExistingFile',
+ ' method: null withCredentials: null responseType: null '
+ 'mimeType: null data: null',
+ 'event listener on http-request load',
+ 'event listener on http-request error',
+ 'success NonExistingFile with progress-event'
+ ]));
+ });
+
+ test('XHR.request responseType arraybuffer', () async {
+ if (Platform.supportsTypedData) {
+ var log = await runWithLogging(() {
+ return HttpRequest.request(url, responseType: 'arraybuffer',
+ requestHeaders: {'Content-Type': 'text/xml'}).then(
+ expectAsync((xhr) {
+ expect(xhr.status, equals(200));
+ var byteBuffer = xhr.response;
+ expect(byteBuffer, new isInstanceOf<ByteBuffer>());
+ expect(byteBuffer, isNotNull);
+ }));
+ });
+ expect(log, equals([
+ 'request $url',
+ ' method: null withCredentials: null responseType: arraybuffer '
+ 'mimeType: null data: null',
+ 'event listener on http-request load',
+ 'event listener on http-request error',
+ 'success $url with http-request'
+ ]));
+ };
+ });
+
+ test('overrideMimeType', () async {
+ var expectation =
+ HttpRequest.supportsOverrideMimeType ? returnsNormally : throws;
+
+ var log = await runWithLogging(() {
+ var completer = new Completer();
+ expect(() {
+ HttpRequest.request(url, mimeType: 'application/binary')
+ .whenComplete(completer.complete);
+ }, expectation);
+ return completer.future;
+ });
+ expect(log, equals([
+ 'request $url',
+ ' method: null withCredentials: null responseType: null '
+ 'mimeType: application/binary data: null',
+ 'event listener on http-request load',
+ 'event listener on http-request error',
+ 'success $url with http-request'
+ ]));
+ });
+
+ if (Platform.supportsTypedData) {
+ test('xhr upload', () async {
+ var log = await runWithLogging(() {
+ var xhr = new HttpRequest();
+ var progressCalled = false;
+ xhr.upload.onProgress.listen((e) {
+ progressCalled = true;
+ });
+
+ xhr.open('POST',
+ '${window.location.protocol}//${window.location.host}/echo');
+
+ // 10MB of payload data w/ a bit of data to make sure it
+ // doesn't get compressed to nil.
+ var data = new Uint8List(1 * 1024 * 1024);
+ for (var i = 0; i < data.length; ++i) {
+ data[i] = i & 0xFF;
+ }
+ xhr.send(new Uint8List.view(data.buffer));
+
+ return xhr.onLoad.first.then((_) {
+ expect(
+ progressCalled, isTrue, reason: 'onProgress should be fired');
+ });
+ });
+ expect(log, equals([
+ 'http-request (no info), data: binary 1048576',
+ 'event listener on http-request load',
+ ]));
+ });
+ }
+
+ test('xhr postFormData', () async {
+ var url = '${window.location.protocol}//${window.location.host}/echo';
+ var log = await runWithLogging(() {
+ var data = { 'name': 'John', 'time': '2 pm'};
+
+ var parts = [];
+ for (var key in data.keys) {
+ parts.add('${Uri.encodeQueryComponent(key)}='
+ '${Uri.encodeQueryComponent(data[key])}');
+ }
+ var encodedData = parts.join('&');
+
+ return HttpRequest.postFormData(url, data).then((xhr) {
+ expect(xhr.responseText, encodedData);
+ });
+ });
+ expect(log, equals([
+ 'request $url',
+ ' method: POST withCredentials: null responseType: null '
+ 'mimeType: null data: name=John&time=2+pm',
+ 'event listener on http-request load',
+ 'event listener on http-request error',
+ 'success $url with http-request'
+ ]));
+ });
+ });
+
+ group('xhr_requestBlob', () {
+ test('XHR.request responseType blob', () async {
+ if (Platform.supportsTypedData) {
+ var log = await runWithLogging(() {
+ return HttpRequest.request(url, responseType: 'blob').then(
+ (xhr) {
+ expect(xhr.status, equals(200));
+ var blob = xhr.response;
+ expect(blob is Blob, isTrue);
+ expect(blob, isNotNull);
+ });
+ });
+ expect(log, equals([
+ 'request $url',
+ ' method: null withCredentials: null responseType: blob '
+ 'mimeType: null data: null',
+ 'event listener on http-request load',
+ 'event listener on http-request error',
+ 'success $url with http-request'
+ ]));
+ }
+ });
+ });
+
+ group('json', () {
+ test('xhr responseType json', () async {
+ var url = '${window.location.protocol}//${window.location.host}/echo';
+ var log = await runWithLogging(() {
+ var completer = new Completer();
+ var data = {
+ 'key': 'value',
+ 'a': 'b',
+ 'one': 2,
+ };
+
+ HttpRequest.request(url,
+ method: 'POST',
+ sendData: JSON.encode(data),
+ responseType: 'json').then(
+ expectAsync((xhr) {
+ expect(xhr.status, equals(200));
+ var json = xhr.response;
+ expect(json, equals(data));
+ completer.complete("done");
+ }));
+ return completer.future;
+ });
+ expect(log, equals([
+ 'request $url',
+ ' method: POST withCredentials: null responseType: json mimeType: null'
+ ' data: {"key":"value","a":"b","one":2}',
+ 'event listener on http-request load',
+ 'event listener on http-request error',
+ 'success $url with http-request'
+ ]));
+ });
+ });
+
+ group('headers', () {
+ test('xhr responseHeaders', () async {
+ var log = await runWithLogging(() {
+ return HttpRequest.request(url).then(
+ (xhr) {
+ var contentTypeHeader = xhr.responseHeaders['content-type'];
+ expect(contentTypeHeader, isNotNull);
+ // Should be like: 'text/plain; charset=utf-8'
+ expect(contentTypeHeader.contains('text/plain'), isTrue);
+ expect(contentTypeHeader.contains('charset=utf-8'), isTrue);
+ });
+ });
+ expect(log, equals([
+ 'request $url',
+ ' method: null withCredentials: null responseType: null'
+ ' mimeType: null data: null',
+ 'event listener on http-request load',
+ 'event listener on http-request error',
+ 'success $url with http-request'
+ ]));
+ });
+ });
+}
diff --git a/tests/lib/async/zone_task_test.dart b/tests/lib/async/zone_task_test.dart
new file mode 100644
index 0000000..aa9b008
--- /dev/null
+++ b/tests/lib/async/zone_task_test.dart
@@ -0,0 +1,310 @@
+// Copyright (c) 2016, 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.
+
+// Tests basic functionality of tasks in zones.
+
+import 'package:expect/expect.dart';
+import 'package:async_helper/async_helper.dart';
+import 'dart:async';
+
+List log = [];
+
+class MySpecification extends TaskSpecification {
+ final Function callback;
+ final bool isOneShot;
+ final int value;
+
+ MySpecification(void this.callback(), this.isOneShot, this.value);
+
+ String get name => "test.specification-name";
+}
+
+class MyTask {
+ final Zone zone;
+ final Function callback;
+ final int id;
+ int invocationCount = 0;
+ bool shouldStop = false;
+
+ MyTask(this.zone, void this.callback(), this.id);
+}
+
+void runMyTask(MyTask task, int value) {
+ log.add("running "
+ "zone: ${Zone.current['name']} "
+ "task-id: ${task.id} "
+ "invocation-count: ${task.invocationCount} "
+ "value: $value");
+ task.callback();
+ task.invocationCount++;
+}
+
+MyTask createMyTask(MySpecification spec, Zone zone) {
+ var task = new MyTask(zone, spec.callback, spec.value);
+ log.add("creating task: ${spec.value} oneshot?: ${spec.isOneShot}");
+ if (spec.isOneShot) {
+ Timer.run(() {
+ zone.runTask(runMyTask, task, task.id);
+ });
+ } else {
+ new Timer.periodic(const Duration(milliseconds: 10), (Timer timer) {
+ zone.runTask(runMyTask, task, task.id);
+ if (task.shouldStop) {
+ timer.cancel();
+ }
+ });
+ }
+ return task;
+}
+
+MyTask startTask(f, bool oneShot, int value) {
+ var spec = new MySpecification(f, oneShot, value);
+ return Zone.current.createTask(createMyTask, spec);
+}
+
+/// Makes sure things are working in a simple setting.
+/// No interceptions, changes, ...
+Future testCustomTask() {
+ var testCompleter = new Completer();
+ asyncStart();
+
+ Object createTaskHandler(Zone self, ZoneDelegate parent, Zone zone,
+ TaskCreate create, TaskSpecification specification) {
+ if (specification is MySpecification) {
+ log.add("create enter "
+ "zone: ${self['name']} "
+ "spec-value: ${specification.value} "
+ "spec-oneshot?: ${specification.isOneShot}");
+ MyTask result = parent.createTask(zone, create, specification);
+ log.add("create leave");
+ return result;
+ }
+ return parent.createTask(zone, create, specification);
+ }
+
+ void runTaskHandler(Zone self, ZoneDelegate parent, Zone zone, TaskRun run,
+ Object task, Object arg) {
+ if (task is MyTask) {
+ log.add("run enter "
+ "zone: ${self['name']} "
+ "task-id: ${task.id} "
+ "invocation-count: ${task.invocationCount} "
+ "arg: $arg");
+ parent.runTask(zone, run, task, arg);
+ log.add("run leave invocation-count: ${task.invocationCount}");
+ return;
+ }
+ parent.runTask(zone, run, task, arg);
+ }
+
+ runZoned(() async {
+ var completer0 = new Completer();
+ startTask(() {
+ completer0.complete("done");
+ }, true, 0);
+ await completer0.future;
+
+ Expect.listEquals([
+ 'create enter zone: custom zone spec-value: 0 spec-oneshot?: true',
+ 'creating task: 0 oneshot?: true',
+ 'create leave',
+ 'run enter zone: custom zone task-id: 0 invocation-count: 0 arg: 0',
+ 'running zone: custom zone task-id: 0 invocation-count: 0 value: 0',
+ 'run leave invocation-count: 1'
+ ], log);
+ log.clear();
+
+ var completer1 = new Completer();
+ MyTask task1;
+ task1 = startTask(() {
+ if (task1.invocationCount == 1) {
+ task1.shouldStop = true;
+ completer1.complete("done");
+ }
+ }, false, 1);
+ await completer1.future;
+
+ Expect.listEquals([
+ 'create enter zone: custom zone spec-value: 1 spec-oneshot?: false',
+ 'creating task: 1 oneshot?: false',
+ 'create leave',
+ 'run enter zone: custom zone task-id: 1 invocation-count: 0 arg: 1',
+ 'running zone: custom zone task-id: 1 invocation-count: 0 value: 1',
+ 'run leave invocation-count: 1',
+ 'run enter zone: custom zone task-id: 1 invocation-count: 1 arg: 1',
+ 'running zone: custom zone task-id: 1 invocation-count: 1 value: 1',
+ 'run leave invocation-count: 2',
+ ], log);
+ log.clear();
+
+ testCompleter.complete("done");
+ asyncEnd();
+ },
+ zoneValues: {'name': 'custom zone'},
+ zoneSpecification: new ZoneSpecification(
+ createTask: createTaskHandler,
+ runTask: runTaskHandler));
+
+ return testCompleter.future;
+}
+
+/// More complicated zone, that intercepts...
+Future testCustomTask2() {
+ var testCompleter = new Completer();
+ asyncStart();
+
+ Object createTaskHandler(Zone self, ZoneDelegate parent, Zone zone,
+ TaskCreate create, TaskSpecification specification) {
+ if (specification is MySpecification) {
+ log.add("create enter "
+ "zone: ${self['name']} "
+ "spec-value: ${specification.value} "
+ "spec-oneshot?: ${specification.isOneShot}");
+ var replacement = new MySpecification(specification.callback,
+ specification.isOneShot, specification.value + 1);
+ MyTask result = parent.createTask(zone, create, replacement);
+ log.add("create leave");
+ return result;
+ }
+ return parent.createTask(zone, create, specification);
+ }
+
+ void runTaskHandler(Zone self, ZoneDelegate parent, Zone zone, TaskRun run,
+ Object task, Object arg) {
+ if (task is MyTask) {
+ log.add("run enter "
+ "zone: ${self['name']} "
+ "task-id: ${task.id} "
+ "invocation-count: ${task.invocationCount} "
+ "arg: $arg");
+ int value = arg;
+ parent.runTask(zone, run, task, value + 101);
+ log.add("run leave invocation-count: ${task.invocationCount}");
+ return;
+ }
+ parent.runTask(zone, run, task, arg);
+ }
+
+ runZoned(() async {
+ var completer0 = new Completer();
+ startTask(() {
+ completer0.complete("done");
+ }, true, 0);
+ await completer0.future;
+
+ Expect.listEquals([
+ 'create enter zone: outer-zone spec-value: 0 spec-oneshot?: true',
+ 'creating task: 1 oneshot?: true',
+ 'create leave',
+ 'run enter zone: outer-zone task-id: 1 invocation-count: 0 arg: 1',
+ 'running zone: outer-zone task-id: 1 invocation-count: 0 value: 102',
+ 'run leave invocation-count: 1'
+ ], log);
+ log.clear();
+
+ var completer1 = new Completer();
+ MyTask task1;
+ task1 = startTask(() {
+ if (task1.invocationCount == 1) {
+ task1.shouldStop = true;
+ completer1.complete("done");
+ }
+ }, false, 1);
+ await completer1.future;
+
+ Expect.listEquals([
+ 'create enter zone: outer-zone spec-value: 1 spec-oneshot?: false',
+ 'creating task: 2 oneshot?: false',
+ 'create leave',
+ 'run enter zone: outer-zone task-id: 2 invocation-count: 0 arg: 2',
+ 'running zone: outer-zone task-id: 2 invocation-count: 0 value: 103',
+ 'run leave invocation-count: 1',
+ 'run enter zone: outer-zone task-id: 2 invocation-count: 1 arg: 2',
+ 'running zone: outer-zone task-id: 2 invocation-count: 1 value: 103',
+ 'run leave invocation-count: 2',
+ ], log);
+ log.clear();
+
+ var nestedCompleter = new Completer();
+
+ runZoned(() async {
+ var completer0 = new Completer();
+ startTask(() {
+ completer0.complete("done");
+ }, true, 0);
+ await completer0.future;
+
+ Expect.listEquals([
+ 'create enter zone: inner-zone spec-value: 0 spec-oneshot?: true',
+ 'create enter zone: outer-zone spec-value: 1 spec-oneshot?: true',
+ 'creating task: 2 oneshot?: true',
+ 'create leave',
+ 'create leave',
+ 'run enter zone: inner-zone task-id: 2 invocation-count: 0 arg: 2',
+ 'run enter zone: outer-zone task-id: 2 invocation-count: 0 arg: 103',
+ 'running zone: inner-zone task-id: 2 invocation-count: 0 value: 204',
+ 'run leave invocation-count: 1',
+ 'run leave invocation-count: 1'
+ ], log);
+ log.clear();
+
+ var completer1 = new Completer();
+ MyTask task1;
+ task1 = startTask(() {
+ if (task1.invocationCount == 1) {
+ task1.shouldStop = true;
+ completer1.complete("done");
+ }
+ }, false, 1);
+ await completer1.future;
+
+ Expect.listEquals([
+ 'create enter zone: inner-zone spec-value: 1 spec-oneshot?: false',
+ 'create enter zone: outer-zone spec-value: 2 spec-oneshot?: false',
+ 'creating task: 3 oneshot?: false',
+ 'create leave',
+ 'create leave',
+ 'run enter zone: inner-zone task-id: 3 invocation-count: 0 arg: 3',
+ 'run enter zone: outer-zone task-id: 3 invocation-count: 0 arg: 104',
+ 'running zone: inner-zone task-id: 3 invocation-count: 0 value: 205',
+ 'run leave invocation-count: 1',
+ 'run leave invocation-count: 1',
+ 'run enter zone: inner-zone task-id: 3 invocation-count: 1 arg: 3',
+ 'run enter zone: outer-zone task-id: 3 invocation-count: 1 arg: 104',
+ 'running zone: inner-zone task-id: 3 invocation-count: 1 value: 205',
+ 'run leave invocation-count: 2',
+ 'run leave invocation-count: 2',
+ ], log);
+ log.clear();
+
+ nestedCompleter.complete("done");
+ },
+ zoneValues: {'name': 'inner-zone'},
+ zoneSpecification: new ZoneSpecification(
+ createTask: createTaskHandler,
+ runTask: runTaskHandler));
+
+ await nestedCompleter.future;
+ testCompleter.complete("done");
+ asyncEnd();
+ },
+ zoneValues: {'name': 'outer-zone'},
+ zoneSpecification: new ZoneSpecification(
+ createTask: createTaskHandler,
+ runTask: runTaskHandler));
+
+ return testCompleter.future;
+}
+
+runTests() async {
+ await testCustomTask();
+ await testCustomTask2();
+}
+
+main() {
+ asyncStart();
+ runTests().then((_) {
+ asyncEnd();
+ });
+}
diff --git a/tests/lib/async/zone_timer_task_test.dart b/tests/lib/async/zone_timer_task_test.dart
new file mode 100644
index 0000000..310f7ca
--- /dev/null
+++ b/tests/lib/async/zone_timer_task_test.dart
@@ -0,0 +1,515 @@
+// Copyright (c) 2016, 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.
+
+// Tests timer tasks.
+
+import 'package:expect/expect.dart';
+import 'package:async_helper/async_helper.dart';
+import 'dart:async';
+import 'dart:collection';
+
+class MyTimerSpecification implements SingleShotTimerTaskSpecification {
+ final Function callback;
+ final Duration duration;
+
+ MyTimerSpecification(this.callback, this.duration);
+
+ bool get isOneShot => true;
+ String get name => "test.timer-override";
+}
+
+class MyPeriodicTimerSpecification implements PeriodicTimerTaskSpecification {
+ final Function callback;
+ final Duration duration;
+
+ MyPeriodicTimerSpecification(this.callback, this.duration);
+
+ bool get isOneShot => true;
+ String get name => "test.periodic-timer-override";
+}
+
+/// Makes sure things are working in a simple setting.
+/// No interceptions, changes, ...
+Future testTimerTask() {
+ List log = [];
+
+ var testCompleter = new Completer();
+ asyncStart();
+
+ int taskIdCounter = 0;
+
+ Object createTaskHandler(Zone self, ZoneDelegate parent, Zone zone,
+ TaskCreate create, TaskSpecification specification) {
+ var taskMap = self['taskMap'];
+ var taskIdMap = self['taskIdMap'];
+ if (specification is SingleShotTimerTaskSpecification) {
+ log.add("create enter "
+ "zone: ${self['name']} "
+ "spec-duration: ${specification.duration} "
+ "spec-oneshot?: ${specification.isOneShot}");
+ var result = parent.createTask(zone, create, specification);
+ taskMap[result] = specification;
+ taskIdMap[specification] = taskIdCounter++;
+ log.add("create leave");
+ return result;
+ } else if (specification is PeriodicTimerTaskSpecification) {
+ log.add("create enter "
+ "zone: ${self['name']} "
+ "spec-duration: ${specification.duration} "
+ "spec-oneshot?: ${specification.isOneShot}");
+ var result = parent.createTask(zone, create, specification);
+ taskMap[result] = specification;
+ taskIdMap[specification] = taskIdCounter++;
+ log.add("create leave");
+ return result;
+ }
+ return parent.createTask(zone, create, specification);
+ }
+
+ void runTaskHandler(Zone self, ZoneDelegate parent, Zone zone, TaskRun run,
+ Object task, Object arg) {
+ var taskMap = self['taskMap'];
+ var taskIdMap = self['taskIdMap'];
+ if (taskMap.containsKey(task)) {
+ var spec = taskMap[task];
+ log.add("run enter "
+ "zone: ${self['name']} "
+ "task-id: ${taskIdMap[spec]} "
+ "arg: $arg");
+ parent.runTask(zone, run, task, arg);
+ log.add("run leave");
+ return;
+ }
+ parent.runTask(zone, run, task, arg);
+ }
+
+ runZoned(() async {
+ var completer0 = new Completer();
+ Timer.run(() {
+ completer0.complete("done");
+ });
+ await completer0.future;
+
+ Expect.listEquals([
+ 'create enter zone: custom zone spec-duration: 0:00:00.000000 '
+ 'spec-oneshot?: true',
+ 'create leave',
+ 'run enter zone: custom zone task-id: 0 arg: null',
+ 'run leave'
+ ], log);
+ log.clear();
+
+ var completer1 = new Completer();
+ var counter1 = 0;
+ new Timer.periodic(const Duration(milliseconds: 5), (Timer timer) {
+ if (counter1++ > 1) {
+ timer.cancel();
+ completer1.complete("done");
+ }
+ });
+ await completer1.future;
+
+ Expect.listEquals([
+ 'create enter zone: custom zone spec-duration: 0:00:00.005000 '
+ 'spec-oneshot?: false',
+ 'create leave',
+ 'run enter zone: custom zone task-id: 1 arg: null',
+ 'run leave',
+ 'run enter zone: custom zone task-id: 1 arg: null',
+ 'run leave',
+ 'run enter zone: custom zone task-id: 1 arg: null',
+ 'run leave'
+ ], log);
+ log.clear();
+
+ testCompleter.complete("done");
+ asyncEnd();
+ },
+ zoneValues: {'name': 'custom zone', 'taskMap': {}, 'taskIdMap': {}},
+ zoneSpecification: new ZoneSpecification(
+ createTask: createTaskHandler,
+ runTask: runTaskHandler));
+
+ return testCompleter.future;
+}
+
+/// More complicated zone, that intercepts...
+Future testTimerTask2() {
+ List log = [];
+
+ var testCompleter = new Completer();
+ asyncStart();
+
+ int taskIdCounter = 0;
+
+ Object createTaskHandler(Zone self, ZoneDelegate parent, Zone zone,
+ TaskCreate create, TaskSpecification specification) {
+ var taskMap = self['taskMap'];
+ var taskIdMap = self['taskIdMap'];
+ if (specification is SingleShotTimerTaskSpecification) {
+ log.add("create enter "
+ "zone: ${self['name']} "
+ "spec-duration: ${specification.duration} "
+ "spec-oneshot?: ${specification.isOneShot}");
+ var mySpec = new MyTimerSpecification(specification.callback,
+ specification.duration + const Duration(milliseconds: 2));
+ var result = parent.createTask(zone, create, mySpec);
+ taskMap[result] = specification;
+ taskIdMap[specification] = taskIdCounter++;
+ log.add("create leave");
+ return result;
+ } else if (specification is PeriodicTimerTaskSpecification) {
+ log.add("create enter "
+ "zone: ${self['name']} "
+ "spec-duration: ${specification.duration} "
+ "spec-oneshot?: ${specification.isOneShot}");
+ var mySpec = new MyPeriodicTimerSpecification(specification.callback,
+ specification.duration + const Duration(milliseconds: 2));
+ var result = parent.createTask(zone, create, specification);
+ taskMap[result] = specification;
+ taskIdMap[specification] = taskIdCounter++;
+ log.add("create leave");
+ return result;
+ }
+ return parent.createTask(zone, create, specification);
+ }
+
+ void runTaskHandler(Zone self, ZoneDelegate parent, Zone zone, TaskRun run,
+ Object task, Object arg) {
+ var taskMap = self['taskMap'];
+ var taskIdMap = self['taskIdMap'];
+ if (taskMap.containsKey(task)) {
+ var spec = taskMap[task];
+ log.add("run enter "
+ "zone: ${self['name']} "
+ "task-id: ${taskIdMap[spec]} "
+ "arg: $arg");
+ parent.runTask(zone, run, task, arg);
+ log.add("run leave");
+ return;
+ }
+ parent.runTask(zone, run, task, arg);
+ }
+
+ runZoned(() async {
+ var completer0 = new Completer();
+ Timer.run(() {
+ completer0.complete("done");
+ });
+ await completer0.future;
+
+ // No visible change (except for the zone name) in the log, compared to the
+ // simple invocations.
+ Expect.listEquals([
+ 'create enter zone: outer-zone spec-duration: 0:00:00.000000 '
+ 'spec-oneshot?: true',
+ 'create leave',
+ 'run enter zone: outer-zone task-id: 0 arg: null',
+ 'run leave'
+ ], log);
+ log.clear();
+
+ var completer1 = new Completer();
+ var counter1 = 0;
+ new Timer.periodic(const Duration(milliseconds: 5), (Timer timer) {
+ if (counter1++ > 1) {
+ timer.cancel();
+ completer1.complete("done");
+ }
+ });
+ await completer1.future;
+
+ // No visible change (except for the zone nome) in the log, compared to the
+ // simple invocations.
+ Expect.listEquals([
+ 'create enter zone: outer-zone spec-duration: 0:00:00.005000 '
+ 'spec-oneshot?: false',
+ 'create leave',
+ 'run enter zone: outer-zone task-id: 1 arg: null',
+ 'run leave',
+ 'run enter zone: outer-zone task-id: 1 arg: null',
+ 'run leave',
+ 'run enter zone: outer-zone task-id: 1 arg: null',
+ 'run leave'
+ ], log);
+ log.clear();
+
+ var nestedCompleter = new Completer();
+
+ runZoned(() async {
+ var completer0 = new Completer();
+ Timer.run(() {
+ completer0.complete("done");
+ });
+ await completer0.future;
+
+ // The outer zone sees the duration change of the inner zone.
+ Expect.listEquals([
+ 'create enter zone: inner-zone spec-duration: 0:00:00.000000 '
+ 'spec-oneshot?: true',
+ 'create enter zone: outer-zone spec-duration: 0:00:00.002000 '
+ 'spec-oneshot?: true',
+ 'create leave',
+ 'create leave',
+ 'run enter zone: inner-zone task-id: 3 arg: null',
+ 'run enter zone: outer-zone task-id: 2 arg: null',
+ 'run leave',
+ 'run leave'
+ ], log);
+ log.clear();
+
+ var completer1 = new Completer();
+ var counter1 = 0;
+ new Timer.periodic(const Duration(milliseconds: 5), (Timer timer) {
+ if (counter1++ > 1) {
+ timer.cancel();
+ completer1.complete("done");
+ }
+ });
+ await completer1.future;
+
+ // The outer zone sees the duration change of the inner zone.
+ Expect.listEquals([
+ 'create enter zone: inner-zone spec-duration: 0:00:00.005000 '
+ 'spec-oneshot?: false',
+ 'create enter zone: outer-zone spec-duration: 0:00:00.005000 '
+ 'spec-oneshot?: false',
+ 'create leave',
+ 'create leave',
+ 'run enter zone: inner-zone task-id: 5 arg: null',
+ 'run enter zone: outer-zone task-id: 4 arg: null',
+ 'run leave',
+ 'run leave',
+ 'run enter zone: inner-zone task-id: 5 arg: null',
+ 'run enter zone: outer-zone task-id: 4 arg: null',
+ 'run leave',
+ 'run leave',
+ 'run enter zone: inner-zone task-id: 5 arg: null',
+ 'run enter zone: outer-zone task-id: 4 arg: null',
+ 'run leave',
+ 'run leave'
+ ], log);
+ log.clear();
+
+ nestedCompleter.complete("done");
+ },
+ zoneValues: {'name': 'inner-zone', 'taskMap': {}, 'taskIdMap': {}},
+ zoneSpecification: new ZoneSpecification(
+ createTask: createTaskHandler,
+ runTask: runTaskHandler));
+
+ await nestedCompleter.future;
+ testCompleter.complete("done");
+ asyncEnd();
+ },
+ zoneValues: {'name': 'outer-zone', 'taskMap': {}, 'taskIdMap': {}},
+ zoneSpecification: new ZoneSpecification(
+ createTask: createTaskHandler,
+ runTask: runTaskHandler));
+
+ return testCompleter.future;
+}
+
+class TimerEntry {
+ final int time;
+ final SimulatedTimer timer;
+
+ TimerEntry(this.time, this.timer);
+}
+
+class SimulatedTimer implements Timer {
+ static int _idCounter = 0;
+
+ Zone _zone;
+ final int _id = _idCounter++;
+ final Duration _duration;
+ final Function _callback;
+ final bool _isPeriodic;
+ bool _isActive = true;
+
+ SimulatedTimer(this._zone, this._duration, this._callback, this._isPeriodic);
+
+ bool get isActive => _isActive;
+
+ void cancel() {
+ _isActive = false;
+ }
+
+ void _run() {
+ if (!isActive) return;
+ _zone.runTask(_runTimer, this, null);
+ }
+
+ static void _runTimer(SimulatedTimer timer, _) {
+ if (timer._isPeriodic) {
+ timer._callback(timer);
+ } else {
+ timer._callback();
+ }
+ }
+}
+
+testSimulatedTimer() {
+ List log = [];
+
+ var currentTime = 0;
+ // Using a simple list as queue. Not very efficient, but the test has only
+ // very few timers running at the same time.
+ var queue = new DoubleLinkedQueue<TimerEntry>();
+
+ // Schedules the given callback at now + duration.
+ void schedule(int scheduledTime, SimulatedTimer timer) {
+ log.add("scheduling timer ${timer._id} for $scheduledTime");
+ if (queue.isEmpty) {
+ queue.add(new TimerEntry(scheduledTime, timer));
+ } else {
+ DoubleLinkedQueueEntry current = queue.firstEntry();
+ while (current != null) {
+ if (current.element.time <= scheduledTime) {
+ current = current.nextEntry();
+ } else {
+ current.prepend(new TimerEntry(scheduledTime, timer));
+ break;
+ }
+ }
+ if (current == null) {
+ queue.add(new TimerEntry(scheduledTime, timer));
+ }
+ }
+ }
+
+ void runQueue() {
+ while (queue.isNotEmpty) {
+ var item = queue.removeFirst();
+ // If multiple callbacks were scheduled at the same time, increment the
+ // current time instead of staying at the same time.
+ currentTime = item.time > currentTime ? item.time : currentTime + 1;
+ SimulatedTimer timer = item.timer;
+ log.add("running timer ${timer._id} at $currentTime "
+ "(active?: ${timer.isActive})");
+ if (!timer.isActive) continue;
+ if (timer._isPeriodic) {
+ schedule(currentTime + timer._duration.inMilliseconds, timer);
+ }
+ item.timer._run();
+ }
+ }
+
+ SimulatedTimer createSimulatedOneShotTimer(
+ SingleShotTimerTaskSpecification spec, Zone zone) {
+ var timer = new SimulatedTimer(zone, spec.duration, spec.callback, false);
+ schedule(currentTime + spec.duration.inMilliseconds, timer);
+ return timer;
+ }
+
+ SimulatedTimer createSimulatedPeriodicTimer(
+ PeriodicTimerTaskSpecification spec, Zone zone) {
+ var timer = new SimulatedTimer(zone, spec.duration, spec.callback, true);
+ schedule(currentTime + spec.duration.inMilliseconds, timer);
+ return timer;
+ }
+
+ Object createSimulatedTaskHandler(Zone self, ZoneDelegate parent, Zone zone,
+ TaskCreate create, TaskSpecification specification) {
+ var taskMap = self['taskMap'];
+ var taskIdMap = self['taskIdMap'];
+ if (specification is SingleShotTimerTaskSpecification) {
+ log.add("create enter "
+ "zone: ${self['name']} "
+ "spec-duration: ${specification.duration} "
+ "spec-oneshot?: ${specification.isOneShot}");
+ var result =
+ parent.createTask(zone, createSimulatedOneShotTimer, specification);
+ log.add("create leave");
+ return result;
+ }
+ if (specification is PeriodicTimerTaskSpecification) {
+ log.add("create enter "
+ "zone: ${self['name']} "
+ "spec-duration: ${specification.duration} "
+ "spec-oneshot?: ${specification.isOneShot}");
+ var result =
+ parent.createTask(zone, createSimulatedPeriodicTimer, specification);
+ log.add("create leave");
+ return result;
+ }
+ return parent.createTask(zone, create, specification);
+ }
+
+ runZoned(() {
+ Timer.run(() {
+ log.add("running Timer.run");
+ });
+
+ var timer0;
+
+ new Timer(const Duration(milliseconds: 10), () {
+ log.add("running Timer(10)");
+ timer0.cancel();
+ log.add("canceled timer0");
+ });
+
+ timer0 = new Timer(const Duration(milliseconds: 15), () {
+ log.add("running Timer(15)");
+ });
+
+ var counter1 = 0;
+ new Timer.periodic(const Duration(milliseconds: 5), (Timer timer) {
+ log.add("running periodic timer $counter1");
+ if (counter1++ > 1) {
+ timer.cancel();
+ }
+ });
+ },
+ zoneSpecification:
+ new ZoneSpecification(createTask: createSimulatedTaskHandler));
+
+ runQueue();
+
+ Expect.listEquals([
+ 'create enter zone: null spec-duration: 0:00:00.000000 spec-oneshot?: true',
+ 'scheduling timer 0 for 0',
+ 'create leave',
+ 'create enter zone: null spec-duration: 0:00:00.010000 spec-oneshot?: true',
+ 'scheduling timer 1 for 10',
+ 'create leave',
+ 'create enter zone: null spec-duration: 0:00:00.015000 spec-oneshot?: true',
+ 'scheduling timer 2 for 15',
+ 'create leave',
+ 'create enter zone: null spec-duration: 0:00:00.005000 '
+ 'spec-oneshot?: false',
+ 'scheduling timer 3 for 5',
+ 'create leave',
+ 'running timer 0 at 1 (active?: true)',
+ 'running Timer.run',
+ 'running timer 3 at 5 (active?: true)',
+ 'scheduling timer 3 for 10',
+ 'running periodic timer 0',
+ 'running timer 1 at 10 (active?: true)',
+ 'running Timer(10)',
+ 'canceled timer0',
+ 'running timer 3 at 11 (active?: true)',
+ 'scheduling timer 3 for 16',
+ 'running periodic timer 1',
+ 'running timer 2 at 15 (active?: false)',
+ 'running timer 3 at 16 (active?: true)',
+ 'scheduling timer 3 for 21',
+ 'running periodic timer 2',
+ 'running timer 3 at 21 (active?: false)'
+ ], log);
+ log.clear();
+}
+
+runTests() async {
+ await testTimerTask();
+ await testTimerTask2();
+ testSimulatedTimer();
+}
+
+main() {
+ asyncStart();
+ runTests().then((_) {
+ asyncEnd();
+ });
+}
diff --git a/tests/lib/lib.status b/tests/lib/lib.status
index 8d3dcee..ecfc10d 100644
--- a/tests/lib/lib.status
+++ b/tests/lib/lib.status
@@ -167,6 +167,8 @@
async/stream_transformation_broadcast_test: RuntimeError # Timer interface not supported: Issue 7728.
async/stream_controller_test: Fail # Timer interface not supported: Issue 7728.
async/future_constructor2_test: Fail # Timer interface not supported: Issue 7728.
+async/zone_timer_task_test: Fail # Timer interface not supported: Issue 7728.
+async/zone_task_test: Fail # Timer interface not supported: Issue 7728.
mirrors/mirrors_reader_test: Skip # Running in v8 suffices. Issue 16589 - RuntimeError. Issue 22130 - Crash (out of memory).
[ $compiler == dart2js && $checked ]
diff --git a/tools/dom/docs/docs.json b/tools/dom/docs/docs.json
index 123447d..98455fd 100644
--- a/tools/dom/docs/docs.json
+++ b/tools/dom/docs/docs.json
@@ -4201,7 +4201,7 @@
" *",
" * Note: Most simple HTTP requests can be accomplished using the [getString],",
" * [request], [requestCrossOrigin], or [postFormData] methods. Use of this",
- " * `open` method is intended only for more complext HTTP requests where",
+ " * `open` method is intended only for more complex HTTP requests where",
" * finer-grained control is needed.",
" */"
],
@@ -4298,7 +4298,7 @@
" *",
" * Note: Most simple HTTP requests can be accomplished using the [getString],",
" * [request], [requestCrossOrigin], or [postFormData] methods. Use of this",
- " * `send` method is intended only for more complext HTTP requests where",
+ " * `send` method is intended only for more complex HTTP requests where",
" * finer-grained control is needed.",
" *",
" * ## Other resources",
diff --git a/tools/dom/scripts/htmlrenamer.py b/tools/dom/scripts/htmlrenamer.py
index 856da50..7ec0685 100644
--- a/tools/dom/scripts/htmlrenamer.py
+++ b/tools/dom/scripts/htmlrenamer.py
@@ -404,6 +404,8 @@
'Window.requestAnimationFrame',
'Window.setInterval',
'Window.setTimeout',
+
+ 'XMLHttpRequest.send',
])
# Members from the standard dom that exist in the dart:html library with
diff --git a/tools/dom/src/EventStreamProvider.dart b/tools/dom/src/EventStreamProvider.dart
index 474a143..b6c32fc 100644
--- a/tools/dom/src/EventStreamProvider.dart
+++ b/tools/dom/src/EventStreamProvider.dart
@@ -118,6 +118,41 @@
StreamSubscription<T> capture(void onData(T event));
}
+/// Task specification for DOM Events.
+///
+/// *Experimental*. May disappear without notice.
+class EventSubscriptionSpecification<T extends Event>
+ implements TaskSpecification {
+ @override
+ final String name;
+ @override
+ final bool isOneShot;
+
+ final EventTarget target;
+ /// The event-type of the event. For example 'click' for click events.
+ final String eventType;
+ // TODO(floitsch): the first generic argument should be 'void'.
+ final ZoneUnaryCallback<dynamic, T> onData;
+ final bool useCapture;
+
+ EventSubscriptionSpecification({this.name, this.isOneShot, this.target,
+ this.eventType, void this.onData(T event), this.useCapture});
+
+ /// Returns a copy of this instance, with every non-null argument replaced
+ /// by the given value.
+ EventSubscriptionSpecification<T> replace(
+ {String name, bool isOneShot, EventTarget target,
+ String eventType, void onData(T event), bool useCapture}) {
+ return new EventSubscriptionSpecification<T>(
+ name: name ?? this.name,
+ isOneShot: isOneShot ?? this.isOneShot,
+ target: target ?? this.target,
+ eventType: eventType ?? this.eventType,
+ onData: onData ?? this.onData,
+ useCapture: useCapture ?? this.useCapture);
+ }
+}
+
/**
* Adapter for exposing DOM events as Dart streams.
*/
@@ -125,8 +160,16 @@
final EventTarget _target;
final String _eventType;
final bool _useCapture;
+ /// The name that is used in the task specification.
+ final String _name;
+ /// Whether the stream can trigger multiple times.
+ final bool _isOneShot;
- _EventStream(this._target, this._eventType, this._useCapture);
+ _EventStream(this._target, String eventType, this._useCapture,
+ {String name, bool isOneShot: false})
+ : _eventType = eventType,
+ _isOneShot = isOneShot,
+ _name = name ?? "dart.html.event.$eventType";
// DOM events are inherently multi-subscribers.
Stream<T> asBroadcastStream({void onListen(StreamSubscription<T> subscription),
@@ -134,13 +177,31 @@
=> this;
bool get isBroadcast => true;
+ StreamSubscription<T> _listen(
+ void onData(T event), {bool useCapture}) {
+
+ if (identical(Zone.current, Zone.ROOT)) {
+ return new _EventStreamSubscription<T>(
+ this._target, this._eventType, onData, this._useCapture,
+ Zone.current);
+ }
+
+ var specification = new EventSubscriptionSpecification<T>(
+ name: this._name, isOneShot: this._isOneShot,
+ target: this._target, eventType: this._eventType,
+ onData: onData, useCapture: useCapture);
+ // We need to wrap the _createStreamSubscription call, since a tear-off
+ // would not bind the generic type 'T'.
+ return Zone.current.createTask((spec, Zone zone) {
+ return _createStreamSubscription/*<T>*/(spec, zone);
+ }, specification);
+ }
+
StreamSubscription<T> listen(void onData(T event),
{ Function onError,
void onDone(),
bool cancelOnError}) {
-
- return new _EventStreamSubscription<T>(
- this._target, this._eventType, onData, this._useCapture);
+ return _listen(onData, useCapture: this._useCapture);
}
}
@@ -155,8 +216,9 @@
*/
class _ElementEventStreamImpl<T extends Event> extends _EventStream<T>
implements ElementStream<T> {
- _ElementEventStreamImpl(target, eventType, useCapture) :
- super(target, eventType, useCapture);
+ _ElementEventStreamImpl(target, eventType, useCapture,
+ {String name, bool isOneShot: false}) :
+ super(target, eventType, useCapture, name: name, isOneShot: isOneShot);
Stream<T> matches(String selector) => this.where(
(event) => _matchesWithAncestors(event, selector)).map((e) {
@@ -164,9 +226,9 @@
return e;
});
- StreamSubscription<T> capture(void onData(T event)) =>
- new _EventStreamSubscription<T>(
- this._target, this._eventType, onData, true);
+ StreamSubscription<T> capture(void onData(T event)) {
+ return _listen(onData, useCapture: true);
+ }
}
/**
@@ -215,7 +277,13 @@
bool get isBroadcast => true;
}
-// We would like this to just be EventListener<T> but that typdef cannot
+StreamSubscription/*<T>*/ _createStreamSubscription/*<T>*/(
+ EventSubscriptionSpecification/*<T>*/ spec, Zone zone) {
+ return new _EventStreamSubscription/*<T>*/(spec.target, spec.eventType,
+ spec.onData, spec.useCapture, zone);
+}
+
+// We would like this to just be EventListener<T> but that typedef cannot
// use generics until dartbug/26276 is fixed.
typedef _EventListener<T extends Event>(T event);
@@ -224,15 +292,19 @@
EventTarget _target;
final String _eventType;
EventListener _onData;
+ EventListener _domCallback;
final bool _useCapture;
+ final Zone _zone;
// TODO(jacobr): for full strong mode correctness we should write
- // _onData = onData == null ? null : _wrapZone/*<Event, dynamic>*/((e) => onData(e as T))
+ // _onData = onData == null ? null : _wrapZone/*<dynamic, Event>*/((e) => onData(e as T))
// but that breaks 114 co19 tests as well as multiple html tests as it is reasonable
// to pass the wrong type of event object to an event listener as part of a
// test.
_EventStreamSubscription(this._target, this._eventType, void onData(T event),
- this._useCapture) : _onData = _wrapZone/*<Event, dynamic>*/(onData) {
+ this._useCapture, Zone zone)
+ : _zone = zone,
+ _onData = _registerZone/*<dynamic, Event>*/(zone, onData) {
_tryResume();
}
@@ -254,7 +326,7 @@
}
// Remove current event listener.
_unlisten();
- _onData = _wrapZone/*<Event, dynamic>*/(handleData);
+ _onData = _registerZone/*<dynamic, Event>*/(_zone, handleData);
_tryResume();
}
@@ -283,14 +355,25 @@
}
void _tryResume() {
- if (_onData != null && !isPaused) {
- _target.addEventListener(_eventType, _onData, _useCapture);
+ if (_onData == null || isPaused) return;
+ if (identical(_zone, Zone.ROOT)) {
+ _domCallback = _onData;
+ } else {
+ _domCallback = (event) {
+ _zone.runTask(_runEventNotification, this, event);
+ };
}
+ _target.addEventListener(_eventType, _domCallback, _useCapture);
+ }
+
+ static void _runEventNotification/*<T>*/(
+ _EventStreamSubscription/*<T>*/ subscription, /*=T*/ event) {
+ subscription._onData(event);
}
void _unlisten() {
if (_onData != null) {
- _target.removeEventListener(_eventType, _onData, _useCapture);
+ _target.removeEventListener(_eventType, _domCallback, _useCapture);
}
}
diff --git a/tools/dom/src/shared_html.dart b/tools/dom/src/shared_html.dart
index 7342cdf..f2c32f3 100644
--- a/tools/dom/src/shared_html.dart
+++ b/tools/dom/src/shared_html.dart
@@ -4,31 +4,26 @@
part of dart.dom.html;
-// TODO(jacobr): remove these typedefs when dart:async supports generic types.
-typedef R _wrapZoneCallback<A, R>(A a);
-typedef R _wrapZoneBinaryCallback<A, B, R>(A a, B b);
-
-_wrapZoneCallback/*<A, R>*/ _wrapZone/*<A, R>*/(_wrapZoneCallback/*<A, R>*/ callback) {
- // For performance reasons avoid wrapping if we are in the root zone.
- if (Zone.current == Zone.ROOT) return callback;
+ZoneUnaryCallback/*<R, T>*/ _registerZone/*<R, T>*/(Zone zone,
+ ZoneUnaryCallback/*<R, T>*/ callback) {
+ // For performance reasons avoid registering if we are in the root zone.
+ if (identical(zone, Zone.ROOT)) return callback;
if (callback == null) return null;
- // TODO(jacobr): we cast to _wrapZoneCallback/*<A, R>*/ to hack around missing
- // generic method support in zones.
- // ignore: STRONG_MODE_DOWN_CAST_COMPOSITE
- _wrapZoneCallback/*<A, R>*/ wrapped =
- Zone.current.bindUnaryCallback(callback, runGuarded: true);
- return wrapped;
+ return zone.registerUnaryCallback(callback);
}
-_wrapZoneBinaryCallback/*<A, B, R>*/ _wrapBinaryZone/*<A, B, R>*/(_wrapZoneBinaryCallback/*<A, B, R>*/ callback) {
- if (Zone.current == Zone.ROOT) return callback;
+ZoneUnaryCallback/*<R, T>*/ _wrapZone/*<R, T>*/(ZoneUnaryCallback/*<R, T>*/ callback) {
+ // For performance reasons avoid wrapping if we are in the root zone.
+ if (identical(Zone.current, Zone.ROOT)) return callback;
if (callback == null) return null;
- // We cast to _wrapZoneBinaryCallback/*<A, B, R>*/ to hack around missing
- // generic method support in zones.
- // ignore: STRONG_MODE_DOWN_CAST_COMPOSITE
- _wrapZoneBinaryCallback/*<A, B, R>*/ wrapped =
- Zone.current.bindBinaryCallback(callback, runGuarded: true);
- return wrapped;
+ return Zone.current.bindUnaryCallback(callback, runGuarded: true);
+}
+
+ZoneBinaryCallback/*<R, A, B>*/ _wrapBinaryZone/*<R, A, B>*/(
+ ZoneBinaryCallback/*<R, A, B>*/ callback) {
+ if (identical(Zone.current, Zone.ROOT)) return callback;
+ if (callback == null) return null;
+ return Zone.current.bindBinaryCallback(callback, runGuarded: true);
}
/**
diff --git a/tools/dom/templates/html/impl/impl_Window.darttemplate b/tools/dom/templates/html/impl/impl_Window.darttemplate
index 8abac37..fe4db3d 100644
--- a/tools/dom/templates/html/impl/impl_Window.darttemplate
+++ b/tools/dom/templates/html/impl/impl_Window.darttemplate
@@ -4,6 +4,99 @@
part of $LIBRARYNAME;
+typedef void RemoveFrameRequestMapping(int id);
+
+/**
+ * The task object representing animation-frame requests.
+ *
+ * For historical reasons, [Window.requestAnimationFrame] returns an integer
+ * to users. However, zone tasks must be unique objects, and an integer can
+ * therefore not be used as task object. The [Window] class thus keeps a mapping
+ * from the integer ID to the corresponding task object. All zone related
+ * operations work on this task object, whereas users of
+ * [Window.requestAnimationFrame] only see the integer ID.
+ *
+ * Since this mapping takes up space, it must be removed when the
+ * animation-frame task has triggered. The default implementation does this
+ * automatically, but intercepting implementations of `requestAnimationFrame`
+ * must make sure to call the [AnimationFrameTask.removeMapping]
+ * function that is provided in the task specification.
+ *
+ * *Experimental*. This class may disappear without notice.
+ */
+abstract class AnimationFrameTask {
+ /** The ID that is returned to users. */
+ int get id;
+
+ /** The zone in which the task will run. */
+ Zone get zone;
+
+ /**
+ * Cancels the animation-frame request.
+ *
+ * A call to [Window.cancelAnimationFrame] with an `id` argument equal to [id]
+ * forwards the request to this function.
+ *
+ * Zones that intercept animation-frame requests implement this method so
+ * that they can react to cancelation requests.
+ */
+ void cancel(Window window);
+
+ /**
+ * Maps animation-frame request IDs to their task objects.
+ */
+ static final Map<int, _AnimationFrameTask> _tasks = {};
+
+ /**
+ * Removes the mapping from [id] to [AnimationFrameTask].
+ *
+ * This function must be invoked by user-implemented animation-frame
+ * tasks, before running [callback].
+ *
+ * See [AnimationFrameTask].
+ */
+ static void removeMapping(int id) {
+ _tasks.remove(id);
+ }
+}
+
+class _AnimationFrameTask implements AnimationFrameTask {
+ final int id;
+ final Zone zone;
+ final FrameRequestCallback _callback;
+
+ _AnimationFrameTask(this.id, this.zone, this._callback);
+
+ void cancel(Window window) {
+ window._cancelAnimationFrame(this.id);
+ }
+}
+
+/**
+ * The task specification for an animation-frame request.
+ *
+ * *Experimental*. This class may disappear without notice.
+ */
+class AnimationFrameRequestSpecification implements TaskSpecification {
+ /**
+ * The window on which [Window.requestAnimationFrame] was invoked.
+ */
+ final Window window;
+
+ /**
+ * The callback that is executed when the animation-frame is ready.
+ *
+ * Note that the callback hasn't been registered in any zone when the `create`
+ * function (passed to [Zone.createTask]) is invoked.
+ */
+ final FrameRequestCallback callback;
+
+ AnimationFrameRequestSpecification(this.window, this.callback);
+
+ String get name => "dart.html.request-animation-frame";
+ bool get isOneShot => true;
+}
+
@DocsEditable()
$if DART2JS
$(ANNOTATIONS)@Native("Window,DOMWindow")
@@ -29,9 +122,7 @@
*/
Future<num> get animationFrame {
var completer = new Completer<num>.sync();
- requestAnimationFrame((time) {
- completer.complete(time);
- });
+ requestAnimationFrame(completer.complete);
return completer.future;
}
@@ -115,7 +206,30 @@
@DomName('Window.requestAnimationFrame')
int requestAnimationFrame(FrameRequestCallback callback) {
_ensureRequestAnimationFrame();
- return _requestAnimationFrame(_wrapZone/*<num, dynamic>*/(callback));
+ if (identical(Zone.current, Zone.ROOT)) {
+ return _requestAnimationFrame(callback);
+ }
+ var spec = new AnimationFrameRequestSpecification(this, callback);
+ var task = Zone.current.createTask/*<AnimationFrameTask>*/(
+ _createAnimationFrameTask, spec);
+ AnimationFrameTask._tasks[task.id] = task;
+ return task.id;
+ }
+
+ static _AnimationFrameTask _createAnimationFrameTask(
+ AnimationFrameRequestSpecification spec, Zone zone) {
+ var task;
+ var id = spec.window._requestAnimationFrame((num time) {
+ AnimationFrameTask.removeMapping(task.id);
+ zone.runTask(_runAnimationFrame, task, time);
+ });
+ var callback = zone.registerUnaryCallback(spec.callback);
+ task = new _AnimationFrameTask(id, zone, callback);
+ return task;
+ }
+
+ static void _runAnimationFrame(_AnimationFrameTask task, num time) {
+ task._callback(time);
}
/**
@@ -128,7 +242,13 @@
*/
void cancelAnimationFrame(int id) {
_ensureRequestAnimationFrame();
- _cancelAnimationFrame(id);
+ var task = AnimationFrameTask._tasks.remove(id);
+ if (task == null) {
+ // Assume that the animation frame request wasn't intercepted by a zone.
+ _cancelAnimationFrame(id);
+ return;
+ }
+ task.cancel(this);
}
@JSName('requestAnimationFrame')
diff --git a/tools/dom/templates/html/impl/impl_XMLHttpRequest.darttemplate b/tools/dom/templates/html/impl/impl_XMLHttpRequest.darttemplate
index 9ad00f6..35683fc 100644
--- a/tools/dom/templates/html/impl/impl_XMLHttpRequest.darttemplate
+++ b/tools/dom/templates/html/impl/impl_XMLHttpRequest.darttemplate
@@ -4,6 +4,109 @@
part of $LIBRARYNAME;
+/**
+ * A task specification for HTTP requests.
+ *
+ * This specification is not available when an HTTP request is sent through
+ * direct use of [HttpRequest.send]. See [HttpRequestSendTaskSpecification].
+ *
+ * A task created from this specification is a `Future<HttpRequest>`.
+ *
+ * *Experimental*. This class may disappear without notice.
+ */
+class HttpRequestTaskSpecification extends TaskSpecification {
+ /// The URL of the request.
+ final String url;
+
+ /// The HTTP request method.
+ ///
+ /// By default (when `null`) this is a `"GET"` request. Alternatively, the
+ /// method can be `"POST"`, `"PUT"`, `"DELETE"`, etc.
+ final String method;
+
+ /// Whether the request should send credentials. Credentials are only useful
+ /// for cross-origin requests.
+ ///
+ /// See [HttpRequest.request] for more information.
+ final bool withCredentials;
+
+ /// The desired response format.
+ ///
+ /// Supported types are:
+ /// - `""`: (same as `"text"`),
+ /// - `"arraybuffer"`,
+ /// - `"blob"`,
+ /// - `"document"`,
+ /// - `"json"`,
+ /// - `"text"`
+ ///
+ /// When no value is provided (when equal to `null`) defaults to `""`.
+ final String responseType;
+
+ /// The desired MIME type.
+ ///
+ /// This overrides the default MIME type which is set up to transfer textual
+ /// data.
+ final String mimeType;
+
+ /// The request headers that should be sent with the request.
+ final Map<String, String> requestHeaders;
+
+ /// The data that is sent with the request.
+ ///
+ /// When data is provided (the value is not `null`), it must be a
+ /// [ByteBuffer], [Blob], [Document], [String], or [FormData].
+ final dynamic sendData;
+
+ /// The function that is invoked on progress updates. This function is
+ /// registered as an event listener on the created [HttpRequest] object, and
+ /// thus has its own task. Further invocations of the progress function do
+ /// *not* use the HTTP request task as task object.
+ ///
+ /// Creating an HTTP request automatically registers the on-progress listener.
+ final ZoneUnaryCallback<dynamic, ProgressEvent> onProgress;
+
+ HttpRequestTaskSpecification(this.url,
+ {String this.method, bool this.withCredentials, String this.responseType,
+ String this.mimeType, Map<String, String> this.requestHeaders,
+ this.sendData,
+ void this.onProgress(ProgressEvent e)});
+
+ String get name => "dart.html.http-request";
+ bool get isOneShot => true;
+}
+
+/**
+ * A task specification for HTTP requests that are initiated through a direct
+ * invocation of [HttpRequest.send].
+ *
+ * This specification serves as signal to zones that an HTTP request has been
+ * initiated. The created task is the [request] object itself, and
+ * no callback is ever executed in this task.
+ *
+ * Note that event listeners on the HTTP request are also registered in the
+ * zone (although with their own task creations), and that a zone can thus
+ * detect when the HTTP request returns.
+ *
+ * HTTP requests that are initiated through `request` methods don't use
+ * this class but use [HttpRequestTaskSpecification].
+ *
+ * *Experimental*. This class may disappear without notice.
+ */
+class HttpRequestSendTaskSpecification extends TaskSpecification {
+ final HttpRequest request;
+ final dynamic sendData;
+
+ HttpRequestSendTaskSpecification(this.request, this.sendData);
+
+ String get name => "dart.html.http-request-send";
+
+ /**
+ * No callback is ever executed in an HTTP request send task.
+ */
+ bool get isOneShot => false;
+}
+
/**
* A client-side XHR request for getting data from a URL,
* formally known as XMLHttpRequest.
@@ -190,7 +293,34 @@
{String method, bool withCredentials, String responseType,
String mimeType, Map<String, String> requestHeaders, sendData,
void onProgress(ProgressEvent e)}) {
+ var spec = new HttpRequestTaskSpecification(
+ url, method: method,
+ withCredentials: withCredentials,
+ responseType: responseType,
+ mimeType: mimeType,
+ requestHeaders: requestHeaders,
+ sendData: sendData,
+ onProgress: onProgress);
+
+ if (identical(Zone.current, Zone.ROOT)) {
+ return _createHttpRequestTask(spec, null);
+ }
+ return Zone.current.createTask(_createHttpRequestTask, spec);
+ }
+
+ static Future<HttpRequest> _createHttpRequestTask(
+ HttpRequestTaskSpecification spec, Zone zone) {
+ String url = spec.url;
+ String method = spec.method;
+ bool withCredentials = spec.withCredentials;
+ String responseType = spec.responseType;
+ String mimeType = spec.mimeType;
+ Map<String, String> requestHeaders = spec.requestHeaders;
+ var sendData = spec.sendData;
+ var onProgress = spec.onProgress;
+
var completer = new Completer<HttpRequest>();
+ var task = completer.future;
var xhr = new HttpRequest();
if (method == null) {
@@ -230,23 +360,42 @@
// redirect case will be handled by the browser before it gets to us,
// so if we see it we should pass it through to the user.
var unknownRedirect = xhr.status > 307 && xhr.status < 400;
-
- if (accepted || fileUri || notModified || unknownRedirect) {
+
+ var isSuccessful = accepted || fileUri || notModified || unknownRedirect;
+
+ if (zone == null && isSuccessful) {
completer.complete(xhr);
- } else {
+ } else if (zone == null) {
completer.completeError(e);
+ } else if (isSuccessful) {
+ zone.runTask((task, value) {
+ completer.complete(value);
+ }, task, xhr);
+ } else {
+ zone.runTask((task, error) {
+ completer.completeError(error);
+ }, task, e);
}
});
- xhr.onError.listen(completer.completeError);
-
- if (sendData != null) {
- xhr.send(sendData);
+ if (zone == null) {
+ xhr.onError.listen(completer.completeError);
} else {
- xhr.send();
+ xhr.onError.listen((error) {
+ zone.runTask((task, error) {
+ completer.completeError(error);
+ }, task, error);
+ });
}
- return completer.future;
+ if (sendData != null) {
+ // TODO(floitsch): should we go through 'send()' and have nested tasks?
+ xhr._send(sendData);
+ } else {
+ xhr._send();
+ }
+
+ return task;
}
/**
@@ -316,6 +465,9 @@
return xhr.responseText;
});
}
+ // TODO(floitsch): the following code doesn't go through task zones.
+ // Since 'XDomainRequest' is an IE9 feature we should probably just remove
+ // it.
$if DART2JS
var completer = new Completer<String>();
if (method == null) {
@@ -396,7 +548,7 @@
*
* Note: Most simple HTTP requests can be accomplished using the [getString],
* [request], [requestCrossOrigin], or [postFormData] methods. Use of this
- * `open` method is intended only for more complext HTTP requests where
+ * `open` method is intended only for more complex HTTP requests where
* finer-grained control is needed.
*/
@DomName('XMLHttpRequest.open')
@@ -413,5 +565,35 @@
void open(String method, String url, {bool async, String user, String password}) native;
$endif
+ /**
+ * Sends the request with any given `data`.
+ *
+ * Note: Most simple HTTP requests can be accomplished using the [getString],
+ * [request], [requestCrossOrigin], or [postFormData] methods. Use of this
+ * `send` method is intended only for more complex HTTP requests where
+ * finer-grained control is needed.
+ *
+ * ## Other resources
+ *
+ * * [XMLHttpRequest.send](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#send%28%29)
+ * from MDN.
+ */
+ @DomName('XMLHttpRequest.send')
+ @DocsEditable()
+ void send([body_OR_data]) {
+ if (identical(Zone.current, Zone.ROOT)) {
+ _send(body_OR_data);
+ } else {
+ Zone.current.createTask(_createHttpRequestSendTask,
+ new HttpRequestSendTaskSpecification(this, body_OR_data));
+ }
+ }
+
+ static HttpRequest _createHttpRequestSendTask(
+ HttpRequestSendTaskSpecification spec, Zone zone) {
+ spec.request._send(spec.sendData);
+ return spec.request;
+ }
+
$!MEMBERS
}