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