| Index: pkg/observe/lib/src/path_observer.dart
|
| diff --git a/pkg/observe/lib/src/path_observer.dart b/pkg/observe/lib/src/path_observer.dart
|
| index 5e59b80d1f592f1321a56d12e54686ca5478280b..8054a3ab19d38cac680f14853aa7c197a445337a 100644
|
| --- a/pkg/observe/lib/src/path_observer.dart
|
| +++ b/pkg/observe/lib/src/path_observer.dart
|
| @@ -5,6 +5,8 @@
|
| library observe.src.path_observer;
|
|
|
| import 'dart:async';
|
| +import 'dart:collection';
|
| +import 'dart:math' show min;
|
| @MirrorsUsed(metaTargets: const [Reflectable, ObservableProperty],
|
| override: 'observe.src.path_observer')
|
| import 'dart:mirrors';
|
| @@ -12,209 +14,216 @@ import 'package:logging/logging.dart' show Logger, Level;
|
| import 'package:observe/observe.dart';
|
| import 'package:observe/src/observable.dart' show objectType;
|
|
|
| -// This code is inspired by ChangeSummary:
|
| -// https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
|
| -// ...which underlies MDV. Since we don't need the functionality of
|
| -// ChangeSummary, we just implement what we need for data bindings.
|
| -// This allows our implementation to be much simpler.
|
| -
|
| -/**
|
| - * A data-bound path starting from a view-model or model object, for example
|
| - * `foo.bar.baz`.
|
| - *
|
| - * When the [values] stream is being listened to, this will observe changes to
|
| - * the object and any intermediate object along the path, and send [values]
|
| - * accordingly. When all listeners are unregistered it will stop observing
|
| - * the objects.
|
| - *
|
| - * This class is used to implement [Node.bind] and similar functionality.
|
| - */
|
| +/// A data-bound path starting from a view-model or model object, for example
|
| +/// `foo.bar.baz`.
|
| +///
|
| +/// When [open] is called, this will observe changes to the object and any
|
| +/// intermediate object along the path, and send updated values accordingly.
|
| +/// When [close] is called it will stop observing the objects.
|
| +///
|
| +/// This class is used to implement `Node.bind` and similar functionality in
|
| +/// the [template_binding](pub.dartlang.org/packages/template_binding) package.
|
| +class PathObserver extends _Observer implements Bindable {
|
| + PropertyPath _path;
|
| + Object _object;
|
| + _ObservedSet _directObserver;
|
| +
|
| + /// Observes [path] on [object] for changes. This returns an object
|
| + /// that can be used to get the changes and get/set the value at this path.
|
| + ///
|
| + /// The path can be a [PropertyPath], or a [String] used to construct it.
|
| + ///
|
| + /// See [open] and [value].
|
| + PathObserver(Object object, [path])
|
| + : _object = object,
|
| + _path = path is PropertyPath ? path : new PropertyPath(path);
|
| +
|
| + bool get _isClosed => _path == null;
|
| +
|
| + /// Sets the value at this path.
|
| + @reflectable void set value(Object newValue) {
|
| + if (_path != null) _path.setValueFrom(_object, newValue);
|
| + }
|
| +
|
| + int get _reportArgumentCount => 2;
|
| +
|
| + /// Initiates observation and returns the initial value.
|
| + /// The callback will be passed the updated [value], and may optionally be
|
| + /// declared to take a second argument, which will contain the previous value.
|
| + open(callback) => super.open(callback);
|
| +
|
| + void _connect() {
|
| + _directObserver = new _ObservedSet(this, _object);
|
| + _check(skipChanges: true);
|
| + }
|
| +
|
| + void _disconnect() {
|
| + _value = null;
|
| + if (_directObserver != null) {
|
| + _directObserver.close(this);
|
| + _directObserver = null;
|
| + }
|
| + // Dart note: the JS impl does not do this, but it seems consistent with
|
| + // CompoundObserver. After closing the PathObserver can't be reopened.
|
| + _path = null;
|
| + _object = null;
|
| + }
|
| +
|
| + void _iterateObjects(void observe(obj)) {
|
| + _path._iterateObjects(_object, observe);
|
| + }
|
| +
|
| + bool _check({bool skipChanges: false}) {
|
| + var oldValue = _value;
|
| + _value = _path.getValueFrom(_object);
|
| + if (skipChanges || _value == oldValue) return false;
|
| +
|
| + _report(_value, oldValue);
|
| + return true;
|
| + }
|
| +}
|
| +
|
| +/// A dot-delimieted property path such as "foo.bar" or "foo.10.bar".
|
| +/// The path specifies how to get a particular value from an object graph, where
|
| +/// the graph can include arrays.
|
| // TODO(jmesserly): consider specialized subclasses for:
|
| // * empty path
|
| // * "value"
|
| // * single token in path, e.g. "foo"
|
| -class PathObserver extends ChangeNotifier {
|
| - /** The path string. */
|
| - final String path;
|
| -
|
| - /** True if the path is valid, otherwise false. */
|
| - final bool _isValid;
|
| -
|
| +class PropertyPath {
|
| + /// The segments of the path.
|
| final List<Object> _segments;
|
| - List<Object> _values;
|
| - List<StreamSubscription> _subs;
|
| -
|
| - final Function _computeValue;
|
| -
|
| - /**
|
| - * Observes [path] on [object] for changes. This returns an object that can be
|
| - * used to get the changes and get/set the value at this path.
|
| - *
|
| - * You can optionally use [computeValue] to apply a function to the result of
|
| - * evaluating the path. The function should be pure, as PathObserver will not
|
| - * know to observe any of its dependencies. If you need to observe mutliple
|
| - * values, use [CompoundPathObserver] instead.
|
| - *
|
| - * See [PathObserver.bindSync] and [PathObserver.value].
|
| - */
|
| - PathObserver(Object object, String path, {computeValue(newValue)})
|
| - : path = path,
|
| - _computeValue = computeValue,
|
| - _isValid = _isPathValid(path),
|
| - _segments = <Object>[] {
|
| -
|
| - if (_isValid) {
|
| - for (var segment in path.trim().split('.')) {
|
| - if (segment == '') continue;
|
| - var index = int.parse(segment, radix: 10, onError: (_) => null);
|
| - _segments.add(index != null ? index : new Symbol(segment));
|
| +
|
| + /// Creates a new [PropertyPath]. These can be stored to avoid excessive
|
| + /// parsing of path strings.
|
| + ///
|
| + /// The provided [path] should be a String or a List. If it is a list it
|
| + /// should contain only Symbols and integers. This can be used to avoid
|
| + /// parsing.
|
| + ///
|
| + /// Note that this constructor will canonicalize identical paths in some cases
|
| + /// to save memory, but this is not guaranteed. Use [==] for comparions
|
| + /// purposes instead of [identical].
|
| + factory PropertyPath([path]) {
|
| + if (path is List) {
|
| + var copy = new List.from(path, growable: false);
|
| + for (var segment in copy) {
|
| + if (segment is! int && segment is! Symbol) {
|
| + throw new ArgumentError('List must contain only ints and Symbols');
|
| + }
|
| }
|
| + return new PropertyPath._(copy);
|
| }
|
|
|
| - // Initialize arrays.
|
| - // Note that the path itself can't change after it is initially
|
| - // constructed, even though the objects along the path can change.
|
| - _values = new List<Object>(_segments.length + 1);
|
| + if (path == null) path = '';
|
|
|
| - // If we have an empty path, we need to apply the transformation function
|
| - // to the value. The "value" property should always show the transformed
|
| - // value.
|
| - if (_segments.isEmpty && computeValue != null) {
|
| - object = computeValue(object);
|
| - }
|
| + var pathObj = _pathCache[path];
|
| + if (pathObj != null) return pathObj;
|
|
|
| - _values[0] = object;
|
| - _subs = new List<StreamSubscription>(_segments.length);
|
| - }
|
| + if (!_isPathValid(path)) return _InvalidPropertyPath._instance;
|
|
|
| - /** The object being observed. If the path is empty this will be [value]. */
|
| - get object => _values[0];
|
| + final segments = [];
|
| + for (var segment in path.trim().split('.')) {
|
| + if (segment == '') continue;
|
| + var index = int.parse(segment, radix: 10, onError: (_) => null);
|
| + segments.add(index != null ? index : new Symbol(segment));
|
| + }
|
|
|
| - /** Gets the last reported value at this path. */
|
| - @reflectable get value {
|
| - if (!_isValid) return null;
|
| - if (!hasObservers) _updateValues();
|
| - return _values.last;
|
| + // TODO(jmesserly): we could use an UnmodifiableListView here, but that adds
|
| + // memory overhead.
|
| + pathObj = new PropertyPath._(segments.toList(growable: false));
|
| + if (_pathCache.length >= _pathCacheLimit) {
|
| + _pathCache.remove(_pathCache.keys.first);
|
| + }
|
| + _pathCache[path] = pathObj;
|
| + return pathObj;
|
| }
|
|
|
| - /** Sets the value at this path. */
|
| - @reflectable void set value(Object newValue) {
|
| - int len = _segments.length;
|
| + PropertyPath._(this._segments);
|
|
|
| - // TODO(jmesserly): throw if property cannot be set?
|
| - // MDV seems tolerant of these errors.
|
| - if (len == 0) return;
|
| - if (!hasObservers) _updateValues(end: len - 1);
|
| + int get length => _segments.length;
|
| + bool get isEmpty => _segments.isEmpty;
|
| + bool get isValid => true;
|
|
|
| - if (_setObjectProperty(_values[len - 1], _segments[len - 1], newValue)) {
|
| - // Technically, this would get updated asynchronously via a change record.
|
| - // However, it is nice if calling the getter will yield the same value
|
| - // that was just set. So we use this opportunity to update our cache.
|
| - _values[len] = newValue;
|
| - }
|
| + String toString() {
|
| + if (!isValid) return '<invalid path>';
|
| + return _segments
|
| + .map((s) => s is Symbol ? MirrorSystem.getName(s) : s)
|
| + .join('.');
|
| }
|
|
|
| - /**
|
| - * Invokes the [callback] immediately with the current [value], and every time
|
| - * the value changes. This is useful for bindings, which want to be up-to-date
|
| - * immediately and stay bound to the value of the path.
|
| - */
|
| - StreamSubscription bindSync(void callback(value)) {
|
| - var result = changes.listen((records) { callback(value); });
|
| - callback(value);
|
| - return result;
|
| - }
|
| + bool operator ==(other) {
|
| + if (identical(this, other)) return true;
|
| + if (other is! PropertyPath) return false;
|
| + if (isValid != other.isValid) return false;
|
|
|
| - void observed() {
|
| - super.observed();
|
| - _updateValues();
|
| - _observePath();
|
| + int len = _segments.length;
|
| + if (len != other._segments.length) return false;
|
| + for (int i = 0; i < len; i++) {
|
| + if (_segments[i] != other._segments[i]) return false;
|
| + }
|
| + return true;
|
| }
|
|
|
| - void unobserved() {
|
| - for (int i = 0; i < _subs.length; i++) {
|
| - if (_subs[i] != null) {
|
| - _subs[i].cancel();
|
| - _subs[i] = null;
|
| - }
|
| + /// This is the [Jenkins hash function][1] but using masking to keep
|
| + /// values in SMI range.
|
| + /// [1]: http://en.wikipedia.org/wiki/Jenkins_hash_function
|
| + // TODO(jmesserly): should reuse this instead, see
|
| + // https://code.google.com/p/dart/issues/detail?id=11617
|
| + int get hashCode {
|
| + int hash = 0;
|
| + for (int i = 0, len = _segments.length; i < len; i++) {
|
| + hash = 0x1fffffff & (hash + _segments[i].hashCode);
|
| + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
| + hash = hash ^ (hash >> 6);
|
| }
|
| - super.unobserved();
|
| + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
| + hash = hash ^ (hash >> 11);
|
| + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
| }
|
|
|
| - // TODO(jmesserly): should we be caching these values if not observing?
|
| - void _updateValues({int end}) {
|
| - if (end == null) end = _segments.length;
|
| - int last = _segments.length - 1;
|
| - for (int i = 0; i < end; i++) {
|
| - var newValue = _getObjectProperty(_values[i], _segments[i]);
|
| - if (i == last && _computeValue != null) {
|
| - newValue = _computeValue(newValue);
|
| - }
|
| - _values[i + 1] = newValue;
|
| + /// Returns the current of the path from the provided [obj]ect.
|
| + getValueFrom(Object obj) {
|
| + if (!isValid) return null;
|
| + for (var segment in _segments) {
|
| + if (obj == null) return null;
|
| + obj = _getObjectProperty(obj, segment);
|
| }
|
| + return obj;
|
| }
|
|
|
| - void _updateObservedValues({int start: 0}) {
|
| - var oldValue, newValue;
|
| - for (int i = start, last = _segments.length - 1; i <= last; i++) {
|
| - oldValue = _values[i + 1];
|
| - newValue = _getObjectProperty(_values[i], _segments[i]);
|
| - if (i == last && _computeValue != null) {
|
| - newValue = _computeValue(newValue);
|
| - }
|
| - if (identical(oldValue, newValue)) {
|
| - _observePath(start, i);
|
| - return;
|
| - }
|
| - _values[i + 1] = newValue;
|
| + /// Attempts to set the [value] of the path from the provided [obj]ect.
|
| + /// Returns true if and only if the path was reachable and set.
|
| + bool setValueFrom(Object obj, Object value) {
|
| + var end = _segments.length - 1;
|
| + if (end < 0) return false;
|
| + for (int i = 0; i < end; i++) {
|
| + if (obj == null) return false;
|
| + obj = _getObjectProperty(obj, _segments[i]);
|
| }
|
| -
|
| - _observePath(start);
|
| - notifyPropertyChange(#value, oldValue, newValue);
|
| + return _setObjectProperty(obj, _segments[end], value);
|
| }
|
|
|
| - void _observePath([int start = 0, int end]) {
|
| - if (end == null) end = _segments.length;
|
| + void _iterateObjects(Object obj, void observe(obj)) {
|
| + if (!isValid || isEmpty) return;
|
|
|
| - for (int i = start; i < end; i++) {
|
| - if (_subs[i] != null) _subs[i].cancel();
|
| - _observeIndex(i);
|
| - }
|
| - }
|
| + int i = 0, last = _segments.length - 1;
|
| + while (obj != null) {
|
| + observe(obj);
|
|
|
| - void _observeIndex(int i) {
|
| - final object = _values[i];
|
| - final segment = _segments[i];
|
| - if (segment is int) {
|
| - if (object is ObservableList) {
|
| - _subs[i] = object.listChanges.listen((List<ListChangeRecord> records) {
|
| - for (var record in records) {
|
| - if (record.indexChanged(segment)) {
|
| - _updateObservedValues(start: i);
|
| - return;
|
| - }
|
| - }
|
| - });
|
| - }
|
| - } else if (object is Observable) {
|
| - // TODO(jmesserly): rather than allocating a new closure for each
|
| - // property, we could try and have one for the entire path. However we'd
|
| - // need to do a linear scan to find the index as soon as we got a change.
|
| - // Also we need to fix ListChangeRecord and MapChangeRecord to contain
|
| - // the target. Not sure if it's worth it.
|
| -
|
| - _subs[i] = object.changes.listen((List<ChangeRecord> records) {
|
| - for (var record in records) {
|
| - if (_changeRecordMatches(record, segment)) {
|
| - _updateObservedValues(start: i);
|
| - return;
|
| - }
|
| - }
|
| - });
|
| + if (i >= last) break;
|
| + obj = _getObjectProperty(obj, _segments[i++]);
|
| }
|
| }
|
| }
|
|
|
| +class _InvalidPropertyPath extends PropertyPath {
|
| + static final _instance = new _InvalidPropertyPath();
|
| +
|
| + bool get isValid => false;
|
| + _InvalidPropertyPath() : super._([]);
|
| +}
|
| +
|
| bool _changeRecordMatches(record, key) {
|
| if (record is PropertyChangeRecord) {
|
| return (record as PropertyChangeRecord).name == key;
|
| @@ -318,10 +327,8 @@ bool _maybeHasSetter(ClassMirror type, Symbol name) {
|
| return false;
|
| }
|
|
|
| -/**
|
| - * True if the type has a method, other than on Object.
|
| - * Doesn't consider noSuchMethod, unless [name] is `#noSuchMethod`.
|
| - */
|
| +/// True if the type has a method, other than on Object.
|
| +/// Doesn't consider noSuchMethod, unless [name] is `#noSuchMethod`.
|
| bool _hasMethod(ClassMirror type, Symbol name) {
|
| while (type != objectType) {
|
| final member = type.declarations[name];
|
| @@ -356,14 +363,376 @@ final _pathRegExp = () {
|
| return new RegExp('^$path\$');
|
| }();
|
|
|
| -final _spacesRegExp = new RegExp(r'\s');
|
| -
|
| bool _isPathValid(String s) {
|
| - s = s.replaceAll(_spacesRegExp, '');
|
| -
|
| + s = s.trim();
|
| if (s == '') return true;
|
| if (s[0] == '.') return false;
|
| return _pathRegExp.hasMatch(s);
|
| }
|
|
|
| final Logger _logger = new Logger('observe.PathObserver');
|
| +
|
| +
|
| +/// This is a simple cache. It's like LRU but we don't update an item on a
|
| +/// cache hit, because that would require allocation. Better to let it expire
|
| +/// and reallocate the PropertyPath.
|
| +// TODO(jmesserly): this optimization is from observe-js, how valuable is it in
|
| +// practice?
|
| +final _pathCache = new LinkedHashMap<String, PropertyPath>();
|
| +
|
| +/// The size of a path like "foo.bar" is approximately 160 bytes, so this
|
| +/// reserves ~16Kb of memory for recently used paths. Since paths are frequently
|
| +/// reused, the theory is that this ends up being a good tradeoff in practice.
|
| +// (Note: the 160 byte estimate is from Dart VM 1.0.0.10_r30798 on x64 without
|
| +// using UnmodifiableListView in PropertyPath)
|
| +const int _pathCacheLimit = 100;
|
| +
|
| +/// [CompoundObserver] is a [Bindable] object which knows how to listen to
|
| +/// multiple values (registered via [addPath] or [addObserver]) and invoke a
|
| +/// callback when one or more of the values have changed.
|
| +///
|
| +/// var obj = new ObservableMap.from({'a': 1, 'b': 2});
|
| +/// var otherObj = new ObservableMap.from({'c': 3});
|
| +///
|
| +/// var observer = new CompoundObserver()
|
| +/// ..addPath(obj, 'a');
|
| +/// ..addObserver(new PathObserver(obj, 'b'));
|
| +/// ..addPath(otherObj, 'c');
|
| +/// ..open((values) {
|
| +/// for (int i = 0; i < values.length; i++) {
|
| +/// print('The value at index $i is now ${values[i]}');
|
| +/// }
|
| +/// });
|
| +///
|
| +/// obj['a'] = 10; // print will be triggered async
|
| +///
|
| +class CompoundObserver extends _Observer implements Bindable {
|
| + _ObservedSet _directObserver;
|
| + List _observed = [];
|
| +
|
| + bool get _isClosed => _observed == null;
|
| +
|
| + CompoundObserver() {
|
| + _value = [];
|
| + }
|
| +
|
| + int get _reportArgumentCount => 3;
|
| +
|
| + /// Initiates observation and returns the initial value.
|
| + /// The callback will be passed the updated [value], and may optionally be
|
| + /// declared to take a second argument, which will contain the previous value.
|
| + ///
|
| + /// Implementation note: a third argument can also be declared, which will
|
| + /// receive a list of objects and paths, such that `list[2 * i]` will access
|
| + /// the object and `list[2 * i + 1]` will access the path, where `i` is the
|
| + /// order of the [addPath] call. This parameter is only used by
|
| + /// `package:polymer` as a performance optimization, and should not be relied
|
| + /// on in new code.
|
| + open(callback) => super.open(callback);
|
| +
|
| + void _connect() {
|
| + _check(skipChanges: true);
|
| +
|
| + for (var i = 0; i < _observed.length; i += 2) {
|
| + var object = _observed[i];
|
| + if (!identical(object, _observerSentinel)) {
|
| + _directObserver = new _ObservedSet(this, object);
|
| + break;
|
| + }
|
| + }
|
| + }
|
| +
|
| + void _disconnect() {
|
| + _value = null;
|
| +
|
| + if (_directObserver != null) {
|
| + _directObserver.close(this);
|
| + _directObserver = null;
|
| + }
|
| +
|
| + for (var i = 0; i < _observed.length; i += 2) {
|
| + if (identical(_observed[i], _observerSentinel)) {
|
| + _observed[i + 1].close();
|
| + }
|
| + }
|
| + _observed = null;
|
| + }
|
| +
|
| + /// Adds a dependency on the property [path] accessed from [object].
|
| + /// [path] can be a [PropertyPath] or a [String]. If it is omitted an empty
|
| + /// path will be used.
|
| + void addPath(Object object, [path]) {
|
| + if (_isOpen || _isClosed) {
|
| + throw new StateError('Cannot add paths once started.');
|
| + }
|
| +
|
| + if (path is! PropertyPath) path = new PropertyPath(path);
|
| + _observed..add(object)..add(path);
|
| + }
|
| +
|
| + void addObserver(Bindable observer) {
|
| + if (_isOpen || _isClosed) {
|
| + throw new StateError('Cannot add observers once started.');
|
| + }
|
| +
|
| + observer.open(_deliver);
|
| + _observed..add(_observerSentinel)..add(observer);
|
| + }
|
| +
|
| + void _iterateObjects(void observe(obj)) {
|
| + for (var i = 0; i < _observed.length; i += 2) {
|
| + var object = _observed[i];
|
| + if (!identical(object, _observerSentinel)) {
|
| + (_observed[i + 1] as PropertyPath)._iterateObjects(object, observe);
|
| + }
|
| + }
|
| + }
|
| +
|
| + bool _check({bool skipChanges: false}) {
|
| + bool changed = false;
|
| + _value.length = _observed.length ~/ 2;
|
| + var oldValues = null;
|
| + for (var i = 0; i < _observed.length; i += 2) {
|
| + var pathOrObserver = _observed[i + 1];
|
| + var object = _observed[i];
|
| + var value = identical(object, _observerSentinel) ?
|
| + (pathOrObserver as Bindable).value :
|
| + (pathOrObserver as PropertyPath).getValueFrom(object);
|
| +
|
| + if (skipChanges) {
|
| + _value[i ~/ 2] = value;
|
| + continue;
|
| + }
|
| +
|
| + if (value == _value[i ~/ 2]) continue;
|
| +
|
| + // don't allocate this unless necessary.
|
| + if (_notifyArgumentCount >= 2) {
|
| + if (oldValues == null) oldValues = new Map();
|
| + oldValues[i ~/ 2] = _value[i ~/ 2];
|
| + }
|
| +
|
| + changed = true;
|
| + _value[i ~/ 2] = value;
|
| + }
|
| +
|
| + if (!changed) return false;
|
| +
|
| + // TODO(rafaelw): Having _observed as the third callback arg here is
|
| + // pretty lame API. Fix.
|
| + _report(_value, oldValues, _observed);
|
| + return true;
|
| + }
|
| +}
|
| +
|
| +const _observerSentinel = const _ObserverSentinel();
|
| +class _ObserverSentinel { const _ObserverSentinel(); }
|
| +
|
| +// A base class for the shared API implemented by PathObserver and
|
| +// CompoundObserver and used in _ObservedSet.
|
| +abstract class _Observer extends Bindable {
|
| + static int _nextBirthId = 0;
|
| +
|
| + /// A number indicating when the object was created.
|
| + final int _birthId = _nextBirthId++;
|
| +
|
| + Function _notifyCallback;
|
| + int _notifyArgumentCount;
|
| + var _value;
|
| +
|
| + // abstract members
|
| + void _iterateObjects(void observe(obj));
|
| + void _connect();
|
| + void _disconnect();
|
| + bool get _isClosed;
|
| + _check({bool skipChanges: false});
|
| +
|
| + bool get _isOpen => _notifyCallback != null;
|
| +
|
| + /// The number of arguments the subclass will pass to [_report].
|
| + int get _reportArgumentCount;
|
| +
|
| + open(callback) {
|
| + if (_isOpen || _isClosed) {
|
| + throw new StateError('Observer has already been opened.');
|
| + }
|
| +
|
| + if (_minArgumentCount(callback) > _reportArgumentCount) {
|
| + throw new ArgumentError('callback should take $_reportArgumentCount or '
|
| + 'fewer arguments');
|
| + }
|
| +
|
| + _notifyCallback = callback;
|
| + _notifyArgumentCount = min(_reportArgumentCount,
|
| + _maxArgumentCount(callback));
|
| +
|
| + _connect();
|
| + return _value;
|
| + }
|
| +
|
| + @reflectable get value {
|
| + _check(skipChanges: true);
|
| + return _value;
|
| + }
|
| +
|
| + void close() {
|
| + if (!_isOpen) return;
|
| +
|
| + _disconnect();
|
| + _value = null;
|
| + _notifyCallback = null;
|
| + }
|
| +
|
| + void _deliver(_) {
|
| + if (_isOpen) _dirtyCheck();
|
| + }
|
| +
|
| + bool _dirtyCheck() {
|
| + var cycles = 0;
|
| + while (cycles < _MAX_DIRTY_CHECK_CYCLES && _check()) {
|
| + cycles++;
|
| + }
|
| + return cycles > 0;
|
| + }
|
| +
|
| + void _report(newValue, oldValue, [extraArg]) {
|
| + try {
|
| + switch (_notifyArgumentCount) {
|
| + case 0: _notifyCallback(); break;
|
| + case 1: _notifyCallback(newValue); break;
|
| + case 2: _notifyCallback(newValue, oldValue); break;
|
| + case 3: _notifyCallback(newValue, oldValue, extraArg); break;
|
| + }
|
| + } catch (e, s) {
|
| + // Deliver errors async, so if a single callback fails it doesn't prevent
|
| + // other things from working.
|
| + new Completer().completeError(e, s);
|
| + }
|
| + }
|
| +}
|
| +
|
| +typedef _Func0();
|
| +typedef _Func1(a);
|
| +typedef _Func2(a, b);
|
| +typedef _Func3(a, b, c);
|
| +
|
| +int _minArgumentCount(fn) {
|
| + if (fn is _Func0) return 0;
|
| + if (fn is _Func1) return 1;
|
| + if (fn is _Func2) return 2;
|
| + if (fn is _Func3) return 3;
|
| + return 4; // at least 4 arguments are required.
|
| +}
|
| +
|
| +int _maxArgumentCount(fn) {
|
| + if (fn is _Func3) return 3;
|
| + if (fn is _Func2) return 2;
|
| + if (fn is _Func1) return 1;
|
| + if (fn is _Func0) return 0;
|
| + return -1;
|
| +}
|
| +
|
| +class _ObservedSet {
|
| + /// To prevent sequential [PathObserver]s and [CompoundObserver]s from
|
| + /// observing the same object, we check if they are observing the same root
|
| + /// as the most recently created observer, and if so merge it into the
|
| + /// existing _ObservedSet.
|
| + ///
|
| + /// See <https://github.com/Polymer/observe-js/commit/f0990b1> and
|
| + /// <https://codereview.appspot.com/46780044/>.
|
| + static _ObservedSet _lastSet;
|
| +
|
| + /// The root object for a [PathObserver]. For a [CompoundObserver], the root
|
| + /// object of the first path observed. This is used by the constructor to
|
| + /// reuse an [_ObservedSet] that starts from the same object.
|
| + Object _rootObject;
|
| +
|
| + /// Observers associated with this root object, in birth order.
|
| + final Map<int, _Observer> _observers = new SplayTreeMap();
|
| +
|
| + // Dart note: the JS implementation is O(N^2) because Array.indexOf is used
|
| + // for lookup in these two arrays. We use HashMap to avoid this problem. It
|
| + // also gives us a nice way of tracking the StreamSubscription.
|
| + Map<Object, StreamSubscription> _objects;
|
| + Map<Object, StreamSubscription> _toRemove;
|
| +
|
| + bool _resetNeeded = false;
|
| +
|
| + factory _ObservedSet(_Observer observer, Object rootObj) {
|
| + if (_lastSet == null || !identical(_lastSet._rootObject, rootObj)) {
|
| + _lastSet = new _ObservedSet._(rootObj);
|
| + }
|
| + _lastSet.open(observer);
|
| + }
|
| +
|
| + _ObservedSet._(this._rootObject);
|
| +
|
| + void open(_Observer obs) {
|
| + _observers[obs._birthId] = obs;
|
| + obs._iterateObjects(observe);
|
| + }
|
| +
|
| + void close(_Observer obs) {
|
| + var anyLeft = false;
|
| +
|
| + _observers.remove(obs._birthId);
|
| +
|
| + if (_observers.isNotEmpty) {
|
| + _resetNeeded = true;
|
| + scheduleMicrotask(reset);
|
| + return;
|
| + }
|
| + _resetNeeded = false;
|
| +
|
| + if (_objects != null) {
|
| + for (var sub in _objects) sub.cancel();
|
| + _objects = null;
|
| + }
|
| + }
|
| +
|
| + void observe(Object obj) {
|
| + if (obj is ObservableList) _observeStream(obj.listChanges);
|
| + if (obj is Observable) _observeStream(obj.changes);
|
| + }
|
| +
|
| + void _observeStream(Stream stream) {
|
| + // TODO(jmesserly): we hash on streams as we have two separate change
|
| + // streams for ObservableList. Not sure if that is the design we will use
|
| + // going forward.
|
| +
|
| + if (_objects == null) _objects = new HashMap();
|
| + StreamSubscription sub = null;
|
| + if (_toRemove != null) sub = _toRemove.remove(stream);
|
| + if (sub != null) {
|
| + _objects[stream] = sub;
|
| + } else if (!_objects.containsKey(stream)) {
|
| + _objects[stream] = stream.listen(_callback);
|
| + }
|
| + }
|
| +
|
| + void reset() {
|
| + if (!_resetNeeded) return;
|
| +
|
| + var objs = _toRemove == null ? new HashMap() : _toRemove;
|
| + _toRemove = _objects;
|
| + _objects = objs;
|
| + for (var observer in _observers.values) {
|
| + if (observer._isOpen) observer._iterateObjects(observe);
|
| + }
|
| +
|
| + for (var sub in _toRemove.values) sub.cancel();
|
| +
|
| + _toRemove = null;
|
| + }
|
| +
|
| + void _callback(records) {
|
| + for (var observer in _observers.values.toList(growable: false)) {
|
| + if (observer._isOpen) observer._check();
|
| + }
|
| +
|
| + _resetNeeded = true;
|
| + scheduleMicrotask(reset);
|
| + }
|
| +}
|
| +
|
| +const int _MAX_DIRTY_CHECK_CYCLES = 1000;
|
|
|