On Github eamodeorubio / tamingasync
By Enrique Amodeo / @eamodeorubio
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 2.0 Generic License// 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