root / dojo / trunk / _base / Deferred.js

Revision 20522, 12.6 kB (checked in by jburke, 4 months ago)

Fixes #9311, nested addCallback calls end up with the nested callback not getting the right, up to date response.

  • Property svn:eol-style set to native
Line 
1dojo.provide("dojo._base.Deferred");
2dojo.require("dojo._base.lang");
3
4dojo.Deferred = function(/*Function?*/ canceller){
5        // summary:
6        //              Encapsulates a sequence of callbacks in response to a value that
7        //              may not yet be available.  This is modeled after the Deferred class
8        //              from Twisted <http://twistedmatrix.com>.
9        // description:
10        //              JavaScript has no threads, and even if it did, threads are hard.
11        //              Deferreds are a way of abstracting non-blocking events, such as the
12        //              final response to an XMLHttpRequest. Deferreds create a promise to
13        //              return a response a some point in the future and an easy way to
14        //              register your interest in receiving that response.
15        //
16        //              The most important methods for Deffered users are:
17        //
18        //                      * addCallback(handler)
19        //                      * addErrback(handler)
20        //                      * callback(result)
21        //                      * errback(result)
22        //
23        //              In general, when a function returns a Deferred, users then "fill
24        //              in" the second half of the contract by registering callbacks and
25        //              error handlers. You may register as many callback and errback
26        //              handlers as you like and they will be executed in the order
27        //              registered when a result is provided. Usually this result is
28        //              provided as the result of an asynchronous operation. The code
29        //              "managing" the Deferred (the code that made the promise to provide
30        //              an answer later) will use the callback() and errback() methods to
31        //              communicate with registered listeners about the result of the
32        //              operation. At this time, all registered result handlers are called
33        //              *with the most recent result value*.
34        //
35        //              Deferred callback handlers are treated as a chain, and each item in
36        //              the chain is required to return a value that will be fed into
37        //              successive handlers. The most minimal callback may be registered
38        //              like this:
39        //
40        //              |       var d = new dojo.Deferred();
41        //              |       d.addCallback(function(result){ return result; });
42        //
43        //              Perhaps the most common mistake when first using Deferreds is to
44        //              forget to return a value (in most cases, the value you were
45        //              passed).
46        //
47        //              The sequence of callbacks is internally represented as a list of
48        //              2-tuples containing the callback/errback pair.  For example, the
49        //              following call sequence:
50        //             
51        //              |       var d = new dojo.Deferred();
52        //              |       d.addCallback(myCallback);
53        //              |       d.addErrback(myErrback);
54        //              |       d.addBoth(myBoth);
55        //              |       d.addCallbacks(myCallback, myErrback);
56        //
57        //              is translated into a Deferred with the following internal
58        //              representation:
59        //
60        //              |       [
61        //              |               [myCallback, null],
62        //              |               [null, myErrback],
63        //              |               [myBoth, myBoth],
64        //              |               [myCallback, myErrback]
65        //              |       ]
66        //
67        //              The Deferred also keeps track of its current status (fired).  Its
68        //              status may be one of three things:
69        //
70        //                      * -1: no value yet (initial condition)
71        //                      * 0: success
72        //                      * 1: error
73        //     
74        //              A Deferred will be in the error state if one of the following three
75        //              conditions are met:
76        //
77        //                      1. The result given to callback or errback is "instanceof" Error
78        //                      2. The previous callback or errback raised an exception while
79        //                         executing
80        //                      3. The previous callback or errback returned a value
81        //                         "instanceof" Error
82        //
83        //              Otherwise, the Deferred will be in the success state. The state of
84        //              the Deferred determines the next element in the callback sequence
85        //              to run.
86        //
87        //              When a callback or errback occurs with the example deferred chain,
88        //              something equivalent to the following will happen (imagine
89        //              that exceptions are caught and returned):
90        //
91        //              |       // d.callback(result) or d.errback(result)
92        //              |       if(!(result instanceof Error)){
93        //              |               result = myCallback(result);
94        //              |       }
95        //              |       if(result instanceof Error){
96        //              |               result = myErrback(result);
97        //              |       }
98        //              |       result = myBoth(result);
99        //              |       if(result instanceof Error){
100        //              |               result = myErrback(result);
101        //              |       }else{
102        //              |               result = myCallback(result);
103        //              |       }
104        //
105        //              The result is then stored away in case another step is added to the
106        //              callback sequence.      Since the Deferred already has a value
107        //              available, any new callbacks added will be called immediately.
108        //
109        //              There are two other "advanced" details about this implementation
110        //              that are useful:
111        //
112        //              Callbacks are allowed to return Deferred instances themselves, so
113        //              you can build complicated sequences of events with ease.
114        //
115        //              The creator of the Deferred may specify a canceller.  The canceller
116        //              is a function that will be called if Deferred.cancel is called
117        //              before the Deferred fires. You can use this to implement clean
118        //              aborting of an XMLHttpRequest, etc. Note that cancel will fire the
119        //              deferred with a CancelledError (unless your canceller returns
120        //              another kind of error), so the errbacks should be prepared to
121        //              handle that error for cancellable Deferreds.
122        // example:
123        //      |       var deferred = new dojo.Deferred();
124        //      |       setTimeout(function(){ deferred.callback({success: true}); }, 1000);
125        //      |       return deferred;
126        // example:
127        //              Deferred objects are often used when making code asynchronous. It
128        //              may be easiest to write functions in a synchronous manner and then
129        //              split code using a deferred to trigger a response to a long-lived
130        //              operation. For example, instead of register a callback function to
131        //              denote when a rendering operation completes, the function can
132        //              simply return a deferred:
133        //
134        //              |       // callback style:
135        //              |       function renderLotsOfData(data, callback){
136        //              |               var success = false
137        //              |               try{
138        //              |                       for(var x in data){
139        //              |                               renderDataitem(data[x]);
140        //              |                       }
141        //              |                       success = true;
142        //              |               }catch(e){ }
143        //              |               if(callback){
144        //              |                       callback(success);
145        //              |               }
146        //              |       }
147        //
148        //              |       // using callback style
149        //              |       renderLotsOfData(someDataObj, function(success){
150        //              |               // handles success or failure
151        //              |               if(!success){
152        //              |                       promptUserToRecover();
153        //              |               }
154        //              |       });
155        //              |       // NOTE: no way to add another callback here!!
156        // example:
157        //              Using a Deferred doesn't simplify the sending code any, but it
158        //              provides a standard interface for callers and senders alike,
159        //              providing both with a simple way to service multiple callbacks for
160        //              an operation and freeing both sides from worrying about details
161        //              such as "did this get called already?". With Deferreds, new
162        //              callbacks can be added at any time.
163        //
164        //              |       // Deferred style:
165        //              |       function renderLotsOfData(data){
166        //              |               var d = new dojo.Deferred();
167        //              |               try{
168        //              |                       for(var x in data){
169        //              |                               renderDataitem(data[x]);
170        //              |                       }
171        //              |                       d.callback(true);
172        //              |               }catch(e){
173        //              |                       d.errback(new Error("rendering failed"));
174        //              |               }
175        //              |               return d;
176        //              |       }
177        //
178        //              |       // using Deferred style
179        //              |       renderLotsOfData(someDataObj).addErrback(function(){
180        //              |               promptUserToRecover();
181        //              |       });
182        //              |       // NOTE: addErrback and addCallback both return the Deferred
183        //              |       // again, so we could chain adding callbacks or save the
184        //              |       // deferred for later should we need to be notified again.
185        // example:
186        //              In this example, renderLotsOfData is syncrhonous and so both
187        //              versions are pretty artificial. Putting the data display on a
188        //              timeout helps show why Deferreds rock:
189        //
190        //              |       // Deferred style and async func
191        //              |       function renderLotsOfData(data){
192        //              |               var d = new dojo.Deferred();
193        //              |               setTimeout(function(){
194        //              |                       try{
195        //              |                               for(var x in data){
196        //              |                                       renderDataitem(data[x]);
197        //              |                               }
198        //              |                               d.callback(true);
199        //              |                       }catch(e){
200        //              |                               d.errback(new Error("rendering failed"));
201        //              |                       }
202        //              |               }, 100);
203        //              |               return d;
204        //              |       }
205        //
206        //              |       // using Deferred style
207        //              |       renderLotsOfData(someDataObj).addErrback(function(){
208        //              |               promptUserToRecover();
209        //              |       });
210        //
211        //              Note that the caller doesn't have to change his code at all to
212        //              handle the asynchronous case.
213
214        this.chain = [];
215        this.id = this._nextId();
216        this.fired = -1;
217        this.paused = 0;
218        this.results = [null, null];
219        this.canceller = canceller;
220        this.silentlyCancelled = false;
221        this.isFiring = false;
222};
223
224dojo.extend(dojo.Deferred, {
225        /*
226        makeCalled: function(){
227                // summary:
228                //              returns a new, empty deferred, which is already in the called
229                //              state. Calling callback() or errback() on this deferred will
230                //              yeild an error and adding new handlers to it will result in
231                //              them being called immediately.
232                var deferred = new dojo.Deferred();
233                deferred.callback();
234                return deferred;
235        },
236
237        toString: function(){
238                var state;
239                if(this.fired == -1){
240                        state = 'unfired';
241                }else{
242                        state = this.fired ? 'success' : 'error';
243                }
244                return 'Deferred(' + this.id + ', ' + state + ')';
245        },
246        */
247
248        _nextId: (function(){
249                var n = 1;
250                return function(){ return n++; };
251        })(),
252
253        cancel: function(){
254                // summary:     
255                //              Cancels a Deferred that has not yet received a value, or is
256                //              waiting on another Deferred as its value.
257                // description:
258                //              If a canceller is defined, the canceller is called. If the
259                //              canceller did not return an error, or there was no canceller,
260                //              then the errback chain is started.
261                var err;
262                if(this.fired == -1){
263                        if(this.canceller){
264                                err = this.canceller(this);
265                        }else{
266                                this.silentlyCancelled = true;
267                        }
268                        if(this.fired == -1){
269                                if(!(err instanceof Error)){
270                                        var res = err;
271                                        var msg = "Deferred Cancelled";
272                                        if(err && err.toString){
273                                                msg += ": " + err.toString();
274                                        }
275                                        err = new Error(msg);
276                                        err.dojoType = "cancel";
277                                        err.cancelResult = res;
278                                }
279                                this.errback(err);
280                        }
281                }else if(       (this.fired == 0) &&
282                                        (this.results[0] instanceof dojo.Deferred)
283                ){
284                        this.results[0].cancel();
285                }
286        },
287                       
288
289        _resback: function(res){
290                // summary:
291                //              The private primitive that means either callback or errback
292                this.fired = ((res instanceof Error) ? 1 : 0);
293                this.results[this.fired] = res;
294                this._fire();
295        },
296
297        _check: function(){
298                if(this.fired != -1){
299                        if(!this.silentlyCancelled){
300                                throw new Error("already called!");
301                        }
302                        this.silentlyCancelled = false;
303                        return;
304                }
305        },
306
307        callback: function(res){
308                //      summary:       
309                //              Begin the callback sequence with a non-error value.
310               
311                /*
312                callback or errback should only be called once on a given
313                Deferred.
314                */
315                this._check();
316                this._resback(res);
317        },
318
319        errback: function(/*Error*/res){
320                //      summary:
321                //              Begin the callback sequence with an error result.
322                this._check();
323                if(!(res instanceof Error)){
324                        res = new Error(res);
325                }
326                this._resback(res);
327        },
328
329        addBoth: function(/*Function|Object*/cb, /*String?*/cbfn){
330                //      summary:
331                //              Add the same function as both a callback and an errback as the
332                //              next element on the callback sequence.This is useful for code
333                //              that you want to guarantee to run, e.g. a finalizer.
334                var enclosed = dojo.hitch.apply(dojo, arguments);
335                return this.addCallbacks(enclosed, enclosed); // dojo.Deferred
336        },
337
338        addCallback: function(/*Function|Object*/cb, /*String?*/cbfn /*...*/){
339                //      summary:
340                //              Add a single callback to the end of the callback sequence.
341                return this.addCallbacks(dojo.hitch.apply(dojo, arguments)); // dojo.Deferred
342        },
343
344        addErrback: function(cb, cbfn){
345                //      summary:
346                //              Add a single callback to the end of the callback sequence.
347                return this.addCallbacks(null, dojo.hitch.apply(dojo, arguments)); // dojo.Deferred
348        },
349
350        addCallbacks: function(cb, eb){
351                // summary:
352                //              Add separate callback and errback to the end of the callback
353                //              sequence.
354                this.chain.push([cb, eb])
355                if(this.fired >= 0 && !this.isFiring){
356                        this._fire();
357                }
358                return this; // dojo.Deferred
359        },
360
361        _fire: function(){
362                // summary:
363                //              Used internally to exhaust the callback sequence when a result
364                //              is available.
365                this.isFiring = true;
366                var chain = this.chain;
367                var fired = this.fired;
368                var res = this.results[fired];
369                var self = this;
370                var cb = null;
371                while(
372                        (chain.length > 0) &&
373                        (this.paused == 0)
374                ){
375                        // Array
376                        var f = chain.shift()[fired];
377                        if(!f){ continue; }
378                        var func = function(){
379                                var ret = f(res);
380                                //If no response, then use previous response.
381                                if(typeof ret != "undefined"){
382                                        res = ret;
383                                }
384                                fired = ((res instanceof Error) ? 1 : 0);
385                                if(res instanceof dojo.Deferred){
386                                        cb = function(res){
387                                                self._resback(res);
388                                                // inlined from _pause()
389                                                self.paused--;
390                                                if(
391                                                        (self.paused == 0) && 
392                                                        (self.fired >= 0)
393                                                ){
394                                                        self._fire();
395                                                }
396                                        }
397                                        // inlined from _unpause
398                                        this.paused++;
399                                }
400                        };
401                        if(dojo.config.debugAtAllCosts){
402                                func.call(this);
403                        }else{
404                                try{
405                                        func.call(this);
406                                }catch(err){
407                                        fired = 1;
408                                        res = err;
409                                }
410                        }
411                }
412                this.fired = fired;
413                this.results[fired] = res;
414                this.isFiring = false;
415                if((cb)&&(this.paused)){
416                        // this is for "tail recursion" in case the dependent
417                        // deferred is already fired
418                        res.addBoth(cb);
419                }
420        }
421});
Note: See TracBrowser for help on using the browser.