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

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

Issue 420673002: Roll polymer packages to version 0.3.4 (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years, 5 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
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'; 8 import 'dart:collection';
9 import 'dart:math' show min; 9 import 'dart:math' show min;
10 10
11 import 'package:logging/logging.dart' show Logger, Level; 11 import 'package:logging/logging.dart' show Logger, Level;
12 import 'package:observe/observe.dart'; 12 import 'package:observe/observe.dart';
13 import 'package:smoke/smoke.dart' as smoke; 13 import 'package:smoke/smoke.dart' as smoke;
14 14
15 import 'package:utf/utf.dart' show stringToCodepoints;
16
15 /// A data-bound path starting from a view-model or model object, for example 17 /// A data-bound path starting from a view-model or model object, for example
16 /// `foo.bar.baz`. 18 /// `foo.bar.baz`.
17 /// 19 ///
18 /// When [open] is called, this will observe changes to the object and any 20 /// When [open] is called, this will observe changes to the object and any
19 /// intermediate object along the path, and send updated values accordingly. 21 /// intermediate object along the path, and send updated values accordingly.
20 /// When [close] is called it will stop observing the objects. 22 /// When [close] is called it will stop observing the objects.
21 /// 23 ///
22 /// This class is used to implement `Node.bind` and similar functionality in 24 /// This class is used to implement `Node.bind` and similar functionality in
23 /// the [template_binding](pub.dartlang.org/packages/template_binding) package. 25 /// the [template_binding](pub.dartlang.org/packages/template_binding) package.
24 class PathObserver extends _Observer implements Bindable { 26 class PathObserver extends _Observer implements Bindable {
25 PropertyPath _path; 27 PropertyPath _path;
26 Object _object; 28 Object _object;
27 _ObservedSet _directObserver; 29 _ObservedSet _directObserver;
28 30
29 /// Observes [path] on [object] for changes. This returns an object 31 /// Observes [path] on [object] for changes. This returns an object
30 /// that can be used to get the changes and get/set the value at this path. 32 /// that can be used to get the changes and get/set the value at this path.
31 /// 33 ///
32 /// The path can be a [PropertyPath], or a [String] used to construct it. 34 /// The path can be a [PropertyPath], or a [String] used to construct it.
33 /// 35 ///
34 /// See [open] and [value]. 36 /// See [open] and [value].
35 PathObserver(Object object, [path]) 37 PathObserver(Object object, [path])
36 : _object = object, 38 : _object = object,
37 _path = path is PropertyPath ? path : new PropertyPath(path); 39 _path = new PropertyPath(path);
38 40
39 bool get _isClosed => _path == null; 41 PropertyPath get path => _path;
40 42
41 /// Sets the value at this path. 43 /// Sets the value at this path.
42 void set value(Object newValue) { 44 void set value(Object newValue) {
43 if (_path != null) _path.setValueFrom(_object, newValue); 45 if (_path != null) _path.setValueFrom(_object, newValue);
44 } 46 }
45 47
46 int get _reportArgumentCount => 2; 48 int get _reportArgumentCount => 2;
47 49
48 /// Initiates observation and returns the initial value. 50 /// Initiates observation and returns the initial value.
49 /// The callback will be passed the updated [value], and may optionally be 51 /// The callback will be passed the updated [value], and may optionally be
(...skipping 10 matching lines...) Expand all
60 if (_directObserver != null) { 62 if (_directObserver != null) {
61 _directObserver.close(this); 63 _directObserver.close(this);
62 _directObserver = null; 64 _directObserver = null;
63 } 65 }
64 // Dart note: the JS impl does not do this, but it seems consistent with 66 // Dart note: the JS impl does not do this, but it seems consistent with
65 // CompoundObserver. After closing the PathObserver can't be reopened. 67 // CompoundObserver. After closing the PathObserver can't be reopened.
66 _path = null; 68 _path = null;
67 _object = null; 69 _object = null;
68 } 70 }
69 71
70 void _iterateObjects(void observe(obj)) { 72 void _iterateObjects(void observe(obj, prop)) {
71 _path._iterateObjects(_object, observe); 73 _path._iterateObjects(_object, observe);
72 } 74 }
73 75
74 bool _check({bool skipChanges: false}) { 76 bool _check({bool skipChanges: false}) {
75 var oldValue = _value; 77 var oldValue = _value;
76 _value = _path.getValueFrom(_object); 78 _value = _path.getValueFrom(_object);
77 if (skipChanges || _value == oldValue) return false; 79 if (skipChanges || _value == oldValue) return false;
78 80
79 _report(_value, oldValue); 81 _report(_value, oldValue, this);
80 return true; 82 return true;
81 } 83 }
82 } 84 }
83 85
84 /// A dot-delimieted property path such as "foo.bar" or "foo.10.bar". 86 /// A dot-delimieted property path such as "foo.bar" or "foo.10.bar".
85 /// 87 ///
86 /// The path specifies how to get a particular value from an object graph, where 88 /// The path specifies how to get a particular value from an object graph, where
87 /// the graph can include arrays and maps. Each segment of the path describes 89 /// the graph can include arrays and maps. Each segment of the path describes
88 /// how to take a single step in the object graph. Properties like 'foo' or 90 /// how to take a single step in the object graph. Properties like 'foo' or
89 /// 'bar' are read as properties on objects, or as keys if the object is a [Map] 91 /// 'bar' are read as properties on objects, or as keys if the object is a [Map]
90 /// or a [Indexable], while integer values are read as indexes in a [List]. 92 /// or a [Indexable], while integer values are read as indexes in a [List].
91 // TODO(jmesserly): consider specialized subclasses for: 93 // TODO(jmesserly): consider specialized subclasses for:
92 // * empty path 94 // * empty path
93 // * "value" 95 // * "value"
94 // * single token in path, e.g. "foo" 96 // * single token in path, e.g. "foo"
95 class PropertyPath { 97 class PropertyPath {
96 /// The segments of the path. 98 /// The segments of the path.
97 final List<Object> _segments; 99 final List<Object> _segments;
98 100
99 /// Creates a new [PropertyPath]. These can be stored to avoid excessive 101 /// Creates a new [PropertyPath]. These can be stored to avoid excessive
100 /// parsing of path strings. 102 /// parsing of path strings.
101 /// 103 ///
102 /// The provided [path] should be a String or a List. If it is a list it 104 /// The provided [path] should be a String or a List. If it is a list it
103 /// should contain only Symbols and integers. This can be used to avoid 105 /// should contain only Symbols and integers. This can be used to avoid
104 /// parsing. 106 /// parsing.
105 /// 107 ///
106 /// Note that this constructor will canonicalize identical paths in some cases 108 /// Note that this constructor will canonicalize identical paths in some cases
107 /// to save memory, but this is not guaranteed. Use [==] for comparions 109 /// to save memory, but this is not guaranteed. Use [==] for comparions
108 /// purposes instead of [identical]. 110 /// purposes instead of [identical].
111 // Dart note: this is ported from `function getPath`.
109 factory PropertyPath([path]) { 112 factory PropertyPath([path]) {
113 if (path is PropertyPath) return path;
114 if (path == null || (path is List && path.isEmpty)) path = '';
115
110 if (path is List) { 116 if (path is List) {
111 var copy = new List.from(path, growable: false); 117 var copy = new List.from(path, growable: false);
112 for (var segment in copy) { 118 for (var segment in copy) {
113 if (segment is! int && segment is! Symbol) { 119 // Dart note: unlike Javascript, we don't support arbitraty objects that
114 throw new ArgumentError('List must contain only ints and Symbols'); 120 // can be converted to a String.
121 // TODO(sigmund): consider whether we should support that here. It might
122 // be easier to add support for that if we switch first to use strings
123 // for everything instead of symbols.
124 if (segment is! int && segment is! String && segment is! Symbol) {
125 throw new ArgumentError(
126 'List must contain only ints, Strings, and Symbols');
115 } 127 }
116 } 128 }
117 return new PropertyPath._(copy); 129 return new PropertyPath._(copy);
118 } 130 }
119 131
120 if (path == null) path = '';
121
122 var pathObj = _pathCache[path]; 132 var pathObj = _pathCache[path];
123 if (pathObj != null) return pathObj; 133 if (pathObj != null) return pathObj;
124 134
125 if (!_isPathValid(path)) return _InvalidPropertyPath._instance;
126 135
127 final segments = []; 136 final segments = new _PathParser().parse(path);
128 for (var segment in path.trim().split('.')) { 137 if (segments == null) return _InvalidPropertyPath._instance;
129 if (segment == '') continue;
130 var index = int.parse(segment, radix: 10, onError: (_) => null);
131 segments.add(index != null ? index : smoke.nameToSymbol(segment));
132 }
133 138
134 // TODO(jmesserly): we could use an UnmodifiableListView here, but that adds 139 // TODO(jmesserly): we could use an UnmodifiableListView here, but that adds
135 // memory overhead. 140 // memory overhead.
136 pathObj = new PropertyPath._(segments.toList(growable: false)); 141 pathObj = new PropertyPath._(segments.toList(growable: false));
137 if (_pathCache.length >= _pathCacheLimit) { 142 if (_pathCache.length >= _pathCacheLimit) {
138 _pathCache.remove(_pathCache.keys.first); 143 _pathCache.remove(_pathCache.keys.first);
139 } 144 }
140 _pathCache[path] = pathObj; 145 _pathCache[path] = pathObj;
141 return pathObj; 146 return pathObj;
142 } 147 }
143 148
144 PropertyPath._(this._segments); 149 PropertyPath._(this._segments);
145 150
146 int get length => _segments.length; 151 int get length => _segments.length;
147 bool get isEmpty => _segments.isEmpty; 152 bool get isEmpty => _segments.isEmpty;
148 bool get isValid => true; 153 bool get isValid => true;
149 154
150 String toString() { 155 String toString() {
151 if (!isValid) return '<invalid path>'; 156 if (!isValid) return '<invalid path>';
152 return _segments 157 var sb = new StringBuffer();
153 .map((s) => s is Symbol ? smoke.symbolToName(s) : s) 158 bool first = true;
154 .join('.'); 159 for (var key in _segments) {
160 if (key is Symbol) {
161 if (!first) sb.write('.');
162 sb.write(smoke.symbolToName(key));
163 } else {
164 _formatAccessor(sb, key);
165 }
166 first = false;
167 }
168 return sb.toString();
169 }
170
171 _formatAccessor(StringBuffer sb, Object key) {
172 if (key is int) {
173 sb.write('[$key]');
174 } else {
175 sb.write('["${key.toString().replaceAll('"', '\\"')}"]');
176 }
155 } 177 }
156 178
157 bool operator ==(other) { 179 bool operator ==(other) {
158 if (identical(this, other)) return true; 180 if (identical(this, other)) return true;
159 if (other is! PropertyPath) return false; 181 if (other is! PropertyPath) return false;
160 if (isValid != other.isValid) return false; 182 if (isValid != other.isValid) return false;
161 183
162 int len = _segments.length; 184 int len = _segments.length;
163 if (len != other._segments.length) return false; 185 if (len != other._segments.length) return false;
164 for (int i = 0; i < len; i++) { 186 for (int i = 0; i < len; i++) {
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after
199 bool setValueFrom(Object obj, Object value) { 221 bool setValueFrom(Object obj, Object value) {
200 var end = _segments.length - 1; 222 var end = _segments.length - 1;
201 if (end < 0) return false; 223 if (end < 0) return false;
202 for (int i = 0; i < end; i++) { 224 for (int i = 0; i < end; i++) {
203 if (obj == null) return false; 225 if (obj == null) return false;
204 obj = _getObjectProperty(obj, _segments[i]); 226 obj = _getObjectProperty(obj, _segments[i]);
205 } 227 }
206 return _setObjectProperty(obj, _segments[end], value); 228 return _setObjectProperty(obj, _segments[end], value);
207 } 229 }
208 230
209 void _iterateObjects(Object obj, void observe(obj)) { 231 void _iterateObjects(Object obj, void observe(obj, prop)) {
210 if (!isValid || isEmpty) return; 232 if (!isValid || isEmpty) return;
211 233
212 int i = 0, last = _segments.length - 1; 234 int i = 0, last = _segments.length - 1;
213 while (obj != null) { 235 while (obj != null) {
214 observe(obj); 236 observe(obj, _segments[0]);
jakemac 2014/07/25 16:40:33 Is segments[0] what we want here? It might be wort
Siggi Cherem (dart-lang) 2014/07/25 17:45:12 yeah - basically the second argument is to optimiz
215 237
216 if (i >= last) break; 238 if (i >= last) break;
217 obj = _getObjectProperty(obj, _segments[i++]); 239 obj = _getObjectProperty(obj, _segments[i++]);
218 } 240 }
219 } 241 }
242
243 // Dart note: it doesn't make sense to have compiledGetValueFromFn in Dart.
220 } 244 }
221 245
246
247 /// Visible only for testing:
248 getSegmentsOfPropertyPathForTesting(p) => p._segments;
249
222 class _InvalidPropertyPath extends PropertyPath { 250 class _InvalidPropertyPath extends PropertyPath {
223 static final _instance = new _InvalidPropertyPath(); 251 static final _instance = new _InvalidPropertyPath();
224 252
225 bool get isValid => false; 253 bool get isValid => false;
226 _InvalidPropertyPath() : super._([]); 254 _InvalidPropertyPath() : super._([]);
227 } 255 }
228 256
229 bool _changeRecordMatches(record, key) { 257 bool _changeRecordMatches(record, key) {
230 if (record is PropertyChangeRecord) { 258 if (record is PropertyChangeRecord) {
231 return (record as PropertyChangeRecord).name == key; 259 return (record as PropertyChangeRecord).name == key;
(...skipping 11 matching lines...) Expand all
243 /// them as part of path-observer segments. 271 /// them as part of path-observer segments.
244 const _MAP_PROPERTIES = const [#keys, #values, #length, #isEmpty, #isNotEmpty]; 272 const _MAP_PROPERTIES = const [#keys, #values, #length, #isEmpty, #isNotEmpty];
245 273
246 _getObjectProperty(object, property) { 274 _getObjectProperty(object, property) {
247 if (object == null) return null; 275 if (object == null) return null;
248 276
249 if (property is int) { 277 if (property is int) {
250 if (object is List && property >= 0 && property < object.length) { 278 if (object is List && property >= 0 && property < object.length) {
251 return object[property]; 279 return object[property];
252 } 280 }
281 } else if (property is String) {
282 return object[property];
253 } else if (property is Symbol) { 283 } else if (property is Symbol) {
254 // Support indexer if available, e.g. Maps or polymer_expressions Scope. 284 // Support indexer if available, e.g. Maps or polymer_expressions Scope.
255 // This is the default syntax used by polymer/nodebind and 285 // This is the default syntax used by polymer/nodebind and
256 // polymer/observe-js PathObserver. 286 // polymer/observe-js PathObserver.
257 // TODO(sigmund): should we also support using checking dynamically for 287 // TODO(sigmund): should we also support using checking dynamically for
258 // whether the type practically implements the indexer API 288 // whether the type practically implements the indexer API
259 // (smoke.hasInstanceMethod(type, const Symbol('[]')))? 289 // (smoke.hasInstanceMethod(type, const Symbol('[]')))?
260 if (object is Indexable<String, dynamic> || 290 if (object is Indexable<String, dynamic> ||
261 object is Map<String, dynamic> && !_MAP_PROPERTIES.contains(property)) { 291 object is Map<String, dynamic> && !_MAP_PROPERTIES.contains(property)) {
262 return object[smoke.symbolToName(property)]; 292 return object[smoke.symbolToName(property)];
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
302 } 332 }
303 333
304 if (_logger.isLoggable(Level.FINER)) { 334 if (_logger.isLoggable(Level.FINER)) {
305 _logger.finer("can't set $property in $object"); 335 _logger.finer("can't set $property in $object");
306 } 336 }
307 return false; 337 return false;
308 } 338 }
309 339
310 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js 340 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
311 341
312 final _pathRegExp = () { 342 final _identRegExp = () {
313 const identStart = '[\$_a-zA-Z]'; 343 const identStart = '[\$_a-zA-Z]';
314 const identPart = '[\$_a-zA-Z0-9]'; 344 const identPart = '[\$_a-zA-Z0-9]';
315 const ident = '$identStart+$identPart*'; 345 return new RegExp('^$identStart+$identPart*\$');
316 const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)';
317 const identOrElementIndex = '(?:$ident|$elementIndex)';
318 const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*';
319 return new RegExp('^$path\$');
320 }(); 346 }();
321 347
322 bool _isPathValid(String s) { 348 _isIdent(s) => _identRegExp.hasMatch(s);
323 s = s.trim(); 349
324 if (s == '') return true; 350 // Dart note: refactored to convert to codepoints once and operate on codepoints
325 if (s[0] == '.') return false; 351 // rather than characters.
326 return _pathRegExp.hasMatch(s); 352 class _PathParser {
353 List keys = [];
354 int index = -1;
355 String key;
356
357 final Map<String, List<String>> _pathStateMachine = {
358 'beforePath': {
359 'ws': ['beforePath'],
360 'ident': ['inIdent', 'append'],
361 '[': ['beforeElement'],
362 'eof': ['afterPath']
363 },
364
365 'inPath': {
366 'ws': ['inPath'],
367 '.': ['beforeIdent'],
368 '[': ['beforeElement'],
369 'eof': ['afterPath']
370 },
371
372 'beforeIdent': {
373 'ws': ['beforeIdent'],
374 'ident': ['inIdent', 'append']
375 },
376
377 'inIdent': {
378 'ident': ['inIdent', 'append'],
379 '0': ['inIdent', 'append'],
380 'number': ['inIdent', 'append'],
381 'ws': ['inPath', 'push'],
382 '.': ['beforeIdent', 'push'],
383 '[': ['beforeElement', 'push'],
384 'eof': ['afterPath', 'push']
385 },
386
387 'beforeElement': {
388 'ws': ['beforeElement'],
389 '0': ['afterZero', 'append'],
390 'number': ['inIndex', 'append'],
391 "'": ['inSingleQuote', 'append', ''],
392 '"': ['inDoubleQuote', 'append', '']
393 },
394
395 'afterZero': {
396 'ws': ['afterElement', 'push'],
397 ']': ['inPath', 'push']
398 },
399
400 'inIndex': {
401 '0': ['inIndex', 'append'],
402 'number': ['inIndex', 'append'],
403 'ws': ['afterElement'],
404 ']': ['inPath', 'push']
405 },
406
407 'inSingleQuote': {
408 "'": ['afterElement'],
409 'eof': ['error'],
410 'else': ['inSingleQuote', 'append']
411 },
412
413 'inDoubleQuote': {
414 '"': ['afterElement'],
415 'eof': ['error'],
416 'else': ['inDoubleQuote', 'append']
417 },
418
419 'afterElement': {
420 'ws': ['afterElement'],
421 ']': ['inPath', 'push']
422 }
423 };
424
425 /// From getPathCharType: determines the type of a given [code]point.
426 String _getPathCharType(code) {
427 if (code == null) return 'eof';
428 switch(code) {
429 case 0x5B: // [
430 case 0x5D: // ]
431 case 0x2E: // .
432 case 0x22: // "
433 case 0x27: // '
434 case 0x30: // 0
435 return _char(code);
436
437 case 0x5F: // _
438 case 0x24: // $
439 return 'ident';
440
441 case 0x20: // Space
442 case 0x09: // Tab
443 case 0x0A: // Newline
444 case 0x0D: // Return
445 case 0xA0: // No-break space
446 case 0xFEFF: // Byte Order Mark
447 case 0x2028: // Line Separator
448 case 0x2029: // Paragraph Separator
449 return 'ws';
450 }
451
452 // a-z, A-Z
453 if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A))
454 return 'ident';
455
456 // 1-9
457 if (0x31 <= code && code <= 0x39)
458 return 'number';
459
460 return 'else';
461 }
462
463 static String _char(int codepoint) => new String.fromCharCodes([codepoint]);
464
465 void push() {
466 if (key == null) return;
467
468 // Dart note: we store the keys with different types, rather than
469 // parsing/converting things later in toString.
470 if (_isIdent(key)) {
471 keys.add(smoke.nameToSymbol(key));
472 } else {
473 var index = int.parse(key, radix: 10, onError: (_) => null);
474 keys.add(index != null ? index : key);
475 }
476 key = null;
477 }
478
479 void append(newChar) {
480 key = (key == null) ? newChar : '$key$newChar';
481 }
482
483 bool _maybeUnescapeQuote(String mode, codePoints) {
484 if (index >= codePoints.length) return false;
485 var nextChar = _char(codePoints[index + 1]);
486 if ((mode == 'inSingleQuote' && nextChar == "'") ||
487 (mode == 'inDoubleQuote' && nextChar == '"')) {
488 index++;
489 append(nextChar);
490 return true;
491 }
492 return false;
493 }
494
495 /// Returns the parsed keys, or null if there was a parse error.
496 List<String> parse(String path) {
497 var codePoints = stringToCodepoints(path);
498 var mode = 'beforePath';
499
500 while (mode != null) {
501 index++;
502 var c = index >= codePoints.length ? null : codePoints[index];
503
504 if (c != null &&
505 _char(c) == '\\' && _maybeUnescapeQuote(mode, codePoints)) continue;
506
507 var type = _getPathCharType(c);
508 if (mode == 'error') return null;
509
510 var typeMap = _pathStateMachine[mode];
511 var transition = typeMap[type];
512 if (transition == null) transition = typeMap['else'];
513 if (transition == null) return null; // parse error;
514
515 mode = transition[0];
516 var actionName = transition.length > 1 ? transition[1] : null;
517 if (actionName == 'push' && key != null) push();
518 if (actionName == 'append') {
519 var newChar = transition.length > 2 && transition[2] != null
520 ? transition[2] : _char(c);
521 append(newChar);
522 }
523
524 if (mode == 'afterPath') return keys;
525 }
526 return null; // parse error
527 }
327 } 528 }
328 529
329 final Logger _logger = new Logger('observe.PathObserver'); 530 final Logger _logger = new Logger('observe.PathObserver');
330 531
331 532
332 /// This is a simple cache. It's like LRU but we don't update an item on a 533 /// This is a simple cache. It's like LRU but we don't update an item on a
333 /// cache hit, because that would require allocation. Better to let it expire 534 /// cache hit, because that would require allocation. Better to let it expire
334 /// and reallocate the PropertyPath. 535 /// and reallocate the PropertyPath.
335 // TODO(jmesserly): this optimization is from observe-js, how valuable is it in 536 // TODO(jmesserly): this optimization is from observe-js, how valuable is it in
336 // practice? 537 // practice?
(...skipping 20 matching lines...) Expand all
357 /// ..open((values) { 558 /// ..open((values) {
358 /// for (int i = 0; i < values.length; i++) { 559 /// for (int i = 0; i < values.length; i++) {
359 /// print('The value at index $i is now ${values[i]}'); 560 /// print('The value at index $i is now ${values[i]}');
360 /// } 561 /// }
361 /// }); 562 /// });
362 /// 563 ///
363 /// obj['a'] = 10; // print will be triggered async 564 /// obj['a'] = 10; // print will be triggered async
364 /// 565 ///
365 class CompoundObserver extends _Observer implements Bindable { 566 class CompoundObserver extends _Observer implements Bindable {
366 _ObservedSet _directObserver; 567 _ObservedSet _directObserver;
568 bool _reportChangesOnOpen;
367 List _observed = []; 569 List _observed = [];
368 570
369 bool get _isClosed => _observed == null; 571 CompoundObserver([this._reportChangesOnOpen = false]) {
370
371 CompoundObserver() {
372 _value = []; 572 _value = [];
373 } 573 }
374 574
375 int get _reportArgumentCount => 3; 575 int get _reportArgumentCount => 3;
376 576
377 /// Initiates observation and returns the initial value. 577 /// Initiates observation and returns the initial value.
378 /// The callback will be passed the updated [value], and may optionally be 578 /// The callback will be passed the updated [value], and may optionally be
379 /// declared to take a second argument, which will contain the previous value. 579 /// declared to take a second argument, which will contain the previous value.
380 /// 580 ///
381 /// Implementation note: a third argument can also be declared, which will 581 /// Implementation note: a third argument can also be declared, which will
382 /// receive a list of objects and paths, such that `list[2 * i]` will access 582 /// receive a list of objects and paths, such that `list[2 * i]` will access
383 /// the object and `list[2 * i + 1]` will access the path, where `i` is the 583 /// the object and `list[2 * i + 1]` will access the path, where `i` is the
384 /// order of the [addPath] call. This parameter is only used by 584 /// order of the [addPath] call. This parameter is only used by
385 /// `package:polymer` as a performance optimization, and should not be relied 585 /// `package:polymer` as a performance optimization, and should not be relied
386 /// on in new code. 586 /// on in new code.
387 open(callback) => super.open(callback); 587 open(callback) => super.open(callback);
388 588
389 void _connect() { 589 void _connect() {
390 _check(skipChanges: true);
391
392 for (var i = 0; i < _observed.length; i += 2) { 590 for (var i = 0; i < _observed.length; i += 2) {
393 var object = _observed[i]; 591 var object = _observed[i];
394 if (!identical(object, _observerSentinel)) { 592 if (!identical(object, _observerSentinel)) {
395 _directObserver = new _ObservedSet(this, object); 593 _directObserver = new _ObservedSet(this, object);
396 break; 594 break;
397 } 595 }
398 } 596 }
597
598 _check(skipChanges: !_reportChangesOnOpen);
399 } 599 }
400 600
401 void _disconnect() { 601 void _disconnect() {
602 for (var i = 0; i < _observed.length; i += 2) {
603 if (identical(_observed[i], _observerSentinel)) {
604 _observed[i + 1].close();
jakemac 2014/07/25 16:40:33 at first this looks like it could be out of bounds
Siggi Cherem (dart-lang) 2014/07/25 17:45:12 Yeah - I also dislike using this representation wi
605 }
606 }
607
608 _observed = null;
402 _value = null; 609 _value = null;
403 610
404 if (_directObserver != null) { 611 if (_directObserver != null) {
405 _directObserver.close(this); 612 _directObserver.close(this);
406 _directObserver = null; 613 _directObserver = null;
407 } 614 }
408
409 for (var i = 0; i < _observed.length; i += 2) {
410 if (identical(_observed[i], _observerSentinel)) {
411 _observed[i + 1].close();
412 }
413 }
414 _observed = null;
415 } 615 }
416 616
417 /// Adds a dependency on the property [path] accessed from [object]. 617 /// Adds a dependency on the property [path] accessed from [object].
418 /// [path] can be a [PropertyPath] or a [String]. If it is omitted an empty 618 /// [path] can be a [PropertyPath] or a [String]. If it is omitted an empty
419 /// path will be used. 619 /// path will be used.
420 void addPath(Object object, [path]) { 620 void addPath(Object object, [path]) {
421 if (_isOpen || _isClosed) { 621 if (_isOpen || _isClosed) {
422 throw new StateError('Cannot add paths once started.'); 622 throw new StateError('Cannot add paths once started.');
423 } 623 }
424 624
425 if (path is! PropertyPath) path = new PropertyPath(path); 625 path = new PropertyPath(path);
426 _observed..add(object)..add(path); 626 _observed..add(object)..add(path);
627 if (!_reportChangesOnOpen) return;
628 _value.add(path.getValueFrom(object));
427 } 629 }
428 630
429 void addObserver(Bindable observer) { 631 void addObserver(Bindable observer) {
430 if (_isOpen || _isClosed) { 632 if (_isOpen || _isClosed) {
431 throw new StateError('Cannot add observers once started.'); 633 throw new StateError('Cannot add observers once started.');
432 } 634 }
433 635
434 observer.open((_) => deliver());
435 _observed..add(_observerSentinel)..add(observer); 636 _observed..add(_observerSentinel)..add(observer);
637 if (!_reportChangesOnOpen) return;
638 _value.add(observer.open((_) => deliver()));
436 } 639 }
437 640
438 void _iterateObjects(void observe(obj)) { 641 void _iterateObjects(void observe(obj, prop)) {
439 for (var i = 0; i < _observed.length; i += 2) { 642 for (var i = 0; i < _observed.length; i += 2) {
440 var object = _observed[i]; 643 var object = _observed[i];
441 if (!identical(object, _observerSentinel)) { 644 if (!identical(object, _observerSentinel)) {
442 (_observed[i + 1] as PropertyPath)._iterateObjects(object, observe); 645 (_observed[i + 1] as PropertyPath)._iterateObjects(object, observe);
443 } 646 }
444 } 647 }
445 } 648 }
446 649
447 bool _check({bool skipChanges: false}) { 650 bool _check({bool skipChanges: false}) {
448 bool changed = false; 651 bool changed = false;
449 _value.length = _observed.length ~/ 2; 652 _value.length = _observed.length ~/ 2;
450 var oldValues = null; 653 var oldValues = null;
451 for (var i = 0; i < _observed.length; i += 2) { 654 for (var i = 0; i < _observed.length; i += 2) {
452 var pathOrObserver = _observed[i + 1];
453 var object = _observed[i]; 655 var object = _observed[i];
454 var value = identical(object, _observerSentinel) ? 656 var path = _observed[i + 1];
455 (pathOrObserver as Bindable).value : 657 var value;
456 (pathOrObserver as PropertyPath).getValueFrom(object); 658 if (identical(object, _observerSentinel)) {
659 var observable = path as Bindable;
660 value = _state == _Observer._UNOPENED ?
661 observable.open((_) => this.deliver()) :
662 observable.value;
663 } else {
664 value = (path as PropertyPath).getValueFrom(object);
665 }
457 666
458 if (skipChanges) { 667 if (skipChanges) {
459 _value[i ~/ 2] = value; 668 _value[i ~/ 2] = value;
460 continue; 669 continue;
461 } 670 }
462 671
463 if (value == _value[i ~/ 2]) continue; 672 if (value == _value[i ~/ 2]) continue;
464 673
465 // don't allocate this unless necessary. 674 // don't allocate this unless necessary.
466 if (_notifyArgumentCount >= 2) { 675 if (_notifyArgumentCount >= 2) {
(...skipping 17 matching lines...) Expand all
484 /// An object accepted by [PropertyPath] where properties are read and written 693 /// An object accepted by [PropertyPath] where properties are read and written
485 /// as indexing operations, just like a [Map]. 694 /// as indexing operations, just like a [Map].
486 abstract class Indexable<K, V> { 695 abstract class Indexable<K, V> {
487 V operator [](K key); 696 V operator [](K key);
488 operator []=(K key, V value); 697 operator []=(K key, V value);
489 } 698 }
490 699
491 const _observerSentinel = const _ObserverSentinel(); 700 const _observerSentinel = const _ObserverSentinel();
492 class _ObserverSentinel { const _ObserverSentinel(); } 701 class _ObserverSentinel { const _ObserverSentinel(); }
493 702
703 // Visible for testing
704 get observerSentinelForTesting => _observerSentinel;
705
494 // A base class for the shared API implemented by PathObserver and 706 // A base class for the shared API implemented by PathObserver and
495 // CompoundObserver and used in _ObservedSet. 707 // CompoundObserver and used in _ObservedSet.
496 abstract class _Observer extends Bindable { 708 abstract class _Observer extends Bindable {
497 static int _nextBirthId = 0;
498
499 /// A number indicating when the object was created.
500 final int _birthId = _nextBirthId++;
501
502 Function _notifyCallback; 709 Function _notifyCallback;
503 int _notifyArgumentCount; 710 int _notifyArgumentCount;
504 var _value; 711 var _value;
505 712
506 // abstract members 713 // abstract members
507 void _iterateObjects(void observe(obj)); 714 void _iterateObjects(void observe(obj, prop));
508 void _connect(); 715 void _connect();
509 void _disconnect(); 716 void _disconnect();
510 bool get _isClosed;
511 bool _check({bool skipChanges: false}); 717 bool _check({bool skipChanges: false});
512 718
513 bool get _isOpen => _notifyCallback != null; 719 static int _UNOPENED = 0;
720 static int _OPENED = 1;
721 static int _CLOSED = 2;
722 int _state = _UNOPENED;
723 bool get _isOpen => _state == _OPENED;
724 bool get _isClosed => _state == _CLOSED;
514 725
515 /// The number of arguments the subclass will pass to [_report]. 726 /// The number of arguments the subclass will pass to [_report].
516 int get _reportArgumentCount; 727 int get _reportArgumentCount;
517 728
518 open(callback) { 729 open(callback) {
519 if (_isOpen || _isClosed) { 730 if (_isOpen || _isClosed) {
520 throw new StateError('Observer has already been opened.'); 731 throw new StateError('Observer has already been opened.');
521 } 732 }
522 733
523 if (smoke.minArgs(callback) > _reportArgumentCount) { 734 if (smoke.minArgs(callback) > _reportArgumentCount) {
524 throw new ArgumentError('callback should take $_reportArgumentCount or ' 735 throw new ArgumentError('callback should take $_reportArgumentCount or '
525 'fewer arguments'); 736 'fewer arguments');
526 } 737 }
527 738
528 _notifyCallback = callback; 739 _notifyCallback = callback;
529 _notifyArgumentCount = min(_reportArgumentCount, smoke.maxArgs(callback)); 740 _notifyArgumentCount = min(_reportArgumentCount, smoke.maxArgs(callback));
530 741
531 _connect(); 742 _connect();
743 _state = _OPENED;
532 return _value; 744 return _value;
533 } 745 }
534 746
535 get value => _discardChanges(); 747 get value => _discardChanges();
536 748
537 void close() { 749 void close() {
538 if (!_isOpen) return; 750 if (!_isOpen) return;
539 751
540 _disconnect(); 752 _disconnect();
541 _value = null; 753 _value = null;
542 _notifyCallback = null; 754 _notifyCallback = null;
755 _state = _CLOSED;
543 } 756 }
544 757
545 _discardChanges() { 758 _discardChanges() {
546 _check(skipChanges: true); 759 _check(skipChanges: true);
547 return _value; 760 return _value;
548 } 761 }
549 762
550 void deliver() { 763 void deliver() {
551 if (_isOpen) _dirtyCheck(); 764 if (_isOpen) _dirtyCheck();
552 } 765 }
(...skipping 15 matching lines...) Expand all
568 case 3: _notifyCallback(newValue, oldValue, extraArg); break; 781 case 3: _notifyCallback(newValue, oldValue, extraArg); break;
569 } 782 }
570 } catch (e, s) { 783 } catch (e, s) {
571 // Deliver errors async, so if a single callback fails it doesn't prevent 784 // Deliver errors async, so if a single callback fails it doesn't prevent
572 // other things from working. 785 // other things from working.
573 new Completer().completeError(e, s); 786 new Completer().completeError(e, s);
574 } 787 }
575 } 788 }
576 } 789 }
577 790
791 /// The observedSet abstraction is a perf optimization which reduces the total
792 /// number of Object.observe observations of a set of objects. The idea is that
793 /// groups of Observers will have some object dependencies in common and this
794 /// observed set ensures that each object in the transitive closure of
795 /// dependencies is only observed once. The observedSet acts as a write barrier
796 /// such that whenever any change comes through, all Observers are checked for
797 /// changed values.
798 ///
799 /// Note that this optimization is explicitly moving work from setup-time to
800 /// change-time.
801 ///
802 /// TODO(rafaelw): Implement "garbage collection". In order to move work off
803 /// the critical path, when Observers are closed, their observed objects are
804 /// not Object.unobserve(d). As a result, it's possible that if the observedSet
805 /// is kept open, but some Observers have been closed, it could cause "leaks"
806 /// (prevent otherwise collectable objects from being collected). At some
807 /// point, we should implement incremental "gc" which keeps a list of
808 /// observedSets which may need clean-up and does small amounts of cleanup on a
809 /// timeout until all is clean.
578 class _ObservedSet { 810 class _ObservedSet {
579 /// To prevent sequential [PathObserver]s and [CompoundObserver]s from 811 /// To prevent sequential [PathObserver]s and [CompoundObserver]s from
580 /// observing the same object, we check if they are observing the same root 812 /// observing the same object, we check if they are observing the same root
581 /// as the most recently created observer, and if so merge it into the 813 /// as the most recently created observer, and if so merge it into the
582 /// existing _ObservedSet. 814 /// existing _ObservedSet.
583 /// 815 ///
584 /// See <https://github.com/Polymer/observe-js/commit/f0990b1> and 816 /// See <https://github.com/Polymer/observe-js/commit/f0990b1> and
585 /// <https://codereview.appspot.com/46780044/>. 817 /// <https://codereview.appspot.com/46780044/>.
586 static _ObservedSet _lastSet; 818 static _ObservedSet _lastSet;
587 819
588 /// The root object for a [PathObserver]. For a [CompoundObserver], the root 820 /// The root object for a [PathObserver]. For a [CompoundObserver], the root
589 /// object of the first path observed. This is used by the constructor to 821 /// object of the first path observed. This is used by the constructor to
590 /// reuse an [_ObservedSet] that starts from the same object. 822 /// reuse an [_ObservedSet] that starts from the same object.
591 Object _rootObject; 823 Object _rootObject;
592 824
825 /// Subset of properties in [_rootObject] that we care about.
826 Set _rootObjectProperties;
827
593 /// Observers associated with this root object, in birth order. 828 /// Observers associated with this root object, in birth order.
594 final Map<int, _Observer> _observers = new SplayTreeMap(); 829 final List<_Observer> _observers = [];
595 830
596 // Dart note: the JS implementation is O(N^2) because Array.indexOf is used 831 // Dart note: the JS implementation is O(N^2) because Array.indexOf is used
597 // for lookup in these two arrays. We use HashMap to avoid this problem. It 832 // for lookup in this array. We use HashMap to avoid this problem. It
598 // also gives us a nice way of tracking the StreamSubscription. 833 // also gives us a nice way of tracking the StreamSubscription.
599 Map<Object, StreamSubscription> _objects; 834 Map<Object, StreamSubscription> _objects;
600 Map<Object, StreamSubscription> _toRemove;
601 835
602 bool _resetNeeded = false; 836 factory _ObservedSet(_Observer observer, Object rootObject) {
603 837 if (_lastSet == null || !identical(_lastSet._rootObject, rootObject)) {
604 factory _ObservedSet(_Observer observer, Object rootObj) { 838 _lastSet = new _ObservedSet._(rootObject);
605 if (_lastSet == null || !identical(_lastSet._rootObject, rootObj)) {
606 _lastSet = new _ObservedSet._(rootObj);
607 } 839 }
608 _lastSet.open(observer); 840 _lastSet.open(observer, rootObject);
609 } 841 }
610 842
611 _ObservedSet._(this._rootObject); 843 _ObservedSet._(rootObject)
844 : _rootObject = rootObject,
845 _rootObjectProperties = rootObject == null ? null : new Set();
612 846
613 void open(_Observer obs) { 847 void open(_Observer obs, Object rootObject) {
614 _observers[obs._birthId] = obs; 848 if (_rootObject == null) {
849 _rootObject = rootObject;
850 _rootObjectProperties = new Set();
851 }
852
853 _observers.add(obs);
615 obs._iterateObjects(observe); 854 obs._iterateObjects(observe);
616 } 855 }
617 856
618 void close(_Observer obs) { 857 void close(_Observer obs) {
619 var anyLeft = false; 858 if (_observers.isNotEmpty) return;
620
621 _observers.remove(obs._birthId);
622
623 if (_observers.isNotEmpty) {
624 _resetNeeded = true;
625 scheduleMicrotask(reset);
626 return;
627 }
628 _resetNeeded = false;
629 859
630 if (_objects != null) { 860 if (_objects != null) {
631 for (var sub in _objects) sub.cancel(); 861 for (var sub in _objects) sub.cancel();
632 _objects = null; 862 _objects = null;
633 } 863 }
864 _rootObject = null;
865 _rootObjectProperties = null;
634 } 866 }
635 867
636 void observe(Object obj) { 868 void observe(Object obj, Object prop) {
869 if (identical(obj, _rootObject)) _rootObjectProperties.add(prop);
637 if (obj is ObservableList) _observeStream(obj.listChanges); 870 if (obj is ObservableList) _observeStream(obj.listChanges);
638 if (obj is Observable) _observeStream(obj.changes); 871 if (obj is Observable) _observeStream(obj.changes);
639 } 872 }
640 873
641 void _observeStream(Stream stream) { 874 void _observeStream(Stream stream) {
642 // TODO(jmesserly): we hash on streams as we have two separate change 875 // TODO(jmesserly): we hash on streams as we have two separate change
643 // streams for ObservableList. Not sure if that is the design we will use 876 // streams for ObservableList. Not sure if that is the design we will use
644 // going forward. 877 // going forward.
645 878
646 if (_objects == null) _objects = new HashMap(); 879 if (_objects == null) _objects = new HashMap();
647 StreamSubscription sub = null; 880 if (!_objects.containsKey(stream)) {
648 if (_toRemove != null) sub = _toRemove.remove(stream);
649 if (sub != null) {
650 _objects[stream] = sub;
651 } else if (!_objects.containsKey(stream)) {
652 _objects[stream] = stream.listen(_callback); 881 _objects[stream] = stream.listen(_callback);
653 } 882 }
654 } 883 }
655 884
656 void reset() { 885 /// Whether we can ignore all change events in [records]. This is true if all
657 if (!_resetNeeded) return; 886 /// records are for properties in the [_rootObject] and we are not observing
887 /// any of those properties. Changes on objects other than [_rootObject], or
888 /// changes for properties in [_rootObjectProperties] can't be ignored.
889 bool _allRootObjectNonObservedProperties(List<ChangeRecord> records) {
jakemac 2014/07/25 16:40:34 This function doesn't seem to be named very well,
Siggi Cherem (dart-lang) 2014/07/25 17:45:12 Done. Yeah - this seems local enough that renaming
890 for (var rec in records) {
891 if (rec is PropertyChangeRecord) {
892 if (!identical(rec.object, _rootObject) ||
893 _rootObjectProperties.contains(rec.name)) {
894 return false;
895 }
896 } else if (rec is ListChangeRecord) {
897 if (!identical(rec.object, _rootObject) ||
898 _rootObjectProperties.contains(rec.index)) {
899 return false;
900 }
901 } else {
902 // TODO(sigmund): consider adding object to MapChangeRecord, and make
903 // this more precise.
904 return false;
905 }
906 }
907 return true;
908 }
658 909
659 var objs = _toRemove == null ? new HashMap() : _toRemove; 910 void _callback(records) {
660 _toRemove = _objects; 911 if (_allRootObjectNonObservedProperties(records)) return;
661 _objects = objs; 912 for (var observer in _observers.toList(growable: false)) {
662 for (var observer in _observers.values) {
663 if (observer._isOpen) observer._iterateObjects(observe); 913 if (observer._isOpen) observer._iterateObjects(observe);
664 } 914 }
665 915
666 for (var sub in _toRemove.values) sub.cancel(); 916 for (var observer in _observers.toList(growable: false)) {
667
668 _toRemove = null;
669 }
670
671 void _callback(records) {
672 for (var observer in _observers.values.toList(growable: false)) {
673 if (observer._isOpen) observer._check(); 917 if (observer._isOpen) observer._check();
674 } 918 }
675
676 _resetNeeded = true;
677 scheduleMicrotask(reset);
678 } 919 }
679 } 920 }
680 921
681 const int _MAX_DIRTY_CHECK_CYCLES = 1000; 922 const int _MAX_DIRTY_CHECK_CYCLES = 1000;
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698