| Index: third_party/pkg/angular/lib/change_detection/dirty_checking_change_detector.dart
|
| diff --git a/third_party/pkg/angular/lib/change_detection/dirty_checking_change_detector.dart b/third_party/pkg/angular/lib/change_detection/dirty_checking_change_detector.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..a51c74687733c77ce6bc0f283aa8c712c1eae877
|
| --- /dev/null
|
| +++ b/third_party/pkg/angular/lib/change_detection/dirty_checking_change_detector.dart
|
| @@ -0,0 +1,1274 @@
|
| +library dirty_checking_change_detector;
|
| +
|
| +import 'dart:mirrors';
|
| +import 'dart:collection';
|
| +import 'package:angular/change_detection/change_detection.dart';
|
| +
|
| +typedef FieldGetter(object);
|
| +
|
| +class GetterCache {
|
| + final Map<String, FieldGetter> _map;
|
| +
|
| + GetterCache(this._map);
|
| +
|
| + FieldGetter call(String name) => _map[name];
|
| +}
|
| +
|
| +/**
|
| + * [DirtyCheckingChangeDetector] determines which object properties have changed
|
| + * by comparing them to the their previous value.
|
| + *
|
| + * GOALS:
|
| + * - Plugable implementation, replaceable with other technologies, such as
|
| + * Object.observe().
|
| + * - SPEED this needs to be as fast as possible.
|
| + * - No GC pressure. Since change detection runs often it should perform no
|
| + * memory allocations.
|
| + * - The changes need to be delivered in a single data-structure at once.
|
| + * There are two reasons for this:
|
| + * 1. It should be easy to measure the cost of change detection vs
|
| + * processing.
|
| + * 2. The feature may move to VM for performance reason. The VM should be
|
| + * free to implement it in any way. The only requirement is that the list of
|
| + * changes need to be delivered.
|
| + *
|
| + * [DirtyCheckingRecord]
|
| + *
|
| + * Each property to be watched is recorded as a [DirtyCheckingRecord] and kept
|
| + * in a linked list. Linked list are faster than Arrays for iteration. They also
|
| + * allow removal of large blocks of watches in an efficient manner.
|
| + *
|
| + * [ChangeRecord]
|
| + *
|
| + * When the results are delivered they are a linked list of [ChangeRecord]s. For
|
| + * efficiency reasons the [DirtyCheckingRecord] and [ChangeRecord] are two
|
| + * different interfaces for the same underlying object this makes reporting
|
| + * efficient since no additional memory allocation is performed.
|
| + */
|
| +class DirtyCheckingChangeDetectorGroup<H> implements ChangeDetectorGroup<H> {
|
| + /**
|
| + * A group must have at least one record so that it can act as a placeholder.
|
| + * This record has minimal cost and never detects change. Once actual records
|
| + * get added the marker record gets removed, but it gets reinserted if all
|
| + * other records are removed.
|
| + */
|
| + final DirtyCheckingRecord _marker = new DirtyCheckingRecord.marker();
|
| +
|
| + final GetterCache _getterCache;
|
| +
|
| + /**
|
| + * All records for group are kept together and are denoted by head/tail.
|
| + * The [_recordHead]-[_recordTail] only include our own records. If you want
|
| + * to see our childGroup records as well use
|
| + * [_head]-[_childInclRecordTail].
|
| + */
|
| + DirtyCheckingRecord _recordHead, _recordTail;
|
| +
|
| + /**
|
| + * Same as [_tail] but includes child-group records as well.
|
| + */
|
| + DirtyCheckingRecord get _childInclRecordTail {
|
| + DirtyCheckingChangeDetectorGroup tail = this, nextTail;
|
| + while ((nextTail = tail._childTail) != null) {
|
| + tail = nextTail;
|
| + }
|
| + return tail._recordTail;
|
| + }
|
| +
|
| +
|
| + DirtyCheckingChangeDetector get _root {
|
| + var root = this;
|
| + var next;
|
| + while((next = root._parent) != null) {
|
| + root = next;
|
| + }
|
| + return root is DirtyCheckingChangeDetector ? root : null;
|
| + }
|
| +
|
| + /**
|
| + * ChangeDetectorGroup is organized hierarchically, a root group can have
|
| + * child groups and so on. We keep track of parent, children and next,
|
| + * previous here.
|
| + */
|
| + DirtyCheckingChangeDetectorGroup _parent, _childHead, _childTail, _prev, _next;
|
| +
|
| + DirtyCheckingChangeDetectorGroup(this._parent, this._getterCache) {
|
| + // we need to insert the marker record at the beginning.
|
| + if (_parent == null) {
|
| + _recordHead = _marker;
|
| + _recordTail = _marker;
|
| + } else {
|
| + _recordTail = _parent._childInclRecordTail;
|
| + // _recordAdd uses _recordTail from above.
|
| + _recordHead = _recordTail = _recordAdd(_marker);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Returns the number of watches in this group (including child groups).
|
| + */
|
| + get count {
|
| + int count = 0;
|
| + DirtyCheckingRecord cursor = _recordHead;
|
| + DirtyCheckingRecord end = _childInclRecordTail;
|
| + while(cursor != null) {
|
| + if (cursor._mode != DirtyCheckingRecord._MODE_MARKER_) {
|
| + count++;
|
| + }
|
| + if (cursor == end) break;
|
| + cursor = cursor._nextRecord;
|
| + }
|
| + return count;
|
| + }
|
| +
|
| + WatchRecord<H> watch(Object object, String field, H handler) {
|
| + assert(_root != null); // prove that we are not deleted connected;
|
| + var getter = field == null ? null : _getterCache(field);
|
| + return _recordAdd(new DirtyCheckingRecord(this, object, field, getter,
|
| + handler));
|
| + }
|
| +
|
| + /**
|
| + * Create a child [ChangeDetector] group.
|
| + */
|
| + DirtyCheckingChangeDetectorGroup<H> newGroup() {
|
| + assert(_root._assertRecordsOk());
|
| + var child = new DirtyCheckingChangeDetectorGroup(this, _getterCache);
|
| + if (_childHead == null) {
|
| + _childHead = _childTail = child;
|
| + } else {
|
| + child._prev = _childTail;
|
| + _childTail._next = child;
|
| + _childTail = child;
|
| + }
|
| + assert(_root._assertRecordsOk());
|
| + return child;
|
| + }
|
| +
|
| + /**
|
| + * Bulk remove all records.
|
| + */
|
| + void remove() {
|
| + var root;
|
| + assert((root = _root) != null);
|
| + assert(root._assertRecordsOk());
|
| + DirtyCheckingRecord prevRecord = _recordHead._prevRecord;
|
| + DirtyCheckingRecord nextRecord = _childInclRecordTail._nextRecord;
|
| +
|
| + if (prevRecord != null) prevRecord._nextRecord = nextRecord;
|
| + if (nextRecord != null) nextRecord._prevRecord = prevRecord;
|
| +
|
| + var cursor = _recordHead;
|
| + while(cursor != nextRecord) {
|
| + cursor = cursor._nextRecord;
|
| + }
|
| +
|
| + var prevGroup = _prev;
|
| + var nextGroup = _next;
|
| +
|
| + if (prevGroup == null) {
|
| + _parent._childHead = nextGroup;
|
| + } else {
|
| + prevGroup._next = nextGroup;
|
| + }
|
| + if (nextGroup == null) {
|
| + _parent._childTail = prevGroup;
|
| + } else {
|
| + nextGroup._prev = prevGroup;
|
| + }
|
| + _parent = null;
|
| + assert(root._assertRecordsOk());
|
| + }
|
| +
|
| + DirtyCheckingRecord _recordAdd(DirtyCheckingRecord record) {
|
| + DirtyCheckingRecord previous = _recordTail;
|
| + DirtyCheckingRecord next = previous == null ? null : previous._nextRecord;
|
| +
|
| + record._nextRecord = next;
|
| + record._prevRecord = previous;
|
| +
|
| + if (previous != null) previous._nextRecord = record;
|
| + if (next != null) next._prevRecord = record;
|
| +
|
| + _recordTail = record;
|
| +
|
| + if (previous == _marker) _recordRemove(_marker);
|
| +
|
| + return record;
|
| + }
|
| +
|
| + void _recordRemove(DirtyCheckingRecord record) {
|
| + DirtyCheckingRecord previous = record._prevRecord;
|
| + DirtyCheckingRecord next = record._nextRecord;
|
| +
|
| + if (record == _recordHead && record == _recordTail) {
|
| + // we are the last one, must leave marker behind.
|
| + _recordHead = _recordTail = _marker;
|
| + _marker._nextRecord = next;
|
| + _marker._prevRecord = previous;
|
| + if (previous != null) previous._nextRecord = _marker;
|
| + if (next != null) next._prevRecord = _marker;
|
| + } else {
|
| + if (record == _recordTail) _recordTail = previous;
|
| + if (record == _recordHead) _recordHead = next;
|
| + if (previous != null) previous._nextRecord = next;
|
| + if (next != null) next._prevRecord = previous;
|
| + }
|
| + }
|
| +
|
| + String toString() {
|
| + var lines = [];
|
| + if (_parent == null) {
|
| + var allRecords = [];
|
| + DirtyCheckingRecord record = _recordHead;
|
| + var includeChildrenTail = _childInclRecordTail;
|
| + do {
|
| + allRecords.add(record.toString());
|
| + record = record._nextRecord;
|
| + }
|
| + while (record != includeChildrenTail);
|
| + lines.add('FIELDS: ${allRecords.join(', ')}');
|
| + }
|
| +
|
| + var records = [];
|
| + DirtyCheckingRecord record = _recordHead;
|
| + while (record != _recordTail) {
|
| + records.add(record.toString());
|
| + record = record._nextRecord;
|
| + }
|
| + records.add(record.toString());
|
| +
|
| + lines.add('DirtyCheckingChangeDetectorGroup(fields: ${records.join(', ')})');
|
| + var childGroup = _childHead;
|
| + while (childGroup != null) {
|
| + lines.add(' ' + childGroup.toString().split('\n').join('\n '));
|
| + childGroup = childGroup._next;
|
| + }
|
| + return lines.join('\n');
|
| + }
|
| +}
|
| +
|
| +class DirtyCheckingChangeDetector<H> extends DirtyCheckingChangeDetectorGroup<H>
|
| + implements ChangeDetector<H> {
|
| + DirtyCheckingChangeDetector(GetterCache getterCache): super(null, getterCache);
|
| +
|
| + DirtyCheckingChangeDetector get _root => this;
|
| +
|
| + _assertRecordsOk() {
|
| + var record = this._recordHead;
|
| + var groups = [this];
|
| + DirtyCheckingChangeDetectorGroup group;
|
| + while(groups.isNotEmpty) {
|
| + group = groups.removeAt(0);
|
| + DirtyCheckingChangeDetectorGroup childGroup = group._childTail;
|
| + while(childGroup != null) {
|
| + groups.insert(0, childGroup);
|
| + childGroup = childGroup._prev;
|
| + }
|
| + var groupRecord = group._recordHead;
|
| + var groupLast = group._recordTail;
|
| + while(true) {
|
| + if (groupRecord == record) {
|
| + record = record._nextRecord;
|
| + } else {
|
| + throw 'lost: $record found $groupRecord\n$this';
|
| + }
|
| +
|
| + if (groupRecord == groupLast) break;
|
| + groupRecord = groupRecord._nextRecord;
|
| + }
|
| + }
|
| + return true;
|
| + }
|
| +
|
| + DirtyCheckingRecord<H> collectChanges({ EvalExceptionHandler exceptionHandler,
|
| + AvgStopwatch stopwatch}) {
|
| + if (stopwatch != null) stopwatch.start();
|
| + DirtyCheckingRecord changeHead = null;
|
| + DirtyCheckingRecord changeTail = null;
|
| + DirtyCheckingRecord current = _recordHead; // current index
|
| +
|
| + int count = 0;
|
| + while (current != null) {
|
| + try {
|
| + if (current.check() != null) {
|
| + if (changeHead == null) {
|
| + changeHead = changeTail = current;
|
| + } else {
|
| + changeTail = changeTail.nextChange = current;
|
| + }
|
| + }
|
| + if (stopwatch != null) count++;
|
| + } catch (e, s) {
|
| + if (exceptionHandler == null) {
|
| + rethrow;
|
| + } else {
|
| + exceptionHandler(e, s);
|
| + }
|
| + }
|
| + current = current._nextRecord;
|
| + }
|
| + if (changeTail != null) changeTail.nextChange = null;
|
| + if (stopwatch != null) stopwatch..stop()..increment(count);
|
| + return changeHead;
|
| + }
|
| +
|
| + void remove() {
|
| + throw new StateError('Root ChangeDetector can not be removed');
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * [DirtyCheckingRecord] represents as single item to check. The heart of the
|
| + * [DirtyCheckingRecord] is a the [check] method which can read the
|
| + * [currentValue] and compare it to the [previousValue].
|
| + *
|
| + * [DirtyCheckingRecord]s form linked list. This makes traversal, adding, and
|
| + * removing efficient. [DirtyCheckingRecord] also has a [nextChange] field which
|
| + * creates a single linked list of all of the changes for efficient traversal.
|
| + */
|
| +class DirtyCheckingRecord<H> implements ChangeRecord<H>, WatchRecord<H> {
|
| + static const List<String> _MODE_NAMES =
|
| + const ['MARKER', 'IDENT', 'REFLECT', 'GETTER', 'MAP[]', 'ITERABLE', 'MAP'];
|
| + static const int _MODE_MARKER_ = 0;
|
| + static const int _MODE_IDENTITY_ = 1;
|
| + static const int _MODE_REFLECT_ = 2;
|
| + static const int _MODE_GETTER_ = 3;
|
| + static const int _MODE_MAP_FIELD_ = 4;
|
| + static const int _MODE_ITERABLE_ = 5;
|
| + static const int _MODE_MAP_ = 6;
|
| +
|
| + final DirtyCheckingChangeDetectorGroup _group;
|
| + final String field;
|
| + final Symbol _symbol;
|
| + final FieldGetter _getter;
|
| + final H handler;
|
| +
|
| + int _mode;
|
| +
|
| + var previousValue;
|
| + var currentValue;
|
| + DirtyCheckingRecord<H> _nextRecord;
|
| + DirtyCheckingRecord<H> _prevRecord;
|
| + ChangeRecord<H> nextChange;
|
| + var _object;
|
| + InstanceMirror _instanceMirror;
|
| +
|
| + DirtyCheckingRecord(this._group, object, fieldName, this._getter, this.handler)
|
| + : field = fieldName,
|
| + _symbol = fieldName == null ? null : new Symbol(fieldName)
|
| + {
|
| + this.object = object;
|
| + }
|
| +
|
| + DirtyCheckingRecord.marker()
|
| + : handler = null,
|
| + field = null,
|
| + _group = null,
|
| + _symbol = null,
|
| + _getter = null,
|
| + _mode = _MODE_MARKER_;
|
| +
|
| + get object => _object;
|
| +
|
| + /**
|
| + * Setting an [object] will cause the setter to introspect it and place
|
| + * [DirtyCheckingRecord] into different access modes. If Object it sets up
|
| + * reflection. If [Map] then it sets up map accessor.
|
| + */
|
| + set object(obj) {
|
| + _object = obj;
|
| + if (obj == null) {
|
| + _mode = _MODE_IDENTITY_;
|
| + return;
|
| + }
|
| +
|
| + if (field == null) {
|
| + _instanceMirror = null;
|
| + if (obj is Map) {
|
| + if (_mode != _MODE_MAP_) {
|
| + // Last one was collection as well, don't reset state.
|
| + _mode = _MODE_MAP_;
|
| + currentValue = new _MapChangeRecord();
|
| + }
|
| + } else if (obj is Iterable) {
|
| + if (_mode != _MODE_ITERABLE_) {
|
| + // Last one was collection as well, don't reset state.
|
| + _mode = _MODE_ITERABLE_;
|
| + currentValue = new _CollectionChangeRecord();
|
| + }
|
| + } else {
|
| + _mode = _MODE_IDENTITY_;
|
| + }
|
| +
|
| + return;
|
| + }
|
| +
|
| + if (obj is Map) {
|
| + _mode = _MODE_MAP_FIELD_;
|
| + _instanceMirror = null;
|
| + } else if (_getter != null) {
|
| + _mode = _MODE_GETTER_;
|
| + _instanceMirror = null;
|
| + } else {
|
| + _mode = _MODE_REFLECT_;
|
| + _instanceMirror = reflect(obj);
|
| + }
|
| + }
|
| +
|
| + ChangeRecord<H> check() {
|
| + assert(_mode != null);
|
| + var current;
|
| + switch (_mode) {
|
| + case _MODE_MARKER_:
|
| + return null;
|
| + case _MODE_REFLECT_:
|
| + current = _instanceMirror.getField(_symbol).reflectee;
|
| + break;
|
| + case _MODE_GETTER_:
|
| + current = _getter(object);
|
| + break;
|
| + case _MODE_MAP_FIELD_:
|
| + current = object[field];
|
| + break;
|
| + case _MODE_IDENTITY_:
|
| + current = object;
|
| + break;
|
| + case _MODE_MAP_:
|
| + return (currentValue as _MapChangeRecord)._check(object) ? this : null;
|
| + case _MODE_ITERABLE_:
|
| + return (currentValue as _CollectionChangeRecord)._check(object) ? this : null;
|
| + default:
|
| + assert(false);
|
| + }
|
| +
|
| + var last = currentValue;
|
| + if (!identical(last, current)) {
|
| + if (last is String && current is String &&
|
| + last == current) {
|
| + // This is false change in strings we need to recover, and pretend it
|
| + // is the same. We save the value so that next time identity will pass
|
| + currentValue = current;
|
| + } else if (last is num && last.isNaN && current is num && current.isNaN) {
|
| + // we need this for the compiled JavaScript since in JS NaN !== NaN.
|
| + } else {
|
| + previousValue = last;
|
| + currentValue = current;
|
| + return this;
|
| + }
|
| + }
|
| + return null;
|
| + }
|
| +
|
| +
|
| + void remove() {
|
| + _group._recordRemove(this);
|
| + }
|
| +
|
| + String toString() => '${_MODE_NAMES[_mode]}[$field]{$hashCode}';
|
| +}
|
| +
|
| +final Object _INITIAL_ = new Object();
|
| +
|
| +class _MapChangeRecord<K, V> implements MapChangeRecord<K, V> {
|
| + final Map<dynamic, KeyValueRecord> _records = new Map<dynamic, KeyValueRecord>();
|
| + Map _map;
|
| + KeyValueRecord _mapHead;
|
| + KeyValueRecord _changesHead, _changesTail;
|
| + KeyValueRecord _additionsHead, _additionsTail;
|
| + KeyValueRecord _removalsHead, _removalsTail;
|
| +
|
| + Map get map => _map;
|
| + KeyValue<K, V> get mapHead => _mapHead;
|
| + ChangedKeyValue<K, V> get changesHead => _changesHead;
|
| + AddedKeyValue<K, V> get additionsHead => _additionsHead;
|
| + RemovedKeyValue<K, V> get removalsHead => _removalsHead;
|
| +
|
| + get isDirty => _additionsHead != null ||
|
| + _changesHead != null ||
|
| + _removalsHead != null;
|
| +
|
| + void forEachChange(void f(ChangedKeyValue<K, V> change)) {
|
| + KeyValueRecord record = _changesHead;
|
| + while(record != null) {
|
| + f(record);
|
| + record = record._nextChangedKeyValue;
|
| + }
|
| + }
|
| +
|
| + void forEachAddition(void f(AddedKeyValue<K, V> addition)){
|
| + KeyValueRecord record = _additionsHead;
|
| + while(record != null) {
|
| + f(record);
|
| + record = record._nextAddedKeyValue;
|
| + }
|
| + }
|
| +
|
| + void forEachRemoval(void f(RemovedKeyValue<K, V> removal)){
|
| + KeyValueRecord record = _removalsHead;
|
| + while(record != null) {
|
| + f(record);
|
| + record = record._nextRemovedKeyValue;
|
| + }
|
| + }
|
| +
|
| +
|
| + bool _check(Map map) {
|
| + _reset();
|
| + _map = map;
|
| + Map records = _records;
|
| + KeyValueRecord oldSeqRecord = _mapHead;
|
| + KeyValueRecord lastOldSeqRecord;
|
| + KeyValueRecord lastNewSeqRecord;
|
| + var seqChanged = false;
|
| + map.forEach((key, value) {
|
| + var newSeqRecord;
|
| + if (oldSeqRecord != null && key == oldSeqRecord.key) {
|
| + newSeqRecord = oldSeqRecord;
|
| + if (!identical(value, oldSeqRecord._currentValue)) {
|
| + var prev = oldSeqRecord._previousValue = oldSeqRecord._currentValue;
|
| + oldSeqRecord._currentValue = value;
|
| + if (!((value is String && prev is String && value == prev) ||
|
| + (value is num && value.isNaN && prev is num && prev.isNaN))) {
|
| + // Check string by value rather than reference
|
| + _addToChanges(oldSeqRecord);
|
| + }
|
| + }
|
| + } else {
|
| + seqChanged = true;
|
| + if (oldSeqRecord != null) {
|
| + oldSeqRecord._nextKeyValue = null;
|
| + _removeFromSeq(lastOldSeqRecord, oldSeqRecord);
|
| + _addToRemovals(oldSeqRecord);
|
| + }
|
| + if (records.containsKey(key)) {
|
| + newSeqRecord = records[key];
|
| + } else {
|
| + newSeqRecord = records[key] = new KeyValueRecord(key);
|
| + newSeqRecord._currentValue = value;
|
| + _addToAdditions(newSeqRecord);
|
| + }
|
| + }
|
| +
|
| + if (seqChanged) {
|
| + if (_isInRemovals(newSeqRecord)) {
|
| + _removeFromRemovals(newSeqRecord);
|
| + }
|
| + if (lastNewSeqRecord == null) {
|
| + _mapHead = newSeqRecord;
|
| + } else {
|
| + lastNewSeqRecord._nextKeyValue = newSeqRecord;
|
| + }
|
| + }
|
| + lastOldSeqRecord = oldSeqRecord;
|
| + lastNewSeqRecord = newSeqRecord;
|
| + oldSeqRecord = oldSeqRecord == null ? null : oldSeqRecord._nextKeyValue;
|
| + });
|
| + _truncate(lastOldSeqRecord, oldSeqRecord);
|
| + return isDirty;
|
| + }
|
| +
|
| + void _reset() {
|
| + var record = _changesHead;
|
| + while (record != null) {
|
| + record._previousValue = record._currentValue;
|
| + record = record._nextChangedKeyValue;
|
| + }
|
| +
|
| + record = _additionsHead;
|
| + while (record != null) {
|
| + record._previousValue = record._currentValue;
|
| + record = record._nextAddedKeyValue;
|
| + }
|
| +
|
| + assert((() {
|
| + var record = _changesHead;
|
| + while (record != null) {
|
| + var nextRecord = record._nextChangedKeyValue;
|
| + record._nextChangedKeyValue = null;
|
| + record = nextRecord;
|
| + }
|
| +
|
| + record = _additionsHead;
|
| + while (record != null) {
|
| + var nextRecord = record._nextAddedKeyValue;
|
| + record._nextAddedKeyValue = null;
|
| + record = nextRecord;
|
| + }
|
| +
|
| + record = _removalsHead;
|
| + while (record != null) {
|
| + var nextRecord = record._nextRemovedKeyValue;
|
| + record._nextRemovedKeyValue = null;
|
| + record = nextRecord;
|
| + }
|
| +
|
| + return true;
|
| + })());
|
| + _changesHead = _changesTail = null;
|
| + _additionsHead = _additionsTail = null;
|
| + _removalsHead = _removalsTail = null;
|
| + }
|
| +
|
| + void _truncate(KeyValueRecord lastRecord, KeyValueRecord record) {
|
| + while(record != null) {
|
| + if (lastRecord == null) {
|
| + _mapHead = null;
|
| + } else {
|
| + lastRecord._nextKeyValue = null;
|
| + }
|
| + var nextRecord = record._nextKeyValue;
|
| + assert((() {
|
| + record._nextKeyValue = null;
|
| + return true;
|
| + })());
|
| + _addToRemovals(record);
|
| + lastRecord = record;
|
| + record = nextRecord;
|
| + }
|
| +
|
| + record = _removalsHead;
|
| + while (record != null) {
|
| + record._previousValue = record._currentValue;
|
| + record._currentValue = null;
|
| + _records.remove(record.key);
|
| + record = record._nextRemovedKeyValue;
|
| + }
|
| + }
|
| +
|
| + bool _isInRemovals(KeyValueRecord record) =>
|
| + record == _removalsHead ||
|
| + record._nextRemovedKeyValue != null ||
|
| + record._prevRemovedKeyValue != null;
|
| +
|
| + void _addToRemovals(KeyValueRecord record) {
|
| + assert(record._nextKeyValue == null);
|
| + assert(record._nextAddedKeyValue == null);
|
| + assert(record._nextChangedKeyValue == null);
|
| + assert(record._nextRemovedKeyValue == null);
|
| + assert(record._prevRemovedKeyValue == null);
|
| + if (_removalsHead == null) {
|
| + _removalsHead = _removalsTail = record;
|
| + } else {
|
| + _removalsTail._nextRemovedKeyValue = record;
|
| + record._prevRemovedKeyValue = _removalsTail;
|
| + _removalsTail = record;
|
| + }
|
| + }
|
| +
|
| + void _removeFromSeq(KeyValueRecord prev, KeyValueRecord record) {
|
| + KeyValueRecord next = record._nextKeyValue;
|
| + if (prev == null) {
|
| + _mapHead = next;
|
| + } else {
|
| + prev._nextKeyValue = next;
|
| + }
|
| + assert((() {
|
| + record._nextKeyValue = null;
|
| + return true;
|
| + })());
|
| + }
|
| +
|
| + void _removeFromRemovals(KeyValueRecord record) {
|
| + assert(record._nextKeyValue == null);
|
| + assert(record._nextAddedKeyValue == null);
|
| + assert(record._nextChangedKeyValue == null);
|
| +
|
| + var prev = record._prevRemovedKeyValue;
|
| + var next = record._nextRemovedKeyValue;
|
| + if (prev == null) {
|
| + _removalsHead = next;
|
| + } else {
|
| + prev._nextRemovedKeyValue = next;
|
| + }
|
| + if (next == null) {
|
| + _removalsTail = prev;
|
| + } else {
|
| + next._prevRemovedKeyValue = prev;
|
| + }
|
| + record._prevRemovedKeyValue = record._nextRemovedKeyValue = null;
|
| + }
|
| +
|
| + void _addToAdditions(KeyValueRecord record) {
|
| + assert(record._nextKeyValue == null);
|
| + assert(record._nextAddedKeyValue == null);
|
| + assert(record._nextChangedKeyValue == null);
|
| + assert(record._nextRemovedKeyValue == null);
|
| + assert(record._prevRemovedKeyValue == null);
|
| + if (_additionsHead == null) {
|
| + _additionsHead = _additionsTail = record;
|
| + } else {
|
| + _additionsTail._nextAddedKeyValue = record;
|
| + _additionsTail = record;
|
| + }
|
| + }
|
| +
|
| + void _addToChanges(KeyValueRecord record) {
|
| + assert(record._nextAddedKeyValue == null);
|
| + assert(record._nextChangedKeyValue == null);
|
| + assert(record._nextRemovedKeyValue == null);
|
| + assert(record._prevRemovedKeyValue == null);
|
| + if (_changesHead == null) {
|
| + _changesHead = _changesTail = record;
|
| + } else {
|
| + _changesTail._nextChangedKeyValue = record;
|
| + _changesTail = record;
|
| + }
|
| + }
|
| +}
|
| +
|
| +class KeyValueRecord<K, V> implements KeyValue<K, V>, AddedKeyValue<K, V>,
|
| + RemovedKeyValue<K, V>, ChangedKeyValue<K, V> {
|
| + final K key;
|
| + V _previousValue, _currentValue;
|
| +
|
| + KeyValueRecord<K, V> _nextKeyValue;
|
| + KeyValueRecord<K, V> _nextAddedKeyValue;
|
| + KeyValueRecord<K, V> _nextRemovedKeyValue, _prevRemovedKeyValue;
|
| + KeyValueRecord<K, V> _nextChangedKeyValue;
|
| +
|
| + KeyValueRecord(this.key);
|
| +
|
| + V get previousValue => _previousValue;
|
| + V get currentValue => _currentValue;
|
| + KeyValue<K, V> get nextKeyValue => _nextKeyValue;
|
| + AddedKeyValue<K, V> get nextAddedKeyValue => _nextAddedKeyValue;
|
| + RemovedKeyValue<K, V> get nextRemovedKeyValue => _nextRemovedKeyValue;
|
| + ChangedKeyValue<K, V> get nextChangedKeyValue => _nextChangedKeyValue;
|
| +
|
| + String toString() => _previousValue == _currentValue
|
| + ? key
|
| + : '$key[$_previousValue -> $_currentValue]';
|
| +}
|
| +
|
| +
|
| +class _CollectionChangeRecord<V> implements CollectionChangeRecord<V> {
|
| + Iterable _iterable;
|
| + /** Used to keep track of items during moves. */
|
| + DuplicateMap _items = new DuplicateMap();
|
| +
|
| + /** Used to keep track of removed items. */
|
| + DuplicateMap _removedItems = new DuplicateMap();
|
| +
|
| + ItemRecord<V> _collectionHead, _collectionTail;
|
| + ItemRecord<V> _additionsHead, _additionsTail;
|
| + ItemRecord<V> _movesHead, _movesTail;
|
| + ItemRecord<V> _removalsHead, _removalsTail;
|
| +
|
| + CollectionChangeItem<V> get collectionHead => _collectionHead;
|
| + CollectionChangeItem<V> get additionsHead => _additionsHead;
|
| + CollectionChangeItem<V> get movesHead => _movesHead;
|
| + CollectionChangeItem<V> get removalsHead => _removalsHead;
|
| +
|
| + void forEachAddition(void f(AddedItem<V> addition)){
|
| + ItemRecord record = _additionsHead;
|
| + while(record != null) {
|
| + f(record);
|
| + record = record._nextAddedRec;
|
| + }
|
| + }
|
| +
|
| + void forEachMove(void f(MovedItem<V> change)) {
|
| + ItemRecord record = _movesHead;
|
| + while(record != null) {
|
| + f(record);
|
| + record = record._nextMovedRec;
|
| + }
|
| + }
|
| +
|
| + void forEachRemoval(void f(RemovedItem<V> removal)){
|
| + ItemRecord record = _removalsHead;
|
| + while(record != null) {
|
| + f(record);
|
| + record = record._nextRemovedRec;
|
| + }
|
| + }
|
| +
|
| + Iterable get iterable => _iterable;
|
| +
|
| + bool _check(Iterable collection) {
|
| + _reset();
|
| + ItemRecord record = _collectionHead;
|
| + bool maybeDirty = false;
|
| + if ((collection is UnmodifiableListView) &&
|
| + identical(_iterable, collection)) {
|
| + // Short circuit and assume that the list has not been modified.
|
| + return false;
|
| + }
|
| +
|
| + if (collection is List) {
|
| + List list = collection;
|
| + for (int index = 0; index < list.length; index++) {
|
| + var item = list[index];
|
| + if (record == null || !identical(item, record.item)) {
|
| + record = mismatch(record, item, index);
|
| + maybeDirty = true;
|
| + } else if (maybeDirty) {
|
| + // TODO(misko): can we limit this to duplicates only?
|
| + record = verifyReinsertion(record, item, index);
|
| + }
|
| + record = record._nextRec;
|
| + }
|
| + } else {
|
| + int index = 0;
|
| + for (var item in collection) {
|
| + if (record == null || !identical(item, record.item)) {
|
| + record = mismatch(record, item, index);
|
| + maybeDirty = true;
|
| + } else if (maybeDirty) {
|
| + // TODO(misko): can we limit this to duplicates only?
|
| + record = verifyReinsertion(record, item, index);
|
| + }
|
| + record = record._nextRec;
|
| + index++;
|
| + }
|
| + }
|
| +
|
| + _truncate(record);
|
| + _iterable = collection;
|
| + return isDirty;
|
| + }
|
| +
|
| + /**
|
| + * Reset the state of the change objects to show no changes. This means set
|
| + * previousKey to currentKey, and clear all of the queues (additions, moves,
|
| + * removals).
|
| + */
|
| + void _reset() {
|
| + ItemRecord record;
|
| +
|
| + record = _additionsHead;
|
| + while(record != null) {
|
| + record.previousIndex = record.currentIndex;
|
| + record = record._nextAddedRec;
|
| + }
|
| + _additionsHead = _additionsTail = null;
|
| +
|
| + record = _movesHead;
|
| + while(record != null) {
|
| + record.previousIndex = record.currentIndex;
|
| + var nextRecord = record._nextMovedRec;
|
| + assert((record._nextMovedRec = null) == null);
|
| + record = nextRecord;
|
| + }
|
| + _movesHead = _movesTail = null;
|
| + _removalsHead = _removalsTail = null;
|
| + assert(isDirty == false);
|
| + }
|
| +
|
| + /**
|
| + * A [_CollectionChangeRecord] is considered dirty if it has additions, moves
|
| + * or removals.
|
| + */
|
| + get isDirty => _additionsHead != null ||
|
| + _movesHead != null ||
|
| + _removalsHead != null;
|
| +
|
| + /**
|
| + * This is the core function which handles differences between collections.
|
| + *
|
| + * - [record] is the record which we saw at this position last time. If `null`
|
| + * then it is a new item.
|
| + * - [item] is the current item in the collection
|
| + * - [index] is the position of the item in the collection
|
| + */
|
| + ItemRecord mismatch(ItemRecord record, item, int index) {
|
| + // Guard against bogus String changes
|
| + if (record != null) {
|
| + if (item is String && record.item is String && record.item == item) {
|
| + // this is false change in strings we need to recover, and pretend it is
|
| + // the same. We save the value so that next time identity can pass
|
| + return record..item = item;
|
| + }
|
| +
|
| + if (item is num && item.isNaN && record.item is num && record.item.isNaN){
|
| + // we need this for JavaScript since in JS NaN !== NaN.
|
| + return record;
|
| + }
|
| + }
|
| +
|
| + // find the previous record so that we know where to insert after.
|
| + ItemRecord prev = record == null ? _collectionTail : record._prevRec;
|
| +
|
| + // Remove the record from the collection since we know it does not match the
|
| + // item.
|
| + if (record != null) _collection_remove(record);
|
| + // Attempt to see if we have seen the item before.
|
| + record = _items.get(item, index);
|
| + if (record != null) {
|
| + // We have seen this before, we need to move it forward in the collection.
|
| + _collection_moveAfter(record, prev, index);
|
| + } else {
|
| + // Never seen it, check evicted list.
|
| + record = _removedItems.get(item);
|
| + if (record != null) {
|
| + // It is an item which we have earlier evict it, reinsert it back into
|
| + // the list.
|
| + _collection_reinsertAfter(record, prev, index);
|
| + } else {
|
| + // It is a new item add it.
|
| + record = _collection_addAfter(new ItemRecord(item), prev, index);
|
| + }
|
| + }
|
| + return record;
|
| + }
|
| +
|
| + /**
|
| + * This check is only needed if an array contains duplicates. (Short circuit
|
| + * of nothing dirty)
|
| + *
|
| + * Use case: `[a, a]` => `[b, a, a]`
|
| + *
|
| + * If we did not have this check then the insertion of `b` would:
|
| + * 1) evict first `a`
|
| + * 2) insert `b` at `0` index.
|
| + * 3) leave `a` at index `1` as is. <-- this is wrong!
|
| + * 3) reinsert `a` at index 2. <-- this is wrong!
|
| + *
|
| + * The correct behavior is:
|
| + * 1) evict first `a`
|
| + * 2) insert `b` at `0` index.
|
| + * 3) reinsert `a` at index 1.
|
| + * 3) move `a` at from `1` to `2`.
|
| + *
|
| + *
|
| + * Double check that we have not evicted a duplicate item. We need to check if
|
| + * the item type may have already been removed:
|
| + * The insertion of b will evict the first 'a'. If we don't reinsert it now it
|
| + * will be reinserted at the end. Which will show up as the two 'a's switching
|
| + * position. This is incorrect, since a better way to think of it is as insert
|
| + * of 'b' rather then switch 'a' with 'b' and then add 'a' at the end.
|
| + */
|
| + ItemRecord verifyReinsertion(ItemRecord record, dynamic item,
|
| + int index) {
|
| + ItemRecord reinsertRecord = _removedItems.get(item);
|
| + if (reinsertRecord != null) {
|
| + record = _collection_reinsertAfter(reinsertRecord, record._prevRec, index);
|
| + } else if (record.currentIndex != index) {
|
| + record.currentIndex = index;
|
| + _moves_add(record);
|
| + }
|
| + return record;
|
| + }
|
| +
|
| + /**
|
| + * Get rid of any excess [ItemRecord]s from the previous collection
|
| + *
|
| + * - [record] The first excess [ItemRecord].
|
| + */
|
| + void _truncate(ItemRecord record) {
|
| + // Anything after that needs to be removed;
|
| + while(record != null) {
|
| + ItemRecord nextRecord = record._nextRec;
|
| + _removals_add(_collection_unlink(record));
|
| + record = nextRecord;
|
| + }
|
| + _removedItems.clear();
|
| + }
|
| +
|
| + ItemRecord _collection_reinsertAfter(ItemRecord record, ItemRecord insertPrev,
|
| + int index) {
|
| + _removedItems.remove(record);
|
| + var prev = record._prevRemovedRec;
|
| + var next = record._nextRemovedRec;
|
| +
|
| + assert((record._prevRemovedRec = null) == null);
|
| + assert((record._nextRemovedRec = null) == null);
|
| +
|
| + if (prev == null) {
|
| + _removalsHead = next;
|
| + } else {
|
| + prev._nextRemovedRec = next;
|
| + }
|
| + if (next == null) {
|
| + _removalsTail = prev;
|
| + } else {
|
| + next._prevRemovedRec = prev;
|
| + }
|
| +
|
| + _collection_insertAfter(record, insertPrev, index);
|
| + _moves_add(record);
|
| + return record;
|
| + }
|
| +
|
| + ItemRecord _collection_moveAfter(ItemRecord record, ItemRecord prev,
|
| + int index) {
|
| + _collection_unlink(record);
|
| + _collection_insertAfter(record, prev, index);
|
| + _moves_add(record);
|
| + return record;
|
| + }
|
| +
|
| + ItemRecord _collection_addAfter(ItemRecord record, ItemRecord prev,
|
| + int index) {
|
| + _collection_insertAfter(record, prev, index);
|
| +
|
| + if (_additionsTail == null) {
|
| + assert(_additionsHead == null);
|
| + _additionsTail = _additionsHead = record;
|
| + } else {
|
| + assert(_additionsTail._nextAddedRec == null);
|
| + assert(record._nextAddedRec == null);
|
| + _additionsTail = _additionsTail._nextAddedRec = record;
|
| + }
|
| + return record;
|
| + }
|
| +
|
| + ItemRecord _collection_insertAfter(ItemRecord record, ItemRecord prev,
|
| + int index) {
|
| + assert(record != prev);
|
| + assert(record._nextRec == null);
|
| + assert(record._prevRec == null);
|
| +
|
| + ItemRecord next = prev == null ? _collectionHead : prev._nextRec;
|
| + assert(next != record);
|
| + assert(prev != record);
|
| + record._nextRec = next;
|
| + record._prevRec = prev;
|
| + if (next == null) {
|
| + _collectionTail = record;
|
| + } else {
|
| + next._prevRec = record;
|
| + }
|
| + if (prev == null) {
|
| + _collectionHead = record;
|
| + } else {
|
| + prev._nextRec = record;
|
| + }
|
| +
|
| + _items.put(record);
|
| + record.currentIndex = index;
|
| + return record;
|
| + }
|
| +
|
| + ItemRecord _collection_remove(ItemRecord record) =>
|
| + _removals_add(_collection_unlink(record));
|
| +
|
| + ItemRecord _collection_unlink(ItemRecord record) {
|
| + _items.remove(record);
|
| +
|
| + var prev = record._prevRec;
|
| + var next = record._nextRec;
|
| +
|
| + assert((record._prevRec = null) == null);
|
| + assert((record._nextRec = null) == null);
|
| +
|
| + if (prev == null) {
|
| + _collectionHead = next;
|
| + } else {
|
| + prev._nextRec = next;
|
| + }
|
| + if (next == null) {
|
| + _collectionTail = prev;
|
| + } else {
|
| + next._prevRec = prev;
|
| + }
|
| +
|
| + return record;
|
| + }
|
| +
|
| + ItemRecord _moves_add(ItemRecord record) {
|
| + assert(record._nextMovedRec == null);
|
| + if (_movesTail == null) {
|
| + assert(_movesHead == null);
|
| + _movesTail = _movesHead = record;
|
| + } else {
|
| + assert(_movesTail._nextMovedRec == null);
|
| + _movesTail = _movesTail._nextMovedRec = record;
|
| + }
|
| +
|
| + return record;
|
| + }
|
| +
|
| + ItemRecord _removals_add(ItemRecord record) {
|
| + record.currentIndex = null;
|
| + _removedItems.put(record);
|
| +
|
| + if (_removalsTail == null) {
|
| + assert(_removalsHead == null);
|
| + _removalsTail = _removalsHead = record;
|
| + } else {
|
| + assert(_removalsTail._nextRemovedRec == null);
|
| + assert(record._nextRemovedRec == null);
|
| + record._prevRemovedRec = _removalsTail;
|
| + _removalsTail = _removalsTail._nextRemovedRec = record;
|
| + }
|
| + return record;
|
| + }
|
| +
|
| + String toString() {
|
| + ItemRecord record;
|
| +
|
| + var list = [];
|
| + record = _collectionHead;
|
| + while(record != null) {
|
| + list.add(record);
|
| + record = record._nextRec;
|
| + }
|
| +
|
| + var additions = [];
|
| + record = _additionsHead;
|
| + while(record != null) {
|
| + additions.add(record);
|
| + record = record._nextAddedRec;
|
| + }
|
| +
|
| + var moves = [];
|
| + record = _movesHead;
|
| + while(record != null) {
|
| + moves.add(record);
|
| + record = record._nextMovedRec;
|
| + }
|
| +
|
| + var removals = [];
|
| + record = _removalsHead;
|
| + while(record != null) {
|
| + removals.add(record);
|
| + record = record._nextRemovedRec;
|
| + }
|
| +
|
| + return """
|
| +collection: ${list.join(", ")}
|
| +additions: ${additions.join(", ")}
|
| +moves: ${moves.join(", ")}
|
| +removals: ${removals.join(", ")}'
|
| + """;
|
| + }
|
| +}
|
| +
|
| +class ItemRecord<V> implements CollectionItem<V>, AddedItem<V>, MovedItem<V>,
|
| + RemovedItem<V> {
|
| + int previousIndex = null;
|
| + int currentIndex = null;
|
| + V item = _INITIAL_;
|
| +
|
| + ItemRecord<V> _prevRec, _nextRec;
|
| + ItemRecord<V> _prevDupRec, _nextDupRec;
|
| + ItemRecord<V> _prevRemovedRec, _nextRemovedRec;
|
| + ItemRecord<V> _nextAddedRec, _nextMovedRec;
|
| +
|
| + CollectionItem<V> get nextCollectionItem => _nextRec;
|
| + RemovedItem<V> get nextRemovedItem => _nextRemovedRec;
|
| + AddedItem<V> get nextAddedItem => _nextAddedRec;
|
| + MovedItem<V> get nextMovedItem => _nextMovedRec;
|
| +
|
| + ItemRecord(this.item);
|
| +
|
| + String toString() => previousIndex == currentIndex
|
| + ? '$item'
|
| + : '$item[$previousIndex -> $currentIndex]';
|
| +}
|
| +
|
| +class _DuplicateItemRecordList {
|
| + ItemRecord head, tail;
|
| +
|
| + void add(ItemRecord record, ItemRecord beforeRecord) {
|
| + assert(record._prevDupRec == null);
|
| + assert(record._nextDupRec == null);
|
| + assert(beforeRecord == null ? true : beforeRecord.item == record.item);
|
| + if (head == null) {
|
| + assert(beforeRecord == null);
|
| + head = tail = record;
|
| + } else {
|
| + assert(record.item == head.item);
|
| + if (beforeRecord == null) {
|
| + tail._nextDupRec = record;
|
| + record._prevDupRec = tail;
|
| + tail = record;
|
| + } else {
|
| + var prev = beforeRecord._prevDupRec;
|
| + var next = beforeRecord;
|
| + record._prevDupRec = prev;
|
| + record._nextDupRec = next;
|
| + if (prev == null) {
|
| + head = record;
|
| + } else {
|
| + prev._nextDupRec = record;
|
| + }
|
| + next._prevDupRec = record;
|
| + }
|
| + }
|
| + }
|
| +
|
| + ItemRecord get(key, int hideIndex) {
|
| + ItemRecord record = head;
|
| + while(record != null) {
|
| + if (hideIndex == null ||
|
| + hideIndex < record.currentIndex && identical(record.item, key)) {
|
| + return record;
|
| + }
|
| + record = record._nextDupRec;
|
| + }
|
| + return record;
|
| + }
|
| +
|
| + bool remove(ItemRecord record) {
|
| + assert(() {
|
| + // verify that the record being removed is someplace in the list.
|
| + ItemRecord cursor = head;
|
| + while(cursor != null) {
|
| + if (identical(cursor, record)) return true;
|
| + cursor = cursor._nextDupRec;
|
| + }
|
| + return false;
|
| + });
|
| +
|
| + var prev = record._prevDupRec;
|
| + var next = record._nextDupRec;
|
| + if (prev == null) {
|
| + head = next;
|
| + } else {
|
| + prev._nextDupRec = next;
|
| + }
|
| + if (next == null) {
|
| + tail = prev;
|
| + } else {
|
| + next._prevDupRec = prev;
|
| + }
|
| +
|
| + assert((record._prevDupRec = null) == null);
|
| + assert((record._nextDupRec = null) == null);
|
| +
|
| + return head == null;
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * This is a custom map which supports duplicate [ItemRecord] values for each
|
| + * key.
|
| + */
|
| +class DuplicateMap {
|
| + final map = <dynamic, _DuplicateItemRecordList>{};
|
| +
|
| + void put(ItemRecord record, [ItemRecord beforeRecord = null]) {
|
| + assert(record._nextDupRec == null);
|
| + assert(record._prevDupRec == null);
|
| + map.putIfAbsent(record.item, () => new _DuplicateItemRecordList())
|
| + .add(record, beforeRecord);
|
| + }
|
| +
|
| + /**
|
| + * Retrieve the `value` using [key]. Because the [ItemRecord] value maybe one
|
| + * which we have already iterated over, we use the [hideIndex] to pretend it
|
| + * is not there.
|
| + *
|
| + * Use case: `[a, b, c, a, a]` if we are at index `3` which is the second `a`
|
| + * then asking if we have any more `a`s needs to return the last `a` not the
|
| + * first or second.
|
| + */
|
| + ItemRecord get(key, [int hideIndex]) {
|
| + _DuplicateItemRecordList recordList = map[key];
|
| + return recordList == null ? null : recordList.get(key, hideIndex);
|
| + }
|
| +
|
| + ItemRecord remove(ItemRecord record) {
|
| + _DuplicateItemRecordList recordList = map[record.item];
|
| + assert(recordList != null);
|
| + if (recordList.remove(record)) map.remove(record.item);
|
| + return record;
|
| + }
|
| +
|
| + void clear() {
|
| + map.clear();
|
| + }
|
| +}
|
|
|