| 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);
|
| + }
|
| + }
|
| + }
|
| + }
|
| +}
|
|
|