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