Robust Network Code with window.setTimeout
by Michael CarterAugust 11th, 2008In Orbited, we sometimes send multiple events in a single payload, particularly with the long-polling and polling transports. After we parse the data of each event, we call some event handler that a user has attached to the Orbited connection. Imagine code that looks something like this:
function rawOnReceive(rawData) { frames = parseFrames(rawData); for (var i = 0; i < frames.length; ++i) { self.onread(frames[i]); } }
Unfortunately, sometimes user code can throw an exception. If this happens, we don’t want to stop sending the remainder of the frames, so we revised our original code to look more like this:
function rawOnReceive(rawData) { frames = parseFrames(rawData); for (var i = 0; i < frames.length; ++i) { try { self.onread(frames[i]); } catch(e) { } } }
But now we have a problem — that is, if the user code fails, we hide the error, and then dispatch the next packet. This is a terrible solution because it will be next to impossible for users to track down bugs.
Consider window.setTimeout, a mechanism generally used to schedule calls after a set amount of time has expired. It has a side effect though; it moves code out of the callstack. So we use setTimeout with 0ms as the specified timeout in order to make the appropriate onread calls without worrying whether they throw exceptions or not:
function rawOnReceive(rawData) { frames = parseFrames(rawData); for (var i = 0; i < frames.length; ++i) { window.setTimeout(function() { self.onread(frames[i]) }, 0); } }
This means that all of the onread callbacks will be scheduled to be executed, but none of them will actually execute until our code, the rawOnReceive function, finishes. Unfortunately, there is a second problem — the value of i used for each onread callback will be the final value of i. We need to hold on to each value separately for each callback. Consider this code:
function rawOnReceive(rawData) { frames = parseFrames(rawData); for (var i = 0; i < frames.length; ++i) { (function() { var j = i; window.setTimeout(function() { self.onread(frames[j]) }, 0); })(); } }
Note the extra function and the “var j = i” — Our solution is to create a closure that saves each value of i as locally scoped variable j. This code will allow us to make all the appropriate onread calls, whether or not one calls user code that causes an exception.
When I first tried this out, I thought it would fail due to the ordering of the messages — after all, we go through a lot of trouble in Orbited to guarantee message order. It would be truly tragic if we destroyed that ordering right at the last possible moment. But its actually not a problem whatsoever. Regardless of how the various browsers implement setTimeout, they have managed to do it in a way that preserves ordering if multiple consecutive calls have the same timeout value. Of course, I would feel more comfortable if this was documented somewhere!







