Written by

Developer
Article Matheus Augusto · 2 hr ago 6m read

Improving legacy asynchronicity in hyperevent(#call)Contestant

Hello, community.

I've been working with Intersystem Caché for two years, and right away I was excited about the rich ecosystem that Caché provides. However, I was disappointed with calls using #call, and I understand that it was a limitation of the time. Well, the frustration is that #call returns null by default, meaning an AJAX request is executed, but there's no hook to retrieve the return from that request. The only way to retrieve the data from that request is by building a callback on the server side using &js<>.

Finally, I built a class to handle these callbacks automatically on the server side. So, basically, there's a method (..Async(data)) that returns the data to the client by overwriting a global variable.

ClassMethod Async(data As %String, callback As %String = "globalAsync" ) As %Status
{
    &js< #(globalAsync)#(#(..QuoteJS(data))#) >
    return $$$OK
}

After implementing this, I encountered two problems: First, global variables are a mistake. Second, the global variable could only be overwritten one request at a time. That is, if I use two requests in parallel, a race condition occurs in this global variable; the request with the fastest response wins, and this is not very productive, although this feature was already serving its purpose well.

I decided to study how CSP handles #server and #call, and I came across the standard JavaScript file 'cspxmlhttp.js'. It works like this: When compiling a CSP, Caché identifies hyperevent calls and changes the calls to client-side request methods. For example, if there is a #call call on your page, upon compilation, Caché will change the #call to the cspCallHttpServerMethod method. You can see this change in the root source code (.int).

Analyzing cspCallHttpServerMethod in cspxmlhttp.js, it calls the cspIntHttpServerMethod method passing the async parameter as true. The code for this method is as follows.

cspxmlhttp.js - cspIntHttpServerMethod

function cspIntHttpServerMethod(method, args, async)
{
	var arg;
	var i;
	var url = "%25CSP.Broker.cls";
	var n;
	var req;

	var data = "WARGC=" + (args.length - 1) + "&WEVENT=" + method.replace(/&amp;/,'&');
	for (i = 1; i < args.length; i++) {
		arg = args[i];
		if (typeof arg != "object") {
			// Convert boolean to Cache value before sending
			if (typeof arg == "boolean") arg = (arg ? 1 : 0);
			data = data + "&WARG_" + i + "=" + encodeURIComponent(arg);
		} else if (arg != null) {
			n = 0;
			for (var el in arg) {
				if (typeof arg[el] != "function") {
					data = data + "&W" + i + "=" + encodeURIComponent(arg[el]);
					n = n + 1;
				}
			}
			data = data + "&WLIST" + i + "=" + n;
		}
	}

	try {
		req=cspXMLHttp
		if (async) {
			if (cspMultipleCall) {
				if (cspActiveXMLHttp == null) cspActiveXMLHttp = new Array();
				cspActiveXMLHttp[cspActiveXMLHttp.length] = req;
				req.onreadystatechange = cspProcessMultipleReq;
			} else {
				req.onreadystatechange = cspProcessReq;
			}
		}
		cspXMLHttp = null;
		if (cspUseGetMethod) {
			req.open("GET", url+"?"+data, async);
			if (cspMozilla) {
				req.send(null);
			} else {
				req.send();
			}
		} else {
			req.open("POST", url, async);
			req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
			req.send(data);
		}
	} catch (e) {
		var err=new cspHyperEventError(400,'Http object request failed, unable to process HyperEvent.',null,'',e);
		return cspHyperEventErrorHandler(err);
	}

	if (async) {
		return null;
	}

	return cspProcessResponse(req);
}

We can see that if the request was asynchronous, #call, the request is made but the return will always be null. Synchronous calls, #server, return the call data.

if (async) {
	return null;
}

return cspProcessResponse(req);

This default null return in asynchronous calls may be a reflection of the technology of that time, since Promises only became native to JavaScript around 2015/2016.

Well, after analyzing and understanding this Hypervents mechanism, the fix was obvious: I need to make #call amenable to JavaScript's async/await syntax.

async function execute () {
    var result = await #call(Test.LongCall.Execute())#
    console.log(result)
}

For the code above to work, it was necessary to override the cspIntHttpServerMethod by implementing a promise that returns the result of the AJAX request. I brought the cspIntHttpServerMethod method into my CSP file by copying and pasting the method, and implemented the promise.

function cspIntHttpServerMethod(method, args, async)
{
            ...
           /// Implementation of the Promise that will return the result of the request.
            return new Promise((resolve, reject) => {
                req.open("POST", url, async);
                req.onload = () => {
                    resolve(cspProcessResponse(req))
                }
                req.onerror = () => reject(cspProcessResponse(req))
                req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
                req.send(data);
            }) 
}

The CSP (test.csp) ended up like this.

function cspIntHttpServerMethod(method, args, async)
{
    console.log("Override method csp xmlhttp")
	var arg;
	var i;
	var url = "%25CSP.Broker.cls";
	var n;
	var req;

	var data = "WARGC=" + (args.length - 1) + "&WEVENT=" + method.replace(/&amp;/,'&');
	for (i = 1; i < args.length; i++) {
		arg = args[i];
		if (typeof arg != "object") {
			// Convert boolean to Cache value before sending
			if (typeof arg == "boolean") arg = (arg ? 1 : 0);
			data = data + "&WARG_" + i + "=" + encodeURIComponent(arg);
		} else if (arg != null) {
			n = 0;
			for (var el in arg) {
				if (typeof arg[el] != "function") {
					data = data + "&W" + i + "=" + encodeURIComponent(arg[el]);
					n = n + 1;
				}
			}
			data = data + "&WLIST" + i + "=" + n;
		}
	}

	try {
		req=cspXMLHttp
		if (async) {
			if (cspMultipleCall) {
				if (cspActiveXMLHttp == null) cspActiveXMLHttp = new Array();
				cspActiveXMLHttp[cspActiveXMLHttp.length] = req;
				req.onreadystatechange = cspProcessMultipleReq;
			} else {
				req.onreadystatechange = cspProcessReq;
			}

            /// Implementation of the Promise that will return the result of the request.
            return new Promise((resolve, reject) => {
                req.open("POST", url, async);
                req.onload = () => {
                    resolve(cspProcessResponse(req))
                }
                req.onerror = () => reject(cspProcessResponse(req))
                req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
                req.send(data);
            }) 
		}
		cspXMLHttp = null;
		if (cspUseGetMethod) {
			req.open("GET", url+"?"+data, async);
			if (cspMozilla) {
				req.send(null);
			} else {
				req.send();
			}
		} else {
			req.open("POST", url, async);
			req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
			req.send(data);
		}
	} catch (e) {
		var err=new cspHyperEventError(400,'Http object request failed, unable to process HyperEvent.',null,'',e);
		return cspHyperEventErrorHandler(err);
	}

	if (async) {
		return null;
	}

	return cspProcessResponse(req);
}

async function execute () {
    var result = await #call(Test.LongCall.Execute())#;
    console.log(result);
}

execute();

Now, it's no longer necessary to build a callback on the server side. That responsibility now lies with the client side. This solves my two problems: global variables and parallel requests.

And that's how I learned more about hypervents and "perfected" the requests made by #call to be amenable to the async/await syntax.