Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(56)

Side by Side Diff: sdk/lib/observe/observable.dart

Issue 14732003: Implement Model-Driven-Views spec for Dart (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: small fix Created 7 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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;
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698