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