| OLD | NEW |
| (Empty) | |
| 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 |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 // This library itself is undocumented and not supported for end use. |
| 6 // Because dart:html must use some of this functionality, it has to be available |
| 7 // via a dart:* library. The public APIs are reexported via package:mdv_observe. |
| 8 // Generally we try to keep this library minimal, with utility types and |
| 9 // functions in the package. |
| 10 library dart.mdv_observe_impl; |
| 11 |
| 12 import 'dart:async'; |
| 13 import 'dart:collection'; |
| 14 |
| 15 /** |
| 16 * Interface representing an observable object. This is used by data in |
| 17 * model-view architectures to notify interested parties of [changes]. |
| 18 * |
| 19 * This object does not require any specific technique to implement |
| 20 * observability. |
| 21 * |
| 22 * You can use [ObservableMixin] as a base class or mixin to implement this. |
| 23 */ |
| 24 abstract class Observable { |
| 25 /** |
| 26 * The stream of change records to this object. |
| 27 * |
| 28 * Changes should be delivered in asynchronous batches by calling |
| 29 * [queueChangeRecords]. |
| 30 * |
| 31 * [deliverChangeRecords] can be called to force delivery. |
| 32 */ |
| 33 Stream<List<ChangeRecord>> get changes; |
| 34 |
| 35 // TODO(jmesserly): remove these ASAP. |
| 36 /** |
| 37 * *Warning*: this method is temporary until dart2js supports mirrors. |
| 38 * Gets the value of a field or index. This should return null if it was |
| 39 * not found. |
| 40 */ |
| 41 getValueWorkaround(key); |
| 42 |
| 43 /** |
| 44 * *Warning*: this method is temporary until dart2js supports mirrors. |
| 45 * Sets the value of a field or index. This should have no effect if the field |
| 46 * was not found. |
| 47 */ |
| 48 void setValueWorkaround(key, Object value); |
| 49 } |
| 50 |
| 51 /** |
| 52 * Base class implementing [Observable]. |
| 53 * |
| 54 * When a field, property, or indexable item is changed, a derived class should |
| 55 * call [notifyPropertyChange]. See that method for an example. |
| 56 */ |
| 57 typedef ObservableBase = Object with ObservableMixin; |
| 58 |
| 59 /** |
| 60 * Mixin for implementing [Observable] objects. |
| 61 * |
| 62 * When a field, property, or indexable item is changed, a derived class should |
| 63 * call [notifyPropertyChange]. See that method for an example. |
| 64 */ |
| 65 abstract class ObservableMixin implements Observable { |
| 66 StreamController<List<ChangeRecord>> _observers; |
| 67 Stream<List<ChangeRecord>> _stream; |
| 68 List<ChangeRecord> _changes; |
| 69 |
| 70 Stream<List<ChangeRecord>> get changes { |
| 71 if (_observers == null) { |
| 72 _observers = new StreamController<List<ChangeRecord>>(); |
| 73 _stream = _observers.stream.asBroadcastStream(); |
| 74 } |
| 75 return _stream; |
| 76 } |
| 77 |
| 78 void _deliverChanges() { |
| 79 var changes = _changes; |
| 80 _changes = null; |
| 81 if (hasObservers && changes != null) { |
| 82 // TODO(jmesserly): make "changes" immutable |
| 83 _observers.add(changes); |
| 84 } |
| 85 } |
| 86 |
| 87 /** |
| 88 * True if this object has any observers, and should call |
| 89 * [notifyPropertyChange] for changes. |
| 90 */ |
| 91 bool get hasObservers => _observers != null && _observers.hasListener; |
| 92 |
| 93 /** |
| 94 * Notify that the field [name] of this object has been changed. |
| 95 * |
| 96 * The [oldValue] and [newValue] are also recorded. If the two values are |
| 97 * identical, no change will be recorded. |
| 98 * |
| 99 * For convenience this returns [newValue]. This makes it easy to use in a |
| 100 * setter: |
| 101 * |
| 102 * var _myField; |
| 103 * get myField => _myField; |
| 104 * set myField(value) { |
| 105 * _myField = notifyPropertyChange( |
| 106 * const Symbol('myField'), _myField, value); |
| 107 * } |
| 108 */ |
| 109 // TODO(jmesserly): should this be == instead of identical, to prevent |
| 110 // spurious loops? |
| 111 notifyPropertyChange(Symbol field, Object oldValue, Object newValue) { |
| 112 if (hasObservers && !identical(oldValue, newValue)) { |
| 113 notifyChange(new PropertyChangeRecord(field)); |
| 114 } |
| 115 return newValue; |
| 116 } |
| 117 |
| 118 /** |
| 119 * Notify observers of a change. For most objects [notifyPropertyChange] is |
| 120 * more convenient, but collections sometimes deliver other types of changes |
| 121 * such as a [ListChangeRecord]. |
| 122 */ |
| 123 void notifyChange(ChangeRecord record) { |
| 124 if (!hasObservers) return; |
| 125 |
| 126 if (_changes == null) { |
| 127 _changes = []; |
| 128 queueChangeRecords(_deliverChanges); |
| 129 } |
| 130 _changes.add(record); |
| 131 } |
| 132 } |
| 133 |
| 134 |
| 135 /** Records a change to an [Observable]. */ |
| 136 abstract class ChangeRecord { |
| 137 /** True if the change affected the given item, otherwise false. */ |
| 138 bool change(key); |
| 139 } |
| 140 |
| 141 /** A change record to a field of an observable object. */ |
| 142 class PropertyChangeRecord extends ChangeRecord { |
| 143 /** The field that was changed. */ |
| 144 final Symbol field; |
| 145 |
| 146 PropertyChangeRecord(this.field); |
| 147 |
| 148 bool changes(key) => key is Symbol && field == key; |
| 149 |
| 150 String toString() => '#<PropertyChangeRecord $field>'; |
| 151 } |
| 152 |
| 153 /** A change record for an observable list. */ |
| 154 class ListChangeRecord extends ChangeRecord { |
| 155 /** The starting index of the change. */ |
| 156 final int index; |
| 157 |
| 158 /** The number of items removed. */ |
| 159 final int removedCount; |
| 160 |
| 161 /** The number of items added. */ |
| 162 final int addedCount; |
| 163 |
| 164 ListChangeRecord(this.index, {this.removedCount: 0, this.addedCount: 0}) { |
| 165 if (addedCount == 0 && removedCount == 0) { |
| 166 throw new ArgumentError('added and removed counts should not both be ' |
| 167 'zero. Use 1 if this was a single item update.'); |
| 168 } |
| 169 } |
| 170 |
| 171 /** Returns true if the provided index was changed by this operation. */ |
| 172 bool changes(key) { |
| 173 // If key isn't an int, or before the index, then it wasn't changed. |
| 174 if (key is! int || key < index) return false; |
| 175 |
| 176 // If this was a shift operation, anything after index is changed. |
| 177 if (addedCount != removedCount) return true; |
| 178 |
| 179 // Otherwise, anything in the update range was changed. |
| 180 return key < index + addedCount; |
| 181 } |
| 182 |
| 183 String toString() => '#<ListChangeRecord index: $index, ' |
| 184 'removed: $removedCount, addedCount: $addedCount>'; |
| 185 } |
| 186 |
| 187 /** |
| 188 * Synchronously deliver [Observable.changes] for all observables. |
| 189 * If new changes are added as a result of delivery, this will keep running |
| 190 * until all pending change records are delivered. |
| 191 */ |
| 192 // TODO(jmesserly): this is a bit different from the ES Harmony version, which |
| 193 // allows delivery of changes to a particular observer: |
| 194 // http://wiki.ecmascript.org/doku.php?id=harmony:observe#object.deliverchangere
cords |
| 195 // However the binding system needs delivery of everything, along the lines of: |
| 196 // https://github.com/toolkitchen/mdv/blob/stable/src/model.js#L19 |
| 197 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js#L590 |
| 198 // TODO(jmesserly): in the future, we can use this to trigger dirty checking. |
| 199 void deliverChangeRecords() { |
| 200 if (_deliverCallbacks == null) return; |
| 201 |
| 202 while (!_deliverCallbacks.isEmpty) { |
| 203 var deliver = _deliverCallbacks.removeFirst(); |
| 204 |
| 205 try { |
| 206 deliver(); |
| 207 } catch (e, s) { |
| 208 // Schedule the error to be top-leveled later. |
| 209 new Completer().completeError(e, s); |
| 210 } |
| 211 } |
| 212 |
| 213 // Null it out, so [queueChangeRecords] will reschedule this method. |
| 214 _deliverCallbacks = null; |
| 215 } |
| 216 |
| 217 /** Queues an action to happen during the [deliverChangeRecords] timeslice. */ |
| 218 void queueChangeRecords(void deliverChanges()) { |
| 219 if (_deliverCallbacks == null) { |
| 220 _deliverCallbacks = new Queue<Function>(); |
| 221 runAsync(deliverChangeRecords); |
| 222 } |
| 223 _deliverCallbacks.add(deliverChanges); |
| 224 } |
| 225 |
| 226 Queue _deliverCallbacks; |
| OLD | NEW |