I am trying to execute following array (avoid callbackHell) of functions(sync/async), in a sequential order, implementing function runCallbacksInSequence
(I need to implement my own function to understand how callbacks work and avoid using Async.js).
Here is what I have so far. The function runCallbacksInSequence
works well till it gets the same callback
more than once. It stops and does not continue to execute the next callback. Ideally if it gets the same callback
more than once it should not execute it second time and continue with the next callback
.
If you have any ideas let me know what I am doing wrong and how I can fix it. - no promises and async/await
function first(cb) {
setTimeout(function() {
console.log('first()');
cb(null, 'one');
}, 0);
}
function second(cb) {
setTimeout(function() {
console.log('second()');
cb(null, 'two');
}, 100);
}
function third(cb) {
setTimeout(function() {
console.log('third()');
cb(null, 'three');
}, 0);
}
function last(cb) {
console.log('last()');
cb(null, 'lastCall');
}
const cache = {};
function runCallbacksInSequence(fns, cb) {
fns.reduce(
function(r, f) {
return function(k) {
return r(function() {
if (cache[f]) {
return;
// f(function(e, x) {
// e ? cb(e) : k(x);
// });
} else {
cache[f] = f;
return f(function(e, x) {
return e ? cb(e) : k(x);
});
}
});
};
},
function(k) {
return k();
}
)(function(r) {
return cb(null, r);
});
}
const fns = [first, second, third, second, last];
runCallbacksInSequence(fns, function(err, results) {
if (err) return console.log('error: ' + err.message);
console.log(results);
});
Your function chaining depends on the call to k()
. Therefore in your cache logic:
if (cache[f]) {
return;
} else {
// ...
The chain breaks.
What you want instead is this:
if (cache[f]) {
return k();
} else {
// ...
One of the problems with the nested function implementation is that it is hard to reason about due to multiple nesting scopes (and multiple functions being juggled at once (r
, f
, k
, cb
).
A simpler approach to this is rather than trying to programmatically build callback hell you can use a queue instead (which is what async.js does). The idea is simple, pop() or shift() functions from an array until the array is empty:
function runCallbacksInSequence(fns, cb) {
let result = [];
let cache = {};
function loop () {
if (fns.length > 0) {
let f = fns.shift(); // remove one function from array
if (cache[f]) {
loop(); // skip this round
return;
}
cache[f] = f;
f(function(err, val) {
if (!err) {
result.push(val); // collect result
loop();
}
else {
// Handle errors however you want.
// Here I'm just terminating the sequence:
cb(err, result);
}
});
}
else {
cb(null, result); // we've collected all the results!!
}
}
loop(); // start the loop
}
As you can see, it's fairly easy to implement any flow logic with this structure. We can easily implement things like waterfall, parallelLimit etc. by controlling how we keep track of results and how many functions we remove from the array per iteration.
I guess with implementation based on cache you may omit doubled step with a direct k()
invocation.
return;
if (cache[f]) {
return;
// f(function(e, x) {
// e ? cb(e) : k(x);
// });
Idea:
if (cache[f]) {
return k(function(e, x) {
return e ? cb(e) : k(x);
});
Your code is a little bit hard to read for me. So here is the alternative solution:
<script>
// The data
function first(cb) {
setTimeout(function () {
console.log('first()');
cb(null, 'one');
}, 0);
}
function second(cb) {
setTimeout(function () {
console.log('second()');
cb(null, 'two');
}, 100);
}
function third(cb) {
setTimeout(function () {
console.log('third()');
cb(null, 'three');
}, 0);
}
function last(cb) {
console.log('last()');
cb(null, 'lastCall');
}
const fns = [first, second, third, second, last];
// We need hash function to create the identifyer of the function
function hashCode(str) {
return Array
.from(str)
.reduce((s, c) => Math.imul(31, s) + c.charCodeAt(0) | 0, 0);
}
const cache = [];
function reducer(accumulator, currentFunction) {
// Take the functon string representation to detect "the same function"
const hash = hashCode(currentFunction.toString());
// Process the result of the current function and call the next one.
// We use "reduceRight" so `accumulator` is the next function in the chain.
const cb = function (fp, result) {
console.log(result);
// Cache the result;
cache[hash] = result;
accumulator();
}
// Run just a callback if we already have the result of the current function
return () => cache[hash] ? cb(null, cache[hash]) : currentFunction(cb);
}
fns.reduceRight(reducer, () => { })();
</script>
Result:
first()
one
second()
two
third()
three
two
last()
lastCall
If you do not want to process the cached result at all, then replace the call to the callback with the call to the accumulator directly.
return () => cache[hash] ? cb(null, cache[hash]) : currentFunction(cb);
replace with:
return () => cache[hash] ? accumulator() : currentFunction(cb);
Result:
first()
one
second()
two
third()
three
last()
lastCall
Solution without cache
It is much cleaner:
<script>
// Use the same data as in the example with cache
function reducer(accumulator, currentFunction) {
const cb = function (fp, result) {
console.log(result);
accumulator();
}
return () => currentFunction(cb)
}
fns.reduceRight(reducer, () => { })();
</script>
Result:
first()
one
second()
two
third()
three
second()
two
last()
lastCall
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With