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], | |
| 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}) => | |
|
floitsch
2013/05/06 17:37:46
This is non-extensible and doesn't really fit. It
Jennifer Messerly
2013/05/07 05:43:38
Done -> moved to package:observe.
For what it's w
| |
| 22 deep ? _toObservableDeep(value) : _toObservableShallow(value); | |
| 23 | |
| 24 _toObserveShallow(value) { | |
| 25 if (value is Observable) return value; | |
| 26 if (value is Map) return new ObservableMap.from(value); | |
| 27 if (value is Iterable) return new ObservableList.from(value); | |
| 28 return value; | |
| 29 } | |
| 30 | |
| 31 _toObservableDeep(value) { | |
| 32 if (value is Observable) return value; | |
| 33 if (value is Map) { | |
| 34 var result = new ObservableMap._createFromType(value); | |
| 35 value.forEach((k, v) { | |
| 36 result[_toObservableDeep(k)] = _toObservableDeep(v); | |
| 37 }); | |
| 38 return result; | |
| 39 } | |
| 40 if (value is Iterable) { | |
| 41 return new ObservableList.from(value.map(_toObservableDeep)); | |
| 42 } | |
| 43 return value; | |
| 44 } | |
| 45 | |
| 46 | |
| 47 /** | |
| 48 * Interface representing an observable object. This is used by data in | |
| 49 * model-view architectures to notify interested parties of [changes]. | |
| 50 * | |
| 51 * This object does not require any specific technique to implement | |
| 52 * observability. | |
| 53 * | |
| 54 * You can use [ObservableMixin] as a base class or mixin to implement this. | |
|
floitsch
2013/05/06 17:37:46
Don't make ObservableMixin a base class. Either pr
Jennifer Messerly
2013/05/07 05:43:38
Done, added ObservableBase.
I'm curious though, c
floitsch
2013/05/07 14:46:48
They can have different semantics. A mixin might d
| |
| 55 */ | |
| 56 abstract class Observable { | |
| 57 // TODO(jmesserly): should this be synchronous, and allow libraries to build | |
| 58 // the async batching? | |
|
floitsch
2013/05/06 17:37:46
I would prefer if streams stay asynchronous. I jus
Jennifer Messerly
2013/05/07 05:43:38
Ah, interesting. Would StreamController.add also b
floitsch
2013/05/07 14:46:48
Yes. This is planned.
| |
| 59 /** | |
| 60 * The stream of change records to this object. | |
| 61 * | |
| 62 * Changes should be delivered in asynchronous batches by calling | |
| 63 * [queueChangeRecords]. | |
| 64 * [deliverChangeRecords] can be called to force delivery. | |
| 65 */ | |
| 66 Stream<List<ChangeRecord>> get changes; | |
|
floitsch
2013/05/06 17:37:46
Why is this not just a Stream<ChangeRecord> ?
Jennifer Messerly
2013/05/07 05:43:38
It's important to get a batch of changes, so you c
floitsch
2013/05/07 14:46:48
But wouldn't it be the task of a transformer to gr
Jennifer Messerly
2013/05/09 18:01:29
Yeah, as long as such a thing can be built, that w
| |
| 67 | |
| 68 // TODO(jmesserly): remove these ASAP. | |
| 69 /** | |
| 70 * *Warning*: this method is temporary until dart2js supports mirrors. | |
| 71 * Gets the value of a field or index. This should return null if it was | |
| 72 * not found. | |
| 73 */ | |
| 74 getValue(key); | |
| 75 | |
| 76 /** | |
| 77 * *Warning*: this method is temporary until dart2js supports mirrors. | |
| 78 * Sets the value of a field or index. This should have no effect if the field | |
| 79 * was not found. | |
| 80 */ | |
| 81 void setValue(key, Object value); | |
| 82 } | |
| 83 | |
| 84 /** | |
| 85 * Mixin for implementing [Observable]. | |
| 86 * | |
| 87 * When a field, property, or indexable item is changed, a derived class should | |
| 88 * call [notifyChange]. See that method for an example. | |
| 89 */ | |
| 90 abstract class ObservableMixin implements Observable { | |
| 91 // TODO(jmesserly): this has way too much overhead. We probably need our own | |
| 92 // stream. | |
| 93 StreamController<List<ChangeRecord>> _observers; | |
| 94 Stream<List<ChangeRecord>> _stream; | |
| 95 List<ChangeRecord> _changes; | |
| 96 | |
| 97 Stream<List<ChangeRecord>> get changes { | |
| 98 if (_observers == null) { | |
| 99 _observers = new StreamController<List<ChangeRecord>>(); | |
| 100 _stream = _observers.stream.asBroadcastStream(); | |
| 101 } | |
| 102 return _stream; | |
| 103 } | |
| 104 | |
| 105 void _deliverChanges() { | |
| 106 var changes = _changes; | |
| 107 _changes = null; | |
| 108 if (hasObservers && changes != null) { | |
| 109 // TODO(jmesserly): make "changes" immutable | |
| 110 _observers.add(changes); | |
| 111 } | |
| 112 } | |
| 113 | |
| 114 /** | |
| 115 * True if this object has any observers, and should call [notifyChange] for | |
| 116 * changes. | |
| 117 */ | |
| 118 bool get hasObservers => _observers != null && _observers.hasListener; | |
| 119 | |
| 120 /** | |
| 121 * Notify that a [key] of this object has been changed. | |
| 122 * | |
| 123 * The key can also represent a field or indexed value of the object or list. | |
| 124 * The [kind] is one of the constants [ChangeRecord.INDEX], | |
| 125 * [ChangeRecord.FIELD], [ChangeRecord.INSERT], or [ChangeRecord.REMOVE]. | |
| 126 * | |
| 127 * The [oldValue] and [newValue] are also recorded. If the change wasn't an | |
| 128 * insert or remove, and the two values are equal, no change will be recorded. | |
| 129 * For INSERT, oldValue should be null. For REMOVE, newValue should be null. | |
| 130 * | |
| 131 * For convenience this returns [newValue]. This makes it easy to use in a | |
| 132 * setter: | |
| 133 * | |
| 134 * var _someField; | |
| 135 * get someField => _someField; | |
| 136 * set someField(value) { | |
| 137 * _someField = notifyChange('someField', _someField, value); | |
| 138 * } | |
| 139 */ | |
| 140 Object notifyChange(key, Object oldValue, Object newValue, | |
| 141 {int kind: ChangeRecord.FIELD}) { | |
| 142 | |
| 143 if (!hasObservers) return newValue; | |
| 144 | |
| 145 // If this is an assignment (and not insert/remove) then check if | |
| 146 // the value actually changed. If not don't signal a change event. | |
| 147 // This helps programmers avoid some common cases of cycles in their code. | |
| 148 if ((kind & (ChangeRecord.INSERT | ChangeRecord.REMOVE)) == 0) { | |
| 149 if (oldValue == newValue) return newValue; | |
|
Lasse Reichstein Nielsen
2013/05/06 10:58:58
Consider using "identical" instead. That won't mak
Jennifer Messerly
2013/05/07 05:43:38
Done.
blois
2013/05/07 16:04:49
We should have a clear contract on what constitute
| |
| 150 } | |
| 151 | |
| 152 if (_changes == null) { | |
| 153 _changes = []; | |
| 154 queueChangeRecords(_deliverChanges); | |
| 155 } | |
| 156 _changes.add(new ChangeRecord(key, oldValue, newValue, kind: kind)); | |
| 157 return newValue; | |
| 158 } | |
| 159 } | |
| 160 | |
| 161 /** Records a change to an [Observable]. */ | |
| 162 class ChangeRecord { | |
|
floitsch
2013/05/06 17:37:46
I'm ok with Observable being a Stream of ChangeRec
Jennifer Messerly
2013/05/07 05:43:38
Agreed. I will try and split it:
* ObjectChangeRe
floitsch
2013/05/07 14:46:48
fine with later. I don't like the "ObjectChangeRec
Jennifer Messerly
2013/05/09 18:01:29
Funny, I had the same thought last night after I w
| |
| 163 // Note: the target object is omitted because it makes it difficult | |
| 164 // to proxy change records if you're using an observable kind to aid | |
| 165 // your implementation. | |
| 166 // However: if we allow one observer to get batched changes for multiple | |
| 167 // objects, we'll need to add target. | |
| 168 | |
| 169 // Note: kind values were chosen for easy masking in the observable expression | |
| 170 // implementation. However in [kind] it will only have one value. | |
| 171 | |
| 172 // TODO(jmesserly): is there any value in keeping FIELD and INDEX distinct? | |
| 173 /** [kind] denoting set of a field. */ | |
| 174 static const FIELD = 1; | |
| 175 | |
| 176 /** [kind] denoting an in-place update event using `[]=`. */ | |
| 177 static const INDEX = 2; | |
| 178 | |
| 179 /** | |
| 180 * [kind] denoting an insertion into a list. Insertions prepend in front of | |
| 181 * the given index, so insert at 0 means an insertion at the beginning of the | |
| 182 * list. The index will be provided in [key]. | |
| 183 */ | |
| 184 static const INSERT = INDEX | 4; | |
| 185 | |
| 186 /** [kind] denoting a remove from a list. */ | |
| 187 static const REMOVE = INDEX | 8; | |
| 188 | |
| 189 /** Whether the change was a [FIELD], [INDEX], [INSERT], or [REMOVE]. */ | |
| 190 final int kind; | |
| 191 | |
| 192 // TODO(jmesserly): for fields, is key a String or Symbol? Right now it's a | |
| 193 // String. | |
| 194 /** | |
| 195 * The key that changed. The value depends on the [kind] of change: | |
| 196 * | |
| 197 * - [FIELD]: the field name that was set. | |
| 198 * - [INDEX], [INSERT], and [REMOVE]: the index or key that was changed. | |
| 199 * This will be an integer for [ObservableList] but can be anything for | |
| 200 * [ObservableMap]. | |
| 201 */ | |
| 202 final key; | |
| 203 | |
| 204 /** The previous value of the member. */ | |
|
Lasse Reichstein Nielsen
2013/05/06 10:58:58
Will this be null for an insert?
Jennifer Messerly
2013/05/07 05:43:38
That's what I had thought. It turns out for a List
| |
| 205 final oldValue; | |
| 206 | |
| 207 /** The new value of the member. */ | |
| 208 final newValue; | |
|
Lasse Reichstein Nielsen
2013/05/06 10:58:58
Will this be null for a remove?
Jennifer Messerly
2013/05/07 05:43:38
yes
| |
| 209 | |
| 210 ChangeRecord(this.key, this.oldValue, this.newValue, | |
| 211 {this.kind: ChangeRecord.FIELD}); | |
| 212 | |
| 213 // Note: these two methods are here mainly to make testing easier. | |
| 214 bool operator ==(other) { | |
| 215 return other is ChangeRecord && kind == other.kind && key == other.key && | |
|
Lasse Reichstein Nielsen
2013/05/06 10:58:58
Consider using identical for the tests. At least t
Jennifer Messerly
2013/05/07 05:43:38
I removed == and hashCode
| |
| 216 oldValue == other.oldValue && newValue == other.newValue; | |
| 217 } | |
| 218 | |
| 219 int get hashCode => _hash4(kind, key, oldValue, newValue); | |
| 220 | |
| 221 String toString() { | |
| 222 // TODO(jmesserly): const map would be nice here, but it must be string | |
| 223 // literal :( | |
| 224 String typeStr; | |
| 225 switch (kind) { | |
| 226 case FIELD: typeStr = 'field'; break; | |
| 227 case INDEX: typeStr = 'index'; break; | |
| 228 case INSERT: typeStr = 'insert'; break; | |
| 229 case REMOVE: typeStr = 'remove'; break; | |
| 230 } | |
| 231 return '#<ChangeRecord $typeStr $key from $oldValue to $newValue>'; | |
| 232 } | |
| 233 } | |
| 234 | |
| 235 // TODO(jmesserly): helpers to combine hash codes. Reuse these from somewhere. | |
| 236 int _hash2(x, y) => x.hashCode * 31 + y.hashCode; | |
| 237 | |
| 238 int _hash3(x, y, z) => _hash2(_hash2(x, y), z); | |
|
Lasse Reichstein Nielsen
2013/05/06 10:58:58
Unused?
Jennifer Messerly
2013/05/07 05:43:38
Done.
| |
| 239 | |
| 240 int _hash4(w, x, y, z) => _hash2(_hash2(w, x), _hash2(y, z)); | |
|
Lasse Reichstein Nielsen
2013/05/06 10:58:58
This gives x and y the same multiplier (31), resul
floitsch
2013/05/06 17:37:46
_hash2(w, _hash2(x, _hash2(y, z))) should solve th
Jennifer Messerly
2013/05/07 05:43:38
removed hash helpers :)
it would be great to have
Lasse Reichstein Nielsen
2013/05/07 07:35:45
It doesn't parallelize as well, but
_hash2(_hash2
| |
| 241 | |
| 242 | |
| 243 /** | |
| 244 * Synchronously deliver [Observable.changes] for all observables. | |
| 245 * If new changes are added as a result of delivery, this will keep running | |
| 246 * until all pending change records are delivered. | |
| 247 */ | |
| 248 // TODO(jmesserly): this is a bit different from the ES Harmony version, which | |
| 249 // allows delivery of changes to a particular observer: | |
| 250 // http://wiki.ecmascript.org/doku.php?id=harmony:observe#object.deliverchangere cords | |
| 251 // However the binding system needs delivery of everything, along the lines of: | |
| 252 // https://github.com/toolkitchen/mdv/blob/stable/src/model.js#L19 | |
| 253 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js#L590 | |
| 254 // TODO(jmesserly): in the future, we can use this to trigger dirty checking. | |
| 255 void deliverChangeRecords() { | |
|
floitsch
2013/05/06 17:37:46
This looks completely wrong.
Why is there global
Jennifer Messerly
2013/05/07 05:43:38
It does to me too :)
The capability is needed, but
floitsch
2013/05/07 14:46:48
I would rather add support for adding something to
| |
| 256 if (_deliverCallbacks == null) return; | |
| 257 | |
| 258 while (_deliverCallbacks.length > 0) { | |
| 259 var deliverCallbacks = _deliverCallbacks; | |
|
Lasse Reichstein Nielsen
2013/05/06 10:58:58
Consider using a Queue instead to avoid doing the
Jennifer Messerly
2013/05/07 05:43:38
Done.
| |
| 260 // Use empty list so [queueChangeRecords] don't reschedule this method. | |
| 261 _deliverCallbacks = []; | |
| 262 | |
| 263 for (var deliver in deliverCallbacks) { | |
| 264 try { | |
| 265 deliver(); | |
| 266 } catch (e, s) { | |
| 267 // Schedule the error to be top-leveled later. | |
| 268 new Completer().completeError(e, s); | |
| 269 } | |
| 270 } | |
| 271 } | |
| 272 | |
| 273 // Use null list so [queueChangeRecords] will reschedule this method. | |
| 274 _deliverCallbacks = null; | |
| 275 } | |
| 276 | |
| 277 /** Queues an action to happen during the [deliverChangeRecords] timeslice. */ | |
| 278 void queueChangeRecords(void deliverChanges()) { | |
| 279 if (_deliverCallbacks == null) { | |
| 280 _deliverCallbacks = []; | |
| 281 runAsync(deliverChangeRecords); | |
| 282 } | |
| 283 _deliverCallbacks.add(deliverChanges); | |
| 284 } | |
| 285 | |
| 286 List<Function> _deliverCallbacks; | |
| OLD | NEW |