	//See Confex::CGI::jscallback for full documentation.

	function FexJavaScriptServer(endpoint) {
		this.endpoint = endpoint;

		// any params that need to be passed w/ every request. recieving scripts need to be able to decode GET request for this to work.
		this.extraqueryargs = '';
		
		var res;
		if(res = this.endpoint.match('^([^?]+)\\?([^?]+)$')) { 
			this.endpoint = res[1];
			this.extraqueryargs = res[2];			
		}
		
		this.inAsyncRequest = false;
		
		var self = this;
		this.errorHandlers  = [function(e) { self.debug(e.message); throw(e); }];	
		this.lockCount = 0;

		this.debug = function() { }; //noop
		// enable these next two lines to debug;
		if(String(window.location).match(/jsrpcdebug$/)) { 
			this.window = window.open('about:blank','debug');
			this.window.document.writeln('<div style="font-family: monospace; font-size: 75%">');
			this.debug = function(msg) { if(this.window) { this.window.document.writeln((new Date()) + " : "  + msg + "<br>"); } }
		}
		this.debug("new fexJavaScriptServer started, debug started");

		if (!this.xmlhttp && window.XMLHttpRequest) {
			this.xmlhttp = new XMLHttpRequest();
		} else {
			this.xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
		}
		this.debug("xmlhttp on hand: " + this.xmlhttp);
		//this.debug(this.toSource());

	}
	FexJavaScriptServer.prototype.getNewXMLHTTP = function() { 
		var xmlhttp;
		if (window.XMLHttpRequest) {
			xmlhttp = new XMLHttpRequest();
		} else {
			xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
		}
		return xmlhttp;
	};
	function ts() { return this.message };

	FexJavaScriptServer.prototype.throwError = 
		function(e) { 
			if(typeof e == 'string') {  e = {toString: ts, message: e}; }
			for(var i in this.errorHandlers) {
				var ret = this.errorHandlers[i](e);
				if(ret) break;
			}
		};


	// this converts javascript data structures
	//to a non-recursive list of query paramaters.
	// Note, the query strings can get pretty long.
	// Each atomic bit is uri-escaped using the built in escape()
	// there is code on the server that can turn this into whatever
	// data type we use there.
	FexJavaScriptServer.prototype.js2query = 
		function(obj) {
			var req = [];

			if(typeof obj == "number" || typeof obj == "string") { 
				req.push("__!s="+escape(obj));
			} else if(typeof obj ==  "object") { 
				var type = '';
				if(obj.constructor == Array) { 
					// it's an array
					req.push("__!a=" + obj.length);
					for(var i in obj) { 
						req.push(this.js2query(obj[i]));
					}
				} else { 
					// it's a hash
					var list = [];
					var count = 0;
					for(var i in obj) { 
						count++;
						this.debug('js2query:' + i);
						this.debug('js2query:' + this.js2query(i));
						list.push(this.js2query(i));
						list.push(this.js2query(obj[i]));
					}
					req.push('__!o=' + count);
					for(var i in list) { req.push(list[i]); }
				}

			} else if (typeof obj == 'function') { 
				req.push("__!f=*function");	
			} else { 
				// undef.
				req.push('__!u=1');
			}

			//req.push(i + "=" + escape(obj[i]));
			return req.join('&');
		};
	
	// Mozilla browers could use .toSource, but microsoft is braindead and doesn't	
	// provide similar functionality.  we have to do it ourselves.
FexJavaScriptServer.prototype.toJSON = function(arg) {
	var i, o, v;
	switch (typeof arg) {
		case 'object':
			if (arg) {
				if (arg.constructor == Array) {
					o = '[';
					for (i = 0; i < arg.length; ++i) {
						v = this.toJSON(arg[i]);
						if (v != 'function' && typeof v !== 'undefined') {
							o += (o != '[' ? ',' : '') + v;
						} else {
							o += ',';
						}
					}
					return o + ']';
				} else if (typeof arg.toString != 'undefined') {
					o = '{';
					for (i in arg) {
						v = this.toJSON(arg[i]);
						if (v != 'function' && typeof v !== 'undefined') {
							o += (o != '{' ? ',' : '') +
								this.toJSON(i) + ':' + v;
						}
					}
					return o + '}';
				} else {
					return;
				}
			}
			return 'null';
		case 'unknown':
		case 'undefined':
			return;
		case 'string':
			return '"' + arg.replace(/(["\\])/g, '\\$1') + '"';
		case 'function':
			return 'function';
		default:
			return String(arg);
	}
};


	//this function signals the start of an asynchronous call.  it returns
	// a function which is the 'key' to unlock the mutex that only allows one call
	// to be outstanding at a time.  The function takes one paramater - whether or not 
	// the asyncqueue should be checked.  It cannot be checked from the thread that the 
	// readystatechage code runs in.
	FexJavaScriptServer.prototype.asyncStart = 	
		function() { 
			var myCount = this.lockCount++;
			this.debug("asyncstart("+myCount+") from " + arguments.caller);
			this.inAsyncRequest = true;	
			var instance = this;
			var unlocked = false;
			return function() {
				if(unlocked) { 
					this.throwError("asyncstop("+myCount+") already unlocked!");
				} else   { 
					instance.debug("asyncstop("+myCount+") unlocking!");
					instance.inAsyncRequest = false;
				}
			};
		};

	// this was seperated out so we don't have to create such a large  
	//closure every time we go into async mode.  This is the code that
	// handles the xmlhttp request state changes, and actually invokes the async code
	// when the status is 4 and successfully parsed.
	FexJavaScriptServer.prototype.asyncCallbackHook = 
		function(unlock,asynccallbacks,method,xmlhttp)  {
			this.debug("docallback/async: triggered.  state: " + xmlhttp.readyState);
			var arg;
			// the 4th ready state means things are okay.
			// at this point we should parse the arguments.
			if(xmlhttp.readyState == 4) {
				this.debug("response text is " + xmlhttp.responseText);
				try { 
					if(xmlhttp.status == 200 )  {
						resp = eval('arg = ' + xmlhttp.responseText + ';');
					} else {
						//the status wasn't 200, so abort the request.
						if(asynccallbacks.onerror) asynccallbacks.onerror({message: "Non-OK status from web server: (expected 200, got: " + xmlhttp.status + ")" });
						this.throwError({message: "Non-OK status from web server: (expected 200, got: " + xmlhttp.status + ")" });
					}
				} catch (e) { 
					var txt;
					try { 
						txt = xmlhttp.responseText;
					} catch (x) { 
						// coulden't read any response.  IE throws error if you access
						//responseText before it's ready.
					}
					unlock();
					if(asynccallbacks.onerror) asynccallbacks.onerror(e);
					this.throwError("Cannot call RPC method " + method + "(in async mode) due to failure with server communication: " + e.message +
							"(Server responded: " + txt + ")");

				}
				try { 
					// now we actually call the code provided by the user.
					if(asynccallbacks.onerror) asynccallbacks.onsuccess(arg) 
				} catch (e) { 
					this.throwError("Error in callback code: " + e.message);
				} finally { 
					unlock();
				}
			} else { 
				this.debug("docallback/async: readystate not 4");
			}

		};

	FexJavaScriptServer.prototype.multiPartEncode = 
		function(parts) { 
			var list = [];
			
			for (var key in parts) {
				list.push(urlencode(key) + '=' + urlencode(parts[key]));
			}

			data = list.join('&');
			return { 
				data: data, 
				'Content-Length': data.length,
				'Content-Type': 'application/x-www-form-urlencoded'
			};

		};
	
	FexJavaScriptServer.prototype.docallback = 
		 function(method,async,args) {
			//This actually does the call back to the code on the server.
			this.debug("docallback " + method + ", " + async + ", " + args);
			var unlock = this.asyncStart();	
			var argcpy = [];
			var resp;

			// This turns the arguments object into an array.  
			// some browsers don't make 'arguments' into a real array object :(
			for(var i=0;i <  args.length;i++) { 
				argcpy.push(args[i]);
			}


			var xmlhttp = this.xmlhttp;
			try { 
				this.debug("docallback: Setting up request");
				
				var asynccallbacks = {};
				if(async) { 
					// If we're doing an async callback, we need to 
					// take the first argument as a function, that will be executed when xmlhttp returns.
					asynccallbacks = argcpy.shift();
					if(asynccallbacks.constructor == Function) { 
						// old style arguments, convert to new style.
						asynccallbacks = {onsuccess: asynccallbacks, onerror: function() {/* noop */}};
					}

					xmlhttp = this.getNewXMLHTTP();
				}
	
				//this.debug('args are: ' + argcpy.toSource() )	;
				var url = this.endpoint + '/-call/' + method;
				var data = this.toJSON(argcpy);

				if(this.extraqueryargs != '') { 
					url+="?" + this.extraqueryargs;
				}

				if(!this.installedonerror) {  
					this.debug("installed error handler for xmlhttp object");
					if(xmlhttp.onerror) { 
						//
						xmlhttp.onerror = function(e) { 
							//this only does something on mozilla, just silently log it.
							this.debug("XMLHTTP reported error?!" + e);
						};
					}
					this.installedonerror = true;

				};

				var request = this.multiPartEncode({
					action: 'jsrpc',
					jsrpc: data
					});

				//var request = data;

				this.debug("Posting the following data: " + request.data);
				this.debug("docallback: sending request, url is " + url + ", async is " + async);

				xmlhttp.open('POST',url, async);
				xmlhttp.setRequestHeader('Content-Type',request['Content-Type']);
				xmlhttp.setRequestHeader('Content-Length',request['Content-Length']);

				// IE Breaks if we install the handler, then open the request.
				// We have to Open first, then install the callback.
				// nothing happens untill we send() below ...
				if(async) { 
					this.debug("docallback: " + method + " starting async setup");
				
					var callingObj = this;
					var callingXMLHTTP = xmlhttp;
					var func = function(e) {
							var xml;
							if(e && e.target) xml = e.target;
							if(!xml) xml = callingXMLHTTP;
							callingObj.asyncCallbackHook(unlock,asynccallbacks,method,xml);
					};

					try { 
						// IE throws an error, try it ie's way first since other browsers
						// support the below
						xmlhttp.onreadystatechange = func;
						
					} catch(e) { 
						xmlhttp.onload = func;
					}
					this.debug("docallback: Installed async handler");
				} else { 
					//clear it if it has been set.
					this.debug("docallback: cleared async handler (installed noop-function)");
					xmlhttp.onreadystatechange = function() {};
				}
								
				this.debug("docallback: sending request ...");
				xmlhttp.send(request.data);
				this.debug("docallback: done sending request, xmlhttp status is " + xmlhttp.readyState);

				if(!async) { 
					this.debug("docallback: not async, parsing response: " + xmlhttp.responseText);
					try { 
						eval('resp = ' + xmlhttp.responseText + ";");
					} catch (e) { 
						if(e.ise) { 
							this.throwError("Server detected an error: " + e.ise);
						} else { 
							this.throwError("Cannot decode server response");
						}
						
						if(asynccallbacks.onerror) { asynccallbacks.onerror(e); }
					}
				}

			} catch (e) { 
				var txt;
				try { 
					txt = xmlhttp.responseText;
				} catch (x) { 
					// coulden't read any response.  IE throws error if you access
					//responseText before it's ready.
				}
				if(asynccallbacks.onerror) { asynccallbacks.onerror(e); }
				this.throwError("Cannot call RPC method " + method + " due to a problem with server communication: " + e.message +
						"(Server responded: " + txt + ")");
			} finally { 
				if(!async) { this.debug("not async, unlocking"); unlock(); }
			}

			return resp;
		};


		//getRemoteObject makes a trip to the server and requests
		//the list of methods matching some prefix.  it then builds an object
		//client siide w/ all the availble methods
	FexJavaScriptServer.prototype.getRemoteObject = 		
		function() { 
			var prefix = '';
			var enumerateArgs  = [];
			if(arguments.length == 1) { 
				enumerateArgs.push(arguments[0]);
				prefix = arguments[0];
			}

			var foo = this.docallback('_enumerateFunctions',false,enumerateArgs);
			
			var re = new RegExp("^" + prefix + "_(.+)$");
			var resobj = {};
			resobj.async = {};
			var instance = this;
			for(var method in foo) {
				var mth = method;
				//regex out the prefix, the user requested only certain items.
				if(prefix != '') { 
					mth = re.exec(method); mth = mth[1];
				}
				// The server only likes fully qualified calls.
				resobj[mth] = (function(callbackMethod) {  
						return function() { return instance.docallback(callbackMethod,false,arguments); };
				})(method);
				resobj.async[mth] = (function(callbackMethod) { 
						return function() { return instance.docallback(callbackMethod,true,arguments); };
				})(method);
			}
			return resobj;
		};

	/*
	 * urlencode
	 *
	 * javascript's escape function does not do a full urlencode.  The urlencode function starts with js's escape then does the extra replaces
	 * necessary to get a true urlencode
	 *
	 */
	function urlencode(str) {
		return escape(str).replace(/\+/g,'%2B').replace(/%20/g, '+').replace(/\*/g, '%2A').replace(/\//g, '%2F').replace(/@/g, '%40');
	}

