Slide 1

Slide 1 text

Async Iterators REAL WORLD USAGE in NODE.JS 10

Slide 2

Slide 2 text

Ryan Paul http://seg.phault.net

Slide 3

Slide 3 text

Iterators & Async Functions Bringing Together

Slide 4

Slide 4 text

Availability Supported Not Yet Chrome 63 Safari (Preview) Node.js 10 Edge Firefox 55

Slide 5

Slide 5 text

Async Iterators in Theory •Write generator functions that yield promises •Loop over async iterators with the new for- await-of syntax •A useful abstraction for representing streams

Slide 6

Slide 6 text

Async Iterators in Practice const timer = time => new Promise(resolve => setTimeout(resolve, time)); async function* counter() { let counter = 0; while (true) { await timer(5000); yield counter++; } }

Slide 7

Slide 7 text

Consuming an Async Iterator let iter = counter(); for await (let item of iter) console.log(item) Using the for-await-of syntax

Slide 8

Slide 8 text

Underneath the Sugar •An iterator is an object with a next() function •The next() function returns an object with value and done properties •The value is the yielded value and done is a boolean indicating whether the iterator is complete

Slide 9

Slide 9 text

Async Iteration Protocol •Use the async iteration protocol to turn any object into an async iterator •Objects that conform with the protocol can be consumed with for-await-of •The protocol uses Symbol.asyncIterator

Slide 10

Slide 10 text

Async Iteration Protocol class Example { async *[Symbol.asyncIterator]() { let counter = 0; while (true) { await timer(5000); yield counter++; } } } let example = new Example(); for await (let item of example) console.log(item);

Slide 11

Slide 11 text

Some Practical Examples Real-World Async Iterators

Slide 12

Slide 12 text

Using Async Iterators in the Real World •You can use async iterators today in node.js 10 •Every ReadableStream instance supports the async iteration protocol •Useful for I/O and many other everyday tasks

Slide 13

Slide 13 text

Reading Files with Async Iterators let file = fs.createReadStream("file.txt"); let content = ""; for await (let chunk of file) content += chunk; console.log(content);

Slide 14

Slide 14 text

Streaming Database Query • RethinkDB is an open source database for building real- time web applications • RethinkDB changefeed lets you stream updates from a table or query • You can wrap a changefeed cursor with an async iterator so that it can be consumed with a for-await-of loop

Slide 15

Slide 15 text

Streaming Database Query const r = require("rethinkdbdash")(); function* stream(cursor) { while (true) yield cursor.next(); } let query = await r.db("rethinkdb").table("stats").changes(); for await (let item of stream(query)) console.log(item);

Slide 16

Slide 16 text

Serving as an SSE Event Stream • Server Sent Events (SSE) is a standard for building real- time web applications • Broadcast a stream of events in response to an HTTP request • Easy to consume in the browser with the EventSource client API

Slide 17

Slide 17 text

Serving as an SSE Event Stream async function iterStream(res, iter) { let header = {"Content-Type": "text/event-stream"}; res.writeHead(200, header); for await (let item of iter) res.write(`data: ${item}\n\n`); } http.createServer((req, res) => iterStream(res, counter())).listen(8000);

Slide 18

Slide 18 text

Serving as an SSE Event Stream let events = new EventSource("/"); events.onmessage = ev => console.log(ev); Create an EventSource instance in the browser and attach a function to onmessage to see events

Slide 19

Slide 19 text

But what happens when the client disconnects?

Slide 20

Slide 20 text

Serving as an SSE Event Stream • The generator continues to run when the client disconnects • We shouldn’t leave a dangling generator every time a user loads the page • Naive solution: what if we invoke iter.return() when the connection closes?

Slide 21

Slide 21 text

Serving as an SSE Event Stream async function iterStream(res, iter) { res.connection.on("close", () => iter.return()); let header = {"Content-Type": "text/event-stream"}; res.writeHead(200, header); for await (let item of iter) res.write(`data: ${item}\n\n`); } Watch for the connection to close and call the iter.return() function

Slide 22

Slide 22 text

Serving as an SSE Event Stream •Calling iter.return() doesn’t terminate the pending promise •The pending promise continues until it rejects or resolves •We need a way to cancel the pending promise

Slide 23

Slide 23 text

Promise Cancellation Introducing

Slide 24

Slide 24 text

Promise Cancellation function timer(time, cancelled) { let handle; return new Promise((resolve, reject) => { handle = setTimeout(resolve, time); cancelled.then(() => clearTimeout(handle)); }); } let cancel, cancelled = new Promise(resolve => cancel = resolve); timer(5000, cancelled); cancel();

Slide 25

Slide 25 text

Promise Cancellation let cancel; let cancelled = new Promise(resolve => cancel = resolve); timer(5000, cancelled); cancel(); Create a promise and assign its resolve function to a variable in the parent scope

Slide 26

Slide 26 text

Promise Cancellation function timer(time, cancelled) { let handle; return new Promise((resolve, reject) => { handle = setTimeout(resolve, time); cancelled.then(() => clearTimeout(handle)); }); } Pass the cancelled promise into the function and terminate the timer when it resolves

Slide 27

Slide 27 text

Promise Cancellation function timer(time, cancelled) { let handle; return new Promise((resolve, reject) => { handle = setTimeout(resolve, time); cancelled.then(() => { reject(new Error("Cancelled")); clearTimeout(handle); }); }); } Reject the promise so that all downstream subscribers know that it was cancelled

Slide 28

Slide 28 text

SSE Example with Cancellation Putting it all Together

Slide 29

Slide 29 text

SSE Example with Cancellation async function iterStream(res, iter, cancel) { let header = {"Content-Type": "text/event-stream"}; res.writeHead(200, header); res.connection.on("close", cancel); for await (let item of iter) res.write(`data: ${item}\n\n`); } Add an extra parameter for the cancel() function and invoke it when the user closes their connection

Slide 30

Slide 30 text

SSE Example with Cancellation async function* counter(cancelled) { let counter = 0; while (true) { try { await timer(5000, cancelled); yield counter++; } catch (err) { if (err.message === "Cancelled") break; } } } Pass the cancelled promise into the timer and force the loop to break when the exception is thrown

Slide 31

Slide 31 text

SSE Example with Cancellation let cancel, cancelled = new Promise(resolve => cancel = resolve); http.createServer((req, res) => iterStream(res, counter(cancelled), cancel)).listen(8000); Create the cancellation promise and pass it into the iterStream() function

Slide 32

Slide 32 text

Resources • My Blog post about cancellation and async iterators: http://seg.phault.net/blog/2018/03/async-iterators- cancellation/ • Async Iterators TC39 proposal: https://github.com/tc39/proposal-async-iteration • Follow me on Twitter: @segphault