Chromium Code Reviews| 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 part of dart.observe; | |
| 6 | |
| 7 /** | |
| 8 * Converts the [Iterable] or [Map] to an [ObservableList] or [ObservableMap], | |
|
Siggi Cherem (dart-lang)
2013/05/01 18:57:56
any reason not to bring over ObservableSet?
Is the
Jennifer Messerly
2013/05/02 02:58:33
It's just more work. Honestly I had it but ported
| |
| 9 * respectively. | |
| 10 * | |
| 11 * If [value] is not one of those collection types, or is already [Observable], | |
| 12 * it will be returned unmodified. | |
| 13 * | |
| 14 * If [value] is a [Map], the resulting value will use the appropriate kind of | |
| 15 * backing map: either [HashMap], [LinkedHashMap], or [SplayTreeMap]. | |
| 16 * | |
| 17 * By default this performs a deep conversion, but you can set [deep] to false | |
| 18 * for a shallow conversion. This does not handle circular data structures. | |
| 19 */ | |
| 20 // TODO(jmesserly): ObservableSet? | |
| 21 toObservable(value, {bool deep: true}) { | |
|
blois
2013/05/01 17:00:42
Are there explicit scenarios for a deep observable
Jennifer Messerly
2013/05/01 17:56:34
Any time you have a array/map literal. It's the mo
| |
| 22 if (deep) return _deepObserve(value); | |
| 23 if (value is Observable) return value; | |
|
Siggi Cherem (dart-lang)
2013/05/01 18:57:56
nit: consider swapping the above two lines. I know
Jennifer Messerly
2013/05/02 02:58:33
yeah, mixing the deep and shallow stuff in the sam
Siggi Cherem (dart-lang)
2013/05/02 16:21:08
looks good. Thanks!
| |
| 24 if (value is Map) return new ObservableMap.from(value); | |
| 25 if (value is Iterable) return new ObservableList.from(value); | |
| 26 return value; | |
| 27 } | |
| 28 | |
| 29 _deepObserve(value) { | |
| 30 if (value is Observable) return value; | |
| 31 if (value is Map) { | |
| 32 var result = new ObservableMap._createFromType(value); | |
| 33 value.forEach((k, v) { result[_deepObserve(k)] = _deepObserve(v); }); | |
| 34 return result; | |
| 35 } | |
| 36 if (value is Iterable) { | |
| 37 return new ObservableList.from(value.map(_deepObserve)); | |
| 38 } | |
| 39 return value; | |
| 40 } | |
| 41 | |
| 42 | |
| 43 /** | |
| 44 * Interface representing an observable object. This is used by data in | |
| 45 * model-view architectures to notify interested parties of [changes]. | |
| 46 * | |
| 47 * This object does not require any specific technique to implement | |
| 48 * observability. | |
| 49 * | |
| 50 * You can use [ObservableMixin] as a base class or mixin to implement this. | |
| 51 */ | |
| 52 abstract class Observable { | |
| 53 // TODO(jmesserly): should this be synchronous, and allow libraries to build | |
| 54 // the async batching? | |
| 55 /** | |
| 56 * The stream of change records to this object. | |
| 57 * | |
| 58 * Changes should be delivered in asynchronous batches by calling | |
| 59 * [queueChangeRecords]. | |
| 60 * [deliverChangeRecords] can be called to force delivery. | |
| 61 */ | |
| 62 Stream<List<ChangeRecord>> get changes; | |
|
Siggi Cherem (dart-lang)
2013/05/01 18:57:56
cool that you got these working!
Jennifer Messerly
2013/05/02 02:58:33
yeah. Kinda worried about overhead, but I guess we
| |
| 63 | |
| 64 // TODO(jmesserly): remove these ASAP. | |
|
Siggi Cherem (dart-lang)
2013/05/01 18:57:56
should we preemptively mark them @deprecated ?
Jennifer Messerly
2013/05/02 02:58:33
yeah, that would be cool. The trouble is: @observa
Siggi Cherem (dart-lang)
2013/05/02 16:21:08
I see, makes sense to wait.
| |
| 65 /** | |
| 66 * *Warning*: this method is temporary until dart2js supports mirrors. | |
| 67 * Gets the value of a field or index. This should return null if it was | |
| 68 * not found. | |
| 69 */ | |
| 70 getValue(key); | |
| 71 | |
| 72 /** | |
| 73 * *Warning*: this method is temporary until dart2js supports mirrors. | |
| 74 * Sets the value of a field or index. This should have no effect if the field | |
| 75 * was not found. | |
| 76 */ | |
| 77 void setValue(key, Object value); | |
| 78 } | |
| 79 | |
| 80 /** | |
| 81 * Mixin for implementing [Observable]. | |
| 82 * | |
| 83 * When a field, property, or indexable item is changed, a derived class should | |
| 84 * call [notifyChange]. See that method for an example. | |
| 85 */ | |
| 86 abstract class ObservableMixin implements Observable { | |
| 87 // TODO(jmesserly): this has way too much overhead. We probably need our own | |
| 88 // stream. | |
| 89 StreamController<List<ChangeRecord>> _observers; | |
| 90 Stream<List<ChangeRecord>> _stream; | |
| 91 List<ChangeRecord> _changes; | |
| 92 | |
| 93 Stream<List<ChangeRecord>> get changes { | |
| 94 if (_observers == null) { | |
| 95 _observers = new StreamController<List<ChangeRecord>>(); | |
| 96 _stream = _observers.stream.asBroadcastStream(); | |
| 97 } | |
| 98 return _stream; | |
| 99 } | |
| 100 | |
| 101 void _deliverChanges() { | |
| 102 var changes = _changes; | |
| 103 _changes = null; | |
| 104 if (_observers != null && changes != null) { | |
|
blois
2013/05/01 17:00:42
hasObservers instead?
Jennifer Messerly
2013/05/02 02:58:33
Done.
| |
| 105 // TODO(jmesserly): make "changes" immutable | |
| 106 _observers.add(changes); | |
| 107 } | |
| 108 } | |
| 109 | |
| 110 /** | |
| 111 * True if this object has any observers, and should call [notifyChange] for | |
| 112 * changes. | |
| 113 */ | |
| 114 bool get hasObservers => _observers != null && _observers.hasListener; | |
| 115 | |
| 116 /** | |
| 117 * Notify that a [key] of this object has been changed. | |
| 118 * | |
| 119 * The key can also represent a field or indexed value of the object or list. | |
| 120 * The [kind] is one of the constants [ChangeRecord.INDEX], | |
| 121 * [ChangeRecord.FIELD], [ChangeRecord.INSERT], or [ChangeRecord.REMOVE]. | |
| 122 * | |
| 123 * The [oldValue] and [newValue] are also recorded. If the change wasn't an | |
| 124 * insert or remove, and the two values are equal, no change will be recorded. | |
| 125 * For INSERT, oldValue should be null. For REMOVE, newValue should be null. | |
| 126 * | |
| 127 * For convenience this returns [newValue]. This makes it easy to use in a | |
| 128 * setter: | |
| 129 * | |
| 130 * var _someField; | |
| 131 * get someField => _someField; | |
| 132 * set someField(value) { | |
| 133 * _someField = notifyChange('someField', _someField, value); | |
| 134 * } | |
| 135 */ | |
| 136 Object notifyChange(key, Object oldValue, Object newValue, | |
|
blois
2013/05/01 17:00:42
Seems like this is a very common method to call, a
Jennifer Messerly
2013/05/01 17:56:34
Oh, I didn't know they had overhead. Is that true
Siggi Cherem (dart-lang)
2013/05/01 18:57:56
given that there is only one optional argument, ho
Jennifer Messerly
2013/05/02 02:58:33
positional would be fine too. does that have overh
| |
| 137 {int kind: ChangeRecord.FIELD}) { | |
| 138 | |
| 139 if (!hasObservers) return newValue; | |
| 140 | |
| 141 // If this is an assignment (and not insert/remove) then check if | |
| 142 // the value actually changed. If not don't signal a change event. | |
| 143 // This helps programmers avoid some common cases of cycles in their code. | |
| 144 if ((kind & (ChangeRecord.INSERT | ChangeRecord.REMOVE)) == 0) { | |
| 145 if (oldValue == newValue) return newValue; | |
| 146 } | |
| 147 | |
| 148 if (_changes == null) { | |
| 149 _changes = []; | |
| 150 queueChangeRecords(_deliverChanges); | |
| 151 } | |
| 152 _changes.add(new ChangeRecord(key, oldValue, newValue, kind: kind)); | |
| 153 return newValue; | |
| 154 } | |
| 155 } | |
| 156 | |
| 157 /** Records a change to an [Observable]. */ | |
| 158 class ChangeRecord { | |
| 159 // Note: the target object is omitted because it makes it difficult | |
| 160 // to proxy change records if you're using an observable kind to aid | |
| 161 // your implementation. | |
| 162 // However: if we allow one observer to get batched changes for multiple | |
| 163 // objects, we'll need to add target. | |
| 164 | |
| 165 // Note: kind values were chosen for easy masking in the observable expression | |
| 166 // implementation. However in [kind] it will only have one value. | |
| 167 | |
| 168 // TODO(jmesserly): is there any value in keeping FIELD and INDEX distinct? | |
| 169 /** [kind] denoting set of a field. */ | |
| 170 static const FIELD = 1; | |
| 171 | |
| 172 /** [kind] denoting an in-place update event using `[]=`. */ | |
| 173 static const INDEX = 2; | |
| 174 | |
| 175 /** | |
| 176 * [kind] denoting an insertion into a list. Insertions prepend in front of | |
| 177 * the given index, so insert at 0 means an insertion at the beginning of the | |
| 178 * list. The index will be provided in [key]. | |
| 179 */ | |
| 180 static const INSERT = INDEX | 4; | |
| 181 | |
| 182 /** [kind] denoting a remove from a list. */ | |
| 183 static const REMOVE = INDEX | 8; | |
| 184 | |
| 185 /** Whether the change was a [FIELD], [INDEX], [INSERT], or [REMOVE]. */ | |
| 186 final int kind; | |
| 187 | |
| 188 // TODO(jmesserly): for fields, is key a String or Symbol? Right now it's a | |
| 189 // String. | |
| 190 /** | |
| 191 * The key that changed. The value depends on the [kind] of change: | |
| 192 * | |
| 193 * - [FIELD]: the field name that was set. | |
| 194 * - [INDEX], [INSERT], and [REMOVE]: the index or key that was changed. | |
| 195 * This will be an integer for [ObservableList] but can be anything for | |
| 196 * [ObservableMap]. | |
| 197 */ | |
| 198 final key; | |
| 199 | |
| 200 /** The previous value of the member. */ | |
| 201 final oldValue; | |
| 202 | |
| 203 /** The new value of the member. */ | |
| 204 final newValue; | |
| 205 | |
| 206 ChangeRecord(this.key, this.oldValue, this.newValue, | |
| 207 {this.kind: ChangeRecord.FIELD}); | |
|
Siggi Cherem (dart-lang)
2013/05/01 18:57:56
I assume the same argument of notifyChange applies
Jennifer Messerly
2013/05/02 02:58:33
I guess. Personally I'd rather not micro optimize
| |
| 208 | |
| 209 // Note: these two methods are here mainly to make testing easier. | |
| 210 bool operator ==(other) { | |
| 211 return other is ChangeRecord && kind == other.kind && key == other.key && | |
| 212 oldValue == other.oldValue && newValue == other.newValue; | |
| 213 } | |
| 214 | |
| 215 int get hashCode => _hash4(kind, key, oldValue, newValue); | |
| 216 | |
| 217 String toString() { | |
| 218 // TODO(jmesserly): const map would be nice here, but it must be string | |
| 219 // literal :( | |
| 220 String typeStr; | |
| 221 switch (kind) { | |
| 222 case FIELD: typeStr = 'field'; break; | |
| 223 case INDEX: typeStr = 'index'; break; | |
| 224 case INSERT: typeStr = 'insert'; break; | |
| 225 case REMOVE: typeStr = 'remove'; break; | |
| 226 } | |
| 227 return '#<ChangeRecord $typeStr $key from $oldValue to $newValue>'; | |
| 228 } | |
| 229 } | |
| 230 | |
| 231 // TODO(jmesserly): helpers to combine hash codes. Reuse these from somewhere. | |
| 232 int _hash2(x, y) => x.hashCode * 31 + y.hashCode; | |
| 233 | |
| 234 int _hash3(x, y, z) => _hash2(_hash2(x, y), z); | |
| 235 | |
| 236 int _hash4(w, x, y, z) => _hash2(_hash2(w, x), _hash2(y, z)); | |
| 237 | |
| 238 | |
| 239 /** | |
| 240 * Synchronously deliver [Observable.changes] for all observables. | |
| 241 * If new changes are added as a result of delivery, this will keep running | |
| 242 * until all pending change records are delivered. | |
| 243 */ | |
| 244 // TODO(jmesserly): this is a bit different from the ES Harmony version, which | |
| 245 // allows delivery of changes to a particular observer: | |
| 246 // http://wiki.ecmascript.org/doku.php?id=harmony:observe#object.deliverchangere cords | |
| 247 // However the binding system needs delivery of everything, along the lines of: | |
| 248 // https://github.com/toolkitchen/mdv/blob/stable/src/model.js#L19 | |
| 249 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js#L590 | |
| 250 // TODO(jmesserly): in the future, we can use this to trigger dirty checking. | |
| 251 void deliverChangeRecords() { | |
| 252 if (_deliverCallbacks == null) return; | |
| 253 | |
| 254 while (_deliverCallbacks.length > 0) { | |
| 255 var deliverCallbacks = _deliverCallbacks; | |
| 256 // Use empty list so [queueChangeRecords] don't reschedule this method. | |
| 257 _deliverCallbacks = []; | |
| 258 | |
| 259 for (var deliver in deliverCallbacks) { | |
| 260 try { | |
| 261 deliver(); | |
| 262 } catch (e, s) { | |
| 263 // Schedule the error to be top-leveled later. | |
| 264 new Completer().completeError(e, s); | |
| 265 } | |
| 266 } | |
| 267 } | |
| 268 | |
| 269 // Use null list so [queueChangeRecords] will reschedule this method. | |
| 270 _deliverCallbacks = null; | |
| 271 } | |
| 272 | |
| 273 /** Queues an action to happen during the [deliverChangeRecords] timeslice. */ | |
| 274 void queueChangeRecords(void deliverChanges()) { | |
| 275 if (_deliverCallbacks == null) { | |
| 276 _deliverCallbacks = []; | |
| 277 new Future(deliverChangeRecords); | |
|
Siggi Cherem (dart-lang)
2013/05/01 18:57:56
how about:
runAsync(deliverChangeRecords)
?
sinc
Jennifer Messerly
2013/05/02 02:58:33
Thanks! I didn't know about runAsync.
| |
| 278 } | |
| 279 _deliverCallbacks.add(deliverChanges); | |
| 280 } | |
| 281 | |
| 282 List<Function> _deliverCallbacks; | |
| OLD | NEW |