Asynchronous Programmingwith JavaScript and Node.js
Timur ShemsedinovSoftware 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-firstYou 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-firstDefine functions separately, descriptive namesHell 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 failHell 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 separatedHell 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 collectorconst 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
Queueconst cq = metasync.queue(3) .wait(2000) .timeout(5000) .throttle(100, 1000) .process((item, cb) => cb(err, result)) .success((item) => {}) .failure((item) => {}) .done(() => {}) .drain(() => {});
Timur Shemsedinovtshemsedinov@github, [email protected]@facebook, marcusaurelius@habrahabr
Github repo: github.com/metarhia/metasynchttp://how.programming.works
Telegram: t.me/metarhia & t.me/nodeuaMetarhia meetups: meetup.com/NodeUA,
meetup.com/HowProgrammingWorks
Metarhia