Initial check-in
[dygraphs.git] / mochikit_v14 / MochiKit / Async.js
1 /***
2
3 MochiKit.Async 1.4
4
5 See <http://mochikit.com/> for documentation, downloads, license, etc.
6
7 (c) 2005 Bob Ippolito. All rights Reserved.
8
9 ***/
10
11 if (typeof(dojo) != 'undefined') {
12 dojo.provide("MochiKit.Async");
13 dojo.require("MochiKit.Base");
14 }
15 if (typeof(JSAN) != 'undefined') {
16 JSAN.use("MochiKit.Base", []);
17 }
18
19 try {
20 if (typeof(MochiKit.Base) == 'undefined') {
21 throw "";
22 }
23 } catch (e) {
24 throw "MochiKit.Async depends on MochiKit.Base!";
25 }
26
27 if (typeof(MochiKit.Async) == 'undefined') {
28 MochiKit.Async = {};
29 }
30
31 MochiKit.Async.NAME = "MochiKit.Async";
32 MochiKit.Async.VERSION = "1.4";
33 MochiKit.Async.__repr__ = function () {
34 return "[" + this.NAME + " " + this.VERSION + "]";
35 };
36 MochiKit.Async.toString = function () {
37 return this.__repr__();
38 };
39
40 /** @id MochiKit.Async.Deferred */
41 MochiKit.Async.Deferred = function (/* optional */ canceller) {
42 this.chain = [];
43 this.id = this._nextId();
44 this.fired = -1;
45 this.paused = 0;
46 this.results = [null, null];
47 this.canceller = canceller;
48 this.silentlyCancelled = false;
49 this.chained = false;
50 };
51
52 MochiKit.Async.Deferred.prototype = {
53 /** @id MochiKit.Async.Deferred.prototype.repr */
54 repr: function () {
55 var state;
56 if (this.fired == -1) {
57 state = 'unfired';
58 } else if (this.fired === 0) {
59 state = 'success';
60 } else {
61 state = 'error';
62 }
63 return 'Deferred(' + this.id + ', ' + state + ')';
64 },
65
66 toString: MochiKit.Base.forwardCall("repr"),
67
68 _nextId: MochiKit.Base.counter(),
69
70 /** @id MochiKit.Async.Deferred.prototype.cancel */
71 cancel: function () {
72 var self = MochiKit.Async;
73 if (this.fired == -1) {
74 if (this.canceller) {
75 this.canceller(this);
76 } else {
77 this.silentlyCancelled = true;
78 }
79 if (this.fired == -1) {
80 this.errback(new self.CancelledError(this));
81 }
82 } else if ((this.fired === 0) && (this.results[0] instanceof self.Deferred)) {
83 this.results[0].cancel();
84 }
85 },
86
87 _resback: function (res) {
88 /***
89
90 The primitive that means either callback or errback
91
92 ***/
93 this.fired = ((res instanceof Error) ? 1 : 0);
94 this.results[this.fired] = res;
95 this._fire();
96 },
97
98 _check: function () {
99 if (this.fired != -1) {
100 if (!this.silentlyCancelled) {
101 throw new MochiKit.Async.AlreadyCalledError(this);
102 }
103 this.silentlyCancelled = false;
104 return;
105 }
106 },
107
108 /** @id MochiKit.Async.Deferred.prototype.callback */
109 callback: function (res) {
110 this._check();
111 if (res instanceof MochiKit.Async.Deferred) {
112 throw new Error("Deferred instances can only be chained if they are the result of a callback");
113 }
114 this._resback(res);
115 },
116
117 /** @id MochiKit.Async.Deferred.prototype.errback */
118 errback: function (res) {
119 this._check();
120 var self = MochiKit.Async;
121 if (res instanceof self.Deferred) {
122 throw new Error("Deferred instances can only be chained if they are the result of a callback");
123 }
124 if (!(res instanceof Error)) {
125 res = new self.GenericError(res);
126 }
127 this._resback(res);
128 },
129
130 /** @id MochiKit.Async.Deferred.prototype.addBoth */
131 addBoth: function (fn) {
132 if (arguments.length > 1) {
133 fn = MochiKit.Base.partial.apply(null, arguments);
134 }
135 return this.addCallbacks(fn, fn);
136 },
137
138 /** @id MochiKit.Async.Deferred.prototype.addCallback */
139 addCallback: function (fn) {
140 if (arguments.length > 1) {
141 fn = MochiKit.Base.partial.apply(null, arguments);
142 }
143 return this.addCallbacks(fn, null);
144 },
145
146 /** @id MochiKit.Async.Deferred.prototype.addErrback */
147 addErrback: function (fn) {
148 if (arguments.length > 1) {
149 fn = MochiKit.Base.partial.apply(null, arguments);
150 }
151 return this.addCallbacks(null, fn);
152 },
153
154 /** @id MochiKit.Async.Deferred.prototype.addCallbacks */
155 addCallbacks: function (cb, eb) {
156 if (this.chained) {
157 throw new Error("Chained Deferreds can not be re-used");
158 }
159 this.chain.push([cb, eb]);
160 if (this.fired >= 0) {
161 this._fire();
162 }
163 return this;
164 },
165
166 _fire: function () {
167 /***
168
169 Used internally to exhaust the callback sequence when a result
170 is available.
171
172 ***/
173 var chain = this.chain;
174 var fired = this.fired;
175 var res = this.results[fired];
176 var self = this;
177 var cb = null;
178 while (chain.length > 0 && this.paused === 0) {
179 // Array
180 var pair = chain.shift();
181 var f = pair[fired];
182 if (f === null) {
183 continue;
184 }
185 try {
186 res = f(res);
187 fired = ((res instanceof Error) ? 1 : 0);
188 if (res instanceof MochiKit.Async.Deferred) {
189 cb = function (res) {
190 self._resback(res);
191 self.paused--;
192 if ((self.paused === 0) && (self.fired >= 0)) {
193 self._fire();
194 }
195 };
196 this.paused++;
197 }
198 } catch (err) {
199 fired = 1;
200 if (!(err instanceof Error)) {
201 err = new MochiKit.Async.GenericError(err);
202 }
203 res = err;
204 }
205 }
206 this.fired = fired;
207 this.results[fired] = res;
208 if (cb && this.paused) {
209 // this is for "tail recursion" in case the dependent deferred
210 // is already fired
211 res.addBoth(cb);
212 res.chained = true;
213 }
214 }
215 };
216
217 MochiKit.Base.update(MochiKit.Async, {
218 /** @id MochiKit.Async.evalJSONRequest */
219 evalJSONRequest: function (/* req */) {
220 return eval('(' + arguments[0].responseText + ')');
221 },
222
223 /** @id MochiKit.Async.succeed */
224 succeed: function (/* optional */result) {
225 var d = new MochiKit.Async.Deferred();
226 d.callback.apply(d, arguments);
227 return d;
228 },
229
230 /** @id MochiKit.Async.fail */
231 fail: function (/* optional */result) {
232 var d = new MochiKit.Async.Deferred();
233 d.errback.apply(d, arguments);
234 return d;
235 },
236
237 /** @id MochiKit.Async.getXMLHttpRequest */
238 getXMLHttpRequest: function () {
239 var self = arguments.callee;
240 if (!self.XMLHttpRequest) {
241 var tryThese = [
242 function () { return new XMLHttpRequest(); },
243 function () { return new ActiveXObject('Msxml2.XMLHTTP'); },
244 function () { return new ActiveXObject('Microsoft.XMLHTTP'); },
245 function () { return new ActiveXObject('Msxml2.XMLHTTP.4.0'); },
246 function () {
247 throw new MochiKit.Async.BrowserComplianceError("Browser does not support XMLHttpRequest");
248 }
249 ];
250 for (var i = 0; i < tryThese.length; i++) {
251 var func = tryThese[i];
252 try {
253 self.XMLHttpRequest = func;
254 return func();
255 } catch (e) {
256 // pass
257 }
258 }
259 }
260 return self.XMLHttpRequest();
261 },
262
263 _xhr_onreadystatechange: function (d) {
264 // MochiKit.Logging.logDebug('this.readyState', this.readyState);
265 var m = MochiKit.Base;
266 if (this.readyState == 4) {
267 // IE SUCKS
268 try {
269 this.onreadystatechange = null;
270 } catch (e) {
271 try {
272 this.onreadystatechange = m.noop;
273 } catch (e) {
274 }
275 }
276 var status = null;
277 try {
278 status = this.status;
279 if (!status && m.isNotEmpty(this.responseText)) {
280 // 0 or undefined seems to mean cached or local
281 status = 304;
282 }
283 } catch (e) {
284 // pass
285 // MochiKit.Logging.logDebug('error getting status?', repr(items(e)));
286 }
287 // 200 is OK, 201 is CREATED, 204 is NO CONTENT
288 // 304 is NOT MODIFIED, 1223 is apparently a bug in IE
289 if (status == 200 || status == 201 || status == 204 ||
290 status == 304 || status == 1223) {
291 d.callback(this);
292 } else {
293 var err = new MochiKit.Async.XMLHttpRequestError(this, "Request failed");
294 if (err.number) {
295 // XXX: This seems to happen on page change
296 d.errback(err);
297 } else {
298 // XXX: this seems to happen when the server is unreachable
299 d.errback(err);
300 }
301 }
302 }
303 },
304
305 _xhr_canceller: function (req) {
306 // IE SUCKS
307 try {
308 req.onreadystatechange = null;
309 } catch (e) {
310 try {
311 req.onreadystatechange = MochiKit.Base.noop;
312 } catch (e) {
313 }
314 }
315 req.abort();
316 },
317
318
319 /** @id MochiKit.Async.sendXMLHttpRequest */
320 sendXMLHttpRequest: function (req, /* optional */ sendContent) {
321 if (typeof(sendContent) == "undefined" || sendContent === null) {
322 sendContent = "";
323 }
324
325 var m = MochiKit.Base;
326 var self = MochiKit.Async;
327 var d = new self.Deferred(m.partial(self._xhr_canceller, req));
328
329 try {
330 req.onreadystatechange = m.bind(self._xhr_onreadystatechange,
331 req, d);
332 req.send(sendContent);
333 } catch (e) {
334 try {
335 req.onreadystatechange = null;
336 } catch (ignore) {
337 // pass
338 }
339 d.errback(e);
340 }
341
342 return d;
343
344 },
345
346 /** @id MochiKit.Async.doXHR */
347 doXHR: function (url, opts) {
348 /*
349 Work around a Firefox bug by dealing with XHR during
350 the next event loop iteration. Maybe it's this one:
351 https://bugzilla.mozilla.org/show_bug.cgi?id=249843
352 */
353 var self = MochiKit.Async;
354 return self.callLater(0, self._doXHR, url, opts);
355 },
356
357 _doXHR: function (url, opts) {
358 var m = MochiKit.Base;
359 opts = m.update({
360 method: 'GET',
361 sendContent: ''
362 /*
363 queryString: undefined,
364 username: undefined,
365 password: undefined,
366 headers: undefined,
367 mimeType: undefined
368 */
369 }, opts);
370 var self = MochiKit.Async;
371 var req = self.getXMLHttpRequest();
372 if (opts.queryString) {
373 var qs = m.queryString(opts.queryString);
374 if (qs) {
375 url += "?" + qs;
376 }
377 }
378 // Safari will send undefined:undefined, so we have to check.
379 // We can't use apply, since the function is native.
380 if ('username' in opts) {
381 req.open(opts.method, url, true, opts.username, opts.password);
382 } else {
383 req.open(opts.method, url, true);
384 }
385 if (req.overrideMimeType && opts.mimeType) {
386 req.overrideMimeType(opts.mimeType);
387 }
388 if (opts.headers) {
389 var headers = opts.headers;
390 if (!m.isArrayLike(headers)) {
391 headers = m.items(headers);
392 }
393 for (var i = 0; i < headers.length; i++) {
394 var header = headers[i];
395 var name = header[0];
396 var value = header[1];
397 req.setRequestHeader(name, value);
398 }
399 }
400 return self.sendXMLHttpRequest(req, opts.sendContent);
401 },
402
403 _buildURL: function (url/*, ...*/) {
404 if (arguments.length > 1) {
405 var m = MochiKit.Base;
406 var qs = m.queryString.apply(null, m.extend(null, arguments, 1));
407 if (qs) {
408 return url + "?" + qs;
409 }
410 }
411 return url;
412 },
413
414 /** @id MochiKit.Async.doSimpleXMLHttpRequest */
415 doSimpleXMLHttpRequest: function (url/*, ...*/) {
416 var self = MochiKit.Async;
417 url = self._buildURL.apply(self, arguments);
418 return self.doXHR(url);
419 },
420
421 /** @id MochiKit.Async.loadJSONDoc */
422 loadJSONDoc: function (url/*, ...*/) {
423 var self = MochiKit.Async;
424 url = self._buildURL.apply(self, arguments);
425 var d = self.doXHR(url, {
426 'mimeType': 'text/plain',
427 'headers': [['Accept', 'application/json']]
428 });
429 d = d.addCallback(self.evalJSONRequest);
430 return d;
431 },
432
433 /** @id MochiKit.Async.wait */
434 wait: function (seconds, /* optional */value) {
435 var d = new MochiKit.Async.Deferred();
436 var m = MochiKit.Base;
437 if (typeof(value) != 'undefined') {
438 d.addCallback(function () { return value; });
439 }
440 var timeout = setTimeout(
441 m.bind("callback", d),
442 Math.floor(seconds * 1000));
443 d.canceller = function () {
444 try {
445 clearTimeout(timeout);
446 } catch (e) {
447 // pass
448 }
449 };
450 return d;
451 },
452
453 /** @id MochiKit.Async.callLater */
454 callLater: function (seconds, func) {
455 var m = MochiKit.Base;
456 var pfunc = m.partial.apply(m, m.extend(null, arguments, 1));
457 return MochiKit.Async.wait(seconds).addCallback(
458 function (res) { return pfunc(); }
459 );
460 }
461 });
462
463
464 /** @id MochiKit.Async.DeferredLock */
465 MochiKit.Async.DeferredLock = function () {
466 this.waiting = [];
467 this.locked = false;
468 this.id = this._nextId();
469 };
470
471 MochiKit.Async.DeferredLock.prototype = {
472 __class__: MochiKit.Async.DeferredLock,
473 /** @id MochiKit.Async.DeferredLock.prototype.acquire */
474 acquire: function () {
475 var d = new MochiKit.Async.Deferred();
476 if (this.locked) {
477 this.waiting.push(d);
478 } else {
479 this.locked = true;
480 d.callback(this);
481 }
482 return d;
483 },
484 /** @id MochiKit.Async.DeferredLock.prototype.release */
485 release: function () {
486 if (!this.locked) {
487 throw TypeError("Tried to release an unlocked DeferredLock");
488 }
489 this.locked = false;
490 if (this.waiting.length > 0) {
491 this.locked = true;
492 this.waiting.shift().callback(this);
493 }
494 },
495 _nextId: MochiKit.Base.counter(),
496 repr: function () {
497 var state;
498 if (this.locked) {
499 state = 'locked, ' + this.waiting.length + ' waiting';
500 } else {
501 state = 'unlocked';
502 }
503 return 'DeferredLock(' + this.id + ', ' + state + ')';
504 },
505 toString: MochiKit.Base.forwardCall("repr")
506
507 };
508
509 /** @id MochiKit.Async.DeferredList */
510 MochiKit.Async.DeferredList = function (list, /* optional */fireOnOneCallback, fireOnOneErrback, consumeErrors, canceller) {
511
512 // call parent constructor
513 MochiKit.Async.Deferred.apply(this, [canceller]);
514
515 this.list = list;
516 var resultList = [];
517 this.resultList = resultList;
518
519 this.finishedCount = 0;
520 this.fireOnOneCallback = fireOnOneCallback;
521 this.fireOnOneErrback = fireOnOneErrback;
522 this.consumeErrors = consumeErrors;
523
524 var cb = MochiKit.Base.bind(this._cbDeferred, this);
525 for (var i = 0; i < list.length; i++) {
526 var d = list[i];
527 resultList.push(undefined);
528 d.addCallback(cb, i, true);
529 d.addErrback(cb, i, false);
530 }
531
532 if (list.length === 0 && !fireOnOneCallback) {
533 this.callback(this.resultList);
534 }
535
536 };
537
538 MochiKit.Async.DeferredList.prototype = new MochiKit.Async.Deferred();
539
540 MochiKit.Async.DeferredList.prototype._cbDeferred = function (index, succeeded, result) {
541 this.resultList[index] = [succeeded, result];
542 this.finishedCount += 1;
543 if (this.fired == -1) {
544 if (succeeded && this.fireOnOneCallback) {
545 this.callback([index, result]);
546 } else if (!succeeded && this.fireOnOneErrback) {
547 this.errback(result);
548 } else if (this.finishedCount == this.list.length) {
549 this.callback(this.resultList);
550 }
551 }
552 if (!succeeded && this.consumeErrors) {
553 result = null;
554 }
555 return result;
556 };
557
558 /** @id MochiKit.Async.gatherResults */
559 MochiKit.Async.gatherResults = function (deferredList) {
560 var d = new MochiKit.Async.DeferredList(deferredList, false, true, false);
561 d.addCallback(function (results) {
562 var ret = [];
563 for (var i = 0; i < results.length; i++) {
564 ret.push(results[i][1]);
565 }
566 return ret;
567 });
568 return d;
569 };
570
571 /** @id MochiKit.Async.maybeDeferred */
572 MochiKit.Async.maybeDeferred = function (func) {
573 var self = MochiKit.Async;
574 var result;
575 try {
576 var r = func.apply(null, MochiKit.Base.extend([], arguments, 1));
577 if (r instanceof self.Deferred) {
578 result = r;
579 } else if (r instanceof Error) {
580 result = self.fail(r);
581 } else {
582 result = self.succeed(r);
583 }
584 } catch (e) {
585 result = self.fail(e);
586 }
587 return result;
588 };
589
590
591 MochiKit.Async.EXPORT = [
592 "AlreadyCalledError",
593 "CancelledError",
594 "BrowserComplianceError",
595 "GenericError",
596 "XMLHttpRequestError",
597 "Deferred",
598 "succeed",
599 "fail",
600 "getXMLHttpRequest",
601 "doSimpleXMLHttpRequest",
602 "loadJSONDoc",
603 "wait",
604 "callLater",
605 "sendXMLHttpRequest",
606 "DeferredLock",
607 "DeferredList",
608 "gatherResults",
609 "maybeDeferred",
610 "doXHR"
611 ];
612
613 MochiKit.Async.EXPORT_OK = [
614 "evalJSONRequest"
615 ];
616
617 MochiKit.Async.__new__ = function () {
618 var m = MochiKit.Base;
619 var ne = m.partial(m._newNamedError, this);
620
621 ne("AlreadyCalledError",
622 /** @id MochiKit.Async.AlreadyCalledError */
623 function (deferred) {
624 /***
625
626 Raised by the Deferred if callback or errback happens
627 after it was already fired.
628
629 ***/
630 this.deferred = deferred;
631 }
632 );
633
634 ne("CancelledError",
635 /** @id MochiKit.Async.CancelledError */
636 function (deferred) {
637 /***
638
639 Raised by the Deferred cancellation mechanism.
640
641 ***/
642 this.deferred = deferred;
643 }
644 );
645
646 ne("BrowserComplianceError",
647 /** @id MochiKit.Async.BrowserComplianceError */
648 function (msg) {
649 /***
650
651 Raised when the JavaScript runtime is not capable of performing
652 the given function. Technically, this should really never be
653 raised because a non-conforming JavaScript runtime probably
654 isn't going to support exceptions in the first place.
655
656 ***/
657 this.message = msg;
658 }
659 );
660
661 ne("GenericError",
662 /** @id MochiKit.Async.GenericError */
663 function (msg) {
664 this.message = msg;
665 }
666 );
667
668 ne("XMLHttpRequestError",
669 /** @id MochiKit.Async.XMLHttpRequestError */
670 function (req, msg) {
671 /***
672
673 Raised when an XMLHttpRequest does not complete for any reason.
674
675 ***/
676 this.req = req;
677 this.message = msg;
678 try {
679 // Strange but true that this can raise in some cases.
680 this.number = req.status;
681 } catch (e) {
682 // pass
683 }
684 }
685 );
686
687
688 this.EXPORT_TAGS = {
689 ":common": this.EXPORT,
690 ":all": m.concat(this.EXPORT, this.EXPORT_OK)
691 };
692
693 m.nameFunctions(this);
694
695 };
696
697 MochiKit.Async.__new__();
698
699 MochiKit.Base._exportSymbols(this, MochiKit.Async);