Found myself working to implement some JavaScript fiddles and, of course, nobody else's was good enough... so it was easy enough to start: just eval()
... but then what about mistakes and infinite loops?
Found this great idea using browser threads, aka HTML5 Web Workers: http://cwestblog.com/2016/03/16/javascript-detecting-infinite-loops/
However, it required a bit of improvement in catching exceptions inside the scripts as well as allow a bit of logging. So, without further ado, my slightly enhanced version is here:
// from http://cwestblog.com/2016/03/16/javascript-detecting-infinite-loops/
function limitEval(code, fnOnStop, opt_timeoutInMS, output) {
var id = Math.random() + 1;
var script = 'onmessage=function(a){' +
'output="";'+
'a=a.data;' +
'postMessage({i:a.i+1});' +
'var e="";' +
'var x="";' +
'try {' +
' x = eval.call(this, a.c);' +
'} catch (ex) {' +
' console.log(ex); e = ex.toString();' +
'};' +
'postMessage({' +
' r:x,' +
' o:output,' +
' e:e,' +
' i:a.i' +
'})};' +
'';
var blob = new Blob([script], { type:'text/javascript' } ),
myWorker = new Worker(URL.createObjectURL(blob));
function onDone() {
URL.revokeObjectURL(blob);
fnOnStop.apply(this, arguments);
}
myWorker.onmessage = function (data) {
data = data.data;
if (data) {
if (data.i === id) {
id = 0;
if(typeof data.r != "undefined") {
onDone(true, data.r,data.o);
myWorker.terminate();
} else if(data.e != "") {
onDone(false, data.e);
myWorker.terminate();
} else {
if(typeof data.o != "undefined" && data.o != "") {
if(typeof output != "undefined") {
output = output+data.o+"\n";
}
}
}
} else if (data.i === id + 1) {
setTimeout(function() {
if (id) {
myWorker.terminate();
onDone(false);
}
}, opt_timeoutInMS || 1000);
}
}
};
myWorker.postMessage({ c: 'var id='+id+';\n'+code, i: id });
}
And you can use it like so - on success or error, this will use some divs and iframes to show the results:
limitEval(moreContext+preamble+"\n"+code, function(success, returnValue, outputStr) {
var res = "";
if (success) {
res = returnValue;
paintErr(false);
}
else {
paintErr(true);
res = returnValue || 'TIMEOUT - infinite loop?';
}
if(typeof res != 'undefined' && res != null) res = res.toString();
else if(typeof res == 'undefined') res = "";
var finalres = res;
if(typeof outputStr != 'undefined' && outputStr != null) finalres = "<b>"+res + "</b>" + "\n--------------\n"+outputStr;
if(code.length > 0) $('#iframe_'+id).html(finalres);
else $('#iframe_'+id).text("Start typing...");
}, 3000);
I'll post up the rest of the fiddle code - you may find it interesting.
In case the expression is really slow, let's say a mean for(;;)
, what do we do?
The callback is a closure, so all you need is a evaluation counter that you maintain and just compare the values... like so:
var seqNum_111 = 0; // counter
var runpill_111 = function(id) {
// ...
seqNum_111 = seqNum_111 +1;
var closureSeq = seqNum_111;
limitEval(..., function(success, returnValue, outputStr) {
if(closureSeq != seqNum_111) {
console.log("ignored old result: "+closureSeq+' current: '+seqNum_111);
return;
}
Now - if someone keeps typing and you didn't debounce it smartly, it will just delay displaying a result instead of seeing possibly inconsistent results, randomly.
You need the closureSeq inside the runpill, to capture the value when the execution occured... this is a simple example, in real life you'd likely have it all be callbacks, using 'onchange' or some other notification - check the code behind the next post to see one implementation.