Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(76)

Side by Side Diff: pkg/observe/lib/src/path_observer.dart

Issue 132403010: big update to observe, template_binding, polymer (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « pkg/observe/lib/src/observer_transform.dart ('k') | pkg/observe/pubspec.yaml » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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;
OLDNEW
« no previous file with comments | « pkg/observe/lib/src/observer_transform.dart ('k') | pkg/observe/pubspec.yaml » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698