Index: lib/observe/observable.dart |
diff --git a/lib/observe/observable.dart b/lib/observe/observable.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..12b93599fa0752ef60540acb3e1077345d68cb99 |
--- /dev/null |
+++ b/lib/observe/observable.dart |
@@ -0,0 +1,237 @@ |
+// Copyright (c) 2012, 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. |
+ |
+/** |
+ * A library for observing changes to observable Dart objects. |
+ * |
+ * Similar in spirit to EcmaScript 6 |
+ * [Object.observe](http://wiki.ecmascript.org/doku.php?id=harmony:observe). |
+ * |
+ * See the [observable] annotation and the [Observable.observe] function. |
+ */ |
+// Note: one difference from ES6 Object.observe is that our change batches are |
+// tracked on a per-observed expression basis, instead of per-observer basis. |
+// |
+// We do this because there is no cheap way to store a pointer on a Dart |
+// function (Expando uses linear search on the VM: http://dartbug.com/7558). |
+// This difference means that a given observer will be called with one batch of |
+// changes for each object it is observing. |
+// |
+// TODO(jmessery): tracking it per-observer is more powerful. It lets one |
+// observer get a complete batch of changes for all objects it observes. |
+// Maybe we should attempt to implement it? The main downside is overhead |
+// in deliverChangesSync -- you need to hash the functions and collect their |
+// change lists. |
+library web_ui.observe.observable; |
+ |
+import 'package:web_ui/src/observe/impl.dart' as impl; |
+import 'package:web_ui/src/linked_list.dart'; |
+import 'package:web_ui/src/utils.dart' show setImmediate; |
+ |
+// TODO(jmesserly): rename to @observe? |
+/** |
+ * Use `@observable` to make a class observable. All fields in the class will |
+ * be transformed to track changes. The overhead will be minimal unless they are |
+ * actually being observed. |
+ * |
+ * **Note**: the class needs to be processed by `dwc` for this annotation |
+ * to have any effect. It is implemented as a Dart-to-Dart compiler transform. |
+ */ |
+const observable = const Object(); |
+ |
+/** |
+ * An observable object. This is used by data in model-view architectures |
+ * to notify interested parties of changes. |
+ */ |
+class Observable { |
+ LinkedListSentinel<ChangeObserver> _observers; |
+ List<ChangeRecord> _changes; |
+ |
+ // TODO(jmesserly): in the spirit of mirrors, should all of this |
+ // implementation be pulled outside of the Observable mixin? |
+ |
+ bool get hasObservers => _observers != null && _observers.next != null; |
+ |
+ /** |
+ * Observes this object and delivers asynchronous notifications of changes |
+ * to the [observer]. |
+ * |
+ * The field is considered to have changed if the values no longer compare |
+ * equal via the equality operator. |
+ * |
+ * Returns a function that can be used to stop observation. |
+ * Calling this makes it possible for the garbage collector to reclaim memory |
+ * associated with the observation and prevents further calls to [callback]. |
+ * |
+ * You can force a synchronous change delivery at any time by calling |
+ * [deliverChangesSync]. Calling this method if there are no changes has no |
+ * effect. If changes are delivered by deliverChangesSync, they will not be |
+ * delivered again asynchronously, unless the value is changed again. |
+ * |
+ * Any errors thrown by [observer] or [comparison] will be caught and sent to |
+ * [onObserveUnhandledError]. |
+ */ |
+ ChangeUnobserver observe(ChangeObserver observer) { |
+ if (_observers == null) _observers = new LinkedListSentinel(); |
+ var node = _observers.prepend(observer); |
+ return node.remove; |
+ } |
+ |
+ // Conceptually, these are protected methods. |
+ |
+ void notifyRead(int type, name) { |
+ impl.activeObserver.addRead(this, type, name); |
+ } |
+ |
+ void notifyChange(int type, name, Object oldValue, Object 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 ((type & (ChangeRecord.INSERT | ChangeRecord.REMOVE)) == 0) { |
+ if (oldValue == newValue) return; |
+ } |
+ |
+ if (_changedObjects == null) { |
+ _changedObjects = []; |
+ setImmediate(deliverChangesSync); |
+ } |
+ if (_changes == null) { |
+ _changes = []; |
+ _changedObjects.add(this); |
+ } |
+ _changes.add(new ChangeRecord(type, name, oldValue, newValue)); |
+ } |
+} |
+ |
+/** |
+ * True if we are observing reads. This should be checked before calling |
+ * [Observable.notifyRead]. |
+ * |
+ * Note: this is used by objects implementing observability. |
+ * You should not need to use this if your type is marked `@observable`. |
+ */ |
+bool get observeReads => impl.activeObserver != null; |
+ |
+ |
+/** Callback fired when an [Observable] changes. */ |
+typedef void ChangeObserver(List<ChangeRecord> records); |
+ |
+/** A function that unregisters the [ChangeObserver]. */ |
+typedef void ChangeUnobserver(); |
+ |
+/** |
+ * Test for equality of two objects. For example [Object.==] and [identical] |
+ * are two kinds of equality tests. |
+ */ |
+typedef bool EqualityTest(Object a, Object b); |
+ |
+/** Records a change to the [target] object. */ |
+class ChangeRecord { |
+ // Note: the target object is omitted because it makes it difficult |
+ // to proxy change notifications if you're using an observable type to aid |
+ // your implementation. |
+ // However: if we allow one observer to get batched changes for multiple |
+ // objects we'll need to add target. |
+ |
+ // Note: type values were chosen for easy masking in the observable expression |
+ // implementation. However in [type] it will only have one value. |
+ |
+ /** [type] denoting set of a field. */ |
+ static const FIELD = 1; |
+ |
+ /** [type] denoting an in-place update event using `[]=`. */ |
+ static const INDEX = 2; |
+ |
+ /** |
+ * [type] 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 [name]. |
+ */ |
+ static const INSERT = INDEX | 4; |
+ |
+ /** [type] denoting a remove from a list. */ |
+ static const REMOVE = INDEX | 8; |
+ |
+ /** Whether the change was a [FIELD], [INDEX], [INSERT], or [REMOVE]. */ |
+ final int type; |
+ |
+ /** |
+ * The name that changed. The value depends on the [type] of change: |
+ * |
+ * - [FIELD]: the field name that was set |
+ * - [INDEX], [INSERT], and [REMOVE]: the index that was changed. Note |
+ * that indexes can be strings too, for example with an observable map or |
+ * set that has string keys. |
+ */ |
+ final name; |
+ |
+ /** The previous value of the member. */ |
+ final oldValue; |
+ |
+ /** The new value of the member. */ |
+ final newValue; |
+ |
+ ChangeRecord(this.type, this.name, this.oldValue, this.newValue); |
+} |
+ |
+ |
+/** |
+ * A function that handles an [error] given the [stackTrace] and [callback] that |
+ * caused the error. |
+ */ |
+typedef void ObserverErrorHandler(error, stackTrace, Function callback); |
+ |
+/** |
+ * Callback to intercept unhandled errors in evaluating an observable. |
+ * Includes the error, stack trace, and the callback that caused the error. |
+ * By default it will use [defaultObserveUnhandledError], which prints the |
+ * error. |
+ */ |
+ObserverErrorHandler onObserveUnhandledError = defaultObserveUnhandledError; |
+ |
+/** The default handler for [onObserveUnhandledError]. Prints the error. */ |
+void defaultObserveUnhandledError(error, trace, callback) { |
+ // TODO(jmesserly): using Logger seems better, but by default it doesn't do |
+ // anything, which leads to silent errors. Not good! |
+ // Ideally we could make this show up as an error in the browser's console. |
+ print('web_ui.observe: unhandled error in callback $callback.\n' |
+ 'error:\n$error\n\nstack trace:\n$trace'); |
+} |
+ |
+/** The per-isolate list of changed objects. */ |
+List<Observable> _changedObjects; |
+ |
+/** |
+ * Delivers observed changes immediately. Normally you should not call this |
+ * directly, but it can be used to force synchronous delivery, which helps in |
+ * certain cases like testing. |
+ * |
+ * Note: this will continue delivering changes as long as some are pending. |
+ */ |
+void deliverChangesSync() { |
+ int iterations = 0; |
+ while (_changedObjects != null) { |
+ var changedObjects = _changedObjects; |
+ _changedObjects = null; |
+ |
+ for (var observable in changedObjects) { |
+ // TODO(jmesserly): freeze the "changes" list? |
+ // If one observer incorrectly mutates it, it will affect what future |
+ // observers see, possibly leading to subtle bugs. |
+ // OTOH, I don't want to add a defensive copy here. Maybe a wrapper that |
+ // prevents mutation, or a ListBuilder of some sort than can be frozen. |
+ var changes = observable._changes; |
+ observable._changes = null; |
+ |
+ for (var n = observable._observers.next; n != null; n = n.next) { |
+ var observer = n.value; |
+ try { |
+ observer(changes); |
+ } catch (error, trace) { |
+ onObserveUnhandledError(error, trace, observer); |
+ } |
+ } |
+ } |
+ } |
+} |