2

I have a problem with a javascript-Function, where I can't use callback-Functions.

The function gets a List of "commands", and iterates over them. It performs one command after another. In order to do that, I have to use a recursive Function with callbacks.

Here's an example to describe it:

function processList( inputArray, cb ){
    if(inputArray.length == 0) return cb();  //No commands left

    var command = inputArray.pop();          //Get last Element from the List
    processCommand(command, function(){      //Execute the command
        return processList(inputArray, cb);  //Continue with the next command, when this one finished
    });
}

function processCommand(command, cb){
    setTimeout(function(){
        console.log("Processed command "+command);
        cb();
    }, 10);
}

This is how I call the function:

processList( ["cmd1", "cmd2", "cmd3"], function(){
    console.log("DONE");
});

It works perfectly, one command is executed after the previous one.

My Problem:

The list contains thousands of Elements, and it's even possible that the list gets new commands during processing. And I reach the maximum call-stack within seconds.

I don't need the call-stack. When I finished the last command, there are just thousands of returns, which guide me back to the function, which started everything.

I have no idea how to solve this problem, and I don't want to use busy waiting (which makes the code extremly inefficient).

Is there another trick? Like signals or trashing the call-stack?

Edit:

Here's a jsFiddle for demonstration: http://jsfiddle.net/db6J8/ (Notice, that your browser-tab might freeze/crash)

The error-Message is Uncaught RangeError: Maximum call stack size exceeded

I tested it with Chrome, you might have to increase the Array in other Browsers (IE has a huge Callstack).

Edit2:

Thanks for you help, I didn't recognise the difference between adding/removing the timeout. Hover, it doesn't solve my Problem. Here are some more details: I have different commands. Some commands are synchronous, others are asynchronous. So I have to use callback-Functions, but still have the problem with the callstack.

Here's an updated example:

var commands = [];
for(var i=15000; i>=0;i--){ commands.push(i); }
    processList(commands, function(){
    alert("DONE");
});


function processList( inputArray, cb ){
    if(inputArray.length == 0) return cb();  //No commands left

    var command = inputArray.pop();          //Get last Element from the List



    processCommand(command, function(){      //Execute the command
        return processList(inputArray, cb);  //Continue with the next command, when this one finished
    });
}

function processCommand(command, cb){
    if(command%2 == 0){     //This command is synchron
        console.log("Processed sync. command "+command);
        cb();
    }else{
        setTimeout(function(){
            console.log("Processed async. command "+command);
            cb();
        }, 1);
    }
}

And the fiddle: http://jsfiddle.net/B7APC/

apxcode
  • 7,696
  • 7
  • 30
  • 41
maja
  • 17,250
  • 17
  • 82
  • 125
  • I can run `processList` with 10,000 elements without facing any problems. – Butt4cak3 Mar 05 '14 at 14:22
  • async comes to mind, async.queue https://github.com/caolan/async#queue – wayne Mar 05 '14 at 14:30
  • I can't detect a serious problem. Can you please show us the stack trace of the stackoverflow error, to identify which functions cause the problem? Are you sure that `processCommand` is always asynchronous? – Bergi Mar 05 '14 at 15:30

3 Answers3

2
// setTimeout(function(){
    console.log("Processed command " + command);
    cb();
// }, 1);

That's the reason. Without setTimeout, you easily will hit the call stack limit (Demo with named functions):

Maximum recursion depth exceeded:
finishedCommand _display/:16
processCommand _display/:23
processList _display/:15
finishedCommand _display/:16
processCommand _display/:23
…
processList _display/:15
finishedCommand _display/:16
processCommand _display/:23
processList _display/:15
<Global Scope> _display/:16

Choose a synchronous loop instead of recursively calling processCommand.

If you put the setTimeout back into effect, each timer event will call the function with a fresh new call stack, and it will never overflow:

setTimeout(function later(){
    console.log("Processed command " + command);
    cb();
}, 1);

You see that the stack always looks like this when "DONE" is logged - regardless of how many commands you processed (Demo):

BreakPoint at console.log()
readyHandler show/:8
processList show/:13
finishedCommand show/:17
later show/:24
<Global Scope> // setTimeout
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thanks, I didn't recognise that. However I can't use that solution, because I have a combination of synchronous and asynchronous commands (see my updated question). If you can provide a working example, I'll except your answer :) – maja Mar 05 '14 at 16:26
  • Having every second command synchronous still should work - the reached stack size will double but still be constant. If it's rather unpredictable, you can make *every* processing asynchronous by putting `setTimeout(cb, 1)` instead of `cb()` in the sync part, or you can "chunk" a few sync commands together which run in a loop and then call the (async) callback. – Bergi Mar 05 '14 at 16:34
  • Thanks. In the worst case, I'll only have synchronous functions (in which case I reach the stack-limit), but using a combination of loops and callbacks should actually work. – maja Mar 05 '14 at 16:38
1

I think maybe something like this wouldn't hold onto a call stack because it doesn't give the callback as a parameter.

function processList( inputArray, cb ){

    var processCommand = function(){
        if(inputArray.length == 0) cb();  //No commands left

        var command = inputArray.pop();
        setTimeout(function(){
            console.log("Processed command "+command);
            processCommand();
        }, 10);
    };

    processCommand();
}
meir shapiro
  • 647
  • 7
  • 16
  • @Bergi It will call processCommand() once and from then on processCommand() will be called from the timeout function when the processing is done, therefore it will wait. – meir shapiro Mar 05 '14 at 15:31
  • Uh, I see, you've merged the `processCommand` directly into the `processList`. I don't think that is what the OP wants. – Bergi Mar 05 '14 at 15:34
  • You maybe right, but I think it might help the OP. Let's let him decide :) – meir shapiro Mar 05 '14 at 15:36
  • I created a fiddle: http://jsfiddle.net/5WC6a/ But unfortunately, your suggestion doesn't solve the problem. I still reach the max. callstack-size, because the browser remembers the return-adresses. – maja Mar 05 '14 at 15:56
  • @Bergi: It doesn't matter how ugly the solution is, as long as it performs one command after another, and isn't limited with the callstack. – maja Mar 05 '14 at 16:02
  • Wow, that is ... strange. Can you explain me, why this code works? Whats the difference, if you pass the callback-Function? – maja Mar 05 '14 at 16:12
0

Sounds like a perfect case for promises. tbh I haven't tried them out yet, but they're at the top of my list. Have a loook at sequencing. Also, it is irrelevant whether the operations in a promise are sync or not.

So you'd have something like

commands.forEach(function(cmd) {
    execCmd(cmd).then(onSuccess, onFailure);
});

Sorry that's not very specific, but you'd have to do some more research.

RecencyEffect
  • 736
  • 7
  • 18