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

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, 4 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/list_path_observer.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'; 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 // _segments[0] is passed to indicate that we are only observing that
237 // property of obj. See observe declaration in _ObserveSet.
238 observe(obj, _segments[0]);
215 239
216 if (i >= last) break; 240 if (i >= last) break;
217 obj = _getObjectProperty(obj, _segments[i++]); 241 obj = _getObjectProperty(obj, _segments[i++]);
218 } 242 }
219 } 243 }
244
245 // Dart note: it doesn't make sense to have compiledGetValueFromFn in Dart.
220 } 246 }
221 247
248
249 /// Visible only for testing:
250 getSegmentsOfPropertyPathForTesting(p) => p._segments;
251
222 class _InvalidPropertyPath extends PropertyPath { 252 class _InvalidPropertyPath extends PropertyPath {
223 static final _instance = new _InvalidPropertyPath(); 253 static final _instance = new _InvalidPropertyPath();
224 254
225 bool get isValid => false; 255 bool get isValid => false;
226 _InvalidPropertyPath() : super._([]); 256 _InvalidPropertyPath() : super._([]);
227 } 257 }
228 258
229 bool _changeRecordMatches(record, key) { 259 bool _changeRecordMatches(record, key) {
230 if (record is PropertyChangeRecord) { 260 if (record is PropertyChangeRecord) {
231 return (record as PropertyChangeRecord).name == key; 261 return (record as PropertyChangeRecord).name == key;
(...skipping 11 matching lines...) Expand all
243 /// them as part of path-observer segments. 273 /// them as part of path-observer segments.
244 const _MAP_PROPERTIES = const [#keys, #values, #length, #isEmpty, #isNotEmpty]; 274 const _MAP_PROPERTIES = const [#keys, #values, #length, #isEmpty, #isNotEmpty];
245 275
246 _getObjectProperty(object, property) { 276 _getObjectProperty(object, property) {
247 if (object == null) return null; 277 if (object == null) return null;
248 278
249 if (property is int) { 279 if (property is int) {
250 if (object is List && property >= 0 && property < object.length) { 280 if (object is List && property >= 0 && property < object.length) {
251 return object[property]; 281 return object[property];
252 } 282 }
283 } else if (property is String) {
284 return object[property];
253 } else if (property is Symbol) { 285 } else if (property is Symbol) {
254 // Support indexer if available, e.g. Maps or polymer_expressions Scope. 286 // Support indexer if available, e.g. Maps or polymer_expressions Scope.
255 // This is the default syntax used by polymer/nodebind and 287 // This is the default syntax used by polymer/nodebind and
256 // polymer/observe-js PathObserver. 288 // polymer/observe-js PathObserver.
257 // TODO(sigmund): should we also support using checking dynamically for 289 // TODO(sigmund): should we also support using checking dynamically for
258 // whether the type practically implements the indexer API 290 // whether the type practically implements the indexer API
259 // (smoke.hasInstanceMethod(type, const Symbol('[]')))? 291 // (smoke.hasInstanceMethod(type, const Symbol('[]')))?
260 if (object is Indexable<String, dynamic> || 292 if (object is Indexable<String, dynamic> ||
261 object is Map<String, dynamic> && !_MAP_PROPERTIES.contains(property)) { 293 object is Map<String, dynamic> && !_MAP_PROPERTIES.contains(property)) {
262 return object[smoke.symbolToName(property)]; 294 return object[smoke.symbolToName(property)];
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
302 } 334 }
303 335
304 if (_logger.isLoggable(Level.FINER)) { 336 if (_logger.isLoggable(Level.FINER)) {
305 _logger.finer("can't set $property in $object"); 337 _logger.finer("can't set $property in $object");
306 } 338 }
307 return false; 339 return false;
308 } 340 }
309 341
310 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js 342 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
311 343
312 final _pathRegExp = () { 344 final _identRegExp = () {
313 const identStart = '[\$_a-zA-Z]'; 345 const identStart = '[\$_a-zA-Z]';
314 const identPart = '[\$_a-zA-Z0-9]'; 346 const identPart = '[\$_a-zA-Z0-9]';
315 const ident = '$identStart+$identPart*'; 347 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 }(); 348 }();
321 349
322 bool _isPathValid(String s) { 350 _isIdent(s) => _identRegExp.hasMatch(s);
323 s = s.trim(); 351
324 if (s == '') return true; 352 // Dart note: refactored to convert to codepoints once and operate on codepoints
325 if (s[0] == '.') return false; 353 // rather than characters.
326 return _pathRegExp.hasMatch(s); 354 class _PathParser {
355 List keys = [];
356 int index = -1;
357 String key;
358
359 final Map<String, List<String>> _pathStateMachine = {
360 'beforePath': {
361 'ws': ['beforePath'],
362 'ident': ['inIdent', 'append'],
363 '[': ['beforeElement'],
364 'eof': ['afterPath']
365 },
366
367 'inPath': {
368 'ws': ['inPath'],
369 '.': ['beforeIdent'],
370 '[': ['beforeElement'],
371 'eof': ['afterPath']
372 },
373
374 'beforeIdent': {
375 'ws': ['beforeIdent'],
376 'ident': ['inIdent', 'append']
377 },
378
379 'inIdent': {
380 'ident': ['inIdent', 'append'],
381 '0': ['inIdent', 'append'],
382 'number': ['inIdent', 'append'],
383 'ws': ['inPath', 'push'],
384 '.': ['beforeIdent', 'push'],
385 '[': ['beforeElement', 'push'],
386 'eof': ['afterPath', 'push']
387 },
388
389 'beforeElement': {
390 'ws': ['beforeElement'],
391 '0': ['afterZero', 'append'],
392 'number': ['inIndex', 'append'],
393 "'": ['inSingleQuote', 'append', ''],
394 '"': ['inDoubleQuote', 'append', '']
395 },
396
397 'afterZero': {
398 'ws': ['afterElement', 'push'],
399 ']': ['inPath', 'push']
400 },
401
402 'inIndex': {
403 '0': ['inIndex', 'append'],
404 'number': ['inIndex', 'append'],
405 'ws': ['afterElement'],
406 ']': ['inPath', 'push']
407 },
408
409 'inSingleQuote': {
410 "'": ['afterElement'],
411 'eof': ['error'],
412 'else': ['inSingleQuote', 'append']
413 },
414
415 'inDoubleQuote': {
416 '"': ['afterElement'],
417 'eof': ['error'],
418 'else': ['inDoubleQuote', 'append']
419 },
420
421 'afterElement': {
422 'ws': ['afterElement'],
423 ']': ['inPath', 'push']
424 }
425 };
426
427 /// From getPathCharType: determines the type of a given [code]point.
428 String _getPathCharType(code) {
429 if (code == null) return 'eof';
430 switch(code) {
431 case 0x5B: // [
432 case 0x5D: // ]
433 case 0x2E: // .
434 case 0x22: // "
435 case 0x27: // '
436 case 0x30: // 0
437 return _char(code);
438
439 case 0x5F: // _
440 case 0x24: // $
441 return 'ident';
442
443 case 0x20: // Space
444 case 0x09: // Tab
445 case 0x0A: // Newline
446 case 0x0D: // Return
447 case 0xA0: // No-break space
448 case 0xFEFF: // Byte Order Mark
449 case 0x2028: // Line Separator
450 case 0x2029: // Paragraph Separator
451 return 'ws';
452 }
453
454 // a-z, A-Z
455 if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A))
456 return 'ident';
457
458 // 1-9
459 if (0x31 <= code && code <= 0x39)
460 return 'number';
461
462 return 'else';
463 }
464
465 static String _char(int codepoint) => new String.fromCharCodes([codepoint]);
466
467 void push() {
468 if (key == null) return;
469
470 // Dart note: we store the keys with different types, rather than
471 // parsing/converting things later in toString.
472 if (_isIdent(key)) {
473 keys.add(smoke.nameToSymbol(key));
474 } else {
475 var index = int.parse(key, radix: 10, onError: (_) => null);
476 keys.add(index != null ? index : key);
477 }
478 key = null;
479 }
480
481 void append(newChar) {
482 key = (key == null) ? newChar : '$key$newChar';
483 }
484
485 bool _maybeUnescapeQuote(String mode, codePoints) {
486 if (index >= codePoints.length) return false;
487 var nextChar = _char(codePoints[index + 1]);
488 if ((mode == 'inSingleQuote' && nextChar == "'") ||
489 (mode == 'inDoubleQuote' && nextChar == '"')) {
490 index++;
491 append(nextChar);
492 return true;
493 }
494 return false;
495 }
496
497 /// Returns the parsed keys, or null if there was a parse error.
498 List<String> parse(String path) {
499 var codePoints = stringToCodepoints(path);
500 var mode = 'beforePath';
501
502 while (mode != null) {
503 index++;
504 var c = index >= codePoints.length ? null : codePoints[index];
505
506 if (c != null &&
507 _char(c) == '\\' && _maybeUnescapeQuote(mode, codePoints)) continue;
508
509 var type = _getPathCharType(c);
510 if (mode == 'error') return null;
511
512 var typeMap = _pathStateMachine[mode];
513 var transition = typeMap[type];
514 if (transition == null) transition = typeMap['else'];
515 if (transition == null) return null; // parse error;
516
517 mode = transition[0];
518 var actionName = transition.length > 1 ? transition[1] : null;
519 if (actionName == 'push' && key != null) push();
520 if (actionName == 'append') {
521 var newChar = transition.length > 2 && transition[2] != null
522 ? transition[2] : _char(c);
523 append(newChar);
524 }
525
526 if (mode == 'afterPath') return keys;
527 }
528 return null; // parse error
529 }
327 } 530 }
328 531
329 final Logger _logger = new Logger('observe.PathObserver'); 532 final Logger _logger = new Logger('observe.PathObserver');
330 533
331 534
332 /// This is a simple cache. It's like LRU but we don't update an item on a 535 /// 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 536 /// cache hit, because that would require allocation. Better to let it expire
334 /// and reallocate the PropertyPath. 537 /// and reallocate the PropertyPath.
335 // TODO(jmesserly): this optimization is from observe-js, how valuable is it in 538 // TODO(jmesserly): this optimization is from observe-js, how valuable is it in
336 // practice? 539 // practice?
(...skipping 20 matching lines...) Expand all
357 /// ..open((values) { 560 /// ..open((values) {
358 /// for (int i = 0; i < values.length; i++) { 561 /// for (int i = 0; i < values.length; i++) {
359 /// print('The value at index $i is now ${values[i]}'); 562 /// print('The value at index $i is now ${values[i]}');
360 /// } 563 /// }
361 /// }); 564 /// });
362 /// 565 ///
363 /// obj['a'] = 10; // print will be triggered async 566 /// obj['a'] = 10; // print will be triggered async
364 /// 567 ///
365 class CompoundObserver extends _Observer implements Bindable { 568 class CompoundObserver extends _Observer implements Bindable {
366 _ObservedSet _directObserver; 569 _ObservedSet _directObserver;
570 bool _reportChangesOnOpen;
367 List _observed = []; 571 List _observed = [];
368 572
369 bool get _isClosed => _observed == null; 573 CompoundObserver([this._reportChangesOnOpen = false]) {
370
371 CompoundObserver() {
372 _value = []; 574 _value = [];
373 } 575 }
374 576
375 int get _reportArgumentCount => 3; 577 int get _reportArgumentCount => 3;
376 578
377 /// Initiates observation and returns the initial value. 579 /// Initiates observation and returns the initial value.
378 /// The callback will be passed the updated [value], and may optionally be 580 /// 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. 581 /// declared to take a second argument, which will contain the previous value.
380 /// 582 ///
381 /// Implementation note: a third argument can also be declared, which will 583 /// 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 584 /// 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 585 /// 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 586 /// order of the [addPath] call. This parameter is only used by
385 /// `package:polymer` as a performance optimization, and should not be relied 587 /// `package:polymer` as a performance optimization, and should not be relied
386 /// on in new code. 588 /// on in new code.
387 open(callback) => super.open(callback); 589 open(callback) => super.open(callback);
388 590
389 void _connect() { 591 void _connect() {
390 _check(skipChanges: true);
391
392 for (var i = 0; i < _observed.length; i += 2) { 592 for (var i = 0; i < _observed.length; i += 2) {
393 var object = _observed[i]; 593 var object = _observed[i];
394 if (!identical(object, _observerSentinel)) { 594 if (!identical(object, _observerSentinel)) {
395 _directObserver = new _ObservedSet(this, object); 595 _directObserver = new _ObservedSet(this, object);
396 break; 596 break;
397 } 597 }
398 } 598 }
599
600 _check(skipChanges: !_reportChangesOnOpen);
399 } 601 }
400 602
401 void _disconnect() { 603 void _disconnect() {
604 for (var i = 0; i < _observed.length; i += 2) {
605 if (identical(_observed[i], _observerSentinel)) {
606 _observed[i + 1].close();
607 }
608 }
609
610 _observed = null;
402 _value = null; 611 _value = null;
403 612
404 if (_directObserver != null) { 613 if (_directObserver != null) {
405 _directObserver.close(this); 614 _directObserver.close(this);
406 _directObserver = null; 615 _directObserver = null;
407 } 616 }
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 } 617 }
416 618
417 /// Adds a dependency on the property [path] accessed from [object]. 619 /// 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 620 /// [path] can be a [PropertyPath] or a [String]. If it is omitted an empty
419 /// path will be used. 621 /// path will be used.
420 void addPath(Object object, [path]) { 622 void addPath(Object object, [path]) {
421 if (_isOpen || _isClosed) { 623 if (_isOpen || _isClosed) {
422 throw new StateError('Cannot add paths once started.'); 624 throw new StateError('Cannot add paths once started.');
423 } 625 }
424 626
425 if (path is! PropertyPath) path = new PropertyPath(path); 627 path = new PropertyPath(path);
426 _observed..add(object)..add(path); 628 _observed..add(object)..add(path);
629 if (!_reportChangesOnOpen) return;
630 _value.add(path.getValueFrom(object));
427 } 631 }
428 632
429 void addObserver(Bindable observer) { 633 void addObserver(Bindable observer) {
430 if (_isOpen || _isClosed) { 634 if (_isOpen || _isClosed) {
431 throw new StateError('Cannot add observers once started.'); 635 throw new StateError('Cannot add observers once started.');
432 } 636 }
433 637
434 observer.open((_) => deliver());
435 _observed..add(_observerSentinel)..add(observer); 638 _observed..add(_observerSentinel)..add(observer);
639 if (!_reportChangesOnOpen) return;
640 _value.add(observer.open((_) => deliver()));
436 } 641 }
437 642
438 void _iterateObjects(void observe(obj)) { 643 void _iterateObjects(void observe(obj, prop)) {
439 for (var i = 0; i < _observed.length; i += 2) { 644 for (var i = 0; i < _observed.length; i += 2) {
440 var object = _observed[i]; 645 var object = _observed[i];
441 if (!identical(object, _observerSentinel)) { 646 if (!identical(object, _observerSentinel)) {
442 (_observed[i + 1] as PropertyPath)._iterateObjects(object, observe); 647 (_observed[i + 1] as PropertyPath)._iterateObjects(object, observe);
443 } 648 }
444 } 649 }
445 } 650 }
446 651
447 bool _check({bool skipChanges: false}) { 652 bool _check({bool skipChanges: false}) {
448 bool changed = false; 653 bool changed = false;
449 _value.length = _observed.length ~/ 2; 654 _value.length = _observed.length ~/ 2;
450 var oldValues = null; 655 var oldValues = null;
451 for (var i = 0; i < _observed.length; i += 2) { 656 for (var i = 0; i < _observed.length; i += 2) {
452 var pathOrObserver = _observed[i + 1];
453 var object = _observed[i]; 657 var object = _observed[i];
454 var value = identical(object, _observerSentinel) ? 658 var path = _observed[i + 1];
455 (pathOrObserver as Bindable).value : 659 var value;
456 (pathOrObserver as PropertyPath).getValueFrom(object); 660 if (identical(object, _observerSentinel)) {
661 var observable = path as Bindable;
662 value = _state == _Observer._UNOPENED ?
663 observable.open((_) => this.deliver()) :
664 observable.value;
665 } else {
666 value = (path as PropertyPath).getValueFrom(object);
667 }
457 668
458 if (skipChanges) { 669 if (skipChanges) {
459 _value[i ~/ 2] = value; 670 _value[i ~/ 2] = value;
460 continue; 671 continue;
461 } 672 }
462 673
463 if (value == _value[i ~/ 2]) continue; 674 if (value == _value[i ~/ 2]) continue;
464 675
465 // don't allocate this unless necessary. 676 // don't allocate this unless necessary.
466 if (_notifyArgumentCount >= 2) { 677 if (_notifyArgumentCount >= 2) {
(...skipping 17 matching lines...) Expand all
484 /// An object accepted by [PropertyPath] where properties are read and written 695 /// An object accepted by [PropertyPath] where properties are read and written
485 /// as indexing operations, just like a [Map]. 696 /// as indexing operations, just like a [Map].
486 abstract class Indexable<K, V> { 697 abstract class Indexable<K, V> {
487 V operator [](K key); 698 V operator [](K key);
488 operator []=(K key, V value); 699 operator []=(K key, V value);
489 } 700 }
490 701
491 const _observerSentinel = const _ObserverSentinel(); 702 const _observerSentinel = const _ObserverSentinel();
492 class _ObserverSentinel { const _ObserverSentinel(); } 703 class _ObserverSentinel { const _ObserverSentinel(); }
493 704
705 // Visible for testing
706 get observerSentinelForTesting => _observerSentinel;
707
494 // A base class for the shared API implemented by PathObserver and 708 // A base class for the shared API implemented by PathObserver and
495 // CompoundObserver and used in _ObservedSet. 709 // CompoundObserver and used in _ObservedSet.
496 abstract class _Observer extends Bindable { 710 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; 711 Function _notifyCallback;
503 int _notifyArgumentCount; 712 int _notifyArgumentCount;
504 var _value; 713 var _value;
505 714
506 // abstract members 715 // abstract members
507 void _iterateObjects(void observe(obj)); 716 void _iterateObjects(void observe(obj, prop));
508 void _connect(); 717 void _connect();
509 void _disconnect(); 718 void _disconnect();
510 bool get _isClosed;
511 bool _check({bool skipChanges: false}); 719 bool _check({bool skipChanges: false});
512 720
513 bool get _isOpen => _notifyCallback != null; 721 static int _UNOPENED = 0;
722 static int _OPENED = 1;
723 static int _CLOSED = 2;
724 int _state = _UNOPENED;
725 bool get _isOpen => _state == _OPENED;
726 bool get _isClosed => _state == _CLOSED;
514 727
515 /// The number of arguments the subclass will pass to [_report]. 728 /// The number of arguments the subclass will pass to [_report].
516 int get _reportArgumentCount; 729 int get _reportArgumentCount;
517 730
518 open(callback) { 731 open(callback) {
519 if (_isOpen || _isClosed) { 732 if (_isOpen || _isClosed) {
520 throw new StateError('Observer has already been opened.'); 733 throw new StateError('Observer has already been opened.');
521 } 734 }
522 735
523 if (smoke.minArgs(callback) > _reportArgumentCount) { 736 if (smoke.minArgs(callback) > _reportArgumentCount) {
524 throw new ArgumentError('callback should take $_reportArgumentCount or ' 737 throw new ArgumentError('callback should take $_reportArgumentCount or '
525 'fewer arguments'); 738 'fewer arguments');
526 } 739 }
527 740
528 _notifyCallback = callback; 741 _notifyCallback = callback;
529 _notifyArgumentCount = min(_reportArgumentCount, smoke.maxArgs(callback)); 742 _notifyArgumentCount = min(_reportArgumentCount, smoke.maxArgs(callback));
530 743
531 _connect(); 744 _connect();
745 _state = _OPENED;
532 return _value; 746 return _value;
533 } 747 }
534 748
535 get value => _discardChanges(); 749 get value => _discardChanges();
536 750
537 void close() { 751 void close() {
538 if (!_isOpen) return; 752 if (!_isOpen) return;
539 753
540 _disconnect(); 754 _disconnect();
541 _value = null; 755 _value = null;
542 _notifyCallback = null; 756 _notifyCallback = null;
757 _state = _CLOSED;
543 } 758 }
544 759
545 _discardChanges() { 760 _discardChanges() {
546 _check(skipChanges: true); 761 _check(skipChanges: true);
547 return _value; 762 return _value;
548 } 763 }
549 764
550 void deliver() { 765 void deliver() {
551 if (_isOpen) _dirtyCheck(); 766 if (_isOpen) _dirtyCheck();
552 } 767 }
(...skipping 15 matching lines...) Expand all
568 case 3: _notifyCallback(newValue, oldValue, extraArg); break; 783 case 3: _notifyCallback(newValue, oldValue, extraArg); break;
569 } 784 }
570 } catch (e, s) { 785 } catch (e, s) {
571 // Deliver errors async, so if a single callback fails it doesn't prevent 786 // Deliver errors async, so if a single callback fails it doesn't prevent
572 // other things from working. 787 // other things from working.
573 new Completer().completeError(e, s); 788 new Completer().completeError(e, s);
574 } 789 }
575 } 790 }
576 } 791 }
577 792
793 /// The observedSet abstraction is a perf optimization which reduces the total
794 /// number of Object.observe observations of a set of objects. The idea is that
795 /// groups of Observers will have some object dependencies in common and this
796 /// observed set ensures that each object in the transitive closure of
797 /// dependencies is only observed once. The observedSet acts as a write barrier
798 /// such that whenever any change comes through, all Observers are checked for
799 /// changed values.
800 ///
801 /// Note that this optimization is explicitly moving work from setup-time to
802 /// change-time.
803 ///
804 /// TODO(rafaelw): Implement "garbage collection". In order to move work off
805 /// the critical path, when Observers are closed, their observed objects are
806 /// not Object.unobserve(d). As a result, it's possible that if the observedSet
807 /// is kept open, but some Observers have been closed, it could cause "leaks"
808 /// (prevent otherwise collectable objects from being collected). At some
809 /// point, we should implement incremental "gc" which keeps a list of
810 /// observedSets which may need clean-up and does small amounts of cleanup on a
811 /// timeout until all is clean.
578 class _ObservedSet { 812 class _ObservedSet {
579 /// To prevent sequential [PathObserver]s and [CompoundObserver]s from 813 /// To prevent sequential [PathObserver]s and [CompoundObserver]s from
580 /// observing the same object, we check if they are observing the same root 814 /// 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 815 /// as the most recently created observer, and if so merge it into the
582 /// existing _ObservedSet. 816 /// existing _ObservedSet.
583 /// 817 ///
584 /// See <https://github.com/Polymer/observe-js/commit/f0990b1> and 818 /// See <https://github.com/Polymer/observe-js/commit/f0990b1> and
585 /// <https://codereview.appspot.com/46780044/>. 819 /// <https://codereview.appspot.com/46780044/>.
586 static _ObservedSet _lastSet; 820 static _ObservedSet _lastSet;
587 821
588 /// The root object for a [PathObserver]. For a [CompoundObserver], the root 822 /// 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 823 /// object of the first path observed. This is used by the constructor to
590 /// reuse an [_ObservedSet] that starts from the same object. 824 /// reuse an [_ObservedSet] that starts from the same object.
591 Object _rootObject; 825 Object _rootObject;
592 826
827 /// Subset of properties in [_rootObject] that we care about.
828 Set _rootObjectProperties;
829
593 /// Observers associated with this root object, in birth order. 830 /// Observers associated with this root object, in birth order.
594 final Map<int, _Observer> _observers = new SplayTreeMap(); 831 final List<_Observer> _observers = [];
595 832
596 // Dart note: the JS implementation is O(N^2) because Array.indexOf is used 833 // 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 834 // for lookup in this array. We use HashMap to avoid this problem. It
598 // also gives us a nice way of tracking the StreamSubscription. 835 // also gives us a nice way of tracking the StreamSubscription.
599 Map<Object, StreamSubscription> _objects; 836 Map<Object, StreamSubscription> _objects;
600 Map<Object, StreamSubscription> _toRemove;
601 837
602 bool _resetNeeded = false; 838 factory _ObservedSet(_Observer observer, Object rootObject) {
603 839 if (_lastSet == null || !identical(_lastSet._rootObject, rootObject)) {
604 factory _ObservedSet(_Observer observer, Object rootObj) { 840 _lastSet = new _ObservedSet._(rootObject);
605 if (_lastSet == null || !identical(_lastSet._rootObject, rootObj)) {
606 _lastSet = new _ObservedSet._(rootObj);
607 } 841 }
608 _lastSet.open(observer); 842 _lastSet.open(observer, rootObject);
609 } 843 }
610 844
611 _ObservedSet._(this._rootObject); 845 _ObservedSet._(rootObject)
846 : _rootObject = rootObject,
847 _rootObjectProperties = rootObject == null ? null : new Set();
612 848
613 void open(_Observer obs) { 849 void open(_Observer obs, Object rootObject) {
614 _observers[obs._birthId] = obs; 850 if (_rootObject == null) {
851 _rootObject = rootObject;
852 _rootObjectProperties = new Set();
853 }
854
855 _observers.add(obs);
615 obs._iterateObjects(observe); 856 obs._iterateObjects(observe);
616 } 857 }
617 858
618 void close(_Observer obs) { 859 void close(_Observer obs) {
619 var anyLeft = false; 860 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 861
630 if (_objects != null) { 862 if (_objects != null) {
631 for (var sub in _objects) sub.cancel(); 863 for (var sub in _objects) sub.cancel();
632 _objects = null; 864 _objects = null;
633 } 865 }
866 _rootObject = null;
867 _rootObjectProperties = null;
634 } 868 }
635 869
636 void observe(Object obj) { 870 /// Observe now takes a second argument to indicate which property of an
871 /// object is being observed, so we don't trigger change notifications on
872 /// changes to unrelated properties.
873 void observe(Object obj, Object prop) {
874 if (identical(obj, _rootObject)) _rootObjectProperties.add(prop);
637 if (obj is ObservableList) _observeStream(obj.listChanges); 875 if (obj is ObservableList) _observeStream(obj.listChanges);
638 if (obj is Observable) _observeStream(obj.changes); 876 if (obj is Observable) _observeStream(obj.changes);
639 } 877 }
640 878
641 void _observeStream(Stream stream) { 879 void _observeStream(Stream stream) {
642 // TODO(jmesserly): we hash on streams as we have two separate change 880 // 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 881 // streams for ObservableList. Not sure if that is the design we will use
644 // going forward. 882 // going forward.
645 883
646 if (_objects == null) _objects = new HashMap(); 884 if (_objects == null) _objects = new HashMap();
647 StreamSubscription sub = null; 885 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); 886 _objects[stream] = stream.listen(_callback);
653 } 887 }
654 } 888 }
655 889
656 void reset() { 890 /// Whether we can ignore all change events in [records]. This is true if all
657 if (!_resetNeeded) return; 891 /// records are for properties in the [_rootObject] and we are not observing
892 /// any of those properties. Changes on objects other than [_rootObject], or
893 /// changes for properties in [_rootObjectProperties] can't be ignored.
894 // Dart note: renamed from `allRootObjNonObservedProps` in the JS code.
895 bool _canIgnoreRecords(List<ChangeRecord> records) {
896 for (var rec in records) {
897 if (rec is PropertyChangeRecord) {
898 if (!identical(rec.object, _rootObject) ||
899 _rootObjectProperties.contains(rec.name)) {
900 return false;
901 }
902 } else if (rec is ListChangeRecord) {
903 if (!identical(rec.object, _rootObject) ||
904 _rootObjectProperties.contains(rec.index)) {
905 return false;
906 }
907 } else {
908 // TODO(sigmund): consider adding object to MapChangeRecord, and make
909 // this more precise.
910 return false;
911 }
912 }
913 return true;
914 }
658 915
659 var objs = _toRemove == null ? new HashMap() : _toRemove; 916 void _callback(records) {
660 _toRemove = _objects; 917 if (_canIgnoreRecords(records)) return;
661 _objects = objs; 918 for (var observer in _observers.toList(growable: false)) {
662 for (var observer in _observers.values) {
663 if (observer._isOpen) observer._iterateObjects(observe); 919 if (observer._isOpen) observer._iterateObjects(observe);
664 } 920 }
665 921
666 for (var sub in _toRemove.values) sub.cancel(); 922 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(); 923 if (observer._isOpen) observer._check();
674 } 924 }
675
676 _resetNeeded = true;
677 scheduleMicrotask(reset);
678 } 925 }
679 } 926 }
680 927
681 const int _MAX_DIRTY_CHECK_CYCLES = 1000; 928 const int _MAX_DIRTY_CHECK_CYCLES = 1000;
OLDNEW
« no previous file with comments | « pkg/observe/lib/src/list_path_observer.dart ('k') | pkg/observe/pubspec.yaml » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698