michael@0: /*** michael@0: michael@0: MochiKit.Async 1.4.2 michael@0: michael@0: See for documentation, downloads, license, etc. michael@0: michael@0: (c) 2005 Bob Ippolito. All rights Reserved. michael@0: michael@0: ***/ michael@0: michael@0: MochiKit.Base._deps('Async', ['Base']); michael@0: michael@0: MochiKit.Async.NAME = "MochiKit.Async"; michael@0: MochiKit.Async.VERSION = "1.4.2"; michael@0: MochiKit.Async.__repr__ = function () { michael@0: return "[" + this.NAME + " " + this.VERSION + "]"; michael@0: }; michael@0: MochiKit.Async.toString = function () { michael@0: return this.__repr__(); michael@0: }; michael@0: michael@0: /** @id MochiKit.Async.Deferred */ michael@0: MochiKit.Async.Deferred = function (/* optional */ canceller) { michael@0: this.chain = []; michael@0: this.id = this._nextId(); michael@0: this.fired = -1; michael@0: this.paused = 0; michael@0: this.results = [null, null]; michael@0: this.canceller = canceller; michael@0: this.silentlyCancelled = false; michael@0: this.chained = false; michael@0: }; michael@0: michael@0: MochiKit.Async.Deferred.prototype = { michael@0: /** @id MochiKit.Async.Deferred.prototype.repr */ michael@0: repr: function () { michael@0: var state; michael@0: if (this.fired == -1) { michael@0: state = 'unfired'; michael@0: } else if (this.fired === 0) { michael@0: state = 'success'; michael@0: } else { michael@0: state = 'error'; michael@0: } michael@0: return 'Deferred(' + this.id + ', ' + state + ')'; michael@0: }, michael@0: michael@0: toString: MochiKit.Base.forwardCall("repr"), michael@0: michael@0: _nextId: MochiKit.Base.counter(), michael@0: michael@0: /** @id MochiKit.Async.Deferred.prototype.cancel */ michael@0: cancel: function () { michael@0: var self = MochiKit.Async; michael@0: if (this.fired == -1) { michael@0: if (this.canceller) { michael@0: this.canceller(this); michael@0: } else { michael@0: this.silentlyCancelled = true; michael@0: } michael@0: if (this.fired == -1) { michael@0: this.errback(new self.CancelledError(this)); michael@0: } michael@0: } else if ((this.fired === 0) && (this.results[0] instanceof self.Deferred)) { michael@0: this.results[0].cancel(); michael@0: } michael@0: }, michael@0: michael@0: _resback: function (res) { michael@0: /*** michael@0: michael@0: The primitive that means either callback or errback michael@0: michael@0: ***/ michael@0: this.fired = ((res instanceof Error) ? 1 : 0); michael@0: this.results[this.fired] = res; michael@0: this._fire(); michael@0: }, michael@0: michael@0: _check: function () { michael@0: if (this.fired != -1) { michael@0: if (!this.silentlyCancelled) { michael@0: throw new MochiKit.Async.AlreadyCalledError(this); michael@0: } michael@0: this.silentlyCancelled = false; michael@0: return; michael@0: } michael@0: }, michael@0: michael@0: /** @id MochiKit.Async.Deferred.prototype.callback */ michael@0: callback: function (res) { michael@0: this._check(); michael@0: if (res instanceof MochiKit.Async.Deferred) { michael@0: throw new Error("Deferred instances can only be chained if they are the result of a callback"); michael@0: } michael@0: this._resback(res); michael@0: }, michael@0: michael@0: /** @id MochiKit.Async.Deferred.prototype.errback */ michael@0: errback: function (res) { michael@0: this._check(); michael@0: var self = MochiKit.Async; michael@0: if (res instanceof self.Deferred) { michael@0: throw new Error("Deferred instances can only be chained if they are the result of a callback"); michael@0: } michael@0: if (!(res instanceof Error)) { michael@0: res = new self.GenericError(res); michael@0: } michael@0: this._resback(res); michael@0: }, michael@0: michael@0: /** @id MochiKit.Async.Deferred.prototype.addBoth */ michael@0: addBoth: function (fn) { michael@0: if (arguments.length > 1) { michael@0: fn = MochiKit.Base.partial.apply(null, arguments); michael@0: } michael@0: return this.addCallbacks(fn, fn); michael@0: }, michael@0: michael@0: /** @id MochiKit.Async.Deferred.prototype.addCallback */ michael@0: addCallback: function (fn) { michael@0: if (arguments.length > 1) { michael@0: fn = MochiKit.Base.partial.apply(null, arguments); michael@0: } michael@0: return this.addCallbacks(fn, null); michael@0: }, michael@0: michael@0: /** @id MochiKit.Async.Deferred.prototype.addErrback */ michael@0: addErrback: function (fn) { michael@0: if (arguments.length > 1) { michael@0: fn = MochiKit.Base.partial.apply(null, arguments); michael@0: } michael@0: return this.addCallbacks(null, fn); michael@0: }, michael@0: michael@0: /** @id MochiKit.Async.Deferred.prototype.addCallbacks */ michael@0: addCallbacks: function (cb, eb) { michael@0: if (this.chained) { michael@0: throw new Error("Chained Deferreds can not be re-used"); michael@0: } michael@0: this.chain.push([cb, eb]); michael@0: if (this.fired >= 0) { michael@0: this._fire(); michael@0: } michael@0: return this; michael@0: }, michael@0: michael@0: _fire: function () { michael@0: /*** michael@0: michael@0: Used internally to exhaust the callback sequence when a result michael@0: is available. michael@0: michael@0: ***/ michael@0: var chain = this.chain; michael@0: var fired = this.fired; michael@0: var res = this.results[fired]; michael@0: var self = this; michael@0: var cb = null; michael@0: while (chain.length > 0 && this.paused === 0) { michael@0: // Array michael@0: var pair = chain.shift(); michael@0: var f = pair[fired]; michael@0: if (f === null) { michael@0: continue; michael@0: } michael@0: try { michael@0: res = f(res); michael@0: fired = ((res instanceof Error) ? 1 : 0); michael@0: if (res instanceof MochiKit.Async.Deferred) { michael@0: cb = function (res) { michael@0: self._resback(res); michael@0: self.paused--; michael@0: if ((self.paused === 0) && (self.fired >= 0)) { michael@0: self._fire(); michael@0: } michael@0: }; michael@0: this.paused++; michael@0: } michael@0: } catch (err) { michael@0: fired = 1; michael@0: if (!(err instanceof Error)) { michael@0: err = new MochiKit.Async.GenericError(err); michael@0: } michael@0: res = err; michael@0: } michael@0: } michael@0: this.fired = fired; michael@0: this.results[fired] = res; michael@0: if (cb && this.paused) { michael@0: // this is for "tail recursion" in case the dependent deferred michael@0: // is already fired michael@0: res.addBoth(cb); michael@0: res.chained = true; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: MochiKit.Base.update(MochiKit.Async, { michael@0: /** @id MochiKit.Async.evalJSONRequest */ michael@0: evalJSONRequest: function (req) { michael@0: return MochiKit.Base.evalJSON(req.responseText); michael@0: }, michael@0: michael@0: /** @id MochiKit.Async.succeed */ michael@0: succeed: function (/* optional */result) { michael@0: var d = new MochiKit.Async.Deferred(); michael@0: d.callback.apply(d, arguments); michael@0: return d; michael@0: }, michael@0: michael@0: /** @id MochiKit.Async.fail */ michael@0: fail: function (/* optional */result) { michael@0: var d = new MochiKit.Async.Deferred(); michael@0: d.errback.apply(d, arguments); michael@0: return d; michael@0: }, michael@0: michael@0: /** @id MochiKit.Async.getXMLHttpRequest */ michael@0: getXMLHttpRequest: function () { michael@0: var self = arguments.callee; michael@0: if (!self.XMLHttpRequest) { michael@0: var tryThese = [ michael@0: function () { return new XMLHttpRequest(); }, michael@0: function () { return new ActiveXObject('Msxml2.XMLHTTP'); }, michael@0: function () { return new ActiveXObject('Microsoft.XMLHTTP'); }, michael@0: function () { return new ActiveXObject('Msxml2.XMLHTTP.4.0'); }, michael@0: function () { michael@0: throw new MochiKit.Async.BrowserComplianceError("Browser does not support XMLHttpRequest"); michael@0: } michael@0: ]; michael@0: for (var i = 0; i < tryThese.length; i++) { michael@0: var func = tryThese[i]; michael@0: try { michael@0: self.XMLHttpRequest = func; michael@0: return func(); michael@0: } catch (e) { michael@0: // pass michael@0: } michael@0: } michael@0: } michael@0: return self.XMLHttpRequest(); michael@0: }, michael@0: michael@0: _xhr_onreadystatechange: function (d) { michael@0: // MochiKit.Logging.logDebug('this.readyState', this.readyState); michael@0: var m = MochiKit.Base; michael@0: if (this.readyState == 4) { michael@0: // IE SUCKS michael@0: try { michael@0: this.onreadystatechange = null; michael@0: } catch (e) { michael@0: try { michael@0: this.onreadystatechange = m.noop; michael@0: } catch (e) { michael@0: } michael@0: } michael@0: var status = null; michael@0: try { michael@0: status = this.status; michael@0: if (!status && m.isNotEmpty(this.responseText)) { michael@0: // 0 or undefined seems to mean cached or local michael@0: status = 304; michael@0: } michael@0: } catch (e) { michael@0: // pass michael@0: // MochiKit.Logging.logDebug('error getting status?', repr(items(e))); michael@0: } michael@0: // 200 is OK, 201 is CREATED, 204 is NO CONTENT michael@0: // 304 is NOT MODIFIED, 1223 is apparently a bug in IE michael@0: if (status == 200 || status == 201 || status == 204 || michael@0: status == 304 || status == 1223) { michael@0: d.callback(this); michael@0: } else { michael@0: var err = new MochiKit.Async.XMLHttpRequestError(this, "Request failed"); michael@0: if (err.number) { michael@0: // XXX: This seems to happen on page change michael@0: d.errback(err); michael@0: } else { michael@0: // XXX: this seems to happen when the server is unreachable michael@0: d.errback(err); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _xhr_canceller: function (req) { michael@0: // IE SUCKS michael@0: try { michael@0: req.onreadystatechange = null; michael@0: } catch (e) { michael@0: try { michael@0: req.onreadystatechange = MochiKit.Base.noop; michael@0: } catch (e) { michael@0: } michael@0: } michael@0: req.abort(); michael@0: }, michael@0: michael@0: michael@0: /** @id MochiKit.Async.sendXMLHttpRequest */ michael@0: sendXMLHttpRequest: function (req, /* optional */ sendContent) { michael@0: if (typeof(sendContent) == "undefined" || sendContent === null) { michael@0: sendContent = ""; michael@0: } michael@0: michael@0: var m = MochiKit.Base; michael@0: var self = MochiKit.Async; michael@0: var d = new self.Deferred(m.partial(self._xhr_canceller, req)); michael@0: michael@0: try { michael@0: req.onreadystatechange = m.bind(self._xhr_onreadystatechange, michael@0: req, d); michael@0: req.send(sendContent); michael@0: } catch (e) { michael@0: try { michael@0: req.onreadystatechange = null; michael@0: } catch (ignore) { michael@0: // pass michael@0: } michael@0: d.errback(e); michael@0: } michael@0: michael@0: return d; michael@0: michael@0: }, michael@0: michael@0: /** @id MochiKit.Async.doXHR */ michael@0: doXHR: function (url, opts) { michael@0: /* michael@0: Work around a Firefox bug by dealing with XHR during michael@0: the next event loop iteration. Maybe it's this one: michael@0: https://bugzilla.mozilla.org/show_bug.cgi?id=249843 michael@0: */ michael@0: var self = MochiKit.Async; michael@0: return self.callLater(0, self._doXHR, url, opts); michael@0: }, michael@0: michael@0: _doXHR: function (url, opts) { michael@0: var m = MochiKit.Base; michael@0: opts = m.update({ michael@0: method: 'GET', michael@0: sendContent: '' michael@0: /* michael@0: queryString: undefined, michael@0: username: undefined, michael@0: password: undefined, michael@0: headers: undefined, michael@0: mimeType: undefined michael@0: */ michael@0: }, opts); michael@0: var self = MochiKit.Async; michael@0: var req = self.getXMLHttpRequest(); michael@0: if (opts.queryString) { michael@0: var qs = m.queryString(opts.queryString); michael@0: if (qs) { michael@0: url += "?" + qs; michael@0: } michael@0: } michael@0: // Safari will send undefined:undefined, so we have to check. michael@0: // We can't use apply, since the function is native. michael@0: if ('username' in opts) { michael@0: req.open(opts.method, url, true, opts.username, opts.password); michael@0: } else { michael@0: req.open(opts.method, url, true); michael@0: } michael@0: if (req.overrideMimeType && opts.mimeType) { michael@0: req.overrideMimeType(opts.mimeType); michael@0: } michael@0: req.setRequestHeader("X-Requested-With", "XMLHttpRequest"); michael@0: if (opts.headers) { michael@0: var headers = opts.headers; michael@0: if (!m.isArrayLike(headers)) { michael@0: headers = m.items(headers); michael@0: } michael@0: for (var i = 0; i < headers.length; i++) { michael@0: var header = headers[i]; michael@0: var name = header[0]; michael@0: var value = header[1]; michael@0: req.setRequestHeader(name, value); michael@0: } michael@0: } michael@0: return self.sendXMLHttpRequest(req, opts.sendContent); michael@0: }, michael@0: michael@0: _buildURL: function (url/*, ...*/) { michael@0: if (arguments.length > 1) { michael@0: var m = MochiKit.Base; michael@0: var qs = m.queryString.apply(null, m.extend(null, arguments, 1)); michael@0: if (qs) { michael@0: return url + "?" + qs; michael@0: } michael@0: } michael@0: return url; michael@0: }, michael@0: michael@0: /** @id MochiKit.Async.doSimpleXMLHttpRequest */ michael@0: doSimpleXMLHttpRequest: function (url/*, ...*/) { michael@0: var self = MochiKit.Async; michael@0: url = self._buildURL.apply(self, arguments); michael@0: return self.doXHR(url); michael@0: }, michael@0: michael@0: /** @id MochiKit.Async.loadJSONDoc */ michael@0: loadJSONDoc: function (url/*, ...*/) { michael@0: var self = MochiKit.Async; michael@0: url = self._buildURL.apply(self, arguments); michael@0: var d = self.doXHR(url, { michael@0: 'mimeType': 'text/plain', michael@0: 'headers': [['Accept', 'application/json']] michael@0: }); michael@0: d = d.addCallback(self.evalJSONRequest); michael@0: return d; michael@0: }, michael@0: michael@0: /** @id MochiKit.Async.wait */ michael@0: wait: function (seconds, /* optional */value) { michael@0: var d = new MochiKit.Async.Deferred(); michael@0: var m = MochiKit.Base; michael@0: if (typeof(value) != 'undefined') { michael@0: d.addCallback(function () { return value; }); michael@0: } michael@0: var timeout = setTimeout( michael@0: m.bind("callback", d), michael@0: Math.floor(seconds * 1000)); michael@0: d.canceller = function () { michael@0: try { michael@0: clearTimeout(timeout); michael@0: } catch (e) { michael@0: // pass michael@0: } michael@0: }; michael@0: return d; michael@0: }, michael@0: michael@0: /** @id MochiKit.Async.callLater */ michael@0: callLater: function (seconds, func) { michael@0: var m = MochiKit.Base; michael@0: var pfunc = m.partial.apply(m, m.extend(null, arguments, 1)); michael@0: return MochiKit.Async.wait(seconds).addCallback( michael@0: function (res) { return pfunc(); } michael@0: ); michael@0: } michael@0: }); michael@0: michael@0: michael@0: /** @id MochiKit.Async.DeferredLock */ michael@0: MochiKit.Async.DeferredLock = function () { michael@0: this.waiting = []; michael@0: this.locked = false; michael@0: this.id = this._nextId(); michael@0: }; michael@0: michael@0: MochiKit.Async.DeferredLock.prototype = { michael@0: __class__: MochiKit.Async.DeferredLock, michael@0: /** @id MochiKit.Async.DeferredLock.prototype.acquire */ michael@0: acquire: function () { michael@0: var d = new MochiKit.Async.Deferred(); michael@0: if (this.locked) { michael@0: this.waiting.push(d); michael@0: } else { michael@0: this.locked = true; michael@0: d.callback(this); michael@0: } michael@0: return d; michael@0: }, michael@0: /** @id MochiKit.Async.DeferredLock.prototype.release */ michael@0: release: function () { michael@0: if (!this.locked) { michael@0: throw TypeError("Tried to release an unlocked DeferredLock"); michael@0: } michael@0: this.locked = false; michael@0: if (this.waiting.length > 0) { michael@0: this.locked = true; michael@0: this.waiting.shift().callback(this); michael@0: } michael@0: }, michael@0: _nextId: MochiKit.Base.counter(), michael@0: repr: function () { michael@0: var state; michael@0: if (this.locked) { michael@0: state = 'locked, ' + this.waiting.length + ' waiting'; michael@0: } else { michael@0: state = 'unlocked'; michael@0: } michael@0: return 'DeferredLock(' + this.id + ', ' + state + ')'; michael@0: }, michael@0: toString: MochiKit.Base.forwardCall("repr") michael@0: michael@0: }; michael@0: michael@0: /** @id MochiKit.Async.DeferredList */ michael@0: MochiKit.Async.DeferredList = function (list, /* optional */fireOnOneCallback, fireOnOneErrback, consumeErrors, canceller) { michael@0: michael@0: // call parent constructor michael@0: MochiKit.Async.Deferred.apply(this, [canceller]); michael@0: michael@0: this.list = list; michael@0: var resultList = []; michael@0: this.resultList = resultList; michael@0: michael@0: this.finishedCount = 0; michael@0: this.fireOnOneCallback = fireOnOneCallback; michael@0: this.fireOnOneErrback = fireOnOneErrback; michael@0: this.consumeErrors = consumeErrors; michael@0: michael@0: var cb = MochiKit.Base.bind(this._cbDeferred, this); michael@0: for (var i = 0; i < list.length; i++) { michael@0: var d = list[i]; michael@0: resultList.push(undefined); michael@0: d.addCallback(cb, i, true); michael@0: d.addErrback(cb, i, false); michael@0: } michael@0: michael@0: if (list.length === 0 && !fireOnOneCallback) { michael@0: this.callback(this.resultList); michael@0: } michael@0: michael@0: }; michael@0: michael@0: MochiKit.Async.DeferredList.prototype = new MochiKit.Async.Deferred(); michael@0: michael@0: MochiKit.Async.DeferredList.prototype._cbDeferred = function (index, succeeded, result) { michael@0: this.resultList[index] = [succeeded, result]; michael@0: this.finishedCount += 1; michael@0: if (this.fired == -1) { michael@0: if (succeeded && this.fireOnOneCallback) { michael@0: this.callback([index, result]); michael@0: } else if (!succeeded && this.fireOnOneErrback) { michael@0: this.errback(result); michael@0: } else if (this.finishedCount == this.list.length) { michael@0: this.callback(this.resultList); michael@0: } michael@0: } michael@0: if (!succeeded && this.consumeErrors) { michael@0: result = null; michael@0: } michael@0: return result; michael@0: }; michael@0: michael@0: /** @id MochiKit.Async.gatherResults */ michael@0: MochiKit.Async.gatherResults = function (deferredList) { michael@0: var d = new MochiKit.Async.DeferredList(deferredList, false, true, false); michael@0: d.addCallback(function (results) { michael@0: var ret = []; michael@0: for (var i = 0; i < results.length; i++) { michael@0: ret.push(results[i][1]); michael@0: } michael@0: return ret; michael@0: }); michael@0: return d; michael@0: }; michael@0: michael@0: /** @id MochiKit.Async.maybeDeferred */ michael@0: MochiKit.Async.maybeDeferred = function (func) { michael@0: var self = MochiKit.Async; michael@0: var result; michael@0: try { michael@0: var r = func.apply(null, MochiKit.Base.extend([], arguments, 1)); michael@0: if (r instanceof self.Deferred) { michael@0: result = r; michael@0: } else if (r instanceof Error) { michael@0: result = self.fail(r); michael@0: } else { michael@0: result = self.succeed(r); michael@0: } michael@0: } catch (e) { michael@0: result = self.fail(e); michael@0: } michael@0: return result; michael@0: }; michael@0: michael@0: michael@0: MochiKit.Async.EXPORT = [ michael@0: "AlreadyCalledError", michael@0: "CancelledError", michael@0: "BrowserComplianceError", michael@0: "GenericError", michael@0: "XMLHttpRequestError", michael@0: "Deferred", michael@0: "succeed", michael@0: "fail", michael@0: "getXMLHttpRequest", michael@0: "doSimpleXMLHttpRequest", michael@0: "loadJSONDoc", michael@0: "wait", michael@0: "callLater", michael@0: "sendXMLHttpRequest", michael@0: "DeferredLock", michael@0: "DeferredList", michael@0: "gatherResults", michael@0: "maybeDeferred", michael@0: "doXHR" michael@0: ]; michael@0: michael@0: MochiKit.Async.EXPORT_OK = [ michael@0: "evalJSONRequest" michael@0: ]; michael@0: michael@0: MochiKit.Async.__new__ = function () { michael@0: var m = MochiKit.Base; michael@0: var ne = m.partial(m._newNamedError, this); michael@0: michael@0: ne("AlreadyCalledError", michael@0: /** @id MochiKit.Async.AlreadyCalledError */ michael@0: function (deferred) { michael@0: /*** michael@0: michael@0: Raised by the Deferred if callback or errback happens michael@0: after it was already fired. michael@0: michael@0: ***/ michael@0: this.deferred = deferred; michael@0: } michael@0: ); michael@0: michael@0: ne("CancelledError", michael@0: /** @id MochiKit.Async.CancelledError */ michael@0: function (deferred) { michael@0: /*** michael@0: michael@0: Raised by the Deferred cancellation mechanism. michael@0: michael@0: ***/ michael@0: this.deferred = deferred; michael@0: } michael@0: ); michael@0: michael@0: ne("BrowserComplianceError", michael@0: /** @id MochiKit.Async.BrowserComplianceError */ michael@0: function (msg) { michael@0: /*** michael@0: michael@0: Raised when the JavaScript runtime is not capable of performing michael@0: the given function. Technically, this should really never be michael@0: raised because a non-conforming JavaScript runtime probably michael@0: isn't going to support exceptions in the first place. michael@0: michael@0: ***/ michael@0: this.message = msg; michael@0: } michael@0: ); michael@0: michael@0: ne("GenericError", michael@0: /** @id MochiKit.Async.GenericError */ michael@0: function (msg) { michael@0: this.message = msg; michael@0: } michael@0: ); michael@0: michael@0: ne("XMLHttpRequestError", michael@0: /** @id MochiKit.Async.XMLHttpRequestError */ michael@0: function (req, msg) { michael@0: /*** michael@0: michael@0: Raised when an XMLHttpRequest does not complete for any reason. michael@0: michael@0: ***/ michael@0: this.req = req; michael@0: this.message = msg; michael@0: try { michael@0: // Strange but true that this can raise in some cases. michael@0: this.number = req.status; michael@0: } catch (e) { michael@0: // pass michael@0: } michael@0: } michael@0: ); michael@0: michael@0: michael@0: this.EXPORT_TAGS = { michael@0: ":common": this.EXPORT, michael@0: ":all": m.concat(this.EXPORT, this.EXPORT_OK) michael@0: }; michael@0: michael@0: m.nameFunctions(this); michael@0: michael@0: }; michael@0: michael@0: MochiKit.Async.__new__(); michael@0: michael@0: MochiKit.Base._exportSymbols(this, MochiKit.Async);