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 |