Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Think Async: Asynchronous Patterns in NodeJS

Think Async: Asynchronous Patterns in NodeJS

JavaScript is single threaded, so understanding the async patterns available in the language is critical to creating maintainable NodeJS applications with good performance. In order to master “thinking in async”, we’ll explore the async patterns available in node and JavaScript including standard callbacks, promises, thunks/tasks, the new async/await, the upcoming asynchronous iteration features, streams, CSP and ES Observables.

Adam L Barrett

March 09, 2018
Tweet

More Decks by Adam L Barrett

Other Decks in Programming

Transcript

  1. The Patterns • Callbacks • Thunks • Promises • Tasks

    • Observables • Generators • Async / Await • Streams • Async Iteration • CSP
  2. fs.readFile('/foo.txt', (err, data) => { // If an error occurred,

    handle it (throw, propagate, etc) if(err) { console.log('Unknown Error'); return; } // Otherwise, log the file contents console.log(data); });
  3. • Can be called multiple times • Controlled is given

    to the producer • Is the basic JavaScript unit of work
  4. let foo = ( callback ) => callback(1 + 2);

    // use it foo(v => console.log(v));
  5. const thunk = require('thunks')(); const fs = require('fs'); thunk( done

    => fs.stat('package.json', done) )( (error, res) => console.log(error, res) );
  6. function makeASandwichWithSecretSauce(forPerson) { return (dispatch) => { return fetchSecretSauce().then( sauce

    => dispatch( makeASandwich(forPerson) ), error => dispatch( apologize(forPerson, error) ) ); }; }
  7. const verifyUser = function(username, password) { database.verifyUser(username, password) .then(userInfo =>

    database.getRoles(userInfo)) .then(rolesInfo => database.logAccess(rolesInfo)) .then(finalResult => { //do whatever the 'callback' would do }) .catch(err => { //do whatever the error handler needs }); };
  8. new Promise((resolve, reject) => { const xhr = new XMLHttpRequest();

    xhr.open("GET", '/api/ponies-of-equestria'); xhr.onload = () => resolve(xhr.responseText); xhr.onerror = () => reject(xhr.statusText); xhr.send(); });
  9. fetch(url) .then(res => res.json()) .then(response => console.log('Success:', response)) .catch(error =>

    console.error('Error:', error)) .finally(() => console.log('I will happen either way'));
  10. • Eager, not lazy • one value per promise •

    immutable value once settled • Can’t be cancelled (at this time) • A crazy combination of map and flatmap
  11. const task = new Task( (reject, resolve) => { fs.readFile(path,

    (error, data) => { if (error) reject(error) else resolve(data) }) }) task.fork( error => { throw error }, data => { console.log(data) } )
  12. • Basically like promises without the eager problem • better

    for composition and such • no crazy behaviour regarding flatmapping
  13. function listen(element, eventName) { return new Observable( observer => {

    // Create an event handler which sends data to the sink let handler = event => observer.next(event); // Attach the event handler element.addEventListener(eventName, handler, true); // Return a cleanup function which will cancel the stream return () => { // Detach the event handler from the element element.removeEventListener(eventName, handler, true); }; }); }
  14. // Return an observable of special key down commands function

    commandKeys(element) { let keyCommands = { "38": "up", "40": "down" }; return listen(element, "keydown") .filter(event => event.keyCode in keyCommands) .map(event => keyCommands[event.keyCode]) }
  15. let subscription = commandKeys(inputElement).subscribe({ next(val) { console.log("Received key command: "

    + val) }, error(err) { console.log("Received an error: " + err) }, complete() { console.log("Stream complete") }, });
  16. Observable.from({ [Symbol.observable]() { return new Observable(observer => { setTimeout(() =>

    { observer.next("hello"); observer.next("world"); observer.complete(); }, 2000); }); } })
  17. • Composable • Lazy (don’t do anything until observer subscribes)

    • declarative • multiple values over time
  18. function* idMaker() { var index = 0; while (true) {

    yield index++; } } var gen = idMaker(); console.log( gen.next().value ); // 0 console.log( gen.next().value ); // 1 console.log( gen.next().value ); // 2 console.log( gen.next().value ); // 3
  19. function* logGenerator() { console.log( 0 ); console.log( 1, yield );

    console.log( 2, yield ); console.log( 3, yield ); } var gen = logGenerator(); gen.next(); // 0 gen.next( 'pretzel' ); // 1 pretzel gen.next( 'california' ); // 2 california gen.next( 'mayonnaise' ); // 3 mayonnaise
  20. function* anotherGenerator( i ) { yield i + 1; yield

    i + 2; } function* generator( i ) { yield i; yield* anotherGenerator( i ); yield i + 10; } var gen = generator(10); console.log( gen.next().value ); // 10 console.log( gen.next().value ); // 11 console.log( gen.next().value ); // 12 console.log( gen.next().value ); // 20
  21. function* yieldAndReturn() { yield "Y"; return "R"; yield "unreachable"; }

    var gen = yieldAndReturn() console.log(gen.next()); // { value: "Y", done: false } console.log(gen.next()); // { value: "R", done: true } console.log(gen.next()); // { value: undefined, done: true }
  22. const myIterable = {}; myIterable[Symbol.iterator] = function* () { yield

    1; yield 2; yield 3; }; [...myIterable]; // [1, 2, 3]
  23. co(function *(){ const userResult = yield fetch('/api/current-user'); const { id:

    user } = yield userResult.json(); const r = yield fetch('/api/books', qs.stringify({ user })); const books = yield r.json(); return books; }) .catch(onerror);
  24. • Crazy powerful primitive for building on top of •

    makes creating iterables a snap • very imperative • led us to async / await
  25. async function getCurrentUserBooks(){ const userResult = await fetch('/api/current-user'); const {

    id: user } = await userResult.json(); const r = await fetch('/api/books', qs.stringify({ user })); return await r.json(); }
  26. async function getAllTheThings(){ try { await doSomethingAsync(); const results =

    await nowGetResults(); return await results.things.getAll(); } catch (err) { handleErrorProperly(err) } }
  27. const fs = require('fs'); const util = require('util'); // Convert

    fs.readFile into Promise version of same const readFile = util.promisify(fs.readFile); async function getStuff() { return await readFile('test'); } getStuff().then(data => { console.log(data); })
  28. • Can clean up so much messy code • highly

    composable • limited in all the ways promises are • no top level await (yet)
  29. const fs = require('fs'); const server = require('http').createServer(); server.on('request', (req,

    res) => { fs.readFile('./big.file', (err, data) => { if (err) throw err; res.end(data); }); }); server.listen(8000);
  30. const fs = require('fs'); const server = require('http').createServer(); server.on('request', (req,

    res) => { const src = fs.createReadStream('./big.file'); src.pipe(res); }); server.listen(8000);
  31. const { Readable, Writable } = require('stream'); const { Transform,

    Duplex } = require('stream'); Readable - example fs.createReadStream() Writable - example fs.createWriteStream() Duplex - example net.Socket Transform - example zlib.createDeflate()
  32. const { Writable } = require('stream'); const outStream = new

    Writable({ write(chunk, encoding, callback) { console.log(chunk.toString()); callback(); } }); process.stdin.pipe(outStream);
  33. const { Transform } = require('stream'); const commaSplitter = new

    Transform({ readableObjectMode: true, transform(chunk, encoding, callback) { this.push( chunk.toString().trim().split(',') ); callback(); } });
  34. • Should be your goto async abstraction for nodejs •

    somewhat declarative composition • handles complex parts, like back pressure for you • Saves memory and resources
  35. async function asyncSum(asyncIterable) { let result = 0; const iterator

    = asyncIterable[Symbol.asyncIterator](); while (true) { const object = await iterator.next(); if (object.done) break; result += object.value; } return result; }
  36. // Signature: AsyncIterable ! AsyncIterable async function* numberLines(lines) { let

    n = 1; for await (const line of lines) { yield `${n} ${line}`; n++; } }
  37. async function* readLines(path) { let file = await fileOpen(path); try

    { while (!file.EOF) { yield await file.readLine(); } } finally { await file.close(); } }
  38. CSP

  39. import Channel from 'async-csp'; let channel = new Channel(); async

    function puts(ch) { await ch.put(1); await ch.put(2); await ch.put(3); } async function takes(ch) { console.log(await ch.take()); // resolves to 1 console.log(await ch.take()); // resolves to 2 console.log(await ch.take()); // resolves to 3 } puts(channel); takes(channel);
  40. // Channel × Channel ! void async function numberLines(inputChannel, outputChannel)

    { for (let n=1;; n++) { const line = await inputChannel.take(); if (line === Channel.DONE) { break; } outputChannel.put(`${n} ${line}`); } outputChannel.close(); }
  41. import Channel from 'async-csp' async function sleep(duration) { return new

    Promise(resolve => setTimeout(resolve, duration)) } async function player(name, table) { while (true) { let ball = await table.take(); if (ball === Channel.DONE) { console.log('${name}: table is gone!'); break; } ball.hits++; console.log('${name}! Hits: ${ball.hits}'); await sleep(100); await table.put(ball); } } async function pingPong() { console.log('Opening ping-pong channel!'); let table = new Channel(); player('ping', table); player('pong', table); console.log('Serving ball...'); let ball = {hits: 0}; await table.put(ball); await sleep(1000); console.log('Closing ping-pong channel...'); table.close(); await table.done(); console.log('Channel is fully closed!'); console.log('Ball was hit ${ball.hits} times!'); } pingPong()
  42. import Channel from 'async-csp' async function sleep(duration) { return new

    Promise(resolve => setTimeout(resolve, duration)) } async function player(name, table) { while (true) { let ball = await table.take(); if (ball === Channel.DONE) { console.log('${name}: table is gone!'); break; } ball.hits++; console.log('${name}! Hits: ${ball.hits}'); await sleep(100); await table.put(ball); } }
  43. async function sleep(duration) { return new Promise(resolve => setTimeout(resolve, duration))

    } async function player(name, table) { while (true) { let ball = await table.take(); if (ball === Channel.DONE) { console.log('${name}: table is gone!'); break; } ball.hits++; console.log('${name}! Hits: ${ball.hits}'); await sleep(100); await table.put(ball); } } async function pingPong() { console.log('Opening ping-pong channel!');
  44. async function pingPong() { console.log('Opening ping-pong channel!'); let table =

    new Channel(); player('ping', table); player('pong', table); console.log('Serving ball...'); let ball = {hits: 0}; await table.put(ball); await sleep(1000); console.log('Closing ping-pong channel...'); table.close(); await table.done(); console.log('Channel is fully closed!'); console.log('Ball was hit ${ball.hits} times!'); }
  45. await table.put(ball); await sleep(1000); console.log('Closing ping-pong channel...'); table.close(); await table.done();

    console.log('Channel is fully closed!'); console.log('Ball was hit ${ball.hits} times!'); } pingPong()
  46. Opening ping-pong channel! Serving ball... ping! Hits: 1 pong! Hits:

    2 ping! Hits: 3 pong! Hits: 4 ping! Hits: 5 pong! Hits: 6 ping! Hits: 7 pong! Hits: 8 ping! Hits: 9 Closing ping-pong channel... pong: table's gone! Channel is fully closed! Ball was hit 9 times! ping: table's gone!
  47. The Patterns • Callbacks • Thunks • Promises • Tasks

    • Observables • Generators • Async / Await • Streams • Async Iteration • CSP