// We are used to this:
function loadUserPic(userId) {
var user = findUserById(userId);
return loadPic(user.picId);
}
ui.show(loadUserPic('john'));
// Both findUserById & loadUserPic are async processes
function loadUserPic(userId, ret) {
findUserById(userId, function(user) {
loadPic(user.picId, ret);
});
}
loadUserPic('john', function(pic) {
ui.show(pic);
});
The Pyramid of DOOM
var loadUserPic = function() {
var callback;
function loadPicForUser(user) {
loadPic(user.picId, callback);
}
return function loadUserPic(userId, ret) {
callback = ret;
findUserById(userId, loadPicForUser);
}
}();
loadUserPic('john', ui.show.bind(ui));
A lot of boilerplate
function loadUserPic(userId) {
var result = findUserById(userId);
if(result.error)
return result;
return loadPic(result.picId);
}
var result = loadUserPic('john')
if(result.error)
return ui.error(result.error);
ui.show(result);
function loadUserPic(userId, ret) {
findUserById(userId, function(err, user) {
if(err)
return ret(err);
loadPic(user.picId, ret);
});
}
loadUserPic('john', function(err, pic) {
if(err)
return ui.error(err);
ui.show(pic);
});
The Node Way :/
function loadUserPic(userId) {
var user = findUserById(userId);
return loadPic(user.picId);
}
try {
ui.show(loadUserPic('john'));
} catch(err) {
ui.error(err);
}
function loadUserPic(userId, ret, thr) {
findUserById(userId, function(user) {
loadPic(user.picId, ret, thr);
}, thr);
}
loadUserPic('john', ui.show.bind(ui), ui.error.bind(ui));
Much better
It is easy to learn... but
Only can access the value after process has finished!
var promisedPic = loadUserPic('john');
// Some time later
promisedPic.then(function(pic) {
ui.show(pic);
});
It doesn't matter if it has been fulfilled yet or not
findUserById('john').then(function(user) {
return findPic(user.picId).then(function(pic) {
ui.show(pic);
});
});
findUserById('john')
.then(function(user) {
return findPic(user.picId);
})
.then(function(pic) {
ui.show(pic);
});
No Pyramid of DOOM! We are saved!
findUserById('john')
.then(function(user) {
return findPic(user.picId);
})
.then(function(pic) {
ui.show(pic);
}, ui.error.bind(ui));
findUserById('john')
.then(function(user) {
return findPic(user.picId);
})
.then(function(pic) {
ui.show(pic);
})
.fail(ui.error.bind(ui));
findUserById('john')
.then(function(user) {
return findPic(user.picId);
})
.fail(function(err) {
if(err.isFatal())
throw err;
return recoverError(err); // Should return a pic
})
.then(function(pic) {
ui.show(pic);
});
findUserById('john')
.then(function(user) {
return findPic(user.picId);
})
.then(function(pic) {
ui.show(pic);
}).done(); // report unhandled errors
findUserById('john')
.then(function(user) {
return findPic(user.picId);
})
.then(function(pic) {
ui.show(pic);
})
.fin(someCleanUp) // Like finally
.done();
Promises are for processing the full response!
You'd need one promise for each event instance
Do it yourself!
function showUserPic(userId) {
var user = findUserById(userId);
var picId = user.picId;
var pic = loadPic(picId);
ui.show(pic);
}
showUserPic('john');
var showUserPic = compose([
findUserById,
getProperty('picId'),
loadPic,
ui.show.bind(ui)
]);
showUserPic('john');
function showUserPic(userId, ret) {
ui.showProgressIndicator();
findUserById(userId, function(user) {
var picId = user.picId;
loadPic(picId, function(pic) {
ui.show(pic, ret);
});
});
}
showUserPic('john', function() {
ui.hideProgressIndicator();
});
Note all the intermediate anonymous functions!
var showUserPic = composeAsync([
perform(ui.showProgressIndicator.bind(ui)),
findUserById, // Async transformation
mapSync(getProperty('picId')),
loadPic, // Async transformation
perform(ui.show.bind(ui))
]);
showUserPic('john', ui.hideProgressIndicator.bind(ui));
var showUserPic = compose([
findUserById,
getProperty('picId'),
loadPic,
ui.show.bind(ui)
]);
Same structure as the sync version!
No need to change paradigm
function loadUserPic(userId, ret, thr) {
findUserById(userId, function(user) {
loadPic(user.picId, ret, thr);
}, thr);
}
function composeTwo(fn1, fn2) {
return function (data, ret, thr) {
fn1(data, function (result) {
fn2(result, ret, thr);
}, thr);
};
}
var loadUserPic = composeTwo(findUserById, loadPic);
function composeAsync(fns) {
if (fns.length === 1)
return fns[0];
return fns.reduce(composeTwo);
};
var showUserPic = composeAsync([
perform(ui.showProgressIndicator.bind(ui)),
findUserById,
mapSync(getProperty('picId')), //Sync transformation
loadPic,
perform(ui.show.bind(ui))
]);
function mapSync(syncFn) {
return function(data, ret, thr) {
try {
ret(syncFn(data));
} catch(err) {
if(typeof thr == 'function')
thr(err);
}
};
}
var showUserPic = composeAsync([
// Side effect
perform(ui.showProgressIndicator.bind(ui)),
findUserById,
mapSync(getProperty('picId')),
loadPic,
// Side effect
perform(ui.show.bind(ui))
]);
No transformation
No need to wait for response
Warning: we cannot undo a side effect
function perform(dangerousEffect) {
return function(data, ret) {
try {
dangerousEffect(data);
} finally {
if(typeof ret == 'function')
ret(data);
}
};
}
// If you liked jQuery or promises chains
var showUserPic =
perform(ui.showProgressIndicator.bind(ui))
.map(findUserById)
.mapSync(getProperty('picId'))
.map(loadPic)
.perform(ui.show.bind(ui));
showUserPic
.perform(ui.hideProgressIndicator.bind(ui))('john');
// Reversed order
var showUserPic = async.compose(
loadPic,
// mapSync not in Async
mapSync(getProperty('picId')),
findUserById,
// perform not in Async
perform(ui.showProgressIndicator.bind(ui))
);
showUserPic('john', function (err, pic) {
// Node convention
if(err)
return ui.showError(err);
ui.show(pic);
});
var liveSearch = composeAsync([
mapSync(keyUpEventsToSearchText),
changes(),
limitThroughput(2),
filter(searchTextShorterThan(3))
perform(ui.startSearching.bind(ui)),
mapSync(searchTermToSearch),
perform(function (search) {
search.on('data', ui.appendResult.bind(ui));
}),
perform(function (search) {
search.on('end', ui.endSearching.bind(ui));
})
]);
searchInputText.addEventListener('keyup', liveSearch);
// Streams are EventEmitters
dataStream.on('data', function(data) {
// Consume a data chunk
});
dataStream.on('error', function(err) {
// Handle error
});
dataStream.on('end', function() {
// No more data
});
// With a pipe method
dataStream.pipe(compressor('zip')).pipe(res);
Do you want to know more? http://bit.ly/Okzywv
var es = require('event-stream'),
domstream = require('domnode-dom');
var searchStream = es.pipeline(
domstream.createReadStream('input.search', 'keyup'),
es.mapSync(trim),
es.mapSync(toLowerCase),
es.mapSync(function(text) {
ui.showProgressIndicator();
return text;
}),
es.map(search),
);
searchStream.on('data', ui.show.bind(ui));
searchStream.on('error', ui.error.bind(ui));
searchStream.on('end', ui.hideProgressIndicator.bind(ui));
Rings a bell?
var searchText = $('input[type="text"][name="search"]')
.asEventStream('keyup')
.map(".target.value")
.map(trimText)
.toProperty(""); // memory !
searchText
.sample(500)
.skipDuplicates()
.filter(shorterThan(3))
.map(searchForSearchTerm)
.onValue(function (search) {
search.on('data', ui.appendResult.bind(ui));
search.on('end', ui.endSearching.bind(ui));
});
Use the most simple tool that could resolve your problem