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 |