Skip to content

Commit 1d29b79

Browse files
committed
feat(core, testability): PendingAsync service
A new PendingAsync service that is used to track pending async operations in Angular. VmTurnZone ties in to this service to track timers as async tasks and correctly handles canceled timers. Http registers async tasks for network requests. The Testability API uses the new API to provide a much more useful whenStable implementation.
1 parent 00960bb commit 1d29b79

12 files changed

+264
-10
lines changed

lib/core/module.dart

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export "package:angular/core_dom/module_internal.dart" show
5151
NgElement,
5252
NoOpAnimation,
5353
NullTreeSanitizer,
54+
PendingAsync,
5455
Animate,
5556
RequestErrorInterceptor,
5657
RequestInterceptor,

lib/core/module_internal.dart

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export 'package:angular/core/formatter.dart';
2626
import 'package:angular/core/parser/utils.dart';
2727
import 'package:angular/core/registry.dart';
2828
import 'package:angular/core/static_keys.dart';
29+
import 'package:angular/core/pending_async.dart';
30+
export 'package:angular/core/pending_async.dart';
2931

3032
part "exception_handler.dart";
3133
part "interpolate.dart";
@@ -41,6 +43,7 @@ class CoreModule extends Module {
4143
bind(FormatterMap);
4244
bind(Interpolate);
4345
bind(RootScope);
46+
bind(PendingAsync);
4447
bind(Scope, toInstanceOf: RootScope);
4548
bind(ClosureMap, toFactory: () => throw "Must provide dynamic/static ClosureMap.");
4649
bind(ScopeStats);

lib/core/pending_async.dart

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
library angular.core.pending_async;
2+
3+
import 'dart:async';
4+
import 'package:di/annotations.dart';
5+
6+
typedef void WhenStableCallback();
7+
8+
/**
9+
* Tracks pending operations and notifies when they are all complete.
10+
*/
11+
@Injectable()
12+
class PendingAsync {
13+
/// a count of the number of pending async operations.
14+
int _numPending = 0;
15+
List<WhenStableCallback> _callbacks;
16+
17+
/**
18+
* A count of the number of tracked pending async operations.
19+
*/
20+
int get numPending => _numPending;
21+
22+
/**
23+
* Register a callback to be called synchronously when the number of tracked
24+
* pending async operations reaches a count of zero from a non-zero count.
25+
*/
26+
void whenStable(WhenStableCallback cb) {
27+
if (_numPending == 0) {
28+
cb();
29+
return;
30+
}
31+
if (_callbacks == null) {
32+
_callbacks = <WhenStableCallback>[cb];
33+
} else {
34+
_callbacks.add(cb);
35+
}
36+
}
37+
38+
/**
39+
* Increase the counter of the number of tracked pending operations. Returns
40+
* the new count of the number of tracked pending operations.
41+
*/
42+
int increaseCount([int delta = 1]) {
43+
if (delta == 0) {
44+
return _numPending;
45+
}
46+
_numPending += delta;
47+
if (_numPending < 0) {
48+
throw "Attempting to reduce pending async count below zero.";
49+
} else if (_numPending == 0) {
50+
_runAllCallbacks();
51+
}
52+
return _numPending;
53+
}
54+
55+
/**
56+
* Decrease the counter of the number of tracked pending operations. Returns
57+
* the new count of the number of tracked pending operations.
58+
*/
59+
int decreaseCount([int delta = 1]) => increaseCount(-delta);
60+
61+
void _runAllCallbacks() {
62+
while (_callbacks != null) {
63+
var callbacks = _callbacks;
64+
_callbacks = null;
65+
callbacks.forEach((fn) { fn(); });
66+
}
67+
}
68+
}

lib/core/scope.dart

+18-2
Original file line numberDiff line numberDiff line change
@@ -750,9 +750,11 @@ class RootScope extends Scope {
750750
*/
751751
String get state => _state;
752752

753+
PendingAsync _pendingAsync;
754+
753755
RootScope(Object context, Parser parser, ASTParser astParser, FieldGetterFactory fieldGetterFactory,
754756
FormatterMap formatters, this._exceptionHandler, this._ttl, this._zone,
755-
ScopeStats _scopeStats, CacheRegister cacheRegister)
757+
ScopeStats _scopeStats, CacheRegister cacheRegister, this._pendingAsync)
756758
: _scopeStats = _scopeStats,
757759
_parser = parser,
758760
_astParser = astParser,
@@ -764,7 +766,19 @@ class RootScope extends Scope {
764766
'',
765767
_scopeStats)
766768
{
767-
_zone.onTurnDone = apply;
769+
_zone.countPendingAsync = _pendingAsync.increaseCount;
770+
_zone.onTurnDone = () {
771+
// NOTE: Ideally, we would just set _zone.onTurnStart = _pendingAsync.increaseCount.
772+
// However, when the RootScope is constructed, we would have already executed the
773+
// nop onTurnStart causing a count mismatch. While we could adjust for it, our
774+
// test set doesn't really enter/leave the VmTurnZone. So for simplicity, we do the
775+
// increaseCount here.
776+
_pendingAsync.increaseCount();
777+
apply();
778+
_pendingAsync.decreaseCount();
779+
_runAsyncFns(); // if any were scheduled by _pendingAsync.whenStable callbacks.
780+
};
781+
768782
_zone.onError = (e, s, ls) => _exceptionHandler(e, s);
769783
_zone.onScheduleMicrotask = runAsync;
770784
cacheRegister.registerCache("ScopeWatchASTs", astCache);
@@ -900,6 +914,7 @@ class RootScope extends Scope {
900914
if (_state == STATE_FLUSH_ASSERT) {
901915
throw "Scheduling microtasks not allowed in $state state.";
902916
}
917+
_pendingAsync.increaseCount();
903918
var chain = new _FunctionChain(fn);
904919
if (_runAsyncHead == null) {
905920
_runAsyncHead = _runAsyncTail = chain;
@@ -918,6 +933,7 @@ class RootScope extends Scope {
918933
} catch (e, s) {
919934
_exceptionHandler(e, s);
920935
}
936+
_pendingAsync.decreaseCount();
921937
_runAsyncHead = _runAsyncHead._next;
922938
}
923939
_runAsyncTail = null;

lib/core/zone.dart

+41
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ part of angular.core_internal;
55
*/
66
typedef void ZoneOnTurnDone();
77

8+
typedef void CountPendingAsync(int count);
9+
810
/**
911
* Handles a [VmTurnZone] onTurnDone event.
1012
*/
@@ -39,6 +41,7 @@ class LongStackTrace {
3941
}
4042
}
4143

44+
4245
/**
4346
* A [Zone] wrapper that lets you schedule tasks after its private microtask
4447
* queue is exhausted but before the next "turn", i.e. event loop iteration.
@@ -75,12 +78,14 @@ class VmTurnZone {
7578
run: _onRun,
7679
runUnary: _onRunUnary,
7780
scheduleMicrotask: _onScheduleMicrotask,
81+
createTimer: _onCreateTimer,
7882
handleUncaughtError: _uncaughtError
7983
));
8084
onError = _defaultOnError;
8185
onTurnDone = _defaultOnTurnDone;
8286
onTurnStart = _defaultOnTurnStart;
8387
onScheduleMicrotask = _defaultOnScheduleMicrotask;
88+
countPendingAsync = _defaultCountPendingAsync;
8489
}
8590

8691
List _asyncQueue = [];
@@ -126,6 +131,15 @@ class VmTurnZone {
126131
}
127132
}
128133

134+
async.Timer _onCreateTimer(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, Duration duration, fn()) {
135+
var s = traceEnter(VmTurnZone_createTimer);
136+
try {
137+
return new _WrappedTimer(this, delegate, zone, duration, fn);
138+
} finally {
139+
traceLeave(s);
140+
}
141+
}
142+
129143
void _uncaughtError(async.Zone self, async.ZoneDelegate delegate, async.Zone zone,
130144
e, StackTrace s) {
131145
if (!_errorThrownFromOnRun) onError(e, s, _longStacktrace);
@@ -199,7 +213,9 @@ class VmTurnZone {
199213
* the turn will _never_ end.
200214
*/
201215
ZoneOnTurnDone onTurnDone;
216+
CountPendingAsync countPendingAsync;
202217
void _defaultOnTurnDone() => null;
218+
void _defaultCountPendingAsync(int count) => null;
203219

204220
/**
205221
* Called any time a microtask is scheduled. If you override [onScheduleMicrotask], you
@@ -276,3 +292,28 @@ class VmTurnZone {
276292
assertInTurn();
277293
}
278294
}
295+
296+
297+
// Automatically adjusts the pending async task count when the timer is
298+
// scheduled, canceled or fired.
299+
class _WrappedTimer implements async.Timer {
300+
async.Timer _realTimer;
301+
VmTurnZone _vmTurnZone;
302+
303+
_WrappedTimer(this._vmTurnZone, async.ZoneDelegate delegate, async.Zone zone, Duration duration, Function fn()) {
304+
_vmTurnZone.countPendingAsync(1);
305+
_realTimer = delegate.createTimer(zone, duration, () {
306+
fn();
307+
_vmTurnZone.countPendingAsync(-1);
308+
});
309+
}
310+
311+
bool get isActive => _realTimer.isActive;
312+
313+
void cancel() {
314+
if (_realTimer.isActive) {
315+
_vmTurnZone.countPendingAsync(-1);
316+
}
317+
_realTimer.cancel();
318+
}
319+
}

lib/core_dom/http.dart

+16-4
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ class Http {
383383
final RootScope _rootScope;
384384
final HttpConfig _httpConfig;
385385
final VmTurnZone _zone;
386+
final PendingAsync _pendingAsync;
386387

387388
final _responseQueue = <Function>[];
388389
async.Timer _responseQueueTimer;
@@ -396,7 +397,7 @@ class Http {
396397
* Constructor, useful for DI.
397398
*/
398399
Http(this._cookies, this._location, this._rewriter, this._backend, this.defaults,
399-
this._interceptors, this._rootScope, this._httpConfig, this._zone);
400+
this._interceptors, this._rootScope, this._httpConfig, this._zone, this._pendingAsync);
400401

401402
/**
402403
* Parse a [requestUrl] and determine whether this is a same-origin request as
@@ -494,14 +495,25 @@ class Http {
494495
return new async.Future.value(new HttpResponse.copy(cachedResponse));
495496
}
496497

497-
requestFromBackend(runCoalesced, onComplete, onError) => _backend.request(
498+
requestFromBackend(runCoalesced, onComplete, onError) {
499+
var request = _backend.request(
498500
url,
499501
method: method,
500502
requestHeaders: config.headers,
501503
sendData: config.data,
502504
withCredentials: withCredentials
503-
).then((dom.HttpRequest req) => _onResponse(req, runCoalesced, onComplete, config, cache, url),
504-
onError: (e) => _onError(e, runCoalesced, onError, config, url));
505+
);
506+
_pendingAsync.increaseCount();
507+
return request.then((dom.HttpRequest req) {
508+
_pendingAsync.decreaseCount();
509+
return _onResponse(req, runCoalesced, onComplete, config, cache, url);
510+
},
511+
onError: (e) {
512+
_pendingAsync.decreaseCount();
513+
return _onError(e, runCoalesced, onError, config, url);
514+
});
515+
return request;
516+
}
505517

506518
async.Future responseFuture;
507519
if (_httpConfig.coalesceDuration != null) {

lib/introspection.dart

+6-3
Original file line numberDiff line numberDiff line change
@@ -264,12 +264,15 @@ typedef List<String> _GetExpressionsFromProbe(ElementProbe probe);
264264
class _Testability implements _JsObjectProxyable {
265265
final dom.Node node;
266266
final ElementProbe probe;
267+
final PendingAsync _pendingAsync;
267268

268-
_Testability(this.node, this.probe);
269+
_Testability(dom.Node node, ElementProbe probe):
270+
node = node,
271+
probe = probe,
272+
_pendingAsync = probe.injector.get(PendingAsync);
269273

270274
whenStable(callback) {
271-
(probe.injector.get(VmTurnZone) as VmTurnZone).run(
272-
() => new async.Timer(Duration.ZERO, callback));
275+
_pendingAsync.whenStable(callback);
273276
}
274277

275278
/**

lib/mock/module.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ part 'mock_cache_register.dart';
5555
* - [Logger]
5656
* - [RethrowExceptionHandler] instead of [ExceptionHandler]
5757
* - [VmTurnZone] which displays errors to console;
58-
* - [MockCacheRegister
58+
* - [MockCacheRegister]
5959
*/
6060
class AngularMockModule extends Module {
6161
AngularMockModule() {

lib/mock/zone.dart

+12
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,15 @@ class _TimerSpec implements dart_async.Timer {
276276
isActive = false;
277277
}
278278
}
279+
280+
281+
class MockZone {
282+
MockZone._internal();
283+
284+
MockZone get current => Zone.current['AngularMockZone'];
285+
286+
static Zone fork(Zone zone) {
287+
MockZone mockZone = new MockZone._internal();
288+
return zone.fork(zoneValues: { 'AngularMockZone': mockZone });
289+
}
290+
}

lib/ng_tracing_scopes.dart

+8
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ final VmTurnZone_run = traceCreateScope('VmTurnZone#run()');
141141
final VmTurnZone_scheduleMicrotask = traceCreateScope('VmTurnZone#scheduleMicrotask()');
142142

143143

144+
/**
145+
* Name: `VmTurnZone#createTimer()`
146+
*
147+
* Designates where new timers are scheduled.
148+
*/
149+
final VmTurnZone_createTimer = traceCreateScope('VmTurnZone#createTimer()');
150+
151+
144152
/**
145153
* Name: `Compiler#compile()`
146154
*

0 commit comments

Comments
 (0)