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

Unified 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: trying upload again Created 7 years, 8 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 side-by-side diff with in-line comments
Download patch
Index: sdk/lib/observe/observable.dart
diff --git a/sdk/lib/observe/observable.dart b/sdk/lib/observe/observable.dart
new file mode 100644
index 0000000000000000000000000000000000000000..7cb94f8dbfb5e8e0236c6376d03740cbe042db0f
--- /dev/null
+++ b/sdk/lib/observe/observable.dart
@@ -0,0 +1,282 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+part of dart.observe;
+
+/**
+ * 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
+ * respectively.
+ *
+ * If [value] is not one of those collection types, or is already [Observable],
+ * it will be returned unmodified.
+ *
+ * If [value] is a [Map], the resulting value will use the appropriate kind of
+ * backing map: either [HashMap], [LinkedHashMap], or [SplayTreeMap].
+ *
+ * By default this performs a deep conversion, but you can set [deep] to false
+ * for a shallow conversion. This does not handle circular data structures.
+ */
+// TODO(jmesserly): ObservableSet?
+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
+ if (deep) return _deepObserve(value);
+ 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!
+ if (value is Map) return new ObservableMap.from(value);
+ if (value is Iterable) return new ObservableList.from(value);
+ return value;
+}
+
+_deepObserve(value) {
+ if (value is Observable) return value;
+ if (value is Map) {
+ var result = new ObservableMap._createFromType(value);
+ value.forEach((k, v) { result[_deepObserve(k)] = _deepObserve(v); });
+ return result;
+ }
+ if (value is Iterable) {
+ return new ObservableList.from(value.map(_deepObserve));
+ }
+ return value;
+}
+
+
+/**
+ * Interface representing an observable object. This is used by data in
+ * model-view architectures to notify interested parties of [changes].
+ *
+ * This object does not require any specific technique to implement
+ * observability.
+ *
+ * You can use [ObservableMixin] as a base class or mixin to implement this.
+ */
+abstract class Observable {
+ // TODO(jmesserly): should this be synchronous, and allow libraries to build
+ // the async batching?
+ /**
+ * The stream of change records to this object.
+ *
+ * Changes should be delivered in asynchronous batches by calling
+ * [queueChangeRecords].
+ * [deliverChangeRecords] can be called to force delivery.
+ */
+ 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
+
+ // 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.
+ /**
+ * *Warning*: this method is temporary until dart2js supports mirrors.
+ * Gets the value of a field or index. This should return null if it was
+ * not found.
+ */
+ getValue(key);
+
+ /**
+ * *Warning*: this method is temporary until dart2js supports mirrors.
+ * Sets the value of a field or index. This should have no effect if the field
+ * was not found.
+ */
+ void setValue(key, Object value);
+}
+
+/**
+ * Mixin for implementing [Observable].
+ *
+ * When a field, property, or indexable item is changed, a derived class should
+ * call [notifyChange]. See that method for an example.
+ */
+abstract class ObservableMixin implements Observable {
+ // TODO(jmesserly): this has way too much overhead. We probably need our own
+ // stream.
+ StreamController<List<ChangeRecord>> _observers;
+ Stream<List<ChangeRecord>> _stream;
+ List<ChangeRecord> _changes;
+
+ Stream<List<ChangeRecord>> get changes {
+ if (_observers == null) {
+ _observers = new StreamController<List<ChangeRecord>>();
+ _stream = _observers.stream.asBroadcastStream();
+ }
+ return _stream;
+ }
+
+ void _deliverChanges() {
+ var changes = _changes;
+ _changes = null;
+ if (_observers != null && changes != null) {
blois 2013/05/01 17:00:42 hasObservers instead?
Jennifer Messerly 2013/05/02 02:58:33 Done.
+ // TODO(jmesserly): make "changes" immutable
+ _observers.add(changes);
+ }
+ }
+
+ /**
+ * True if this object has any observers, and should call [notifyChange] for
+ * changes.
+ */
+ bool get hasObservers => _observers != null && _observers.hasListener;
+
+ /**
+ * Notify that a [key] of this object has been changed.
+ *
+ * The key can also represent a field or indexed value of the object or list.
+ * The [kind] is one of the constants [ChangeRecord.INDEX],
+ * [ChangeRecord.FIELD], [ChangeRecord.INSERT], or [ChangeRecord.REMOVE].
+ *
+ * The [oldValue] and [newValue] are also recorded. If the change wasn't an
+ * insert or remove, and the two values are equal, no change will be recorded.
+ * For INSERT, oldValue should be null. For REMOVE, newValue should be null.
+ *
+ * For convenience this returns [newValue]. This makes it easy to use in a
+ * setter:
+ *
+ * var _someField;
+ * get someField => _someField;
+ * set someField(value) {
+ * _someField = notifyChange('someField', _someField, value);
+ * }
+ */
+ 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
+ {int kind: ChangeRecord.FIELD}) {
+
+ if (!hasObservers) return newValue;
+
+ // If this is an assignment (and not insert/remove) then check if
+ // the value actually changed. If not don't signal a change event.
+ // This helps programmers avoid some common cases of cycles in their code.
+ if ((kind & (ChangeRecord.INSERT | ChangeRecord.REMOVE)) == 0) {
+ if (oldValue == newValue) return newValue;
+ }
+
+ if (_changes == null) {
+ _changes = [];
+ queueChangeRecords(_deliverChanges);
+ }
+ _changes.add(new ChangeRecord(key, oldValue, newValue, kind: kind));
+ return newValue;
+ }
+}
+
+/** Records a change to an [Observable]. */
+class ChangeRecord {
+ // Note: the target object is omitted because it makes it difficult
+ // to proxy change records if you're using an observable kind to aid
+ // your implementation.
+ // However: if we allow one observer to get batched changes for multiple
+ // objects, we'll need to add target.
+
+ // Note: kind values were chosen for easy masking in the observable expression
+ // implementation. However in [kind] it will only have one value.
+
+ // TODO(jmesserly): is there any value in keeping FIELD and INDEX distinct?
+ /** [kind] denoting set of a field. */
+ static const FIELD = 1;
+
+ /** [kind] denoting an in-place update event using `[]=`. */
+ static const INDEX = 2;
+
+ /**
+ * [kind] denoting an insertion into a list. Insertions prepend in front of
+ * the given index, so insert at 0 means an insertion at the beginning of the
+ * list. The index will be provided in [key].
+ */
+ static const INSERT = INDEX | 4;
+
+ /** [kind] denoting a remove from a list. */
+ static const REMOVE = INDEX | 8;
+
+ /** Whether the change was a [FIELD], [INDEX], [INSERT], or [REMOVE]. */
+ final int kind;
+
+ // TODO(jmesserly): for fields, is key a String or Symbol? Right now it's a
+ // String.
+ /**
+ * The key that changed. The value depends on the [kind] of change:
+ *
+ * - [FIELD]: the field name that was set.
+ * - [INDEX], [INSERT], and [REMOVE]: the index or key that was changed.
+ * This will be an integer for [ObservableList] but can be anything for
+ * [ObservableMap].
+ */
+ final key;
+
+ /** The previous value of the member. */
+ final oldValue;
+
+ /** The new value of the member. */
+ final newValue;
+
+ ChangeRecord(this.key, this.oldValue, this.newValue,
+ {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
+
+ // Note: these two methods are here mainly to make testing easier.
+ bool operator ==(other) {
+ return other is ChangeRecord && kind == other.kind && key == other.key &&
+ oldValue == other.oldValue && newValue == other.newValue;
+ }
+
+ int get hashCode => _hash4(kind, key, oldValue, newValue);
+
+ String toString() {
+ // TODO(jmesserly): const map would be nice here, but it must be string
+ // literal :(
+ String typeStr;
+ switch (kind) {
+ case FIELD: typeStr = 'field'; break;
+ case INDEX: typeStr = 'index'; break;
+ case INSERT: typeStr = 'insert'; break;
+ case REMOVE: typeStr = 'remove'; break;
+ }
+ return '#<ChangeRecord $typeStr $key from $oldValue to $newValue>';
+ }
+}
+
+// TODO(jmesserly): helpers to combine hash codes. Reuse these from somewhere.
+int _hash2(x, y) => x.hashCode * 31 + y.hashCode;
+
+int _hash3(x, y, z) => _hash2(_hash2(x, y), z);
+
+int _hash4(w, x, y, z) => _hash2(_hash2(w, x), _hash2(y, z));
+
+
+/**
+ * Synchronously deliver [Observable.changes] for all observables.
+ * If new changes are added as a result of delivery, this will keep running
+ * until all pending change records are delivered.
+ */
+// TODO(jmesserly): this is a bit different from the ES Harmony version, which
+// allows delivery of changes to a particular observer:
+// http://wiki.ecmascript.org/doku.php?id=harmony:observe#object.deliverchangerecords
+// However the binding system needs delivery of everything, along the lines of:
+// https://github.com/toolkitchen/mdv/blob/stable/src/model.js#L19
+// https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js#L590
+// TODO(jmesserly): in the future, we can use this to trigger dirty checking.
+void deliverChangeRecords() {
+ if (_deliverCallbacks == null) return;
+
+ while (_deliverCallbacks.length > 0) {
+ var deliverCallbacks = _deliverCallbacks;
+ // Use empty list so [queueChangeRecords] don't reschedule this method.
+ _deliverCallbacks = [];
+
+ for (var deliver in deliverCallbacks) {
+ try {
+ deliver();
+ } catch (e, s) {
+ // Schedule the error to be top-leveled later.
+ new Completer().completeError(e, s);
+ }
+ }
+ }
+
+ // Use null list so [queueChangeRecords] will reschedule this method.
+ _deliverCallbacks = null;
+}
+
+/** Queues an action to happen during the [deliverChangeRecords] timeslice. */
+void queueChangeRecords(void deliverChanges()) {
+ if (_deliverCallbacks == null) {
+ _deliverCallbacks = [];
+ 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.
+ }
+ _deliverCallbacks.add(deliverChanges);
+}
+
+List<Function> _deliverCallbacks;

Powered by Google App Engine
This is Rietveld 408576698