├── .babelrc ├── .gitignore ├── README.md ├── callbacks ├── lineage.js └── southpark.js ├── csp ├── lineage.js └── southpark.js ├── fakeAjax.js ├── fakeAjaxAsPromise.js ├── generators └── southpark.js ├── logger.js ├── nodeAjax.js ├── package.json ├── promises └── southpark.js ├── reactive └── southpark.js └── thunks ├── fileSizes.js ├── files ├── bar.txt ├── foo.txt └── qux.txt ├── lineage.js └── southpark.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["syntax-async-functions","transform-regenerator"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-async-patterns 2 | Async Patterns in Javascript 3 | 4 | A showcase of various async patterns available in Javascript. Although each pattern or abstraction examined here is not directly comparable with the other (some even depend on others), the idea is to tackle the same scenarios with all of them. 5 | 6 | Inspired by a great [forward](http://forwardconference.org) workshop on async Javascript from [Kyle Simpson](http://getify.me) 7 | 8 | ## Scenarios 9 | 2 basic scenarios so far. Can't tell which is worse. 10 | 11 | ### South Park 12 | A sequence of discrete asynchronous steps. 13 | 14 | `A` -> (`B + C`) -> `D` 15 | 16 | `A` followed by `B` and `C` which run in parallel and in turn followed by `D` 17 | 18 | Ok let's make up a scenario that serves our purpose. Let's say we have a South Park fan site where we let people view the profiles of the various characters in the series. But we choose our data wisely, in the true spirit of the series, if the character has any bad reputation we want to enrich his/her profile with the episodes he/she is known for. We can tell bad reputation from the number of friends and the existence of a criminal record. Yes that is what I came up with at 2:00am. Took me some time as well! 19 | 20 | * step1 (A): get basic profile `{name, bio, spdb_url}` from another fictitious `imdb.com/:hero` service. `spdb` comes from south park db :) 21 | * step2 (B + C): get number of friends from `/friends` and criminal record from `/record` and merge with profile 22 | * step3 (D): if the character has bad reputation fetch the episodes that made him famous from `/known_for` and add them to his profile. 23 | 24 | We'll start with Eric Cartman. 25 | 26 | ### Aragorn Lineage 27 | Repeated application of the same async step until a terminating condition is met. 28 | This is also a sequence of async steps but one that can be solved using 29 | recursion or loop depending on the async mechanism employed. 30 | 31 | We need to print out Aragorn's lineage. A fictitious `lotr.com/:name` API provides us with `{title, ancestor}` records with the `title` being the title of the LOTR hero and `ancestor` the name of their ancestor. 32 | 33 | We are given the Aragorn's one `lotr.com/aragorn` to start with. 34 | 35 | ## Running the scenarios 36 | 37 | ```bash 38 | npm install 39 | babel-node 40 | ``` 41 | 42 | 43 | -------------------------------------------------------------------------------- /callbacks/lineage.js: -------------------------------------------------------------------------------- 1 | import fakeAjax from '../fakeAjax'; 2 | 3 | function lineage(url) { 4 | fakeAjax(url, 5 | character => { 6 | console.log('> %s', character.title); 7 | if (character.ancestor) { 8 | lineage(character.ancestor); 9 | } 10 | }, 11 | error => { 12 | console.log('Error: %s', error); 13 | console.log('Falling back to %s', 'lotr.com/arvedui'); 14 | return lineage('lotr.com/arvedui'); 15 | } 16 | ) 17 | } 18 | 19 | lineage('lotr.com/aragorn'); 20 | -------------------------------------------------------------------------------- /callbacks/southpark.js: -------------------------------------------------------------------------------- 1 | import fakeAjax from '../fakeAjax'; 2 | import logger from '../logger'; 3 | 4 | function fetchCharacter(url, onSuccess, onError) { 5 | let friendsFetched = false; 6 | let recordFetched = false; 7 | 8 | fakeAjax(url, (character) => { 9 | 10 | function getKnownForIfInfamous() { 11 | if (!friendsFetched || !recordFetched) { 12 | return; 13 | } 14 | if (character.friends < 2 && character.criminalRec) { 15 | logger.strong('Friends: %s. CR: %s => infamous!\n', character.friends, character.criminalRec); 16 | fakeAjax(character.spdbUrl + '/known_for', 17 | (knownFor) => { 18 | Object.assign(character, {knownFor}); 19 | onSuccess(character); 20 | }, 21 | (error) => onError('customized error', error) 22 | ); 23 | } else { 24 | onSuccess(character); 25 | } 26 | } 27 | 28 | // What happens if both calls below fail? 29 | 30 | fakeAjax(character.spdbUrl + '/friends', (friends) => { 31 | Object.assign(character, {friends}); 32 | friendsFetched = true; 33 | getKnownForIfInfamous(); 34 | }, onError); 35 | 36 | fakeAjax(character.spdbUrl + '/record', (criminalRec) => { 37 | Object.assign(character, {criminalRec}); 38 | recordFetched = true; 39 | getKnownForIfInfamous(); 40 | }, onError); 41 | 42 | }, onError); 43 | 44 | } 45 | 46 | fetchCharacter('imdb.com/cartman', 47 | (eric) => logger.success(eric), 48 | (error) => logger.error(error) 49 | ); 50 | -------------------------------------------------------------------------------- /csp/lineage.js: -------------------------------------------------------------------------------- 1 | import fakeAjax from '../fakeAjax.js'; 2 | import csp from 'js-csp'; 3 | 4 | function fakeAjaxAsChannel(url) { 5 | const ch = csp.chan(); 6 | fakeAjax(url, 7 | response => csp.putAsync(ch, response), 8 | response => csp.putAsync(ch, new Error(response)) 9 | ); 10 | return ch; 11 | } 12 | 13 | function* lineage(url) { 14 | let character; 15 | do { 16 | character = yield csp.take(fakeAjaxAsChannel(url)); 17 | 18 | if (character instanceof Error) { // wouldn't it be nice if csp.take would throw it? 19 | console.log('Error: %s', character.message); 20 | console.log('Falling back to %s', 'lotr.com/arvedui'); 21 | url = 'lotr.com/arvedui'; 22 | } else { 23 | console.log('> %s\n', character.title); 24 | url = character.ancestor; 25 | } 26 | } while(character !== csp.CLOSED && url); 27 | } 28 | 29 | csp.spawn(lineage('lotr.com/aragorn')); 30 | -------------------------------------------------------------------------------- /csp/southpark.js: -------------------------------------------------------------------------------- 1 | import fakeAjax from '../fakeAjax'; 2 | import csp from 'js-csp'; 3 | import logger from '../logger'; 4 | 5 | function fakeAjaxAsChannel(url) { 6 | const ch = csp.chan(); 7 | fakeAjax(url, 8 | response => csp.putAsync(ch, response), 9 | response => csp.putAsync(ch, new Error(response)) 10 | ); 11 | return ch; 12 | } 13 | 14 | // a csp.tryToTake(channel) which would throw if the value 15 | // returned from the channel was an Error 16 | // would make this method unnecessary 17 | // Check http://swannodette.github.io/2013/08/31/asynchronous-error-handling/ 18 | function throwIfError(obj) { 19 | if (obj instanceof Error) { 20 | throw obj; 21 | } 22 | } 23 | 24 | function* fetchCharacter(url) { 25 | try { 26 | const character = yield csp.take(fakeAjaxAsChannel(url)); 27 | 28 | throwIfError(character); 29 | 30 | function* fetchFriends() { 31 | return yield fakeAjaxAsChannel(character.spdbUrl + '/friends'); 32 | } 33 | 34 | function* fetchRecord() { 35 | return yield fakeAjaxAsChannel(character.spdbUrl + '/record'); 36 | } 37 | 38 | const [friendsChannel, recordChannel] = [csp.go(fetchFriends), csp.go(fetchRecord)]; 39 | 40 | const friends = yield csp.take(friendsChannel); 41 | const criminalRec = yield csp.take(recordChannel); 42 | 43 | throwIfError(friends); 44 | throwIfError(criminalRec); 45 | 46 | Object.assign(character, { 47 | friends, criminalRec 48 | }); 49 | 50 | if (friends < 2 && criminalRec) { 51 | logger.strong('Friends: %s. CR: %s => infamous!\n', friends, criminalRec); 52 | const knownFor = yield csp.take(fakeAjaxAsChannel(character.spdbUrl + '/known_for')); 53 | Object.assign(character, { 54 | knownFor 55 | }); 56 | } 57 | 58 | return character; 59 | 60 | } catch (error) { 61 | return error; 62 | } 63 | } 64 | 65 | const characterChannel = csp.spawn(fetchCharacter('imdb.com/cartman')); 66 | 67 | csp.takeAsync(characterChannel, (eric) => { 68 | if (eric instanceof Error) { 69 | logger.error(eric.stack); 70 | } else { 71 | logger.success(eric); 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /fakeAjax.js: -------------------------------------------------------------------------------- 1 | import logger from './logger'; 2 | 3 | const fakeResponses = { 4 | // lineage 5 | 'lotr.com/aragorn': {title: 'Aragorn, King Elessar', ancestor: 'lotr.com/arathorn'}, 6 | 'lotr.com/arathorn': {title: 'Arathorn II, Chieftain of the Dúnedain', ancestor: 'lotr.com/arador'}, 7 | 'lotr.com/arador': {title: 'Arador, 14th Chieftain of the Dúnedain', ancestor: 'lotr.com/'}, 8 | 'lotr.com/arvedui': {title: 'The last King of Arnor'}, 9 | 10 | // south park 11 | 'imdb.com/cartman': {name: 'Eric Cartman', traits: 'Spoilt, extremely selfish and frequently seeks personal gain', spdbUrl: 'spdb.com/eric'}, 12 | 'spdb.com/eric/friends': 0, 13 | 'spdb.com/eric/record': 'Murder, Unlicensed surgery, Animal abuse and Attempted Genocide', 14 | 'spdb.com/eric/known_for': ['Scott Tenorman Must Die', 'Make Love, Not Warcraft', 'A Ladder to Heaven'] 15 | }; 16 | 17 | export default function fakeAjax(url, success, error) { 18 | logger.info(`Requesting '${url}'...\n`); 19 | 20 | const randomDelay = (Math.round(Math.random() * 1E4) % 4000) + 1000; 21 | const response = fakeResponses[url]; 22 | 23 | if (response !== void(0)) { 24 | setTimeout(() => success(response), randomDelay); 25 | } else { 26 | setTimeout(() => error(`404, ${url} not found`), randomDelay); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /fakeAjaxAsPromise.js: -------------------------------------------------------------------------------- 1 | import fakeAjax from './fakeAjax'; 2 | 3 | export default function fakeAjaxAsPromise(url) { 4 | return new Promise(function (resolve, reject) { 5 | fakeAjax(url, 6 | (character) => resolve(character), 7 | (error) => reject(error) 8 | ); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /generators/southpark.js: -------------------------------------------------------------------------------- 1 | import fakeAjax from '../fakeAjaxAsPromise'; 2 | import logger from '../logger'; 3 | 4 | async function fetchCharacter(url) { 5 | try { 6 | const character = await fakeAjax(url); 7 | 8 | const [friends, criminalRec] = await Promise.all([ 9 | fakeAjax(character.spdbUrl + '/friends'), 10 | fakeAjax(character.spdbUrl + '/record') 11 | ]); 12 | 13 | Object.assign(character, {friends, criminalRec}); 14 | 15 | if (friends < 2 && criminalRec !== null) { 16 | logger.strong('Friends: %s. CR: %s => infamous!\n', friends, criminalRec); 17 | const knownFor = await fakeAjax(character.spdbUrl + '/known_for'); 18 | Object.assign(character, {knownFor}); 19 | } 20 | 21 | return character; 22 | 23 | } catch(error) { 24 | throw error; 25 | } 26 | } 27 | 28 | fetchCharacter('imdb.com/cartman') 29 | .then((eric) => logger.success(eric)) 30 | .catch((error) => logger.error(error)); 31 | -------------------------------------------------------------------------------- /logger.js: -------------------------------------------------------------------------------- 1 | import colors from 'colors'; 2 | import stringifyObject from 'stringify-object'; 3 | 4 | function stringify(obj) { 5 | return typeof obj === 'string' ? obj : stringifyObject(obj); 6 | } 7 | 8 | export function success(object, ...args) { 9 | console.log(stringify(object).green, ...args); 10 | } 11 | 12 | export function info(object, ...args) { 13 | console.log(stringify(object).magenta, ...args); 14 | } 15 | 16 | export function strong(object, ...args) { 17 | console.error(stringify(object).blue, ...args); 18 | } 19 | 20 | export function warn(object, ...args) { 21 | console.log(stringify(object).yellow, ...args); 22 | } 23 | 24 | export function error(object, ...args) { 25 | console.error(stringify(object).red, ...args); 26 | } 27 | 28 | export default { 29 | success, info, strong, warn, error 30 | }; 31 | -------------------------------------------------------------------------------- /nodeAjax.js: -------------------------------------------------------------------------------- 1 | import fakeAjax from './fakeAjax'; 2 | 3 | export default function nodeAjax(url, callback) { 4 | return fakeAjax(url, 5 | (character) => callback(null, character), 6 | (error) => callback(error) 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-async-patterns", 3 | "version": "1.0.0", 4 | "description": "A collection of async patterns in Javascript", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/th3hunt/js-async-patterns.git" 11 | }, 12 | "keywords": [ 13 | "js", 14 | "async", 15 | "patterns" 16 | ], 17 | "author": "Stratos Pavlakis", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/th3hunt/js-async-patterns/issues" 21 | }, 22 | "homepage": "https://github.com/th3hunt/js-async-patterns#readme", 23 | "devDependencies": { 24 | "babel-cli": "^6.4.0", 25 | "js-csp": "^0.5.0" 26 | }, 27 | "dependencies": { 28 | "colors": "^1.1.2", 29 | "stringify-object": "^2.3.1", 30 | "thunks": "^4.1.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /promises/southpark.js: -------------------------------------------------------------------------------- 1 | import fakeAjax from '../fakeAjaxAsPromise'; 2 | import logger from '../logger'; 3 | 4 | function fetchCharacter(url) { 5 | return fakeAjax(url) 6 | 7 | .then((character) => { 8 | return Promise.all([ 9 | fakeAjax(character.spdbUrl + '/friends'), 10 | fakeAjax(character.spdbUrl + '/record') 11 | ]).then(([friends, criminalRec]) => { 12 | return Object.assign(character, {friends, criminalRec}); 13 | }); 14 | }) 15 | 16 | .then((character) => { 17 | if (character.friends < 2 && character.criminalRec !== null) { 18 | logger.strong('Friends: %s. CR: %s => infamous!\n', character.friends, character.criminalRec); 19 | return fakeAjax(character.spdbUrl + '/known_for') 20 | .then((knownFor) => Object.assign(character, {knownFor})); 21 | } 22 | return character; 23 | }); 24 | } 25 | 26 | fetchCharacter('imdb.com/cartman') 27 | .then((eric) => logger.success(eric)) 28 | .catch((error) => logger.error(error)); 29 | -------------------------------------------------------------------------------- /reactive/southpark.js: -------------------------------------------------------------------------------- 1 | import nodeAjax from '../nodeAjax'; 2 | import Rx from 'rx'; 3 | import logger from '../logger'; 4 | 5 | const fetch = Rx.Observable.fromNodeCallback(nodeAjax); 6 | 7 | function fetchCharacter(url) { 8 | return Rx.Observable 9 | .just(url) 10 | .flatMapLatest(url => fetch(url)) 11 | .flatMapLatest( 12 | (character) => { 13 | return Rx.Observable.combineLatest( 14 | fetch(character.spdbUrl + '/friends'), 15 | fetch(character.spdbUrl + '/record') 16 | ).map(([friends, criminalRec]) => { 17 | return Object.assign(character, {friends, criminalRec}); 18 | }); 19 | } 20 | ) 21 | .flatMapLatest( 22 | (character) => { 23 | if (character.friends < 2 && character.criminalRec) { 24 | logger.strong('Friends: %s. CR: %s => infamous!\n', character.friends, character.criminalRec); 25 | return fetch(character.spdbUrl + '/known_for') 26 | // .onErrorResumeNext(Rx.Observable.just('quotes not available')) 27 | .map(knownFor => Object.assign(character, {knownFor})); 28 | } 29 | return Rx.Observable.just(character); 30 | } 31 | ) 32 | // .timeout(2000) 33 | } 34 | 35 | fetchCharacter('imdb.com/cartman').subscribe( 36 | (eric) => logger.success(eric), 37 | (error) => logger.error('Error: %s', error) 38 | ); 39 | -------------------------------------------------------------------------------- /thunks/fileSizes.js: -------------------------------------------------------------------------------- 1 | const thunk = require('thunks')() 2 | const fs = require('fs') 3 | 4 | const size = thunk.thunkify(fs.stat); 5 | 6 | // sequential 7 | size('thunks/files/foo.txt')(function (error, res) { 8 | console.log('foo.txt', res.size); 9 | return size('thunks/files/bar.txt') 10 | 11 | })(function (error, res) { 12 | console.log('bar.txt', res.size); 13 | return size('thunks/files/qux.txt') 14 | 15 | })(function (error, res) { 16 | console.log('qux.txt', res.size); 17 | return 'The End'; 18 | 19 | })(function (error, res) { 20 | console.log(res); 21 | 22 | })(function (error, res) { 23 | console.log(res); 24 | }); 25 | 26 | // sequential 2 27 | thunk.seq([ 28 | size('thunks/files/foo.txt'), 29 | size('thunks/files/bar.txt'), 30 | size('thunks/files/qux.txt') 31 | ])(function (error, res) { 32 | console.log('foo.txt %s | bar.txt %s | bar.txt %s', ...res.map(fileStats => fileStats.size)); 33 | }) 34 | 35 | // parallel 36 | thunk.all([ 37 | size('thunks/files/foo.txt'), 38 | size('thunks/files/bar.txt'), 39 | size('thunks/files/qux.txt') 40 | ])(function (error, res) { 41 | console.log('foo.txt %s | bar.txt %s | bar.txt %s', ...res.map(fileStats => fileStats.size)); 42 | }) 43 | -------------------------------------------------------------------------------- /thunks/files/bar.txt: -------------------------------------------------------------------------------- 1 | bar file contents! 2 | -------------------------------------------------------------------------------- /thunks/files/foo.txt: -------------------------------------------------------------------------------- 1 | foo file contents 2 | -------------------------------------------------------------------------------- /thunks/files/qux.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/th3hunt/js-async-patterns/b91a2e5ddafd37c9dc5e0ef61e28784e75304252/thunks/files/qux.txt -------------------------------------------------------------------------------- /thunks/lineage.js: -------------------------------------------------------------------------------- 1 | import fakeAjax from '../fakeAjax.js'; 2 | import thunks from 'thunks'; 3 | 4 | const thunk = thunks(); 5 | 6 | // node style required 7 | const fetchCharacter = thunk.thunkify(function (url, callback) { 8 | return fakeAjax(url, 9 | response => callback(null, response), 10 | response => callback(response) 11 | ); 12 | }); 13 | 14 | function lineage(url) { 15 | return fetchCharacter(url)((error, character) => { 16 | if (error) { 17 | console.log('Error: %s', error.message); 18 | console.log('Falling back to %s', 'lotr.com/arvedui'); 19 | return lineage('lotr.com/arvedui'); 20 | } 21 | 22 | console.log('> %s', character.title); 23 | 24 | if (character.ancestor) { 25 | return lineage(character.ancestor); 26 | } 27 | }); 28 | } 29 | 30 | lineage('lotr.com/aragorn'); 31 | 32 | // Currently seq does not pass results from one to another 33 | 34 | // thunk.seq([ 35 | // fetchCharacter('lotr.com/aragorn'), 36 | // fetchCharacter('lotr.com/arathorn'), 37 | // fetchCharacter('lotr.com/arador') 38 | // ])(function (error, characters) { 39 | // if (error) { 40 | // console.log(error); 41 | // } else { 42 | // console.log(...characters.map(ch => ch.title)); 43 | // } 44 | // }); 45 | -------------------------------------------------------------------------------- /thunks/southpark.js: -------------------------------------------------------------------------------- 1 | import fakeAjax from '../fakeAjax'; 2 | import thunks from 'thunks'; 3 | import logger from '../logger'; 4 | 5 | const thunk = thunks(); 6 | 7 | // node style required 8 | const thunkedAjax = thunk.thunkify(function (url, callback) { 9 | return fakeAjax(url, 10 | response => callback(null, response), 11 | response => callback(response) 12 | ); 13 | }); 14 | 15 | const fetchCharacter = thunk.thunkify(function (url, callback) { 16 | let friendsFetched = false; 17 | let recordFetched = false; 18 | 19 | const getImdbProfile = thunkedAjax(url); 20 | 21 | getImdbProfile((error, character) => { 22 | function getKnownForIfInfamous() { 23 | if (!friendsFetched || !recordFetched) { 24 | return; 25 | } 26 | if (character.friends < 2 && character.criminalRec) { 27 | logger.strong('Friends: %s. CR: %s => infamous!\n', character.friends, character.criminalRec); 28 | const getKnownFor = thunkedAjax(character.spdbUrl + '/known_for'); 29 | getKnownFor((error, knownFor) => { 30 | if (error) { 31 | callback(error); 32 | } else { 33 | Object.assign(character, {knownFor}); 34 | callback(null, character); 35 | } 36 | }); 37 | } else { 38 | callback(null, character); 39 | } 40 | } 41 | 42 | const getFriends = thunkedAjax(character.spdbUrl + '/friends'); 43 | 44 | getFriends((error, friends) => { 45 | if (error) { 46 | return callback(error); 47 | } 48 | friendsFetched = true; 49 | Object.assign(character, {friends}); 50 | getKnownForIfInfamous(); 51 | }); 52 | 53 | const getRecord = thunkedAjax(character.spdbUrl + '/record'); 54 | 55 | getRecord((error, criminalRec) => { 56 | if (error) { 57 | return callback(error); 58 | } 59 | recordFetched = true; 60 | Object.assign(character, {criminalRec}); 61 | getKnownForIfInfamous(); 62 | }); 63 | 64 | }); 65 | }); 66 | 67 | const fetchCartman = fetchCharacter('imdb.com/cartman'); 68 | 69 | fetchCartman((error, eric) => { 70 | if (error) { 71 | logger.error('Error: %s', error); 72 | } else { 73 | logger.success(eric); 74 | } 75 | }); 76 | --------------------------------------------------------------------------------