| OLD | NEW | 
|    1 // Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file |    1 // Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file | 
|    2 // for details. All rights reserved. Use of this source code is governed by a |    2 // for details. All rights reserved. Use of this source code is governed by a | 
|    3 // BSD-style license that can be found in the LICENSE file. |    3 // BSD-style license that can be found in the LICENSE file. | 
|    4  |    4  | 
|    5 library observe.src.path_observer; |    5 library observe.src.path_observer; | 
|    6  |    6  | 
|    7 import 'dart:async'; |    7 import 'dart:async'; | 
 |    8 import 'dart:collection'; | 
 |    9 import 'dart:math' show min; | 
|    8 @MirrorsUsed(metaTargets: const [Reflectable, ObservableProperty], |   10 @MirrorsUsed(metaTargets: const [Reflectable, ObservableProperty], | 
|    9     override: 'observe.src.path_observer') |   11     override: 'observe.src.path_observer') | 
|   10 import 'dart:mirrors'; |   12 import 'dart:mirrors'; | 
|   11 import 'package:logging/logging.dart' show Logger, Level; |   13 import 'package:logging/logging.dart' show Logger, Level; | 
|   12 import 'package:observe/observe.dart'; |   14 import 'package:observe/observe.dart'; | 
|   13 import 'package:observe/src/observable.dart' show objectType; |   15 import 'package:observe/src/observable.dart' show objectType; | 
|   14  |   16  | 
|   15 // This code is inspired by ChangeSummary: |   17 /// A data-bound path starting from a view-model or model object, for example | 
|   16 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |   18 /// `foo.bar.baz`. | 
|   17 // ...which underlies MDV. Since we don't need the functionality of |   19 /// | 
|   18 // ChangeSummary, we just implement what we need for data bindings. |   20 /// When [open] is called, this will observe changes to the object and any | 
|   19 // This allows our implementation to be much simpler. |   21 /// intermediate object along the path, and send updated values accordingly. | 
|   20  |   22 /// When [close] is called it will stop observing the objects. | 
|   21 /** |   23 /// | 
|   22  * A data-bound path starting from a view-model or model object, for example |   24 /// This class is used to implement `Node.bind` and similar functionality in | 
|   23  * `foo.bar.baz`. |   25 /// the [template_binding](pub.dartlang.org/packages/template_binding) package. | 
|   24  * |   26 class PathObserver extends _Observer implements Bindable { | 
|   25  * When the [values] stream is being listened to, this will observe changes to |   27   PropertyPath _path; | 
|   26  * the object and any intermediate object along the path, and send [values] |   28   Object _object; | 
|   27  * accordingly. When all listeners are unregistered it will stop observing |   29   _ObservedSet _directObserver; | 
|   28  * the objects. |   30  | 
|   29  * |   31   /// Observes [path] on [object] for changes. This returns an object | 
|   30  * This class is used to implement [Node.bind] and similar functionality. |   32   /// that can be used to get the changes and get/set the value at this path. | 
|   31  */ |   33   /// | 
 |   34   /// The path can be a [PropertyPath], or a [String] used to construct it. | 
 |   35   /// | 
 |   36   /// See [open] and [value]. | 
 |   37   PathObserver(Object object, [path]) | 
 |   38       : _object = object, | 
 |   39         _path = path is PropertyPath ? path : new PropertyPath(path); | 
 |   40  | 
 |   41   bool get _isClosed => _path == null; | 
 |   42  | 
 |   43   /// Sets the value at this path. | 
 |   44   @reflectable void set value(Object newValue) { | 
 |   45     if (_path != null) _path.setValueFrom(_object, newValue); | 
 |   46   } | 
 |   47  | 
 |   48   int get _reportArgumentCount => 2; | 
 |   49  | 
 |   50   /// Initiates observation and returns the initial value. | 
 |   51   /// The callback will be passed the updated [value], and may optionally be | 
 |   52   /// declared to take a second argument, which will contain the previous value. | 
 |   53   open(callback) => super.open(callback); | 
 |   54  | 
 |   55   void _connect() { | 
 |   56     _directObserver = new _ObservedSet(this, _object); | 
 |   57     _check(skipChanges: true); | 
 |   58   } | 
 |   59  | 
 |   60   void _disconnect() { | 
 |   61     _value = null; | 
 |   62     if (_directObserver != null) { | 
 |   63       _directObserver.close(this); | 
 |   64       _directObserver = null; | 
 |   65     } | 
 |   66     // Dart note: the JS impl does not do this, but it seems consistent with | 
 |   67     // CompoundObserver. After closing the PathObserver can't be reopened. | 
 |   68     _path = null; | 
 |   69     _object = null; | 
 |   70   } | 
 |   71  | 
 |   72   void _iterateObjects(void observe(obj)) { | 
 |   73     _path._iterateObjects(_object, observe); | 
 |   74   } | 
 |   75  | 
 |   76   bool _check({bool skipChanges: false}) { | 
 |   77     var oldValue = _value; | 
 |   78     _value = _path.getValueFrom(_object); | 
 |   79     if (skipChanges || _value == oldValue) return false; | 
 |   80  | 
 |   81     _report(_value, oldValue); | 
 |   82     return true; | 
 |   83   } | 
 |   84 } | 
 |   85  | 
 |   86 /// A dot-delimieted property path such as "foo.bar" or "foo.10.bar". | 
 |   87 /// The path specifies how to get a particular value from an object graph, where | 
 |   88 /// the graph can include arrays. | 
|   32 // TODO(jmesserly): consider specialized subclasses for: |   89 // TODO(jmesserly): consider specialized subclasses for: | 
|   33 // * empty path |   90 // * empty path | 
|   34 // * "value" |   91 // * "value" | 
|   35 // * single token in path, e.g. "foo" |   92 // * single token in path, e.g. "foo" | 
|   36 class PathObserver extends ChangeNotifier { |   93 class PropertyPath { | 
|   37   /** The path string. */ |   94   /// The segments of the path. | 
|   38   final String path; |  | 
|   39  |  | 
|   40   /** True if the path is valid, otherwise false. */ |  | 
|   41   final bool _isValid; |  | 
|   42  |  | 
|   43   final List<Object> _segments; |   95   final List<Object> _segments; | 
|   44   List<Object> _values; |   96  | 
|   45   List<StreamSubscription> _subs; |   97   /// Creates a new [PropertyPath]. These can be stored to avoid excessive | 
|   46  |   98   /// parsing of path strings. | 
|   47   final Function _computeValue; |   99   /// | 
|   48  |  100   /// The provided [path] should be a String or a List. If it is a list it | 
|   49   /** |  101   /// should contain only Symbols and integers. This can be used to avoid | 
|   50    * Observes [path] on [object] for changes. This returns an object that can be |  102   /// parsing. | 
|   51    * used to get the changes and get/set the value at this path. |  103   /// | 
|   52    * |  104   /// Note that this constructor will canonicalize identical paths in some cases | 
|   53    * You can optionally use [computeValue] to apply a function to the result of |  105   /// to save memory, but this is not guaranteed. Use [==] for comparions | 
|   54    * evaluating the path. The function should be pure, as PathObserver will not |  106   /// purposes instead of [identical]. | 
|   55    * know to observe any of its dependencies. If you need to observe mutliple |  107   factory PropertyPath([path]) { | 
|   56    * values, use [CompoundPathObserver] instead. |  108     if (path is List) { | 
|   57    * |  109       var copy = new List.from(path, growable: false); | 
|   58    * See [PathObserver.bindSync] and [PathObserver.value]. |  110       for (var segment in copy) { | 
|   59    */ |  111         if (segment is! int && segment is! Symbol) { | 
|   60   PathObserver(Object object, String path, {computeValue(newValue)}) |  112           throw new ArgumentError('List must contain only ints and Symbols'); | 
|   61       : path = path, |  113         } | 
|   62         _computeValue = computeValue, |  | 
|   63         _isValid = _isPathValid(path), |  | 
|   64         _segments = <Object>[] { |  | 
|   65  |  | 
|   66     if (_isValid) { |  | 
|   67       for (var segment in path.trim().split('.')) { |  | 
|   68         if (segment == '') continue; |  | 
|   69         var index = int.parse(segment, radix: 10, onError: (_) => null); |  | 
|   70         _segments.add(index != null ? index : new Symbol(segment)); |  | 
|   71       } |  114       } | 
|   72     } |  115       return new PropertyPath._(copy); | 
|   73  |  116     } | 
|   74     // Initialize arrays. |  117  | 
|   75     // Note that the path itself can't change after it is initially |  118     if (path == null) path = ''; | 
|   76     // constructed, even though the objects along the path can change. |  119  | 
|   77     _values = new List<Object>(_segments.length + 1); |  120     var pathObj = _pathCache[path]; | 
|   78  |  121     if (pathObj != null) return pathObj; | 
|   79     // If we have an empty path, we need to apply the transformation function |  122  | 
|   80     // to the value. The "value" property should always show the transformed |  123     if (!_isPathValid(path)) return _InvalidPropertyPath._instance; | 
|   81     // value. |  124  | 
|   82     if (_segments.isEmpty && computeValue != null) { |  125     final segments = []; | 
|   83       object = computeValue(object); |  126     for (var segment in path.trim().split('.')) { | 
|   84     } |  127       if (segment == '') continue; | 
|   85  |  128       var index = int.parse(segment, radix: 10, onError: (_) => null); | 
|   86     _values[0] = object; |  129       segments.add(index != null ? index : new Symbol(segment)); | 
|   87     _subs = new List<StreamSubscription>(_segments.length); |  130     } | 
|   88   } |  131  | 
|   89  |  132     // TODO(jmesserly): we could use an UnmodifiableListView here, but that adds | 
|   90   /** The object being observed. If the path is empty this will be [value]. */ |  133     // memory overhead. | 
|   91   get object => _values[0]; |  134     pathObj = new PropertyPath._(segments.toList(growable: false)); | 
|   92  |  135     if (_pathCache.length >= _pathCacheLimit) { | 
|   93   /** Gets the last reported value at this path. */ |  136       _pathCache.remove(_pathCache.keys.first); | 
|   94   @reflectable get value { |  137     } | 
|   95     if (!_isValid) return null; |  138     _pathCache[path] = pathObj; | 
|   96     if (!hasObservers) _updateValues(); |  139     return pathObj; | 
|   97     return _values.last; |  140   } | 
|   98   } |  141  | 
|   99  |  142   PropertyPath._(this._segments); | 
|  100   /** Sets the value at this path. */ |  143  | 
|  101   @reflectable void set value(Object newValue) { |  144   int get length => _segments.length; | 
 |  145   bool get isEmpty => _segments.isEmpty; | 
 |  146   bool get isValid => true; | 
 |  147  | 
 |  148   String toString() { | 
 |  149     if (!isValid) return '<invalid path>'; | 
 |  150     return _segments | 
 |  151         .map((s) => s is Symbol ? MirrorSystem.getName(s) : s) | 
 |  152         .join('.'); | 
 |  153   } | 
 |  154  | 
 |  155   bool operator ==(other) { | 
 |  156     if (identical(this, other)) return true; | 
 |  157     if (other is! PropertyPath) return false; | 
 |  158     if (isValid != other.isValid) return false; | 
 |  159  | 
|  102     int len = _segments.length; |  160     int len = _segments.length; | 
|  103  |  161     if (len != other._segments.length) return false; | 
|  104     // TODO(jmesserly): throw if property cannot be set? |  162     for (int i = 0; i < len; i++) { | 
|  105     // MDV seems tolerant of these errors. |  163       if (_segments[i] != other._segments[i]) return false; | 
|  106     if (len == 0) return; |  164     } | 
|  107     if (!hasObservers) _updateValues(end: len - 1); |  165     return true; | 
|  108  |  166   } | 
|  109     if (_setObjectProperty(_values[len - 1], _segments[len - 1], newValue)) { |  167  | 
|  110       // Technically, this would get updated asynchronously via a change record. |  168   /// This is the [Jenkins hash function][1] but using masking to keep | 
|  111       // However, it is nice if calling the getter will yield the same value |  169   /// values in SMI range. | 
|  112       // that was just set. So we use this opportunity to update our cache. |  170   /// [1]: http://en.wikipedia.org/wiki/Jenkins_hash_function | 
|  113       _values[len] = newValue; |  171   // TODO(jmesserly): should reuse this instead, see | 
|  114     } |  172   // https://code.google.com/p/dart/issues/detail?id=11617 | 
|  115   } |  173   int get hashCode { | 
|  116  |  174     int hash = 0; | 
|  117   /** |  175     for (int i = 0, len = _segments.length; i < len; i++) { | 
|  118    * Invokes the [callback] immediately with the current [value], and every time |  176       hash = 0x1fffffff & (hash + _segments[i].hashCode); | 
|  119    * the value changes. This is useful for bindings, which want to be up-to-date |  177       hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); | 
|  120    * immediately and stay bound to the value of the path. |  178       hash = hash ^ (hash >> 6); | 
|  121    */ |  179     } | 
|  122   StreamSubscription bindSync(void callback(value)) { |  180     hash = 0x1fffffff & (hash + ((0x03ffffff & hash) <<  3)); | 
|  123     var result = changes.listen((records) { callback(value); }); |  181     hash = hash ^ (hash >> 11); | 
|  124     callback(value); |  182     return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); | 
|  125     return result; |  183   } | 
|  126   } |  184  | 
|  127  |  185   /// Returns the current of the path from the provided [obj]ect. | 
|  128   void observed() { |  186   getValueFrom(Object obj) { | 
|  129     super.observed(); |  187     if (!isValid) return null; | 
|  130     _updateValues(); |  188     for (var segment in _segments) { | 
|  131     _observePath(); |  189       if (obj == null) return null; | 
|  132   } |  190       obj = _getObjectProperty(obj, segment); | 
|  133  |  191     } | 
|  134   void unobserved() { |  192     return obj; | 
|  135     for (int i = 0; i < _subs.length; i++) { |  193   } | 
|  136       if (_subs[i] != null) { |  194  | 
|  137         _subs[i].cancel(); |  195   /// Attempts to set the [value] of the path from the provided [obj]ect. | 
|  138         _subs[i] = null; |  196   /// Returns true if and only if the path was reachable and set. | 
|  139       } |  197   bool setValueFrom(Object obj, Object value) { | 
|  140     } |  198     var end = _segments.length - 1; | 
|  141     super.unobserved(); |  199     if (end < 0) return false; | 
|  142   } |  | 
|  143  |  | 
|  144   // TODO(jmesserly): should we be caching these values if not observing? |  | 
|  145   void _updateValues({int end}) { |  | 
|  146     if (end == null) end = _segments.length; |  | 
|  147     int last = _segments.length - 1; |  | 
|  148     for (int i = 0; i < end; i++) { |  200     for (int i = 0; i < end; i++) { | 
|  149       var newValue = _getObjectProperty(_values[i], _segments[i]); |  201       if (obj == null) return false; | 
|  150       if (i == last && _computeValue != null) { |  202       obj = _getObjectProperty(obj, _segments[i]); | 
|  151         newValue = _computeValue(newValue); |  203     } | 
|  152       } |  204     return _setObjectProperty(obj, _segments[end], value); | 
|  153       _values[i + 1] = newValue; |  205   } | 
|  154     } |  206  | 
|  155   } |  207   void _iterateObjects(Object obj, void observe(obj)) { | 
|  156  |  208     if (!isValid || isEmpty) return; | 
|  157   void _updateObservedValues({int start: 0}) { |  209  | 
|  158     var oldValue, newValue; |  210     int i = 0, last = _segments.length - 1; | 
|  159     for (int i = start, last = _segments.length - 1; i <= last; i++) { |  211     while (obj != null) { | 
|  160       oldValue = _values[i + 1]; |  212       observe(obj); | 
|  161       newValue = _getObjectProperty(_values[i], _segments[i]); |  213  | 
|  162       if (i == last && _computeValue != null) { |  214       if (i >= last) break; | 
|  163         newValue = _computeValue(newValue); |  215       obj = _getObjectProperty(obj, _segments[i++]); | 
|  164       } |  216     } | 
|  165       if (identical(oldValue, newValue)) { |  217   } | 
|  166         _observePath(start, i); |  218 } | 
|  167         return; |  219  | 
|  168       } |  220 class _InvalidPropertyPath extends PropertyPath { | 
|  169       _values[i + 1] = newValue; |  221   static final _instance = new _InvalidPropertyPath(); | 
|  170     } |  222  | 
|  171  |  223   bool get isValid => false; | 
|  172     _observePath(start); |  224   _InvalidPropertyPath() : super._([]); | 
|  173     notifyPropertyChange(#value, oldValue, newValue); |  | 
|  174   } |  | 
|  175  |  | 
|  176   void _observePath([int start = 0, int end]) { |  | 
|  177     if (end == null) end = _segments.length; |  | 
|  178  |  | 
|  179     for (int i = start; i < end; i++) { |  | 
|  180       if (_subs[i] != null) _subs[i].cancel(); |  | 
|  181       _observeIndex(i); |  | 
|  182     } |  | 
|  183   } |  | 
|  184  |  | 
|  185   void _observeIndex(int i) { |  | 
|  186     final object = _values[i]; |  | 
|  187     final segment = _segments[i]; |  | 
|  188     if (segment is int) { |  | 
|  189       if (object is ObservableList) { |  | 
|  190         _subs[i] = object.listChanges.listen((List<ListChangeRecord> records) { |  | 
|  191           for (var record in records) { |  | 
|  192             if (record.indexChanged(segment)) { |  | 
|  193               _updateObservedValues(start: i); |  | 
|  194               return; |  | 
|  195             } |  | 
|  196           } |  | 
|  197         }); |  | 
|  198       } |  | 
|  199     } else if (object is Observable) { |  | 
|  200       // TODO(jmesserly): rather than allocating a new closure for each |  | 
|  201       // property, we could try and have one for the entire path. However we'd |  | 
|  202       // need to do a linear scan to find the index as soon as we got a change. |  | 
|  203       // Also we need to fix ListChangeRecord and MapChangeRecord to contain |  | 
|  204       // the target. Not sure if it's worth it. |  | 
|  205  |  | 
|  206       _subs[i] = object.changes.listen((List<ChangeRecord> records) { |  | 
|  207         for (var record in records) { |  | 
|  208           if (_changeRecordMatches(record, segment)) { |  | 
|  209             _updateObservedValues(start: i); |  | 
|  210             return; |  | 
|  211           } |  | 
|  212         } |  | 
|  213       }); |  | 
|  214     } |  | 
|  215   } |  | 
|  216 } |  225 } | 
|  217  |  226  | 
|  218 bool _changeRecordMatches(record, key) { |  227 bool _changeRecordMatches(record, key) { | 
|  219   if (record is PropertyChangeRecord) { |  228   if (record is PropertyChangeRecord) { | 
|  220     return (record as PropertyChangeRecord).name == key; |  229     return (record as PropertyChangeRecord).name == key; | 
|  221   } |  230   } | 
|  222   if (record is MapChangeRecord) { |  231   if (record is MapChangeRecord) { | 
|  223     if (key is Symbol) key = MirrorSystem.getName(key); |  232     if (key is Symbol) key = MirrorSystem.getName(key); | 
|  224     return (record as MapChangeRecord).key == key; |  233     return (record as MapChangeRecord).key == key; | 
|  225   } |  234   } | 
| (...skipping 85 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
|  311   while (type != objectType) { |  320   while (type != objectType) { | 
|  312     final members = type.declarations; |  321     final members = type.declarations; | 
|  313     if (members[name] is VariableMirror) return true; |  322     if (members[name] is VariableMirror) return true; | 
|  314     if (members.containsKey(setterName)) return true; |  323     if (members.containsKey(setterName)) return true; | 
|  315     if (members.containsKey(#noSuchMethod)) return true; |  324     if (members.containsKey(#noSuchMethod)) return true; | 
|  316     type = _safeSuperclass(type); |  325     type = _safeSuperclass(type); | 
|  317   } |  326   } | 
|  318   return false; |  327   return false; | 
|  319 } |  328 } | 
|  320  |  329  | 
|  321 /** |  330 /// True if the type has a method, other than on Object. | 
|  322  * True if the type has a method, other than on Object. |  331 /// Doesn't consider noSuchMethod, unless [name] is `#noSuchMethod`. | 
|  323  * Doesn't consider noSuchMethod, unless [name] is `#noSuchMethod`. |  | 
|  324  */ |  | 
|  325 bool _hasMethod(ClassMirror type, Symbol name) { |  332 bool _hasMethod(ClassMirror type, Symbol name) { | 
|  326   while (type != objectType) { |  333   while (type != objectType) { | 
|  327     final member = type.declarations[name]; |  334     final member = type.declarations[name]; | 
|  328     if (member is MethodMirror && member.isRegularMethod) return true; |  335     if (member is MethodMirror && member.isRegularMethod) return true; | 
|  329     type = _safeSuperclass(type); |  336     type = _safeSuperclass(type); | 
|  330   } |  337   } | 
|  331   return false; |  338   return false; | 
|  332 } |  339 } | 
|  333  |  340  | 
|  334 ClassMirror _safeSuperclass(ClassMirror type) { |  341 ClassMirror _safeSuperclass(ClassMirror type) { | 
| (...skipping 14 matching lines...) Expand all  Loading... | 
|  349 final _pathRegExp = () { |  356 final _pathRegExp = () { | 
|  350   const identStart = '[\$_a-zA-Z]'; |  357   const identStart = '[\$_a-zA-Z]'; | 
|  351   const identPart = '[\$_a-zA-Z0-9]'; |  358   const identPart = '[\$_a-zA-Z0-9]'; | 
|  352   const ident = '$identStart+$identPart*'; |  359   const ident = '$identStart+$identPart*'; | 
|  353   const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)'; |  360   const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)'; | 
|  354   const identOrElementIndex = '(?:$ident|$elementIndex)'; |  361   const identOrElementIndex = '(?:$ident|$elementIndex)'; | 
|  355   const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*'; |  362   const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*'; | 
|  356   return new RegExp('^$path\$'); |  363   return new RegExp('^$path\$'); | 
|  357 }(); |  364 }(); | 
|  358  |  365  | 
|  359 final _spacesRegExp = new RegExp(r'\s'); |  | 
|  360  |  | 
|  361 bool _isPathValid(String s) { |  366 bool _isPathValid(String s) { | 
|  362   s = s.replaceAll(_spacesRegExp, ''); |  367   s = s.trim(); | 
|  363  |  | 
|  364   if (s == '') return true; |  368   if (s == '') return true; | 
|  365   if (s[0] == '.') return false; |  369   if (s[0] == '.') return false; | 
|  366   return _pathRegExp.hasMatch(s); |  370   return _pathRegExp.hasMatch(s); | 
|  367 } |  371 } | 
|  368  |  372  | 
|  369 final Logger _logger = new Logger('observe.PathObserver'); |  373 final Logger _logger = new Logger('observe.PathObserver'); | 
 |  374  | 
 |  375  | 
 |  376 /// This is a simple cache. It's like LRU but we don't update an item on a | 
 |  377 /// cache hit, because that would require allocation. Better to let it expire | 
 |  378 /// and reallocate the PropertyPath. | 
 |  379 // TODO(jmesserly): this optimization is from observe-js, how valuable is it in | 
 |  380 // practice? | 
 |  381 final _pathCache = new LinkedHashMap<String, PropertyPath>(); | 
 |  382  | 
 |  383 /// The size of a path like "foo.bar" is approximately 160 bytes, so this | 
 |  384 /// reserves ~16Kb of memory for recently used paths. Since paths are frequently | 
 |  385 /// reused, the theory is that this ends up being a good tradeoff in practice. | 
 |  386 // (Note: the 160 byte estimate is from Dart VM 1.0.0.10_r30798 on x64 without | 
 |  387 // using UnmodifiableListView in PropertyPath) | 
 |  388 const int _pathCacheLimit = 100; | 
 |  389  | 
 |  390 /// [CompoundObserver] is a [Bindable] object which knows how to listen to | 
 |  391 /// multiple values (registered via [addPath] or [addObserver]) and invoke a | 
 |  392 /// callback when one or more of the values have changed. | 
 |  393 /// | 
 |  394 ///    var obj = new ObservableMap.from({'a': 1, 'b': 2}); | 
 |  395 ///    var otherObj = new ObservableMap.from({'c': 3}); | 
 |  396 /// | 
 |  397 ///    var observer = new CompoundObserver() | 
 |  398 ///      ..addPath(obj, 'a'); | 
 |  399 ///      ..addObserver(new PathObserver(obj, 'b')); | 
 |  400 ///      ..addPath(otherObj, 'c'); | 
 |  401 ///      ..open((values) { | 
 |  402 ///        for (int i = 0; i < values.length; i++) { | 
 |  403 ///          print('The value at index $i is now ${values[i]}'); | 
 |  404 ///        } | 
 |  405 ///      }); | 
 |  406 /// | 
 |  407 ///   obj['a'] = 10; // print will be triggered async | 
 |  408 /// | 
 |  409 class CompoundObserver extends _Observer implements Bindable { | 
 |  410   _ObservedSet _directObserver; | 
 |  411   List _observed = []; | 
 |  412  | 
 |  413   bool get _isClosed => _observed == null; | 
 |  414  | 
 |  415   CompoundObserver() { | 
 |  416     _value = []; | 
 |  417   } | 
 |  418  | 
 |  419   int get _reportArgumentCount => 3; | 
 |  420  | 
 |  421   /// Initiates observation and returns the initial value. | 
 |  422   /// The callback will be passed the updated [value], and may optionally be | 
 |  423   /// declared to take a second argument, which will contain the previous value. | 
 |  424   /// | 
 |  425   /// Implementation note: a third argument can also be declared, which will | 
 |  426   /// receive a list of objects and paths, such that `list[2 * i]` will access | 
 |  427   /// the object and `list[2 * i + 1]` will access the path, where `i` is the | 
 |  428   /// order of the [addPath] call. This parameter is only used by | 
 |  429   /// `package:polymer` as a performance optimization, and should not be relied | 
 |  430   /// on in new code. | 
 |  431   open(callback) => super.open(callback); | 
 |  432  | 
 |  433   void _connect() { | 
 |  434     _check(skipChanges: true); | 
 |  435  | 
 |  436     for (var i = 0; i < _observed.length; i += 2) { | 
 |  437       var object = _observed[i]; | 
 |  438       if (!identical(object, _observerSentinel)) { | 
 |  439         _directObserver = new _ObservedSet(this, object); | 
 |  440         break; | 
 |  441       } | 
 |  442     } | 
 |  443   } | 
 |  444  | 
 |  445   void _disconnect() { | 
 |  446     _value = null; | 
 |  447  | 
 |  448     if (_directObserver != null) { | 
 |  449       _directObserver.close(this); | 
 |  450       _directObserver = null; | 
 |  451     } | 
 |  452  | 
 |  453     for (var i = 0; i < _observed.length; i += 2) { | 
 |  454       if (identical(_observed[i], _observerSentinel)) { | 
 |  455         _observed[i + 1].close(); | 
 |  456       } | 
 |  457     } | 
 |  458     _observed = null; | 
 |  459   } | 
 |  460  | 
 |  461   /// Adds a dependency on the property [path] accessed from [object]. | 
 |  462   /// [path] can be a [PropertyPath] or a [String]. If it is omitted an empty | 
 |  463   /// path will be used. | 
 |  464   void addPath(Object object, [path]) { | 
 |  465     if (_isOpen || _isClosed) { | 
 |  466       throw new StateError('Cannot add paths once started.'); | 
 |  467     } | 
 |  468  | 
 |  469     if (path is! PropertyPath) path = new PropertyPath(path); | 
 |  470     _observed..add(object)..add(path); | 
 |  471   } | 
 |  472  | 
 |  473   void addObserver(Bindable observer) { | 
 |  474     if (_isOpen || _isClosed) { | 
 |  475       throw new StateError('Cannot add observers once started.'); | 
 |  476     } | 
 |  477  | 
 |  478     observer.open(_deliver); | 
 |  479     _observed..add(_observerSentinel)..add(observer); | 
 |  480   } | 
 |  481  | 
 |  482   void _iterateObjects(void observe(obj)) { | 
 |  483     for (var i = 0; i < _observed.length; i += 2) { | 
 |  484       var object = _observed[i]; | 
 |  485       if (!identical(object, _observerSentinel)) { | 
 |  486         (_observed[i + 1] as PropertyPath)._iterateObjects(object, observe); | 
 |  487       } | 
 |  488     } | 
 |  489   } | 
 |  490  | 
 |  491   bool _check({bool skipChanges: false}) { | 
 |  492     bool changed = false; | 
 |  493     _value.length = _observed.length ~/ 2; | 
 |  494     var oldValues = null; | 
 |  495     for (var i = 0; i < _observed.length; i += 2) { | 
 |  496       var pathOrObserver = _observed[i + 1]; | 
 |  497       var object = _observed[i]; | 
 |  498       var value = identical(object, _observerSentinel) ? | 
 |  499           (pathOrObserver as Bindable).value : | 
 |  500           (pathOrObserver as PropertyPath).getValueFrom(object); | 
 |  501  | 
 |  502       if (skipChanges) { | 
 |  503         _value[i ~/ 2] = value; | 
 |  504         continue; | 
 |  505       } | 
 |  506  | 
 |  507       if (value == _value[i ~/ 2]) continue; | 
 |  508  | 
 |  509       // don't allocate this unless necessary. | 
 |  510       if (_notifyArgumentCount >= 2) { | 
 |  511         if (oldValues == null) oldValues = new Map(); | 
 |  512         oldValues[i ~/ 2] = _value[i ~/ 2]; | 
 |  513       } | 
 |  514  | 
 |  515       changed = true; | 
 |  516       _value[i ~/ 2] = value; | 
 |  517     } | 
 |  518  | 
 |  519     if (!changed) return false; | 
 |  520  | 
 |  521     // TODO(rafaelw): Having _observed as the third callback arg here is | 
 |  522     // pretty lame API. Fix. | 
 |  523     _report(_value, oldValues, _observed); | 
 |  524     return true; | 
 |  525   } | 
 |  526 } | 
 |  527  | 
 |  528 const _observerSentinel = const _ObserverSentinel(); | 
 |  529 class _ObserverSentinel { const _ObserverSentinel(); } | 
 |  530  | 
 |  531 // A base class for the shared API implemented by PathObserver and | 
 |  532 // CompoundObserver and used in _ObservedSet. | 
 |  533 abstract class _Observer extends Bindable { | 
 |  534   static int _nextBirthId = 0; | 
 |  535  | 
 |  536   /// A number indicating when the object was created. | 
 |  537   final int _birthId = _nextBirthId++; | 
 |  538  | 
 |  539   Function _notifyCallback; | 
 |  540   int _notifyArgumentCount; | 
 |  541   var _value; | 
 |  542  | 
 |  543   // abstract members | 
 |  544   void _iterateObjects(void observe(obj)); | 
 |  545   void _connect(); | 
 |  546   void _disconnect(); | 
 |  547   bool get _isClosed; | 
 |  548   _check({bool skipChanges: false}); | 
 |  549  | 
 |  550   bool get _isOpen => _notifyCallback != null; | 
 |  551  | 
 |  552   /// The number of arguments the subclass will pass to [_report]. | 
 |  553   int get _reportArgumentCount; | 
 |  554  | 
 |  555   open(callback) { | 
 |  556     if (_isOpen || _isClosed) { | 
 |  557       throw new StateError('Observer has already been opened.'); | 
 |  558     } | 
 |  559  | 
 |  560     if (_minArgumentCount(callback) > _reportArgumentCount) { | 
 |  561       throw new ArgumentError('callback should take $_reportArgumentCount or ' | 
 |  562           'fewer arguments'); | 
 |  563     } | 
 |  564  | 
 |  565     _notifyCallback = callback; | 
 |  566     _notifyArgumentCount = min(_reportArgumentCount, | 
 |  567         _maxArgumentCount(callback)); | 
 |  568  | 
 |  569     _connect(); | 
 |  570     return _value; | 
 |  571   } | 
 |  572  | 
 |  573   @reflectable get value { | 
 |  574     _check(skipChanges: true); | 
 |  575     return _value; | 
 |  576   } | 
 |  577  | 
 |  578   void close() { | 
 |  579     if (!_isOpen) return; | 
 |  580  | 
 |  581     _disconnect(); | 
 |  582     _value = null; | 
 |  583     _notifyCallback = null; | 
 |  584   } | 
 |  585  | 
 |  586   void _deliver(_) { | 
 |  587     if (_isOpen) _dirtyCheck(); | 
 |  588   } | 
 |  589  | 
 |  590   bool _dirtyCheck() { | 
 |  591     var cycles = 0; | 
 |  592     while (cycles < _MAX_DIRTY_CHECK_CYCLES && _check()) { | 
 |  593       cycles++; | 
 |  594     } | 
 |  595     return cycles > 0; | 
 |  596   } | 
 |  597  | 
 |  598   void _report(newValue, oldValue, [extraArg]) { | 
 |  599     try { | 
 |  600       switch (_notifyArgumentCount) { | 
 |  601         case 0: _notifyCallback(); break; | 
 |  602         case 1: _notifyCallback(newValue); break; | 
 |  603         case 2: _notifyCallback(newValue, oldValue); break; | 
 |  604         case 3: _notifyCallback(newValue, oldValue, extraArg); break; | 
 |  605       } | 
 |  606     } catch (e, s) { | 
 |  607       // Deliver errors async, so if a single callback fails it doesn't prevent | 
 |  608       // other things from working. | 
 |  609       new Completer().completeError(e, s); | 
 |  610     } | 
 |  611   } | 
 |  612 } | 
 |  613  | 
 |  614 typedef _Func0(); | 
 |  615 typedef _Func1(a); | 
 |  616 typedef _Func2(a, b); | 
 |  617 typedef _Func3(a, b, c); | 
 |  618  | 
 |  619 int _minArgumentCount(fn) { | 
 |  620   if (fn is _Func0) return 0; | 
 |  621   if (fn is _Func1) return 1; | 
 |  622   if (fn is _Func2) return 2; | 
 |  623   if (fn is _Func3) return 3; | 
 |  624   return 4; // at least 4 arguments are required. | 
 |  625 } | 
 |  626  | 
 |  627 int _maxArgumentCount(fn) { | 
 |  628   if (fn is _Func3) return 3; | 
 |  629   if (fn is _Func2) return 2; | 
 |  630   if (fn is _Func1) return 1; | 
 |  631   if (fn is _Func0) return 0; | 
 |  632   return -1; | 
 |  633 } | 
 |  634  | 
 |  635 class _ObservedSet { | 
 |  636   /// To prevent sequential [PathObserver]s and [CompoundObserver]s from | 
 |  637   /// observing the same object, we check if they are observing the same root | 
 |  638   /// as the most recently created observer, and if so merge it into the | 
 |  639   /// existing _ObservedSet. | 
 |  640   /// | 
 |  641   /// See <https://github.com/Polymer/observe-js/commit/f0990b1> and | 
 |  642   /// <https://codereview.appspot.com/46780044/>. | 
 |  643   static _ObservedSet _lastSet; | 
 |  644  | 
 |  645   /// The root object for a [PathObserver]. For a [CompoundObserver], the root | 
 |  646   /// object of the first path observed. This is used by the constructor to | 
 |  647   /// reuse an [_ObservedSet] that starts from the same object. | 
 |  648   Object _rootObject; | 
 |  649  | 
 |  650   /// Observers associated with this root object, in birth order. | 
 |  651   final Map<int, _Observer> _observers = new SplayTreeMap(); | 
 |  652  | 
 |  653   // Dart note: the JS implementation is O(N^2) because Array.indexOf is used | 
 |  654   // for lookup in these two arrays. We use HashMap to avoid this problem. It | 
 |  655   // also gives us a nice way of tracking the StreamSubscription. | 
 |  656   Map<Object, StreamSubscription> _objects; | 
 |  657   Map<Object, StreamSubscription> _toRemove; | 
 |  658  | 
 |  659   bool _resetNeeded = false; | 
 |  660  | 
 |  661   factory _ObservedSet(_Observer observer, Object rootObj) { | 
 |  662     if (_lastSet == null || !identical(_lastSet._rootObject, rootObj)) { | 
 |  663       _lastSet = new _ObservedSet._(rootObj); | 
 |  664     } | 
 |  665     _lastSet.open(observer); | 
 |  666   } | 
 |  667  | 
 |  668   _ObservedSet._(this._rootObject); | 
 |  669  | 
 |  670   void open(_Observer obs) { | 
 |  671     _observers[obs._birthId] = obs; | 
 |  672     obs._iterateObjects(observe); | 
 |  673   } | 
 |  674  | 
 |  675   void close(_Observer obs) { | 
 |  676     var anyLeft = false; | 
 |  677  | 
 |  678     _observers.remove(obs._birthId); | 
 |  679  | 
 |  680     if (_observers.isNotEmpty) { | 
 |  681       _resetNeeded = true; | 
 |  682       scheduleMicrotask(reset); | 
 |  683       return; | 
 |  684     } | 
 |  685     _resetNeeded = false; | 
 |  686  | 
 |  687     if (_objects != null) { | 
 |  688       for (var sub in _objects) sub.cancel(); | 
 |  689       _objects = null; | 
 |  690     } | 
 |  691   } | 
 |  692  | 
 |  693   void observe(Object obj) { | 
 |  694     if (obj is ObservableList) _observeStream(obj.listChanges); | 
 |  695     if (obj is Observable) _observeStream(obj.changes); | 
 |  696   } | 
 |  697  | 
 |  698   void _observeStream(Stream stream) { | 
 |  699     // TODO(jmesserly): we hash on streams as we have two separate change | 
 |  700     // streams for ObservableList. Not sure if that is the design we will use | 
 |  701     // going forward. | 
 |  702  | 
 |  703     if (_objects == null) _objects = new HashMap(); | 
 |  704     StreamSubscription sub = null; | 
 |  705     if (_toRemove != null) sub = _toRemove.remove(stream); | 
 |  706     if (sub != null) { | 
 |  707       _objects[stream] = sub; | 
 |  708     } else if (!_objects.containsKey(stream)) { | 
 |  709       _objects[stream] = stream.listen(_callback); | 
 |  710     } | 
 |  711   } | 
 |  712  | 
 |  713   void reset() { | 
 |  714     if (!_resetNeeded) return; | 
 |  715  | 
 |  716     var objs = _toRemove == null ? new HashMap() : _toRemove; | 
 |  717     _toRemove = _objects; | 
 |  718     _objects = objs; | 
 |  719     for (var observer in _observers.values) { | 
 |  720       if (observer._isOpen) observer._iterateObjects(observe); | 
 |  721     } | 
 |  722  | 
 |  723     for (var sub in _toRemove.values) sub.cancel(); | 
 |  724  | 
 |  725     _toRemove = null; | 
 |  726   } | 
 |  727  | 
 |  728   void _callback(records) { | 
 |  729     for (var observer in _observers.values.toList(growable: false)) { | 
 |  730       if (observer._isOpen) observer._check(); | 
 |  731     } | 
 |  732  | 
 |  733     _resetNeeded = true; | 
 |  734     scheduleMicrotask(reset); | 
 |  735   } | 
 |  736 } | 
 |  737  | 
 |  738 const int _MAX_DIRTY_CHECK_CYCLES = 1000; | 
| OLD | NEW |