Asynchronous Programming
with JavaScript and Node.js
Timur Shemsedinov
Software Architect at Metarhia, Lecturer at KPI
Metarhia
Asynchronous programming in JavaScript as of today
● callbacks
● async.js
● promises
● async/await
● ?
Asynchronous programming in JavaScript as of today
● callbacks
● async.js
● promises
● async/await
● generators/yield
● events
● functor + chaining + composition
Asynchronous programming in JavaScript as of today
● callbacks > async.js
● promises > async/await
● events
● functor + chaining + composition
Callbacks
(callback) => callback(data)
(...args, callback) => callback(err, data)
Use contracts: callback-last, error-first
You can implement hell easely
Callbacks
readConfig('myConfig', (e, data) => {
query('select * from cities', (e, data) => {
httpGet('http://kpi.ua', (e, data) => {
readFile('README.md', (e, data) => {
});
});
});
});
Callbacks
readConfig('myConfig',
query.bind(null, 'select * from cities',
httpGet.bind(null, 'http://kpi.ua',
readFile.bind('README.md', () => {
});
});
});
});
Callbacks
readConfig('myConfig');
function readConfig(fileName) {
...; query('select * from cities');
}
function query(statement) {
...; httpGet('http://kpi.ua');
}
...
Library async.js or analogues
async.method(
[... (data, cb) => cb(err, result) ...],
(err, result) => {}
);
Use callback-last, error-first
Define functions separately, descriptive names
Hell remains
Events
const ee = new EventEmitter();
const f1 = () => ee.emit('step2');
const f2 = () => ee.emit('step3');
const f3 = () => ee.emit('done');
ee.on('step1', f1.bind(null, par));
ee.on('step2', f2.bind(null, par));
ee.on('step3', f3.bind(null, par));
ee.on('done', () => console.log('done'));
ee.emit('step1');
Promise
new Promise((resolve, reject) => {
resolve(data);
reject(new Error(...));
})
.then(result => {}, reason => {})
.catch(err => {});
Separated control flow for success and fail
Hell remains for complex parallel/sequential code
Promise Sequential
Promise.resolve()
.then(readConfig.bind(null, 'myConfig'))
.then(query.bind(null, 'select * from cities'))
.then(httpGet.bind(null, 'http://kpi.ua'))
.catch((err) => console.log(err.message))
.then(readFile.bind(null, 'README.md'))
.catch((err) => console.log(err.message))
.then((data) => {
console.dir({ data });
});
Promise Parallel
Promise.all([
readConfig('myConfig'),
doQuery('select * from cities'),
httpGet('http://kpi.ua'),
readFile('README.md')
]).then((data) => {
console.log('Done');
console.dir({ data });
});
Promise Mixed: parallel / sequential
Promise.resolve()
.then(readConfig.bind(null, 'myConfig'))
.then(() => Promise.all([
query('select * from cities'),
gttpGet('http://kpi.ua')
]))
.then(readFile.bind(null, 'README.md'))
.then((data) => {
console.log('Done');
console.dir({ data });
});
async/await
async function f() {
return await new Promise(...);
}
f().then(console.log).catch(console.error);
Promises under the hood, Control-flow separated
Hell remains, Performance reduced
Functor + Chaining + composition
const c1 = chain()
.do(readConfig, 'myConfig')
.do(doQuery, 'select * from cities')
.do(httpGet, 'http://kpi.ua')
.do(readFile, 'README.md');
c1();
Functor + chaining + composition
function chain(prev = null) {
const cur = () => {
if (cur.prev) {
cur.prev.next = cur;
cur.prev();
} else {
cur.forward();
}
};
cur.prev = prev;
cur.fn = null;
cur.args = null;
...
...
cur.do = (fn, ...args) => {
cur.fn = fn;
cur.args = args;
return chain(cur);
};
cur.forward = () => {
if (cur.fn) cur.fn(cur.args, () => {
if (cur.next) cur.next.forward();
});
};
return cur;
}
Problems
of callbacks, async.js, Promise, async/await
● Nesting and syntax
● Different contracts
● Not cancellable, no timeouts
● Complexity and Performance
Tricks
Add timeout to any function
const fn = (par) => {
console.log('Function called, par: ' + par);
};
const fn100 = timeout(100, fn);
const fn200 = timeout(200, fn);
setTimeout(() => {
fn100('first'); fn200('second');
}, 150);
Add timeout to any function
function timeout(msec, fn) {
let timer = setTimeout(() => {
if (timer) console.log('Function timedout');
timer = null;
}, msec);
return (...args) => {
if (timer) {
timer = null;
fn(...args);
}
};
}
Make function cancelable
const fn = (par) => {
console.log('Function called, par: ' + par);
};
const f = cancelable(fn);
f('first');
f.cancel();
f('second');
Make function cancelable
const cancelable = (fn) => {
const wrapper = (...args) => {
if (fn) return fn(...args);
};
wrapper.cancel = () => {
fn = null;
};
return wrapper;
};
More wrappers
const f1 = timeout(1000, fn);
const f2 = cancelable(fn);
const f3 = once(fn);
const f4 = limit(10, fn);
const f5 = throttle(10, 1000, fn);
const f6 = debounce(1000, fn);
const f7 = utils(fn)
.limit(10)
.throttle(10, 100)
.timeout(1000);
Promisify and Callbackify
const promise = promisify(asyncFunction);
promise.then(...).catch(...);
const callback = callbackify(promise);
callback((err, value) => { ... });
Sync function to async
const f1 = par => par; const f2 = par => par;
const f3 = par => par; const f4 = par => par;
console.log(f4(f3(f2(f1('value')))));
const af1 = toAsync(f1); const af2 = toAsync(f2);
const af3 = toAsync(f3); const af4 = toAsync(f4);
af1('value', (e, data) => {
af2(data, (e, data) => {
af3(data, (e, data) => {
af4(data, (e, data) => {
console.log(data);
});
});
});
});
Sync function to async
const last = arr => arr[arr.length - 1];
const toAsync = fn => (...args) => {
const callback = last(args);
args.pop();
callback(null, fn(...args));
};
Sync function to Promise
const f1 = par => par; const f2 = par => par;
const f3 = par => par; const f4 = par => par;
console.log(f4(f3(f2(f1('value')))));
const pf1 = toPromise(f1); const pf2 = toPromise(f2);
const pf3 = toPromise(f3); const pf4 = toPromise(f4);
Promise.resolve()
.then(pf1.bind(null, 'value'))
.then(pf2())
.then(pf3())
.then(pf4())
.then((data) => {
console.log(data);
});
Sync function to Promise
const toPromise = fn => (...args) =>
new Promise(resolve => resolve(fn(...args)));
Convertors
● err-back to Promise
● Promise to err-back
● sync function to Promise
● sync function to err-back
● Events to Promise
● Promise to Events
● Events to err-back
● err-back to Events
Metasync
Metasync
● Function composition for asynchronous I/O
● Specific asynchronous abstractions
● Short and expressive syntax
● We use errback compatible contract
● IH
Function composition
inc = a => ++a;
square = a => a * a;
lg = x => log(10, x);
f = compose(inc, square, lg);
...but it’s synchronous
Function composition
Function composition is a great idea for asynchronous I/O
But there are questions:
● What about contracts?
○ for calls and callbacks, arguments and errors
○ timeouts, queueing, throttling
● How to add asynchronicity?
○ parallel and sequential
Asynchronous function composition
const readCfg = (name, cb) => fs.readFile(name, cb);
const netReq = (data, cb) => http.get(data.url, cb);
const dbReq = (query, cb) => db.select(query, cb);
const f1 = sequential(readCfg, netReq, dbReq);
const f2 = parallel(dbReq1, dbReq2, dbReq3);
// f1 & f2 contracts (...args, cb) => cb(err, data)
Flow commutation like in electronics
const fx = metasync.flow(
[f1, f2, f3, [[f4, f5, [f6, f7], f8]], f9]
);
Data collector
const dc1 = new metasync.DataCollector(4);
const dc2 = new metasync.DataCollector(4, 5000);
dc1.on('error', (err, key) => {});
dc2.on('timeout', (err, data) => {});
dc2.on('done', (errs, data) => {});
dc1.collect(data);
Key collector
const keyCollector = new KeyCollector(
['k1', 'k2'], (data) => console.dir(data)
);
keyCollector.collect('k1', {});
fs.readFile('HISTORY.md', (err, data) => {
keyCollector.collect('history', data);
});
Key collector
const kc = new metasync.KeyCollector(
['user', 'config', 'readme', 'timer'], (data) => console.dir(data)
);
kc.collect('user', { name: 'Marcus Aurelius' });
fs.readFile('HISTORY.md', (err,data) => kc.collect('history', data));
fs.readFile('README.md', (err,data) => kc.collect('readme', data));
setTimeout(
() => keyCollector.collect('timer', { date: new Date() }),
ASYNC_TIMEOUT
);
Collector
const dc1 = metasync
.collect(3)
.timeout(5000)
.done((err, data) => {});
dc1(item);
const dc2 = metasync
.collect(['key1', 'key2', 'key3'])
.timeout(5000)
.done((err, data) => {});
dc2(key, value);
Collector features
const dc = metasync
.collect(count)
.distinct()
.done((err, data) => {});
dc(key, error, value);
dc.pick(key, value);
dc.fail(key, error);
fs.readFile(filename, dc.bind(null, key));
dc.take(key, fs.readFile, filename);
Throttle
const t1 = metasync.throttle(5000, f);
t1();
t1();
t1(); // single call
setTimeout(t1, 7000); // another call
setTimeout(t1, 7100);
// will be fired at about 7000+5000
Queue
const cq = metasync.queue(3)
.wait(2000)
.timeout(5000)
.throttle(100, 1000)
.process((item, cb) => cb(err, result))
.success((item) => {})
.failure((item) => {})
.done(() => {})
.drain(() => {});
Timur Shemsedinov
tshemsedinov@github, timur.shemsedinov@gmail.com
tshemsedinov@facebook, marcusaurelius@habrahabr
Github repo: github.com/metarhia/metasync
http://how.programming.works
Telegram: t.me/metarhia & t.me/nodeua
Metarhia meetups: meetup.com/NodeUA,
meetup.com/HowProgrammingWorks
Metarhia

Asynchronous programming with java script and node.js

  • 1.
    Asynchronous Programming with JavaScriptand Node.js Timur Shemsedinov Software Architect at Metarhia, Lecturer at KPI
  • 2.
  • 3.
    Asynchronous programming inJavaScript as of today ● callbacks ● async.js ● promises ● async/await ● ?
  • 4.
    Asynchronous programming inJavaScript as of today ● callbacks ● async.js ● promises ● async/await ● generators/yield ● events ● functor + chaining + composition
  • 5.
    Asynchronous programming inJavaScript as of today ● callbacks > async.js ● promises > async/await ● events ● functor + chaining + composition
  • 6.
    Callbacks (callback) => callback(data) (...args,callback) => callback(err, data) Use contracts: callback-last, error-first You can implement hell easely
  • 7.
    Callbacks readConfig('myConfig', (e, data)=> { query('select * from cities', (e, data) => { httpGet('http://kpi.ua', (e, data) => { readFile('README.md', (e, data) => { }); }); }); });
  • 8.
    Callbacks readConfig('myConfig', query.bind(null, 'select *from cities', httpGet.bind(null, 'http://kpi.ua', readFile.bind('README.md', () => { }); }); }); });
  • 9.
    Callbacks readConfig('myConfig'); function readConfig(fileName) { ...;query('select * from cities'); } function query(statement) { ...; httpGet('http://kpi.ua'); } ...
  • 10.
    Library async.js oranalogues async.method( [... (data, cb) => cb(err, result) ...], (err, result) => {} ); Use callback-last, error-first Define functions separately, descriptive names Hell remains
  • 11.
    Events const ee =new EventEmitter(); const f1 = () => ee.emit('step2'); const f2 = () => ee.emit('step3'); const f3 = () => ee.emit('done'); ee.on('step1', f1.bind(null, par)); ee.on('step2', f2.bind(null, par)); ee.on('step3', f3.bind(null, par)); ee.on('done', () => console.log('done')); ee.emit('step1');
  • 12.
    Promise new Promise((resolve, reject)=> { resolve(data); reject(new Error(...)); }) .then(result => {}, reason => {}) .catch(err => {}); Separated control flow for success and fail Hell remains for complex parallel/sequential code
  • 13.
    Promise Sequential Promise.resolve() .then(readConfig.bind(null, 'myConfig')) .then(query.bind(null,'select * from cities')) .then(httpGet.bind(null, 'http://kpi.ua')) .catch((err) => console.log(err.message)) .then(readFile.bind(null, 'README.md')) .catch((err) => console.log(err.message)) .then((data) => { console.dir({ data }); });
  • 14.
    Promise Parallel Promise.all([ readConfig('myConfig'), doQuery('select *from cities'), httpGet('http://kpi.ua'), readFile('README.md') ]).then((data) => { console.log('Done'); console.dir({ data }); });
  • 15.
    Promise Mixed: parallel/ sequential Promise.resolve() .then(readConfig.bind(null, 'myConfig')) .then(() => Promise.all([ query('select * from cities'), gttpGet('http://kpi.ua') ])) .then(readFile.bind(null, 'README.md')) .then((data) => { console.log('Done'); console.dir({ data }); });
  • 16.
    async/await async function f(){ return await new Promise(...); } f().then(console.log).catch(console.error); Promises under the hood, Control-flow separated Hell remains, Performance reduced
  • 17.
    Functor + Chaining+ composition const c1 = chain() .do(readConfig, 'myConfig') .do(doQuery, 'select * from cities') .do(httpGet, 'http://kpi.ua') .do(readFile, 'README.md'); c1();
  • 18.
    Functor + chaining+ composition function chain(prev = null) { const cur = () => { if (cur.prev) { cur.prev.next = cur; cur.prev(); } else { cur.forward(); } }; cur.prev = prev; cur.fn = null; cur.args = null; ... ... cur.do = (fn, ...args) => { cur.fn = fn; cur.args = args; return chain(cur); }; cur.forward = () => { if (cur.fn) cur.fn(cur.args, () => { if (cur.next) cur.next.forward(); }); }; return cur; }
  • 19.
    Problems of callbacks, async.js,Promise, async/await ● Nesting and syntax ● Different contracts ● Not cancellable, no timeouts ● Complexity and Performance
  • 20.
  • 21.
    Add timeout toany function const fn = (par) => { console.log('Function called, par: ' + par); }; const fn100 = timeout(100, fn); const fn200 = timeout(200, fn); setTimeout(() => { fn100('first'); fn200('second'); }, 150);
  • 22.
    Add timeout toany function function timeout(msec, fn) { let timer = setTimeout(() => { if (timer) console.log('Function timedout'); timer = null; }, msec); return (...args) => { if (timer) { timer = null; fn(...args); } }; }
  • 23.
    Make function cancelable constfn = (par) => { console.log('Function called, par: ' + par); }; const f = cancelable(fn); f('first'); f.cancel(); f('second');
  • 24.
    Make function cancelable constcancelable = (fn) => { const wrapper = (...args) => { if (fn) return fn(...args); }; wrapper.cancel = () => { fn = null; }; return wrapper; };
  • 25.
    More wrappers const f1= timeout(1000, fn); const f2 = cancelable(fn); const f3 = once(fn); const f4 = limit(10, fn); const f5 = throttle(10, 1000, fn); const f6 = debounce(1000, fn); const f7 = utils(fn) .limit(10) .throttle(10, 100) .timeout(1000);
  • 26.
    Promisify and Callbackify constpromise = promisify(asyncFunction); promise.then(...).catch(...); const callback = callbackify(promise); callback((err, value) => { ... });
  • 27.
    Sync function toasync const f1 = par => par; const f2 = par => par; const f3 = par => par; const f4 = par => par; console.log(f4(f3(f2(f1('value'))))); const af1 = toAsync(f1); const af2 = toAsync(f2); const af3 = toAsync(f3); const af4 = toAsync(f4); af1('value', (e, data) => { af2(data, (e, data) => { af3(data, (e, data) => { af4(data, (e, data) => { console.log(data); }); }); }); });
  • 28.
    Sync function toasync const last = arr => arr[arr.length - 1]; const toAsync = fn => (...args) => { const callback = last(args); args.pop(); callback(null, fn(...args)); };
  • 29.
    Sync function toPromise const f1 = par => par; const f2 = par => par; const f3 = par => par; const f4 = par => par; console.log(f4(f3(f2(f1('value'))))); const pf1 = toPromise(f1); const pf2 = toPromise(f2); const pf3 = toPromise(f3); const pf4 = toPromise(f4); Promise.resolve() .then(pf1.bind(null, 'value')) .then(pf2()) .then(pf3()) .then(pf4()) .then((data) => { console.log(data); });
  • 30.
    Sync function toPromise const toPromise = fn => (...args) => new Promise(resolve => resolve(fn(...args)));
  • 31.
    Convertors ● err-back toPromise ● Promise to err-back ● sync function to Promise ● sync function to err-back ● Events to Promise ● Promise to Events ● Events to err-back ● err-back to Events
  • 32.
  • 33.
    Metasync ● Function compositionfor asynchronous I/O ● Specific asynchronous abstractions ● Short and expressive syntax ● We use errback compatible contract ● IH
  • 34.
    Function composition inc =a => ++a; square = a => a * a; lg = x => log(10, x); f = compose(inc, square, lg); ...but it’s synchronous
  • 35.
    Function composition Function compositionis a great idea for asynchronous I/O But there are questions: ● What about contracts? ○ for calls and callbacks, arguments and errors ○ timeouts, queueing, throttling ● How to add asynchronicity? ○ parallel and sequential
  • 36.
    Asynchronous function composition constreadCfg = (name, cb) => fs.readFile(name, cb); const netReq = (data, cb) => http.get(data.url, cb); const dbReq = (query, cb) => db.select(query, cb); const f1 = sequential(readCfg, netReq, dbReq); const f2 = parallel(dbReq1, dbReq2, dbReq3); // f1 & f2 contracts (...args, cb) => cb(err, data)
  • 37.
    Flow commutation likein electronics const fx = metasync.flow( [f1, f2, f3, [[f4, f5, [f6, f7], f8]], f9] );
  • 38.
    Data collector const dc1= new metasync.DataCollector(4); const dc2 = new metasync.DataCollector(4, 5000); dc1.on('error', (err, key) => {}); dc2.on('timeout', (err, data) => {}); dc2.on('done', (errs, data) => {}); dc1.collect(data);
  • 39.
    Key collector const keyCollector= new KeyCollector( ['k1', 'k2'], (data) => console.dir(data) ); keyCollector.collect('k1', {}); fs.readFile('HISTORY.md', (err, data) => { keyCollector.collect('history', data); });
  • 40.
    Key collector const kc= new metasync.KeyCollector( ['user', 'config', 'readme', 'timer'], (data) => console.dir(data) ); kc.collect('user', { name: 'Marcus Aurelius' }); fs.readFile('HISTORY.md', (err,data) => kc.collect('history', data)); fs.readFile('README.md', (err,data) => kc.collect('readme', data)); setTimeout( () => keyCollector.collect('timer', { date: new Date() }), ASYNC_TIMEOUT );
  • 41.
    Collector const dc1 =metasync .collect(3) .timeout(5000) .done((err, data) => {}); dc1(item); const dc2 = metasync .collect(['key1', 'key2', 'key3']) .timeout(5000) .done((err, data) => {}); dc2(key, value);
  • 42.
    Collector features const dc= metasync .collect(count) .distinct() .done((err, data) => {}); dc(key, error, value); dc.pick(key, value); dc.fail(key, error); fs.readFile(filename, dc.bind(null, key)); dc.take(key, fs.readFile, filename);
  • 44.
    Throttle const t1 =metasync.throttle(5000, f); t1(); t1(); t1(); // single call setTimeout(t1, 7000); // another call setTimeout(t1, 7100); // will be fired at about 7000+5000
  • 45.
    Queue const cq =metasync.queue(3) .wait(2000) .timeout(5000) .throttle(100, 1000) .process((item, cb) => cb(err, result)) .success((item) => {}) .failure((item) => {}) .done(() => {}) .drain(() => {});
  • 46.
    Timur Shemsedinov tshemsedinov@github, [email protected] tshemsedinov@facebook,marcusaurelius@habrahabr Github repo: github.com/metarhia/metasync http://how.programming.works Telegram: t.me/metarhia & t.me/nodeua Metarhia meetups: meetup.com/NodeUA, meetup.com/HowProgrammingWorks
  • 49.