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

Side by Side Diff: lib/observe.dart

Issue 12225039: Support for observable models, fixes #259 (Closed) Base URL: https://github.com/dart-lang/web-ui.git@master
Patch Set: small formatting fixes Created 7 years, 10 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
OLDNEW
(Empty)
1 // Copyright (c) 2012, 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 /**
6 * A library for observing changes to observable Dart objects.
7 * Similar in spirit to EcmaScript 6
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 6 => 7 (and below)
Jennifer Messerly 2013/02/13 05:43:15 yeah. Or maybe "EcmaScript Harmony"--to match the
8 * [Object.observe](http://wiki.ecmascript.org/doku.php?id=harmony:observe), but
9 * able to observe expressions and not just objects, so long as the expressions
10 * are computed from observable objects.
11 *
12 * See the [observable] annotation and the [observe] function.
13 */
14 // Note: one intentional difference from ES6 Object.observe is that our change
15 // batches are tracked on a per-observed expression basis, instead of per-observ er basis.
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 long line
Jennifer Messerly 2013/02/13 05:43:15 Done.
16 // We do this because there is no cheap way to store a pointer on a Dart
17 // function (Expando uses linear search on the VM: http://dartbug.com/7558).
18 // This difference means that a given observer will be called with one batch of
19 // changes for each object it is observing.
20 library observe;
21
22 import 'dart:collection';
23 import 'src/utils.dart' show setImmediate;
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 funny, I never considered using 'src/utils.dart' f
Jennifer Messerly 2013/02/13 05:43:15 Agreed, I hope our version goes away soon. Added T
24
25 // TODO(jmesserly): support detailed change records on our collections, such as
26 // INSERT/REMOVE, so we can use them from templates. Unlike normal objects,
27 // list/map/set can add or remove new observable things at runtime, so it's
28 // important to provide a way to listen for that.
29 export 'observe/list.dart';
30 export 'observe/map.dart';
31 export 'observe/reference.dart';
32 export 'observe/set.dart';
33
34 // TODO(jmesserly): notifyRead/notifyWrite are only used by people
35 // implementating advanced observable functionality. The need to be public, but
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 The => they
Jennifer Messerly 2013/02/13 05:43:15 Done. Good catch!
36 // ideally they would not be in the top level "observe" library.
37
38 /**
39 * Use `@observable` to make a class observable. All fields in the class will
40 * be transformed to track changes. The overhead will be minimal unless they are
41 * actually being observed.
42 */
43 const observable = const Object();
44
45 /** Callback fired when an expression changes. */
46 typedef void ChangeObserver(ChangeNotification e);
47
48 /** A function that unregisters the [ChangeObserver]. */
49 typedef void ChangeUnobserver();
50
51 /** A function that computes a value. */
52 typedef Object ObservableExpression();
53
54 /**
55 * A function that handles an [error] given the [stackTrace] and [callback] that
56 * caused the error.
57 */
58 typedef void ObserverErrorHandler(error, stackTrace, Function callback);
59
60 /**
61 * Test for equality of two objects. For example [Object.==] and [identical]
62 * are two kinds of equality tests.
63 */
64 typedef bool EqualityTest(Object a, Object b);
65
66 /**
67 * A notification of a change to an [ObservableExpression] that is passed to a
68 * [ChangeObserver].
69 */
70 class ChangeNotification {
71
72 /** Previous value seen on the watched expression. */
73 final oldValue;
74
75 /** New value seen on the watched expression. */
76 final newValue;
77
78 ChangeNotification(this.oldValue, this.newValue);
79
80 // Note: these two methods are here mainly to make testing easier.
81 bool operator ==(other) {
82 return other is ChangeNotification && oldValue == other.oldValue &&
83 newValue == other.newValue;
84 }
85
86 String toString() => 'change from $oldValue to $newValue';
87 }
88
89 /**
90 * Observes the [expression] and delivers asynchronous notifications of changes
91 * to the [callback].
92 *
93 * The expression is considered to have changed if the values no longer compare
94 * equal via the equality operator. You can perform additional comparisons in
95 * the [callback] if desired.
96 *
97 * Returns a function that can be used to stop observation.
98 * Calling this makes it possible for the garbage collector to reclaim memory
99 * associated with the observation and prevents further calls to [callback].
100 *
101 * Because notifications are delivered asynchronously and batched, only a single
102 * notification is provided for all changes that were made prior to running
103 * callback. Intermediate values of the expression are not saved. Instead,
104 * [ChangeNotification.oldValue] represents the value before any changes, and
105 * [ChangeNotification.newValue] represents the current value of [expression]
106 * at the time that [callback] is called.
107 *
108 * You can force a synchronous change delivery at any time by calling
109 * [deliverChangesSync]. Calling this method if there are no changes has no
110 * effect. If changes are delivered by deliverChangesSync, they will not be
111 * delivered again asynchronously, unless the value is changed again.
112 *
113 * Any errors thrown by [expression] and [callback] will be caught and sent to
114 * [onObserveUnhandledError]
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 add '.' at the end of the line
Jennifer Messerly 2013/02/13 05:43:15 Done.
115 */
116 // TODO(jmesserly): debugName is here to workaround http://dartbug.com/8419.
117 ChangeUnobserver observe(ObservableExpression expression,
118 ChangeObserver callback, [String debugName]) {
119
120 var observer = new _ExpressionObserver(expression, callback, debugName);
121 if (!observer._observe()) {
122 // If we didn't actually read anything, return a pointer to a no-op
123 // function so the observer can be reclaimed immediately.
124 return _doNothing;
125 }
126
127 return observer._unobserve;
128 }
129
130 // Optimizations to avoid extra work if observing const/final data.
131 void _doNothing() {}
132
133 /**
134 * The current observer that is tracking reads, or null if we aren't tracking
135 * reads. Reads are tracked when executing [_ExpressionObserver._observe].
136 */
137 _ExpressionObserver _activeObserver;
138
139 /**
140 * True if we are observing reads. This should be checked before calling
141 * [notifyRead].
142 *
143 * Note: this type is used by objects implementing observability.
144 * You should not need it if your type is marked `@observable`.
145 */
146 bool get observeReads => _activeObserver != null;
147
148 /**
149 * Notify the system of a new read. This will add the current change observer
150 * to the set of observers for this field. This should *only* be called when
151 * [observeReads] is true, and it will initialize [observers] if it is null.
152 * For example:
153 *
154 * get foo {
155 * if (observeReads) _fooObservers = notifyRead(_fooObservers);
156 * return _foo;
157 * }
158 *
159 * Note: this function is used to implement observability.
160 * You should not need it if your type is marked `@observable`.
161 *
162 * See also: [notifyWrite]
163 */
164 Object notifyRead(fieldObservers) {
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 maybe document the possible types for fieldObserve
Jennifer Messerly 2013/02/13 05:43:15 Added comment inside the method. I intentionally d
165 _activeObserver._wasRead = true;
166
167 // Note: there's some optimization here to avoid allocating an observer list
168 // unless we really need it.
169 if (fieldObservers == null) {
170 return _activeObserver;
171 }
172 if (fieldObservers is _ExpressionObserver) {
173 if (identical(fieldObservers, _activeObserver) || fieldObservers._dead) {
174 return _activeObserver;
175 }
176 return [fieldObservers, _activeObserver];
177 }
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 maybe assert (fieldObservers is List)?
Jennifer Messerly 2013/02/13 05:43:15 Trying to avoid anything here that will make this
178 return fieldObservers..add(_activeObserver);
179 }
180
181 /**
182 * Notify the system of a new write. This will deliver a change notification
183 * to the set of observers for this field. This should *only* be called for a
184 * non-null list of [observers]. For example:
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 would it be sensible to accept a null list and ret
Jennifer Messerly 2013/02/13 05:43:15 It's faster to avoid the call in the common case w
185 *
186 * set foo(value) {
187 * if (_fooObservers != null && _foo != value) {
188 * _fooObservers = notifyWrite(_fooObservers);
189 * }
190 * _foo = value;
191 * }
192 *
193 * Note: this function is used to implement observability.
194 * You should not need it if your type is marked `@observable`.
195 *
196 * See also: [notifyRead]
197 */
198 Object notifyWrite(Object fieldObservers) {
199 if (_pendingWrites == null) {
200 _pendingWrites = [];
201 setImmediate(deliverChangesSync);
202 }
203 _pendingWrites.add(fieldObservers);
204
205 // Clear fieldObservers. This will prevent a second notification for this
206 // same set of observers on the current event loop. It also frees associated
207 // memory. If the item needs to be observed again, that will happen in
208 // _ExpressionObserver._deliver.
209
210 // NOTE: ObservableMap depends on this returning null.
211 return null;
212 }
213
214 List _pendingWrites;
215
216 /**
217 * The limit of times we will attempt to deliver a set of pending changes.
218 *
219 * [deliverChangesSync] will attempt to deliver pending changes until there are
220 * no more. If one of the pending changes causes another batch of changes, it
221 * will iterate again and increment the iteration counter. Once it reaches
222 * this limit it will throw [CircularChangeNotifyError].
223 *
224 * Note that there is no limit to the number of changes per batch, only to the
225 * number of iterations.
226 */
227 int circularNotificationLimit = 100;
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 100 feels big, but maybe because in watchers 10 fe
Jennifer Messerly 2013/02/13 05:43:15 Watchers are very different :). It meant you're co
228
229 /**
230 * Delivers observed changes immediately. Normally you should not call this
231 * directly, but it can be used to force synchronous delivery, which helps in
232 * certain cases like testing.
233 */
234 void deliverChangesSync() {
235 int iterations = 0;
236 while (_pendingWrites != null) {
237 var pendingWrites = _pendingWrites;
238 _pendingWrites = null;
239
240 // Sort pending observers by order added.
241 // TODO(jmesserly): this is here to help our template system, which relies
242 // on earlier observers removing later ones to prevent them from firing.
243 // See if we can find a better solution at the template level.
244 var pendingObservers = new SplayTreeMap<int, _ExpressionObserver>();
245 for (var pending in pendingWrites) {
246 if (pending is _ExpressionObserver) {
247 pendingObservers[pending._id] = pending;
248 } else {
249 for (var observer in pending) {
250 pendingObservers[observer._id] = observer;
251 }
252 }
253 }
254
255 if (iterations++ == circularNotificationLimit) {
256 _diagnoseCircularLimit(pendingObservers);
257 }
258
259 // TODO(jmesserly): we are avoiding SplayTreeMap.values because it performs
260 // an unnecessary copy. If that gets fixed we can use .values here.
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 should we file a bug?
Jennifer Messerly 2013/02/13 05:43:15 Done.
261 pendingObservers.forEach((id, obs) { obs._deliver(); });
262 }
263 }
264
265 /**
266 * Attempt to provide diagnostics about what change is causing a loop in
267 * observers. Unfortunately it is hard to help the programmer unless they have
268 * provided a `debugName` to [observe], as callbacks are hard to debug
269 * because of <http://dartbug.com/8419>. However we can print the records that
270 * changed which has proved helpful.
271 */
272 void _diagnoseCircularLimit(Map<int, _ExpressionObserver> pendingObservers) {
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 would it also be possible to detect circularity du
Jennifer Messerly 2013/02/13 05:43:15 Interesting idea. Essentially, a runtime check for
Siggi Cherem (dart-lang) 2013/02/13 19:28:54 Yeah - basically I think of it as a combination of
Jennifer Messerly 2013/02/13 20:21:20 Yeah, I think the challenge is the cycles don't ne
273 var trace = new StringBuffer('exceeded notifiction limit of '
274 '${circularNotificationLimit}, possible '
275 'circular reference in observers: ');
276
277 int i = 0;
278 pendingObservers.forEach((id, obs) {
279 if (i < 10) {
280 if (i != 0) trace.add(', ');
281 var change = obs._deliver();
282 trace.add('$obs ${change != null ? change : "no change"}');
283 } else {
284 obs._deliver();
285 }
286
287 i++;
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 consider only incrementing and printing the top-10
Jennifer Messerly 2013/02/13 05:43:15 Great idea. Done.
288 });
289
290 // Throw away pending changes to prevent repeating this error.
291 _pendingWrites = null;
292
293 // TODO(jmesserly): should we print or log this instead?
294 // We throw an error because it probably means a bug in the program structure.
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 In watchers it was nice that we kept going - a bug
Jennifer Messerly 2013/02/13 05:43:15 Not sure you really want the application to keep g
295 throw new CircularChangeNotifyError(trace.toString());
296 }
297
298
299 /**
300 * Error thrown when change notifications get stuck in a circular loop, which
301 * can happen if one [ChangeObserver] causes another change to happen, and that
302 * change causes another, etc.
303 *
304 * This is thrown when [circularNotificationLimit] is reached by
305 * [deliverChangesSync]. Circular references are commonly the result of not
306 * correctly implementing equality for objects.
307 */
308 class CircularChangeNotifyError implements Error {
309 final String message;
310 CircularChangeNotifyError(this.message);
311 String toString() => message;
312 }
313
314
315 class _ExpressionObserver {
316 static int _nextId = 0;
317
318 /**
319 * The ID indicating creation order. We will call observers in ID order.
320 * See the TODO in [deliverChangesSync].
321 */
322 final int _id = ++_ExpressionObserver._nextId;
323
324 // Note: fields in this class are private because instances of this class are
325 // exposed via notifyRead.
326 ObservableExpression _expression;
327
328 ChangeObserver _callback;
329
330 // TODO(jmesserly): should we provide the old value? This will keep it
331 // alive until we have a new value. Watchers needed to keep it alive anyway,
332 // but we don't need it in observers, except to pass it to the ChangeObserver.
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 In that case, wouldn't an event get trigger as soo
Jennifer Messerly 2013/02/13 05:43:15 Good point. Removed comment!
333 Object _value;
334
335 bool _wasRead;
336
337 String _debugName;
338
339 _ExpressionObserver(this._expression, this._callback, this._debugName);
340
341 /** True if this observer has been unobserved. */
342 // Note: any time we call out to user-provided code, they might call
343 // unobserve, so we need to guard against that.
344 bool get _dead => _callback == null;
345
346 String toString() =>
347 _debugName != null ? '<observer $_id: $_debugName>' : '<observer $_id>';
348
349 bool _observe() {
350 // If an observe call starts another observation, we need to make sure that
351 // the outer observe is tracked correctly.
352 var parent = _activeObserver;
353 _activeObserver = this;
354
355 _wasRead = false;
356 try {
357 _value = _expression();
358 } catch (e, trace) {
359 onObserveUnhandledError(e, trace, _expression);
360 _value = null;
361 }
362
363 // TODO(jmesserly): should the parent also observe us?
364 assert(_activeObserver == this);
365 _activeObserver = parent;
366
367 return _wasRead;
368 }
369
370 void _unobserve() {
371 if (_dead) return;
372
373 // Note: we don't remove ourselves from objects that we are observing.
374 // That will happen automatically when those fields are written.
375 // Instead, we release our own memory and wait for notifyWrite and
376 // deliverChangesSync to do the rest.
377 // TODO(jmesserly): this is probably too over-optimized. We'll need to
378 // revisit this to provide detailed change records.
379 _expression = null;
380 _callback = null;
381 _value = null;
382 _wasRead = null;
383 _debugName = null;
384 }
385
386 /**
387 * _deliver does two things:
388 * 1. Evaluate the expression to compute the new value.
389 * 2. Invoke observer for this expression.
390 *
391 * Note: if you mutate a shared value from one observer, future
392 * observers will see the updated value. Essentially, we collapse
393 * the two change notifications into one.
394 *
395 * We could split _deliver into two methods, one to compute the new value
396 * and another to call observers. But the current order has benefits too: it
397 * preserves the invariant that ChangeNotification.newValue equals the current
398 * value of the expression.
399 */
400 ChangeNotification _deliver() {
401 if (_dead) return null;
402
403 // Call the expression again to compute the new value, and to get the new
404 // list of dependencies.
405 var oldValue = _value;
406 _observe();
407
408 if (_dead) return null;
Siggi Cherem (dart-lang) 2013/02/13 01:43:24 can _dead change in _observe?, can _dead change in
Jennifer Messerly 2013/02/13 05:43:15 added comment: // Note: whenever we run code
409
410 bool equal;
411 try {
412 equal = oldValue == _value;
413 } catch (e, trace) {
414 onObserveUnhandledError(e, trace, null);
415 return;
416 }
417
418 if (equal || _dead) return null;
419
420 var change = new ChangeNotification(oldValue, _value);
421 try {
422 _callback(change);
423 } catch (e, trace) {
424 onObserveUnhandledError(e, trace, _callback);
425 }
426 return change;
427 }
428
429 // TODO(jmesserly): workaround for terrible VM hash code performance.
430 int get hashCode => _id;
431 }
432
433
434 /**
435 * Callback to intercept unhandled errors in evaluating an observable.
436 * Includes the error, stack trace, and the callback that caused the error.
437 * By default it will use [defaultObserveUnhandledError], which prints the
438 * error.
439 */
440 ObserverErrorHandler onObserveUnhandledError = defaultObserveUnhandledError;
441
442 /** The default handler for [onObserveUnhandledError]. Prints the error. */
443 void defaultObserveUnhandledError(error, trace, callback) {
444 // TODO(jmesserly): using Logger seems better, but by default it doesn't do
445 // anything, which leads to unobserved errors.
446 // Ideally we could make this show up as an error in the browser's console.
447 print('web_ui.observe: unhandled error in callback $callback.\n'
448 'error:\n$error\n\nstack trace:\n$trace');
449 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698