JavaScript - how to make buffered async loop class with next iteration confirmation?
In this article, we're going to have a look at how to write own asynchronous buffered loop class (window loop class) that uses callback functions to execute next iterations in proper time or breaks loop - processes asynchronous tasks as resources are available.
This kind of loop is useful when we have to make some iterations and shedule them in groups, e.g. optimisation that has to execute 50 computations but available are only 5 machines and we don't want to make new interations until machine is avaialble.
Note: read this article to see simple async loop with next iteration confirmation example.
This implementation uses setTimeout
function to call iterations in proper time. To be sure that next iteration will be executed it is necessary to call resume()
method in each onIteration
method. To break loop it is just necessary to call finish()
method.
Note: in below example we shedule next iterations after 1s and break loop after 10 iterations to show how it works.
xxxxxxxxxx
function AsyncLoop(index, count, buffer, onIteration, onFinished) {
var STOPPED = 0; // loop is stopped
var STARTED = 1; // loop is started
var AVAILABLE = 2; // loop is started and ready to execute next iterations
var ITERATING = 3; // loop is progressing iterations
var FINISHING = 4; // loop is finishing
var self = this;
var i, c, b; // executed indexes, confirmed indexs, free buffer
var state = STOPPED;
function shedule(i) {
var called = false;
var cover = function(action) {
return function() {
if (called) {
throw new Error('Only once callback function can be executed in iteration.');
}
called = true;
c -= 1;
b += 1;
if (state == ITERATING) {
state = AVAILABLE;
}
action();
};
};
var callback = function() {
if (state == FINISHING) {
return;
}
onIteration(i, cover(resume), cover(finish));
};
setTimeout(callback, 0);
}
function resume() {
if (state == STARTED || state == AVAILABLE) {
if (i < count) {
state = ITERATING;
while(i < count && b > 0) {
i += 1;
b -= 1;
shedule(i - 1);
}
} else {
if (c == 0) {
state = FINISHING;
var callback = function() {
state = STOPPED;
onFinished(true);
};
setTimeout(callback, 0);
}
}
}
}
function finish() {
if (state == STARTED || state == AVAILABLE || state == ITERATING) {
state = FINISHING;
var callback = function() {
state = STOPPED;
onFinished(false);
};
setTimeout(callback, 0);
}
}
self.run = function() {
if (state == STOPPED) {
i = index;
c = count - index;
b = buffer;
if (onIteration.length > 1) {
// for onIteration(index, resume, finish)
state = STARTED;
resume();
} else {
// for onIteration(index)
state = STARTED;
try {
while(i < count) {
i += 1;
onIteration(i - 1);
}
} finally {
state = STOPPED;
if (onFinished) {
onFinished(true);
}
}
}
}
};
self.end = finish;
}
// Helper logic
var t1 = new Date();
function getTime() {
var t2 = new Date();
return t2 - t1;
}
// Usage example
function onIteration(i, resume, finish) {
console.log('[loop iteration ' + i + ', time=' + getTime() + ']');
// resume() call continues loop
// finish() call breaks loop
if (i == 10) {
console.log('finish() method called...');
finish();
} else {
setTimeout(resume, 1000); // continued after 1s
}
}
function onFinished(completed) {
console.log('[loop finished, time=' + getTime() + '] ' + (completed ? 'completed' : 'forced'));
}
var initialIndex = 0; // initial iteration number
var iterationsCount = 50; // number of iterations to execute
var bufferSize = 3; // maximum number of executed iterations in same time
var loop = new AsyncLoop(initialIndex, iterationsCount, bufferSize, onIteration, onFinished);
loop.run();
console.log('[loop started, time=' + getTime() + ']');