| Index: third_party/pkg/angular/lib/core/scope.dart
|
| diff --git a/third_party/pkg/angular/lib/core/scope.dart b/third_party/pkg/angular/lib/core/scope.dart
|
| index db063051d69a2051c6777e647bceae0fcb717f71..128cf77e992708f299924df3eb814feb3b376eb9 100644
|
| --- a/third_party/pkg/angular/lib/core/scope.dart
|
| +++ b/third_party/pkg/angular/lib/core/scope.dart
|
| @@ -1,8 +1,4 @@
|
| -part of angular.core;
|
| -
|
| -NOT_IMPLEMENTED() {
|
| - throw new StateError('Not Implemented');
|
| -}
|
| +part of angular.core_internal;
|
|
|
| typedef EvalFunction0();
|
| typedef EvalFunction1(context);
|
| @@ -81,7 +77,7 @@ class ScopeEvent {
|
| * triggers watch A. If the system does not stabilize in TTL iterations then
|
| * the digest is stopped and an exception is thrown.
|
| */
|
| -@NgInjectableService()
|
| +@Injectable()
|
| class ScopeDigestTTL {
|
| final int ttl;
|
| ScopeDigestTTL(): ttl = 5;
|
| @@ -102,7 +98,8 @@ class ScopeLocals implements Map {
|
| _scope[name] = value;
|
| }
|
| dynamic operator [](String name) =>
|
| - (_locals.containsKey(name) ? _locals : _scope)[name];
|
| + // as Map needed to clear Dart2js warning
|
| + ((_locals.containsKey(name) ? _locals : _scope) as Map)[name];
|
|
|
| bool get isEmpty => _scope.isEmpty && _locals.isEmpty;
|
| bool get isNotEmpty => _scope.isNotEmpty || _locals.isNotEmpty;
|
| @@ -128,11 +125,13 @@ class ScopeLocals implements Map {
|
| /**
|
| * [Scope] is represents a collection of [watch]es [observe]ers, and [context]
|
| * for the watchers, observers and [eval]uations. Scopes structure loosely
|
| - * mimics the DOM structure. Scopes and [Block]s are bound to each other.
|
| - * As scopes are created and destroyed by [BlockFactory] they are responsible
|
| + * mimics the DOM structure. Scopes and [View]s are bound to each other.
|
| + * As scopes are created and destroyed by [ViewFactory] they are responsible
|
| * for change detection, change processing and memory management.
|
| */
|
| class Scope {
|
| + final String id;
|
| + int _childScopeNextId = 0;
|
|
|
| /**
|
| * The default execution context for [watch]es [observe]ers, and [eval]uation.
|
| @@ -151,13 +150,15 @@ class Scope {
|
| */
|
| Scope get parentScope => _parentScope;
|
|
|
| + final ScopeStats _stats;
|
| +
|
| /**
|
| * Return `true` if the scope has been destroyed. Once scope is destroyed
|
| * No operations are allowed on it.
|
| */
|
| bool get isDestroyed {
|
| var scope = this;
|
| - while(scope != null) {
|
| + while (scope != null) {
|
| if (scope == rootScope) return false;
|
| scope = scope._parentScope;
|
| }
|
| @@ -182,24 +183,38 @@ class Scope {
|
| bool get hasOwnStreams => _streams != null && _streams._scope == this;
|
|
|
| Scope(Object this.context, this.rootScope, this._parentScope,
|
| - this._readWriteGroup, this._readOnlyGroup);
|
| + this._readWriteGroup, this._readOnlyGroup, this.id,
|
| + this._stats);
|
|
|
| /**
|
| - * A [watch] sets up a watch in the [digest] phase of the [apply] cycle.
|
| + * Use [watch] to set up change detection on an expression.
|
| *
|
| - * Use [watch] if the reaction function can cause updates to model. In your
|
| - * controller code you will most likely use [watch].
|
| + * * [expression]: The expression to watch for changes.
|
| + * * [reactionFn]: The reaction function to execute when a change is detected in the watched
|
| + * expression.
|
| + * * [context]: The object against which the expression is evaluated. This defaults to the
|
| + * [Scope.context] if no context is specified.
|
| + * * [formatters]: If the watched expression contains formatters,
|
| + * this map specifies the set of formatters that are used by the expression.
|
| + * * [canChangeModel]: Specifies whether the [reactionFn] changes the model. Reaction
|
| + * functions that change the model are processed as part of the [digest] cycle. Otherwise,
|
| + * they are processed as part of the [flush] cycle.
|
| + * * [collection]: If [:true:], then the expression points to a collection (a list or a map),
|
| + * and the collection should be shallow watched. If [:false:] then the expression is watched
|
| + * by reference. When watching a collection, the reaction function receives a
|
| + * [CollectionChangeItem] that lists all the changes.
|
| */
|
| - Watch watch(expression, ReactionFn reactionFn,
|
| - {context, FilterMap filters, bool readOnly: false}) {
|
| + Watch watch(String expression, ReactionFn reactionFn, {context,
|
| + FormatterMap formatters, bool canChangeModel: true, bool collection: false}) {
|
| assert(isAttached);
|
| - assert(expression != null);
|
| - AST ast;
|
| + assert(expression is String);
|
| + assert(canChangeModel is bool);
|
| +
|
| Watch watch;
|
| ReactionFn fn = reactionFn;
|
| - if (expression is AST) {
|
| - ast = expression;
|
| - } else if (expression is String) {
|
| + if (expression.isEmpty) {
|
| + expression = '""';
|
| + } else {
|
| if (expression.startsWith('::')) {
|
| expression = expression.substring(2);
|
| fn = (value, last) {
|
| @@ -210,13 +225,17 @@ class Scope {
|
| };
|
| } else if (expression.startsWith(':')) {
|
| expression = expression.substring(1);
|
| - fn = (value, last) => value == null ? null : reactionFn(value, last);
|
| + fn = (value, last) {
|
| + if (value != null) reactionFn(value, last);
|
| + };
|
| }
|
| - ast = rootScope._astParser(expression, context: context, filters: filters);
|
| - } else {
|
| - throw 'expressions must be String or AST got $expression.';
|
| }
|
| - return watch = (readOnly ? _readOnlyGroup : _readWriteGroup).watch(ast, fn);
|
| +
|
| + AST ast = rootScope._astParser(expression, context: context,
|
| + formatters: formatters, collection: collection);
|
| +
|
| + WatchGroup group = canChangeModel ? _readWriteGroup : _readOnlyGroup;
|
| + return watch = group.watch(ast, fn);
|
| }
|
|
|
| dynamic eval(expression, [Map locals]) {
|
| @@ -235,9 +254,6 @@ class Scope {
|
| return null;
|
| }
|
|
|
| - dynamic applyInZone([expression, Map locals]) =>
|
| - rootScope._zone.run(() => apply(expression, locals));
|
| -
|
| dynamic apply([expression, Map locals]) {
|
| _assertInternalStateConsistency();
|
| rootScope._transitionState(null, RootScope.STATE_APPLY);
|
| @@ -246,10 +262,9 @@ class Scope {
|
| } catch (e, s) {
|
| rootScope._exceptionHandler(e, s);
|
| } finally {
|
| - rootScope
|
| - .._transitionState(RootScope.STATE_APPLY, null)
|
| - ..digest()
|
| - ..flush();
|
| + rootScope.._transitionState(RootScope.STATE_APPLY, null)
|
| + ..digest()
|
| + ..flush();
|
| }
|
| }
|
|
|
| @@ -257,10 +272,12 @@ class Scope {
|
| assert(isAttached);
|
| return _Streams.emit(this, name, data);
|
| }
|
| +
|
| ScopeEvent broadcast(String name, [data]) {
|
| assert(isAttached);
|
| return _Streams.broadcast(this, name, data);
|
| }
|
| +
|
| ScopeStream on(String name) {
|
| assert(isAttached);
|
| return _Streams.on(this, rootScope._exceptionHandler, name);
|
| @@ -270,13 +287,14 @@ class Scope {
|
| assert(isAttached);
|
| var child = new Scope(childContext, rootScope, this,
|
| _readWriteGroup.newGroup(childContext),
|
| - _readOnlyGroup.newGroup(childContext));
|
| - var next = null;
|
| + _readOnlyGroup.newGroup(childContext),
|
| + '$id:${_childScopeNextId++}',
|
| + _stats);
|
| +
|
| var prev = _childTail;
|
| - child._next = next;
|
| child._prev = prev;
|
| if (prev == null) _childHead = child; else prev._next = child;
|
| - if (next == null) _childTail = child; else next._prev = child;
|
| + _childTail = child;
|
| return child;
|
| }
|
|
|
| @@ -301,7 +319,6 @@ class Scope {
|
| _readWriteGroup.remove();
|
| _readOnlyGroup.remove();
|
| _parentScope = null;
|
| - _assertInternalStateConsistency();
|
| }
|
|
|
| _assertInternalStateConsistency() {
|
| @@ -326,7 +343,7 @@ class Scope {
|
| });
|
| }
|
| var childScope = _childHead;
|
| - while(childScope != null) {
|
| + while (childScope != null) {
|
| childScope._verifyStreams(this, ' $prefix', log).forEach((k, v) {
|
| counts[k] = v + (counts.containsKey(k) ? counts[k] : 0);
|
| });
|
| @@ -344,70 +361,176 @@ class Scope {
|
| _mapEqual(Map a, Map b) => a.length == b.length &&
|
| a.keys.every((k) => b.containsKey(k) && a[k] == b[k]);
|
|
|
| +/**
|
| + * ScopeStats collects and emits statistics about a [Scope].
|
| + *
|
| + * ScopeStats supports emitting the results. Result emission can be started or
|
| + * stopped at runtime. The result emission can is configured by supplying a
|
| + * [ScopeStatsEmitter].
|
| + */
|
| +@Injectable()
|
| class ScopeStats {
|
| - bool report = true;
|
| - final nf = new NumberFormat.decimalPattern();
|
| + final fieldStopwatch = new AvgStopwatch();
|
| + final evalStopwatch = new AvgStopwatch();
|
| + final processStopwatch = new AvgStopwatch();
|
|
|
| - final digestFieldStopwatch = new AvgStopwatch();
|
| - final digestEvalStopwatch = new AvgStopwatch();
|
| - final digestProcessStopwatch = new AvgStopwatch();
|
| - int _digestLoopNo = 0;
|
| + List<int> _digestLoopTimes = [];
|
| + int _flushPhaseDuration = 0 ;
|
| + int _assertFlushPhaseDuration = 0;
|
|
|
| - final flushFieldStopwatch = new AvgStopwatch();
|
| - final flushEvalStopwatch = new AvgStopwatch();
|
| - final flushProcessStopwatch = new AvgStopwatch();
|
| + int _loopNo = 0;
|
| + ScopeStatsEmitter _emitter;
|
| + ScopeStatsConfig _config;
|
|
|
| - ScopeStats({this.report: false}) {
|
| - nf.maximumFractionDigits = 0;
|
| - }
|
| + /**
|
| + * Construct a new instance of ScopeStats.
|
| + */
|
| + ScopeStats(this._emitter, this._config);
|
|
|
| void digestStart() {
|
| - _digestStopwatchReset();
|
| - _digestLoopNo = 0;
|
| + _digestLoopTimes = [];
|
| + _stopwatchReset();
|
| + _loopNo = 0;
|
| + }
|
| +
|
| + int _allStagesDuration() {
|
| + return fieldStopwatch.elapsedMicroseconds +
|
| + evalStopwatch.elapsedMicroseconds +
|
| + processStopwatch.elapsedMicroseconds;
|
| }
|
|
|
| - _digestStopwatchReset() {
|
| - digestFieldStopwatch.reset();
|
| - digestEvalStopwatch.reset();
|
| - digestProcessStopwatch.reset();
|
| + _stopwatchReset() {
|
| + fieldStopwatch.reset();
|
| + evalStopwatch.reset();
|
| + processStopwatch.reset();
|
| }
|
|
|
| void digestLoop(int changeCount) {
|
| - _digestLoopNo++;
|
| - if (report) {
|
| - print(this);
|
| + _loopNo++;
|
| + if (_config.emit && _emitter != null) {
|
| + _emitter.emit(_loopNo.toString(), fieldStopwatch, evalStopwatch,
|
| + processStopwatch);
|
| }
|
| - _digestStopwatchReset();
|
| + _digestLoopTimes.add( _allStagesDuration() );
|
| + _stopwatchReset();
|
| }
|
|
|
| - String _stat(AvgStopwatch s) {
|
| - return '${nf.format(s.count)}'
|
| - ' / ${nf.format(s.elapsedMicroseconds)} us'
|
| - ' = ${nf.format(s.ratePerMs)} #/ms';
|
| + void digestEnd() {
|
| }
|
|
|
| - void digestEnd() {
|
| + void domWriteStart() {}
|
| + void domWriteEnd() {}
|
| + void domReadStart() {}
|
| + void domReadEnd() {}
|
| + void flushStart() {
|
| + _stopwatchReset();
|
| + }
|
| + void flushEnd() {
|
| + if (_config.emit && _emitter != null) {
|
| + _emitter.emit(RootScope.STATE_FLUSH, fieldStopwatch, evalStopwatch,
|
| + processStopwatch);
|
| + }
|
| + _flushPhaseDuration = _allStagesDuration();
|
| + }
|
| + void flushAssertStart() {
|
| + _stopwatchReset();
|
| + }
|
| + void flushAssertEnd() {
|
| + if (_config.emit && _emitter != null) {
|
| + _emitter.emit(RootScope.STATE_FLUSH_ASSERT, fieldStopwatch, evalStopwatch,
|
| + processStopwatch);
|
| + }
|
| + _assertFlushPhaseDuration = _allStagesDuration();
|
| }
|
|
|
| - toString() =>
|
| - 'digest #$_digestLoopNo:'
|
| - 'Field: ${_stat(digestFieldStopwatch)} '
|
| - 'Eval: ${_stat(digestEvalStopwatch)} '
|
| - 'Process: ${_stat(digestProcessStopwatch)}';
|
| + void cycleEnd() {
|
| + }
|
| }
|
|
|
| +/**
|
| + * ScopeStatsEmitter is in charge of formatting the [ScopeStats] and outputting
|
| + * a message.
|
| + */
|
| +@Injectable()
|
| +class ScopeStatsEmitter {
|
| + static String _PAD_ = ' ';
|
| + static String _HEADER_ = pad('APPLY', 7) + ':'+
|
| + pad('FIELD', 19) + pad('|', 20) +
|
| + pad('EVAL', 19) + pad('|', 20) +
|
| + pad('REACTION', 19) + pad('|', 20) +
|
| + pad('TOTAL', 10) + '\n';
|
| + final _nfDec = new NumberFormat("0.00", "en_US");
|
| + final _nfInt = new NumberFormat("0", "en_US");
|
| +
|
| + static pad(String str, int size) => _PAD_.substring(0, max(size - str.length, 0)) + str;
|
| +
|
| + _ms(num value) => '${pad(_nfDec.format(value), 9)} ms';
|
| + _us(num value) => _ms(value / 1000);
|
| + _tally(num value) => '${pad(_nfInt.format(value), 6)}';
|
| +
|
| + /**
|
| + * Emit a message based on the phase and state of stopwatches.
|
| + */
|
| + void emit(String phaseOrLoopNo, AvgStopwatch fieldStopwatch,
|
| + AvgStopwatch evalStopwatch, AvgStopwatch processStopwatch) {
|
| + var total = fieldStopwatch.elapsedMicroseconds +
|
| + evalStopwatch.elapsedMicroseconds +
|
| + processStopwatch.elapsedMicroseconds;
|
| + print('${_formatPrefix(phaseOrLoopNo)} '
|
| + '${_stat(fieldStopwatch)} | '
|
| + '${_stat(evalStopwatch)} | '
|
| + '${_stat(processStopwatch)} | '
|
| + '${_ms(total/1000)}');
|
| + }
|
| +
|
| + String _formatPrefix(String prefix) {
|
| + if (prefix == RootScope.STATE_FLUSH) return ' flush:';
|
| + if (prefix == RootScope.STATE_FLUSH_ASSERT) return ' assert:';
|
| +
|
| + return (prefix == '1' ? _HEADER_ : '') + ' #$prefix:';
|
| + }
|
| +
|
| + String _stat(AvgStopwatch s) {
|
| + return '${_tally(s.count)} / ${_us(s.elapsedMicroseconds)} @(${_tally(s.ratePerMs)} #/ms)';
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * ScopeStatsConfig is used to modify behavior of [ScopeStats]. You can use this
|
| + * object to modify behavior at runtime too.
|
| + */
|
| +class ScopeStatsConfig {
|
| + var emit = false;
|
|
|
| + ScopeStatsConfig();
|
| + ScopeStatsConfig.enabled() {
|
| + emit = true;
|
| + }
|
| +}
|
| +/**
|
| + *
|
| + * Every Angular application has exactly one RootScope. RootScope extends Scope, adding
|
| + * services related to change detection, async unit-of-work processing, and DOM read/write queues.
|
| + * The RootScope can not be destroyed.
|
| + *
|
| + * ## Lifecycle
|
| + *
|
| + * All work in Angular must be done within a context of a VmTurnZone. VmTurnZone detects the end
|
| + * of the VM turn, and calls the Apply method to process the changes at the end of VM turn.
|
| + *
|
| + */
|
| +@Injectable()
|
| class RootScope extends Scope {
|
| static final STATE_APPLY = 'apply';
|
| static final STATE_DIGEST = 'digest';
|
| - static final STATE_FLUSH = 'digest';
|
| + static final STATE_FLUSH = 'flush';
|
| + static final STATE_FLUSH_ASSERT = 'assert';
|
|
|
| final ExceptionHandler _exceptionHandler;
|
| - final AstParser _astParser;
|
| + final _AstParser _astParser;
|
| final Parser _parser;
|
| final ScopeDigestTTL _ttl;
|
| - final ExpressionVisitor visitor = new ExpressionVisitor(); // TODO(misko): delete me
|
| - final NgZone _zone;
|
| + final VmTurnZone _zone;
|
|
|
| _FunctionChain _runAsyncHead, _runAsyncTail;
|
| _FunctionChain _domWriteHead, _domWriteTail;
|
| @@ -417,13 +540,68 @@ class RootScope extends Scope {
|
|
|
| String _state;
|
|
|
| - RootScope(Object context, this._astParser, this._parser,
|
| - GetterCache cacheGetter, FilterMap filterMap,
|
| - this._exceptionHandler, this._ttl, this._zone,
|
| - this._scopeStats)
|
| - : super(context, null, null,
|
| - new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context),
|
| - new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context))
|
| + /**
|
| + *
|
| + * While processing data bindings, Angular passes through multiple states. When testing or
|
| + * debugging, it can be useful to access the current `state`, which is one of the following:
|
| + *
|
| + * * null
|
| + * * apply
|
| + * * digest
|
| + * * flush
|
| + * * assert
|
| + *
|
| + * ##null
|
| + *
|
| + * Angular is not currently processing changes
|
| + *
|
| + * ##apply
|
| + *
|
| + * The apply state begins by executing the optional expression within the context of
|
| + * angular change detection mechanism. Any exceptions are delegated to [ExceptionHandler]. At the
|
| + * end of apply state RootScope enters the digest followed by flush phase (optionally if asserts
|
| + * enabled run assert phase.)
|
| + *
|
| + * ##digest
|
| + *
|
| + * The apply state begins by processing the async queue,
|
| + * followed by change detection
|
| + * on non-DOM listeners. Any changes detected are process using the reaction function. The digest
|
| + * phase is repeated as long as at least one change has been detected. By default, after 5
|
| + * iterations the model is considered unstable and angular exists with an exception. (See
|
| + * ScopeDigestTTL)
|
| + *
|
| + * ##flush
|
| + *
|
| + * The flush phase consists of these steps:
|
| + *
|
| + * 1. processing the DOM write queue
|
| + * 2. change detection on DOM only updates (these are reaction functions which must
|
| + * not change the model state and hence don't need stabilization as in digest phase).
|
| + * 3. processing the DOM read queue
|
| + * 4. repeat steps 1 and 3 (not 2) until queues are empty
|
| + *
|
| + * ##assert
|
| + *
|
| + * Optionally if Dart assert is on, verify that flush reaction functions did not make any changes
|
| + * to model and throw error if changes detected.
|
| + *
|
| + */
|
| + String get state => _state;
|
| +
|
| + RootScope(Object context, Parser parser, FieldGetterFactory fieldGetterFactory,
|
| + FormatterMap formatters, this._exceptionHandler, this._ttl, this._zone,
|
| + ScopeStats _scopeStats, ClosureMap closureMap)
|
| + : _scopeStats = _scopeStats,
|
| + _parser = parser,
|
| + _astParser = new _AstParser(parser, closureMap),
|
| + super(context, null, null,
|
| + new RootWatchGroup(fieldGetterFactory,
|
| + new DirtyCheckingChangeDetector(fieldGetterFactory), context),
|
| + new RootWatchGroup(fieldGetterFactory,
|
| + new DirtyCheckingChangeDetector(fieldGetterFactory), context),
|
| + '',
|
| + _scopeStats)
|
| {
|
| _zone.onTurnDone = apply;
|
| _zone.onError = (e, s, ls) => _exceptionHandler(e, s);
|
| @@ -432,10 +610,27 @@ class RootScope extends Scope {
|
| RootScope get rootScope => this;
|
| bool get isAttached => true;
|
|
|
| +/**
|
| + * Propagates changes between different parts of the application model. Normally called by
|
| + * [VMTurnZone] right before DOM rendering to initiate data binding. May also be called directly
|
| + * for unit testing.
|
| + *
|
| + * Before each iteration of change detection, [digest] first processes the async queue. Any
|
| + * work scheduled on the queue is executed before change detection. Since work scheduled on
|
| + * the queue may generate more async calls, [digest] must process the queue multiple times before
|
| + * it completes. The async queue must be empty before the model is considered stable.
|
| + *
|
| + * Next, [digest] collects the changes that have occurred in the model. For each change,
|
| + * [digest] calls the associated [ReactionFn]. Since a [ReactionFn] may further change the model,
|
| + * [digest] processes changes multiple times until no more changes are detected.
|
| + *
|
| + * If the model does not stabilize within 5 iterations, an exception is thrown. See
|
| + * [ScopeDigestTTL].
|
| + */
|
| void digest() {
|
| _transitionState(null, STATE_DIGEST);
|
| try {
|
| - var rootWatchGroup = (_readWriteGroup as RootWatchGroup);
|
| + var rootWatchGroup = _readWriteGroup as RootWatchGroup;
|
|
|
| int digestTTL = _ttl.ttl;
|
| const int LOG_COUNT = 3;
|
| @@ -445,7 +640,7 @@ class RootScope extends Scope {
|
| ChangeLog changeLog;
|
| _scopeStats.digestStart();
|
| do {
|
| - while(_runAsyncHead != null) {
|
| + while (_runAsyncHead != null) {
|
| try {
|
| _runAsyncHead.fn();
|
| } catch (e, s) {
|
| @@ -453,14 +648,15 @@ class RootScope extends Scope {
|
| }
|
| _runAsyncHead = _runAsyncHead._next;
|
| }
|
| + _runAsyncTail = null;
|
|
|
| digestTTL--;
|
| count = rootWatchGroup.detectChanges(
|
| exceptionHandler: _exceptionHandler,
|
| changeLog: changeLog,
|
| - fieldStopwatch: _scopeStats.digestFieldStopwatch,
|
| - evalStopwatch: _scopeStats.digestEvalStopwatch,
|
| - processStopwatch: _scopeStats.digestProcessStopwatch);
|
| + fieldStopwatch: _scopeStats.fieldStopwatch,
|
| + evalStopwatch: _scopeStats.evalStopwatch,
|
| + processStopwatch: _scopeStats.processStopwatch);
|
|
|
| if (digestTTL <= LOG_COUNT) {
|
| if (changeLog == null) {
|
| @@ -485,50 +681,69 @@ class RootScope extends Scope {
|
| }
|
|
|
| void flush() {
|
| + _stats.flushStart();
|
| _transitionState(null, STATE_FLUSH);
|
| - var observeGroup = this._readOnlyGroup as RootWatchGroup;
|
| + RootWatchGroup readOnlyGroup = this._readOnlyGroup as RootWatchGroup;
|
| bool runObservers = true;
|
| try {
|
| do {
|
| - while(_domWriteHead != null) {
|
| + if (_domWriteHead != null) _stats.domWriteStart();
|
| + while (_domWriteHead != null) {
|
| try {
|
| _domWriteHead.fn();
|
| } catch (e, s) {
|
| _exceptionHandler(e, s);
|
| }
|
| _domWriteHead = _domWriteHead._next;
|
| + if (_domWriteHead == null) _stats.domWriteEnd();
|
| }
|
| + _domWriteTail = null;
|
| if (runObservers) {
|
| runObservers = false;
|
| - observeGroup.detectChanges(exceptionHandler:_exceptionHandler);
|
| + readOnlyGroup.detectChanges(exceptionHandler:_exceptionHandler,
|
| + fieldStopwatch: _scopeStats.fieldStopwatch,
|
| + evalStopwatch: _scopeStats.evalStopwatch,
|
| + processStopwatch: _scopeStats.processStopwatch);
|
| }
|
| - while(_domReadHead != null) {
|
| + if (_domReadHead != null) _stats.domReadStart();
|
| + while (_domReadHead != null) {
|
| try {
|
| _domReadHead.fn();
|
| } catch (e, s) {
|
| _exceptionHandler(e, s);
|
| }
|
| _domReadHead = _domReadHead._next;
|
| + if (_domReadHead == null) _stats.domReadEnd();
|
| }
|
| + _domReadTail = null;
|
| } while (_domWriteHead != null || _domReadHead != null);
|
| + _stats.flushEnd();
|
| assert((() {
|
| - var watchLog = [];
|
| - var observeLog = [];
|
| + _stats.flushAssertStart();
|
| + var digestLog = [];
|
| + var flushLog = [];
|
| (_readWriteGroup as RootWatchGroup).detectChanges(
|
| - changeLog: (s, c, p) => watchLog.add('$s: $c <= $p'));
|
| - (observeGroup as RootWatchGroup).detectChanges(
|
| - changeLog: (s, c, p) => watchLog.add('$s: $c <= $p'));
|
| - if (watchLog.isNotEmpty || observeLog.isNotEmpty) {
|
| + changeLog: (s, c, p) => digestLog.add('$s: $c <= $p'),
|
| + fieldStopwatch: _scopeStats.fieldStopwatch,
|
| + evalStopwatch: _scopeStats.evalStopwatch,
|
| + processStopwatch: _scopeStats.processStopwatch);
|
| + (_readOnlyGroup as RootWatchGroup).detectChanges(
|
| + changeLog: (s, c, p) => flushLog.add('$s: $c <= $p'),
|
| + fieldStopwatch: _scopeStats.fieldStopwatch,
|
| + evalStopwatch: _scopeStats.evalStopwatch,
|
| + processStopwatch: _scopeStats.processStopwatch);
|
| + if (digestLog.isNotEmpty || flushLog.isNotEmpty) {
|
| throw 'Observer reaction functions should not change model. \n'
|
| - 'These watch changes were detected: ${watchLog.join('; ')}\n'
|
| - 'These observe changes were detected: ${observeLog.join('; ')}';
|
| + 'These watch changes were detected: ${digestLog.join('; ')}\n'
|
| + 'These observe changes were detected: ${flushLog.join('; ')}';
|
| }
|
| + _stats.flushAssertEnd();
|
| return true;
|
| })());
|
| } finally {
|
| + _stats.cycleEnd();
|
| _transitionState(STATE_FLUSH, null);
|
| }
|
| -
|
| }
|
|
|
| // QUEUES
|
| @@ -607,7 +822,7 @@ class _Streams {
|
| static ScopeEvent emit(Scope scope, String name, data) {
|
| var event = new ScopeEvent(name, scope, data);
|
| var scopeCursor = scope;
|
| - while(scopeCursor != null) {
|
| + while (scopeCursor != null) {
|
| if (scopeCursor._streams != null &&
|
| scopeCursor._streams._scope == scopeCursor) {
|
| ScopeStream stream = scopeCursor._streams._streams[name];
|
| @@ -638,7 +853,7 @@ class _Streams {
|
| }
|
| // Reverse traversal so that when the queue is read it is correct order.
|
| var childScope = scope._childTail;
|
| - while(childScope != null) {
|
| + while (childScope != null) {
|
| scopeStreams = childScope._streams;
|
| if (scopeStreams != null &&
|
| scopeStreams._typeCounts.containsKey(name)) {
|
| @@ -651,9 +866,9 @@ class _Streams {
|
| return event;
|
| }
|
|
|
| - static ScopeStream on(Scope scope,
|
| - ExceptionHandler _exceptionHandler,
|
| - String name) {
|
| + static async.Stream<ScopeEvent> on(Scope scope,
|
| + ExceptionHandler _exceptionHandler,
|
| + String name) {
|
| _forceNewScopeStream(scope, _exceptionHandler);
|
| return scope._streams._get(scope, name);
|
| }
|
| @@ -662,7 +877,7 @@ class _Streams {
|
| _Streams streams = scope._streams;
|
| Scope scopeCursor = scope;
|
| bool splitMode = false;
|
| - while(scopeCursor != null) {
|
| + while (scopeCursor != null) {
|
| _Streams cursorStreams = scopeCursor._streams;
|
| var hasStream = cursorStreams != null;
|
| var hasOwnStream = hasStream && cursorStreams._scope == scopeCursor;
|
| @@ -734,6 +949,9 @@ class ScopeStream extends async.Stream<ScopeEvent> {
|
| final _Streams _streams;
|
| final String _name;
|
| final subscriptions = <ScopeStreamSubscription>[];
|
| + final List<Function> _work = <Function>[];
|
| + bool _firing = false;
|
| +
|
|
|
| ScopeStream(this._streams, this._exceptionHandler, this._name);
|
|
|
| @@ -741,29 +959,46 @@ class ScopeStream extends async.Stream<ScopeEvent> {
|
| { Function onError,
|
| void onDone(),
|
| bool cancelOnError }) {
|
| - if (subscriptions.isEmpty) _streams._addCount(_name, 1);
|
| var subscription = new ScopeStreamSubscription(this, onData);
|
| - subscriptions.add(subscription);
|
| + _concurrentSafeWork(() {
|
| + if (subscriptions.isEmpty) _streams._addCount(_name, 1);
|
| + subscriptions.add(subscription);
|
| + });
|
| return subscription;
|
| }
|
|
|
| + void _concurrentSafeWork([fn]) {
|
| + if (fn != null) _work.add(fn);
|
| + while(!_firing && _work.isNotEmpty) {
|
| + _work.removeLast()();
|
| + }
|
| + }
|
| +
|
| void _fire(ScopeEvent event) {
|
| - for (ScopeStreamSubscription subscription in subscriptions) {
|
| - try {
|
| - subscription._onData(event);
|
| - } catch (e, s) {
|
| - _exceptionHandler(e, s);
|
| + _firing = true;
|
| + try {
|
| + for (ScopeStreamSubscription subscription in subscriptions) {
|
| + try {
|
| + subscription._onData(event);
|
| + } catch (e, s) {
|
| + _exceptionHandler(e, s);
|
| + }
|
| }
|
| + } finally {
|
| + _firing = false;
|
| + _concurrentSafeWork();
|
| }
|
| }
|
|
|
| void _remove(ScopeStreamSubscription subscription) {
|
| - assert(subscription._scopeStream == this);
|
| - if (subscriptions.remove(subscription)) {
|
| - if (subscriptions.isEmpty) _streams._addCount(_name, -1);
|
| - } else {
|
| - throw new StateError('AlreadyCanceled');
|
| - }
|
| + _concurrentSafeWork(() {
|
| + assert(subscription._scopeStream == this);
|
| + if (subscriptions.remove(subscription)) {
|
| + if (subscriptions.isEmpty) _streams._addCount(_name, -1);
|
| + } else {
|
| + throw new StateError('AlreadyCanceled');
|
| + }
|
| + });
|
| }
|
| }
|
|
|
| @@ -772,64 +1007,74 @@ class ScopeStreamSubscription implements async.StreamSubscription<ScopeEvent> {
|
| final Function _onData;
|
| ScopeStreamSubscription(this._scopeStream, this._onData);
|
|
|
| - // TODO(vbe) should return a Future
|
| - cancel() => _scopeStream._remove(this);
|
| + async.Future cancel() {
|
| + _scopeStream._remove(this);
|
| + return null;
|
| + }
|
| +
|
| + void onData(void handleData(ScopeEvent data)) => _NOT_IMPLEMENTED();
|
| + void onError(Function handleError) => _NOT_IMPLEMENTED();
|
| + void onDone(void handleDone()) => _NOT_IMPLEMENTED();
|
| + void pause([async.Future resumeSignal]) => _NOT_IMPLEMENTED();
|
| + void resume() => _NOT_IMPLEMENTED();
|
| + bool get isPaused => _NOT_IMPLEMENTED();
|
| + async.Future asFuture([var futureValue]) => _NOT_IMPLEMENTED();
|
| +}
|
|
|
| - void onData(void handleData(ScopeEvent data)) => NOT_IMPLEMENTED();
|
| - void onError(Function handleError) => NOT_IMPLEMENTED();
|
| - void onDone(void handleDone()) => NOT_IMPLEMENTED();
|
| - void pause([async.Future resumeSignal]) => NOT_IMPLEMENTED();
|
| - void resume() => NOT_IMPLEMENTED();
|
| - bool get isPaused => NOT_IMPLEMENTED();
|
| - async.Future asFuture([var futureValue]) => NOT_IMPLEMENTED();
|
| +_NOT_IMPLEMENTED() {
|
| + throw new StateError('Not Implemented');
|
| }
|
|
|
| +
|
| class _FunctionChain {
|
| final Function fn;
|
| _FunctionChain _next;
|
|
|
| - _FunctionChain(fn())
|
| - : fn = fn
|
| - {
|
| + _FunctionChain(fn()): fn = fn {
|
| assert(fn != null);
|
| }
|
| }
|
|
|
| -class AstParser {
|
| +class _AstParser {
|
| final Parser _parser;
|
| int _id = 0;
|
| - ExpressionVisitor _visitor = new ExpressionVisitor();
|
| + final ExpressionVisitor _visitor;
|
|
|
| - AstParser(this._parser);
|
| + _AstParser(this._parser, ClosureMap closureMap)
|
| + : _visitor = new ExpressionVisitor(closureMap);
|
|
|
| - AST call(String exp, { FilterMap filters,
|
| - bool collection:false,
|
| - Object context:null }) {
|
| - _visitor.filters = filters;
|
| + AST call(String input, {FormatterMap formatters,
|
| + bool collection: false,
|
| + Object context: null }) {
|
| + _visitor.formatters = formatters;
|
| AST contextRef = _visitor.contextRef;
|
| try {
|
| if (context != null) {
|
| _visitor.contextRef = new ConstantAST(context, '#${_id++}');
|
| }
|
| - var ast = _parser(exp);
|
| - return collection ? _visitor.visitCollection(ast) : _visitor.visit(ast);
|
| + var exp = _parser(input);
|
| + return collection ? _visitor.visitCollection(exp) : _visitor.visit(exp);
|
| } finally {
|
| _visitor.contextRef = contextRef;
|
| - _visitor.filters = null;
|
| + _visitor.formatters = null;
|
| }
|
| }
|
| }
|
|
|
| class ExpressionVisitor implements Visitor {
|
| static final ContextReferenceAST scopeContextRef = new ContextReferenceAST();
|
| + final ClosureMap _closureMap;
|
| AST contextRef = scopeContextRef;
|
|
|
| +
|
| + ExpressionVisitor(this._closureMap);
|
| +
|
| AST ast;
|
| - FilterMap filters;
|
| + FormatterMap formatters;
|
|
|
| AST visit(Expression exp) {
|
| exp.accept(this);
|
| - assert(this.ast != null);
|
| + assert(ast != null);
|
| try {
|
| return ast;
|
| } finally {
|
| @@ -843,11 +1088,24 @@ class ExpressionVisitor implements Visitor {
|
| List<AST> _toAst(List<Expression> expressions) =>
|
| expressions.map(_mapToAst).toList();
|
|
|
| + Map<Symbol, AST> _toAstMap(Map<String, Expression> expressions) {
|
| + if (expressions.isEmpty) return const {};
|
| + Map<Symbol, AST> result = new Map<Symbol, AST>();
|
| + expressions.forEach((String name, Expression expression) {
|
| + result[_closureMap.lookupSymbol(name)] = _mapToAst(expression);
|
| + });
|
| + return result;
|
| + }
|
| +
|
| void visitCallScope(CallScope exp) {
|
| - ast = new MethodAST(contextRef, exp.name, _toAst(exp.arguments));
|
| + List<AST> positionals = _toAst(exp.arguments.positionals);
|
| + Map<Symbol, AST> named = _toAstMap(exp.arguments.named);
|
| + ast = new MethodAST(contextRef, exp.name, positionals, named);
|
| }
|
| void visitCallMember(CallMember exp) {
|
| - ast = new MethodAST(visit(exp.object), exp.name, _toAst(exp.arguments));
|
| + List<AST> positionals = _toAst(exp.arguments.positionals);
|
| + Map<Symbol, AST> named = _toAstMap(exp.arguments.named);
|
| + ast = new MethodAST(visit(exp.object), exp.name, positionals, named);
|
| }
|
| visitAccessScope(AccessScope exp) {
|
| ast = new FieldReadAST(contextRef, exp.name);
|
| @@ -871,7 +1129,7 @@ class ExpressionVisitor implements Visitor {
|
| visit(exp.no)]);
|
| }
|
| void visitAccessKeyed(AccessKeyed exp) {
|
| - ast = new PureFunctionAST('[]', _operation_bracket,
|
| + ast = new ClosureAST('[]', _operation_bracket,
|
| [visit(exp.object), visit(exp.key)]);
|
| }
|
| void visitLiteralPrimitive(LiteralPrimitive exp) {
|
| @@ -897,7 +1155,10 @@ class ExpressionVisitor implements Visitor {
|
| }
|
|
|
| void visitFilter(Filter exp) {
|
| - Function filterFunction = filters(exp.name);
|
| + if (formatters == null) {
|
| + throw new Exception("No formatters have been registered");
|
| + }
|
| + Function filterFunction = formatters(exp.name);
|
| List<AST> args = [visitCollection(exp.expression)];
|
| args.addAll(_toAst(exp.arguments).map((ast) => new CollectionAST(ast)));
|
| ast = new PureFunctionAST('|${exp.name}',
|
| @@ -951,19 +1212,19 @@ Function _operationToFunction(String operation) {
|
|
|
| _operation_negate(value) => !toBool(value);
|
| _operation_add(left, right) => autoConvertAdd(left, right);
|
| -_operation_subtract(left, right) => left - right;
|
| -_operation_multiply(left, right) => left * right;
|
| -_operation_divide(left, right) => left / right;
|
| -_operation_divide_int(left, right) => left ~/ right;
|
| -_operation_remainder(left, right) => left % right;
|
| +_operation_subtract(left, right) => (left != null && right != null) ? left - right : (left != null ? left : (right != null ? 0 - right : 0));
|
| +_operation_multiply(left, right) => (left == null || right == null) ? null : left * right;
|
| +_operation_divide(left, right) => (left == null || right == null) ? null : left / right;
|
| +_operation_divide_int(left, right) => (left == null || right == null) ? null : left ~/ right;
|
| +_operation_remainder(left, right) => (left == null || right == null) ? null : left % right;
|
| _operation_equals(left, right) => left == right;
|
| _operation_not_equals(left, right) => left != right;
|
| -_operation_less_then(left, right) => left < right;
|
| -_operation_greater_then(left, right) => (left == null || right == null) ? false : left > right;
|
| -_operation_less_or_equals_then(left, right) => left <= right;
|
| -_operation_greater_or_equals_then(left, right) => left >= right;
|
| -_operation_power(left, right) => left ^ right;
|
| -_operation_bitwise_and(left, right) => left & right;
|
| +_operation_less_then(left, right) => (left == null || right == null) ? null : left < right;
|
| +_operation_greater_then(left, right) => (left == null || right == null) ? null : left > right;
|
| +_operation_less_or_equals_then(left, right) => (left == null || right == null) ? null : left <= right;
|
| +_operation_greater_or_equals_then(left, right) => (left == null || right == null) ? null : left >= right;
|
| +_operation_power(left, right) => (left == null || right == null) ? null : left ^ right;
|
| +_operation_bitwise_and(left, right) => (left == null || right == null) ? null : left & right;
|
| // TODO(misko): these should short circuit the evaluation.
|
| _operation_logical_and(left, right) => toBool(left) && toBool(right);
|
| _operation_logical_or(left, right) => toBool(left) || toBool(right);
|
| @@ -981,7 +1242,7 @@ class MapFn extends FunctionApply {
|
|
|
| MapFn(this.keys);
|
|
|
| - apply(List values) {
|
| + Map apply(List values) {
|
| // TODO(misko): figure out why do we need to make a copy instead of reusing instance?
|
| assert(values.length == keys.length);
|
| return new Map.fromIterables(keys, values);
|
| @@ -1003,6 +1264,8 @@ class _FilterWrapper extends FunctionApply {
|
| if (!identical(value, lastValue)) {
|
| if (value is CollectionChangeRecord) {
|
| args[i] = (value as CollectionChangeRecord).iterable;
|
| + } else if (value is MapChangeRecord) {
|
| + args[i] = (value as MapChangeRecord).map;
|
| } else {
|
| args[i] = value;
|
| }
|
| @@ -1010,7 +1273,7 @@ class _FilterWrapper extends FunctionApply {
|
| }
|
| var value = Function.apply(filterFn, args);
|
| if (value is Iterable) {
|
| - // Since filters are pure we can guarantee that this well never change.
|
| + // Since formatters are pure we can guarantee that this well never change.
|
| // By wrapping in UnmodifiableListView we can hint to the dirty checker
|
| // and short circuit the iterator.
|
| value = new UnmodifiableListView(value);
|
|
|