SlideShare a Scribd company logo
Finding a lost song
with Node.js & async iterators
Luciano Mammino ( )
@loige
Dublin, Node.js Meetup
2021-04-29
loige.link/async-it
1
loige.link/async-it
Get these slides!
loige 2
Photo by  on
Darius Bashar Unsplash
 A random song you haven't listened to
in years pops into your head...
3
It doesn't matter what you do all day...
It keeps coming back to you!
Photo by on
Attentie Attentie Unsplash 4
And now you want to listen to it!
Photo by on
Volodymyr Hryshchenko Unsplash 5
But, what if you can't remember
the title or the author?!
Photo by on
Tachina Lee Unsplash 6
THERE MUST BE A WAY TO REMEMBER!
Photo by on
Marius Niveri Unsplash 7
Today, I'll tell you how I solved this problem using
- Last.fm API
- Node.js
- Async Iterators
Photo by on
Quinton Coetzee Unsplash 8
Let me introduce myself first...
9
Let me introduce myself first...
I'm Luciano ( 🍕🍝) 👋
9
Let me introduce myself first...
I'm Luciano ( 🍕🍝) 👋
Senior Architect @ fourTheorem (Dublin )
9
Let me introduce myself first...
I'm Luciano ( 🍕🍝) 👋
Senior Architect @ fourTheorem (Dublin )
nodejsdp.link
Co-Author of Node.js Design Patterns  👉
9
Let me introduce myself first...
I'm Luciano ( 🍕🍝) 👋
Senior Architect @ fourTheorem (Dublin )
nodejsdp.link
Co-Author of Node.js Design Patterns  👉
20% eBook discount on Packt
20NODEDUBLIN
9
Let me introduce myself first...
I'm Luciano ( 🍕🍝) 👋
Senior Architect @ fourTheorem (Dublin )
nodejsdp.link
Co-Author of Node.js Design Patterns  👉
Connect with me:
 
  (blog)
  (twitter)
  (github)
loige.co
@loige
lmammino
20% eBook discount on Packt
20NODEDUBLIN
9
We are business focused technologists that
deliver.
 |  |
Accelerated Serverless AI as a Service Platform Modernisation
Do you want to ?
work with us
loige 10
There was this song in my mind...
loige 11
I could only remember some random
parts and the word "dark" (probably
in the title)
loige 12
13
14
loige 15
Luciano - scrobbling since 12 Feb 2007
loige 15
Luciano - scrobbling since 12 Feb 2007
loige
~250k scrobbles... that song must be there!
15
loige 16
loige
~5k pages of history &
 no search functionality! 😓
16
loige
But there's an API!
https://www.last.fm/api
17
loige 18
loige
Let's give it a shot
curl "http://ws.audioscrobbler.com/2.0/?
method=user.getrecenttracks&user=loige&api_key
=${API_KEY}&format=json" | jq .
19
loige 20
It works! 🥳
Let's convert this to JavaScript
loige 21
import querystring from 'querystring'
import axios from 'axios'
const query = querystring.stringify({
method: 'user.getrecenttracks',
user: 'loige',
api_key: process.env.API_KEY,
format: 'json'
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
console.log(response.data)
loige 22
loige 23
loige
We are getting a "paginated" response
with 50 tracks per page
23
loige
We are getting a "paginated" response
with 50 tracks per page
but there are 51 here! 🤔
23
loige
We are getting a "paginated" response
with 50 tracks per page
but there are 51 here! 🤔
(let's ignore this for now...)
23
loige
We are getting a "paginated" response
with 50 tracks per page
but there are 51 here! 🤔
How do we fetch the next pages?
(let's ignore this for now...)
23
loige 24
let page = 1
while (true) {
const query = querystring.stringify({
method: 'user.getrecenttracks',
user: 'loige',
api_key: process.env.API_KEY,
format: 'json',
page
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
console.log(response.data)
if (page === Number(response.data.recenttracks['@attr'].totalPages)) {
break // it's the last page!
}
loige 25
let page = 1
while (true) {
const query = querystring.stringify({
method: 'user.getrecenttracks',
user: 'loige',
api_key: process.env.API_KEY,
format: 'json',
page
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
console.log(response.data)
if (page === Number(response.data.recenttracks['@attr'].totalPages)) {
break // it's the last page!
}
loige 25
let page = 1
while (true) {
const query = querystring.stringify({
method: 'user.getrecenttracks',
user: 'loige',
api_key: process.env.API_KEY,
format: 'json',
page
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
console.log(response.data)
if (page === Number(response.data.recenttracks['@attr'].totalPages)) {
break // it's the last page!
}
loige 25
let page = 1
while (true) {
const query = querystring.stringify({
method: 'user.getrecenttracks',
user: 'loige',
api_key: process.env.API_KEY,
format: 'json',
page
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
console.log(response.data)
if (page === Number(response.data.recenttracks['@attr'].totalPages)) {
break // it's the last page!
}
loige 25
let page = 1
while (true) {
const query = querystring.stringify({
method: 'user.getrecenttracks',
user: 'loige',
api_key: process.env.API_KEY,
format: 'json',
page
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
console.log(response.data)
if (page === Number(response.data.recenttracks['@attr'].totalPages)) {
break // it's the last page!
}
loige 25
let page = 1
while (true) {
const query = querystring.stringify({
method: 'user.getrecenttracks',
user: 'loige',
api_key: process.env.API_KEY,
format: 'json',
page
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
console.log(response.data)
if (page === Number(response.data.recenttracks['@attr'].totalPages)) {
break // it's the last page!
}
loige 25
loige 26
loige 26
Seems good!
Let's look at the tracks...
loige 27
// ...
for (const track of response.data.recenttracks.track) {
console.log(
track.date?.['#text'],
`${track.artist['#text']} - ${track.name}`
)
}
console.log('--- end page ---')
// ...
loige 28
loige 29
loige
* Note that page size
here is 10 tracks per
page
29
loige
* Note that page size
here is 10 tracks per
page
Every page has a song with undefined time...
This is the song I am currently listening to!
It appears at the top of every page.
29
loige
* Note that page size
here is 10 tracks per
page
Sometimes there are duplicated tracks
between pages... 😨
29
The "sliding windows" problem 😩
loige 30
loige
...
tracks (newest to oldest)
31
loige
...
tracks (newest to oldest)
31
Page1 Page2
loige
...
tracks (newest to oldest)
31
Page1 Page2
...
loige
...
tracks (newest to oldest)
31
Page1 Page2
...
new track
loige
...
tracks (newest to oldest)
31
Page1 Page2
...
Page1 Page2
new track
loige
...
tracks (newest to oldest)
31
Page1 Page2
...
Page1 Page2
new track
moved from page 1 to page 2
loige 32
Time based windows 😎
loige 33
loige
...*
tracks (newest to oldest)
34
* we are done when we get an empty page (or num pages is 1)
loige
...*
tracks (newest to oldest)
34
Page1
* we are done when we get an empty page (or num pages is 1)
loige
...*
tracks (newest to oldest)
34
Page1
t1
* we are done when we get an empty page (or num pages is 1)
loige
...*
tracks (newest to oldest)
34
Page1 before t1
(page 1 "to" t1)
t1
* we are done when we get an empty page (or num pages is 1)
loige
...*
tracks (newest to oldest)
34
Page1 before t1
(page 1 "to" t1)
t1 t2
* we are done when we get an empty page (or num pages is 1)
loige
...*
tracks (newest to oldest)
34
Page1 before t1
(page 1 "to" t1)
t1 t2
before t2
(page 1 "to" t2)
* we are done when we get an empty page (or num pages is 1)
let to
while (true) {
const query = querystring.stringify({
method: 'user.getrecenttracks',
user: 'loige',
api_key: process.env.API_KEY,
format: 'json',
limit: '10',
to
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
const tracks = response.data.recenttracks.track
console.log(
`--- ↓ page to ${to}`,
`remaining pages: ${response.data.recenttracks['@attr'].totalPages} ---`
)
for (const track of tracks) {
console.log(track.date?.uts, `${track.artist['#text']} - ${track.name}`)
}
if (response.data.recenttracks['@attr'].totalPages <= 1) {
break // it's the last page!
}
const lastTrackInPage = tracks[tracks.length - 1]
to = lastTrackInPage.date.uts
} loige 35
let to
while (true) {
const query = querystring.stringify({
method: 'user.getrecenttracks',
user: 'loige',
api_key: process.env.API_KEY,
format: 'json',
limit: '10',
to
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
const tracks = response.data.recenttracks.track
console.log(
`--- ↓ page to ${to}`,
`remaining pages: ${response.data.recenttracks['@attr'].totalPages} ---`
)
for (const track of tracks) {
console.log(track.date?.uts, `${track.artist['#text']} - ${track.name}`)
}
if (response.data.recenttracks['@attr'].totalPages <= 1) {
break // it's the last page!
}
const lastTrackInPage = tracks[tracks.length - 1]
to = lastTrackInPage.date.uts
} loige 35
let to
while (true) {
const query = querystring.stringify({
method: 'user.getrecenttracks',
user: 'loige',
api_key: process.env.API_KEY,
format: 'json',
limit: '10',
to
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
const tracks = response.data.recenttracks.track
console.log(
`--- ↓ page to ${to}`,
`remaining pages: ${response.data.recenttracks['@attr'].totalPages} ---`
)
for (const track of tracks) {
console.log(track.date?.uts, `${track.artist['#text']} - ${track.name}`)
}
if (response.data.recenttracks['@attr'].totalPages <= 1) {
break // it's the last page!
}
const lastTrackInPage = tracks[tracks.length - 1]
to = lastTrackInPage.date.uts
} loige 35
let to
while (true) {
const query = querystring.stringify({
method: 'user.getrecenttracks',
user: 'loige',
api_key: process.env.API_KEY,
format: 'json',
limit: '10',
to
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
const tracks = response.data.recenttracks.track
console.log(
`--- ↓ page to ${to}`,
`remaining pages: ${response.data.recenttracks['@attr'].totalPages} ---`
)
for (const track of tracks) {
console.log(track.date?.uts, `${track.artist['#text']} - ${track.name}`)
}
if (response.data.recenttracks['@attr'].totalPages <= 1) {
break // it's the last page!
}
const lastTrackInPage = tracks[tracks.length - 1]
to = lastTrackInPage.date.uts
} loige 35
let to
while (true) {
const query = querystring.stringify({
method: 'user.getrecenttracks',
user: 'loige',
api_key: process.env.API_KEY,
format: 'json',
limit: '10',
to
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
const tracks = response.data.recenttracks.track
console.log(
`--- ↓ page to ${to}`,
`remaining pages: ${response.data.recenttracks['@attr'].totalPages} ---`
)
for (const track of tracks) {
console.log(track.date?.uts, `${track.artist['#text']} - ${track.name}`)
}
if (response.data.recenttracks['@attr'].totalPages <= 1) {
break // it's the last page!
}
const lastTrackInPage = tracks[tracks.length - 1]
to = lastTrackInPage.date.uts
} loige 35
loige 36
loige
The track of the last timestamp becomes the
boundary for the next page
36
We have a working solution! 🎉
Can we generalise it?
loige 37
We know how to iterate over every
page/track.
How do we expose this information?
loige 38
const reader = LastFmRecentTracks({
apikey: process.env.API_KEY,
user: 'loige'
})
// callbacks
reader.readPages(
(page) => { /* ... */ }, // on page
(err) => { /* ... */} // on completion (or error)
)
loige 39
const reader = LastFmRecentTracks({
apikey: process.env.API_KEY,
user: 'loige'
})
// event emitter
reader.read()
reader.on('page', (page) => { /* ... */ })
reader.on('completed', (err) => { /* ... */ })
loige 40
const reader = LastFmRecentTracks({
apikey: process.env.API_KEY,
user: 'loige'
})
// streams <3
reader.pipe(/* transform or writable stream here */)
reader.on('end', () => { /* ... */ })
reader.on('error', () => { /* ... */ })
loige 41
import { pipeline } from 'stream'
const reader = LastFmRecentTracks({
apikey: process.env.API_KEY,
user: 'loige'
})
// streams pipeline <3 <3
pipeline(
reader,
yourProcessingStream,
(err) => {
// handle completion or err
}
) loige 42
const reader = LastFmRecentTracks({
apikey: process.env.API_KEY,
user: 'loige'
})
// ASYNC ITERATORS!
for await (const page of reader) {
/* ... */
}
// ... do more stuff when all the data is consumed
loige 43
const reader = LastFmRecentTracks({
apikey: process.env.API_KEY,
user: 'loige'
})
// ASYNC ITERATORS WITH ERROR HANDLING!
try {
for await (const page of reader) {
/* ... */
}
} catch (err) {
// handle errors
}
// ... do more stuff when all the data is consumed loige 44
How can we build an async iterator?
🧐
loige 45
Meet the iteration protocols!
loige
loige.co/javascript-iterator-patterns
46
The iterator protocol
An object is an iterator if it has a next() method.
Every time you call it, it returns an object with
the keys done (boolean) and value.
loige 47
function createCountdown (from) {
let nextVal = from
return {
next () {
if (nextVal < 0) {
return { done: true }
}
return {
done: false,
value: nextVal--
}
}
}
} loige 48
function createCountdown (from) {
let nextVal = from
return {
next () {
if (nextVal < 0) {
return { done: true }
}
return {
done: false,
value: nextVal--
}
}
}
} loige 48
function createCountdown (from) {
let nextVal = from
return {
next () {
if (nextVal < 0) {
return { done: true }
}
return {
done: false,
value: nextVal--
}
}
}
} loige 48
function createCountdown (from) {
let nextVal = from
return {
next () {
if (nextVal < 0) {
return { done: true }
}
return {
done: false,
value: nextVal--
}
}
}
} loige 48
function createCountdown (from) {
let nextVal = from
return {
next () {
if (nextVal < 0) {
return { done: true }
}
return {
done: false,
value: nextVal--
}
}
}
} loige 48
const countdown = createCountdown(3)
console.log(countdown.next())
// { done: false, value: 3 }
console.log(countdown.next())
// { done: false, value: 2 }
console.log(countdown.next())
// { done: false, value: 1 }
console.log(countdown.next())
// { done: false, value: 0 }
console.log(countdown.next())
// { done: true } loige 49
Generator functions "produce" iterators!
loige 50
function * createCountdown (from) {
for (let i = from; i >= 0; i--) {
yield i
}
}
loige 51
function * createCountdown (from) {
for (let i = from; i >= 0; i--) {
yield i
}
}
loige 51
function * createCountdown (from) {
for (let i = from; i >= 0; i--) {
yield i
}
}
loige 51
const countdown = createCountdown(3)
console.log(countdown.next())
// { done: false, value: 3 }
console.log(countdown.next())
// { done: false, value: 2 }
console.log(countdown.next())
// { done: false, value: 1 }
console.log(countdown.next())
// { done: false, value: 0 }
console.log(countdown.next())
// { done: true, value: undefined } loige 52
The iterable protocol
An object is iterable if it implements the
@@iterator* method, a zero-argument function
that returns an iterator.
loige
*Symbol.iterator
53
function createCountdown (from) {
let nextVal = from
return {
[Symbol.iterator]: () => ({
next () {
if (nextVal < 0) {
return { done: true }
}
return { done: false, value: nextVal-- }
}
})
}
}
loige 54
function createCountdown (from) {
let nextVal = from
return {
[Symbol.iterator]: () => ({
next () {
if (nextVal < 0) {
return { done: true }
}
return { done: false, value: nextVal-- }
}
})
}
}
loige 54
function createCountdown (from) {
let nextVal = from
return {
[Symbol.iterator]: () => ({
next () {
if (nextVal < 0) {
return { done: true }
}
return { done: false, value: nextVal-- }
}
})
}
}
loige 54
function createCountdown (from) {
return {
[Symbol.iterator]: function * () {
for (let i = from; i >= 0; i--) {
yield i
}
}
}
}
loige 55
const countdown = createCountdown(3)
for (const value of countdown) {
console.log(value)
}
// 3
// 2
// 1
// 0
loige 56
OK. So far this is all synchronous iteration.
What about async? 🙄
loige 57
The async iterator protocol
An object is an async iterator if it has a next()
method. Every time you call it, it returns a
promise that resolves to an object with the keys
done (boolean) and value.
loige 58
import { setTimeout } from 'timers/promises'
function createAsyncCountdown (from, delay = 1000) {
let nextVal = from
return {
async next () {
await setTimeout(delay)
if (nextVal < 0) {
return { done: true }
}
return { done: false, value: nextVal-- }
}
}
} loige 59
import { setTimeout } from 'timers/promises'
function createAsyncCountdown (from, delay = 1000) {
let nextVal = from
return {
async next () {
await setTimeout(delay)
if (nextVal < 0) {
return { done: true }
}
return { done: false, value: nextVal-- }
}
}
} loige 59
import { setTimeout } from 'timers/promises'
function createAsyncCountdown (from, delay = 1000) {
let nextVal = from
return {
async next () {
await setTimeout(delay)
if (nextVal < 0) {
return { done: true }
}
return { done: false, value: nextVal-- }
}
}
} loige 59
const countdown = createAsyncCountdown(3)
console.log(await countdown.next())
// { done: false, value: 3 }
console.log(await countdown.next())
// { done: false, value: 2 }
console.log(await countdown.next())
// { done: false, value: 1 }
console.log(await countdown.next())
// { done: false, value: 0 }
console.log(await countdown.next())
// { done: true } loige 60
const countdown = createAsyncCountdown(3)
console.log(await countdown.next())
// { done: false, value: 3 }
console.log(await countdown.next())
// { done: false, value: 2 }
console.log(await countdown.next())
// { done: false, value: 1 }
console.log(await countdown.next())
// { done: false, value: 0 }
console.log(await countdown.next())
// { done: true } loige 60
loige 61
import { setTimeout } from 'timers/promises'
// async generators "produce" async iterators!
async function * createAsyncCountdown (from, delay = 1000) {
for (let i = from; i >= 0; i--) {
await setTimeout(delay)
yield i
}
}
loige 62
import { setTimeout } from 'timers/promises'
// async generators "produce" async iterators!
async function * createAsyncCountdown (from, delay = 1000) {
for (let i = from; i >= 0; i--) {
await setTimeout(delay)
yield i
}
}
loige 62
import { setTimeout } from 'timers/promises'
// async generators "produce" async iterators!
async function * createAsyncCountdown (from, delay = 1000) {
for (let i = from; i >= 0; i--) {
await setTimeout(delay)
yield i
}
}
loige 62
The async iterable protocol
An object is an async iterable if it implements
the @@asyncIterator* method, a zero-argument
function that returns an async iterator.
loige
*Symbol.asyncIterator
63
import { setTimeout } from 'timers/promises'
function createAsyncCountdown (from, delay = 1000) {
return {
[Symbol.asyncIterator]: async function * () {
for (let i = from; i >= 0; i--) {
await setTimeout(delay)
yield i
}
}
}
}
loige 64
import { setTimeout } from 'timers/promises'
function createAsyncCountdown (from, delay = 1000) {
return {
[Symbol.asyncIterator]: async function * () {
for (let i = from; i >= 0; i--) {
await setTimeout(delay)
yield i
}
}
}
}
loige 64
const countdown = createAsyncCountdown(3)
for await (const value of countdown) {
console.log(value)
}
loige 65
const countdown = createAsyncCountdown(3)
for await (const value of countdown) {
console.log(value)
}
loige 65
Now we know how to make our
LastFmRecentTracks an Async Iterable 🤩
loige 66
function createLastFmRecentTracks (apiKey, user) {
return {
[Symbol.asyncIterator]: async function * () {
let to
while (true) {
const query = querystring.stringify({
method: 'user.getrecenttracks',
user,
api_key: apiKey,
format: 'json',
to
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
const tracks = response.data.recenttracks.track
yield tracks
if (response.data.recenttracks['@attr'].totalPages <= 1) {
break // it's the last page!
}
const lastTrackInPage = tracks[tracks.length - 1]
to = lastTrackInPage.date.uts
}
}
}
}
loige 67
function createLastFmRecentTracks (apiKey, user) {
return {
[Symbol.asyncIterator]: async function * () {
let to
while (true) {
const query = querystring.stringify({
method: 'user.getrecenttracks',
user,
api_key: apiKey,
format: 'json',
to
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
const tracks = response.data.recenttracks.track
yield tracks
if (response.data.recenttracks['@attr'].totalPages <= 1) {
break // it's the last page!
}
const lastTrackInPage = tracks[tracks.length - 1]
to = lastTrackInPage.date.uts
}
}
}
}
loige 67
function createLastFmRecentTracks (apiKey, user) {
return {
[Symbol.asyncIterator]: async function * () {
let to
while (true) {
const query = querystring.stringify({
method: 'user.getrecenttracks',
user,
api_key: apiKey,
format: 'json',
to
})
const url = `https://ws.audioscrobbler.com/2.0/?${query}`
const response = await axios.get(url)
const tracks = response.data.recenttracks.track
yield tracks
if (response.data.recenttracks['@attr'].totalPages <= 1) {
break // it's the last page!
}
const lastTrackInPage = tracks[tracks.length - 1]
to = lastTrackInPage.date.uts
}
}
}
}
loige 67
const recentTracks = createLastFmRecentTracks(
process.env.API_KEY,
'loige'
)
for await (const page of recentTracks) {
console.log(page)
}
loige 68
Let's search for all the songs that contain the
word "dark" in their title! 🧐
loige 69
async function main () {
const recentTracks = createLastFmRecentTracks(
process.env.API_KEY,
'loige'
)
for await (const page of recentTracks) {
for (const track of page) {
if (track.name.toLowerCase().includes('dark')) {
console.log(`${track.artist['#text']} - ${track.name}`)
}
}
}
}
loige 70
loige 71
loige
OMG! This is the song! 😱
...from 8 years ago!
71
For a more serious package that allows you to
fetch data from Last.fm:
loige
npm install scrobbles
72
Cover picture by on
Thanks to Jacek Spera, , , ,
  for reviews and suggestions.
Rod Long Unsplash
@eoins @pelger @gbinside
@ManuEomm
   -  
loige.link/async-it loige.link/async-it-code
for await (const _ of createAsyncCountdown(1_000_000)) {
console.log("THANK YOU! 😍")
}
loige
nodejsdp.link
20% eBook discount on Packt
20NODEDUBLIN
73

More Related Content

What's hot

ZeroMQ Is The Answer
ZeroMQ Is The AnswerZeroMQ Is The Answer
ZeroMQ Is The Answer
Ian Barber
 
How to stand on the shoulders of giants
How to stand on the shoulders of giantsHow to stand on the shoulders of giants
How to stand on the shoulders of giants
Ian Barber
 
Asynchronous PHP and Real-time Messaging
Asynchronous PHP and Real-time MessagingAsynchronous PHP and Real-time Messaging
Asynchronous PHP and Real-time Messaging
Steve Rhoades
 
JFugue, Music, and the Future of Java [JavaOne 2016, CON1851]
JFugue, Music, and the Future of Java [JavaOne 2016, CON1851]JFugue, Music, and the Future of Java [JavaOne 2016, CON1851]
JFugue, Music, and the Future of Java [JavaOne 2016, CON1851]
David Koelle
 
When RegEx is not enough
When RegEx is not enoughWhen RegEx is not enough
When RegEx is not enough
Nati Cohen
 
Mining the social web ch1
Mining the social web ch1Mining the social web ch1
Mining the social web ch1
HyeonSeok Choi
 
Kotlin Coroutines. Flow is coming
Kotlin Coroutines. Flow is comingKotlin Coroutines. Flow is coming
Kotlin Coroutines. Flow is coming
Kirill Rozov
 
The Art, Joy, and Power of Creating Musical Programs (JFugue at SXSW Interact...
The Art, Joy, and Power of Creating Musical Programs (JFugue at SXSW Interact...The Art, Joy, and Power of Creating Musical Programs (JFugue at SXSW Interact...
The Art, Joy, and Power of Creating Musical Programs (JFugue at SXSW Interact...
David Koelle
 
Clojure@Nuday
Clojure@NudayClojure@Nuday
Clojure@Nuday
Josh Glover
 
Music as data
Music as dataMusic as data
Music as data
John Vlachoyiannis
 
Java Streams Interview short reminder with examples
Java Streams Interview short reminder with examplesJava Streams Interview short reminder with examples
Java Streams Interview short reminder with examples
Mark Papis
 
Service intergration
Service intergration Service intergration
Service intergration
재민 장
 
Type script in practice
Type script in practiceType script in practice
Type script in practice
Bryan Hughes
 
ES6 generators
ES6 generatorsES6 generators
ES6 generators
Steven Foote
 
CGI.pm - 3ло?!
CGI.pm - 3ло?!CGI.pm - 3ло?!
CGI.pm - 3ло?!
Anatoly Sharifulin
 
Tomáš Čorej - OpenSSH
Tomáš Čorej - OpenSSHTomáš Čorej - OpenSSH
Tomáš Čorej - OpenSSH
webelement
 
WebSockets, Unity3D, and Clojure
WebSockets, Unity3D, and ClojureWebSockets, Unity3D, and Clojure
WebSockets, Unity3D, and Clojure
Josh Glover
 
Absolute Beginners Guide to Puppet Through Types - PuppetConf 2014
Absolute Beginners Guide to Puppet Through Types - PuppetConf 2014Absolute Beginners Guide to Puppet Through Types - PuppetConf 2014
Absolute Beginners Guide to Puppet Through Types - PuppetConf 2014
Puppet
 
Java Unicode with Live GUI Examples
Java Unicode with Live GUI ExamplesJava Unicode with Live GUI Examples
Java Unicode with Live GUI Examples
Abdul Rahman Sherzad
 
Java Unicode with Cool GUI Examples
Java Unicode with Cool GUI ExamplesJava Unicode with Cool GUI Examples
Java Unicode with Cool GUI Examples
OXUS 20
 

What's hot (20)

ZeroMQ Is The Answer
ZeroMQ Is The AnswerZeroMQ Is The Answer
ZeroMQ Is The Answer
 
How to stand on the shoulders of giants
How to stand on the shoulders of giantsHow to stand on the shoulders of giants
How to stand on the shoulders of giants
 
Asynchronous PHP and Real-time Messaging
Asynchronous PHP and Real-time MessagingAsynchronous PHP and Real-time Messaging
Asynchronous PHP and Real-time Messaging
 
JFugue, Music, and the Future of Java [JavaOne 2016, CON1851]
JFugue, Music, and the Future of Java [JavaOne 2016, CON1851]JFugue, Music, and the Future of Java [JavaOne 2016, CON1851]
JFugue, Music, and the Future of Java [JavaOne 2016, CON1851]
 
When RegEx is not enough
When RegEx is not enoughWhen RegEx is not enough
When RegEx is not enough
 
Mining the social web ch1
Mining the social web ch1Mining the social web ch1
Mining the social web ch1
 
Kotlin Coroutines. Flow is coming
Kotlin Coroutines. Flow is comingKotlin Coroutines. Flow is coming
Kotlin Coroutines. Flow is coming
 
The Art, Joy, and Power of Creating Musical Programs (JFugue at SXSW Interact...
The Art, Joy, and Power of Creating Musical Programs (JFugue at SXSW Interact...The Art, Joy, and Power of Creating Musical Programs (JFugue at SXSW Interact...
The Art, Joy, and Power of Creating Musical Programs (JFugue at SXSW Interact...
 
Clojure@Nuday
Clojure@NudayClojure@Nuday
Clojure@Nuday
 
Music as data
Music as dataMusic as data
Music as data
 
Java Streams Interview short reminder with examples
Java Streams Interview short reminder with examplesJava Streams Interview short reminder with examples
Java Streams Interview short reminder with examples
 
Service intergration
Service intergration Service intergration
Service intergration
 
Type script in practice
Type script in practiceType script in practice
Type script in practice
 
ES6 generators
ES6 generatorsES6 generators
ES6 generators
 
CGI.pm - 3ло?!
CGI.pm - 3ло?!CGI.pm - 3ло?!
CGI.pm - 3ло?!
 
Tomáš Čorej - OpenSSH
Tomáš Čorej - OpenSSHTomáš Čorej - OpenSSH
Tomáš Čorej - OpenSSH
 
WebSockets, Unity3D, and Clojure
WebSockets, Unity3D, and ClojureWebSockets, Unity3D, and Clojure
WebSockets, Unity3D, and Clojure
 
Absolute Beginners Guide to Puppet Through Types - PuppetConf 2014
Absolute Beginners Guide to Puppet Through Types - PuppetConf 2014Absolute Beginners Guide to Puppet Through Types - PuppetConf 2014
Absolute Beginners Guide to Puppet Through Types - PuppetConf 2014
 
Java Unicode with Live GUI Examples
Java Unicode with Live GUI ExamplesJava Unicode with Live GUI Examples
Java Unicode with Live GUI Examples
 
Java Unicode with Cool GUI Examples
Java Unicode with Cool GUI ExamplesJava Unicode with Cool GUI Examples
Java Unicode with Cool GUI Examples
 

Similar to Finding a lost song with Node.js and async iterators

Analyze one year of radio station songs aired with Spark SQL, Spotify, and Da...
Analyze one year of radio station songs aired with Spark SQL, Spotify, and Da...Analyze one year of radio station songs aired with Spark SQL, Spotify, and Da...
Analyze one year of radio station songs aired with Spark SQL, Spotify, and Da...
Paul Leclercq
 
Stream Puzzlers – Traps and Pitfalls in Using Java 8 Streams
Stream Puzzlers – Traps and Pitfalls in Using Java 8 Streams Stream Puzzlers – Traps and Pitfalls in Using Java 8 Streams
Stream Puzzlers – Traps and Pitfalls in Using Java 8 Streams
langer4711
 
Amazing SQL your ORM can (or can't) do | PGConf EU 2019 | Louise Grandjonc
Amazing SQL your ORM can (or can't) do | PGConf EU 2019 | Louise GrandjoncAmazing SQL your ORM can (or can't) do | PGConf EU 2019 | Louise Grandjonc
Amazing SQL your ORM can (or can't) do | PGConf EU 2019 | Louise Grandjonc
Citus Data
 
Kotlin+MicroProfile: Enseñando trucos de 20 años a un nuevo lenguaje
Kotlin+MicroProfile: Enseñando trucos de 20 años a un nuevo lenguajeKotlin+MicroProfile: Enseñando trucos de 20 años a un nuevo lenguaje
Kotlin+MicroProfile: Enseñando trucos de 20 años a un nuevo lenguaje
Víctor Leonel Orozco López
 
Criando app Android utilizando Kotlin
Criando app Android utilizando KotlinCriando app Android utilizando Kotlin
Criando app Android utilizando Kotlin
Luiz Henrique Santana
 
À la découverte des Observables - HumanTalks Paris 2017
À la découverte des Observables - HumanTalks Paris 2017À la découverte des Observables - HumanTalks Paris 2017
À la découverte des Observables - HumanTalks Paris 2017
Nicolas Carlo
 
Extreme Swift
Extreme SwiftExtreme Swift
Extreme Swift
Movel
 
Refactoring to Macros with Clojure
Refactoring to Macros with ClojureRefactoring to Macros with Clojure
Refactoring to Macros with Clojure
Dmitry Buzdin
 
創作 MusicKit 告白情歌
創作 MusicKit 告白情歌創作 MusicKit 告白情歌
創作 MusicKit 告白情歌
彼得潘 Pan
 
Go Concurrency Patterns
Go Concurrency PatternsGo Concurrency Patterns
Go Concurrency Patterns
ElifTech
 
Diagnostics & Debugging webinar
Diagnostics & Debugging webinarDiagnostics & Debugging webinar
Diagnostics & Debugging webinar
MongoDB
 
Data visualization by Kenneth Odoh
Data visualization by Kenneth OdohData visualization by Kenneth Odoh
Data visualization by Kenneth Odoh
pyconfi
 
What can be done with Java, but should better be done with Erlang (@pavlobaron)
What can be done with Java, but should better be done with Erlang (@pavlobaron)What can be done with Java, but should better be done with Erlang (@pavlobaron)
What can be done with Java, but should better be done with Erlang (@pavlobaron)
Pavlo Baron
 
Tuga IT 2018 Summer Edition - The Future of C#
Tuga IT 2018 Summer Edition - The Future of C#Tuga IT 2018 Summer Edition - The Future of C#
Tuga IT 2018 Summer Edition - The Future of C#
Paulo Morgado
 
Reactive stream processing using Akka streams
Reactive stream processing using Akka streams Reactive stream processing using Akka streams
Reactive stream processing using Akka streams
Johan Andrén
 
Building android apps with kotlin
Building android apps with kotlinBuilding android apps with kotlin
Building android apps with kotlin
Shem Magnezi
 
festival ICT 2013: Solid as diamond: use ruby in an web application penetrati...
festival ICT 2013: Solid as diamond: use ruby in an web application penetrati...festival ICT 2013: Solid as diamond: use ruby in an web application penetrati...
festival ICT 2013: Solid as diamond: use ruby in an web application penetrati...
festival ICT 2016
 
API Development and Scala @ SoundCloud
API Development and Scala @ SoundCloudAPI Development and Scala @ SoundCloud
API Development and Scala @ SoundCloud
Bora Tunca
 
Writing a REST Interconnection Library in Swift
Writing a REST Interconnection Library in SwiftWriting a REST Interconnection Library in Swift
Writing a REST Interconnection Library in Swift
Pablo Villar
 
Практическое применения Akka Streams
Практическое применения Akka StreamsПрактическое применения Akka Streams
Практическое применения Akka Streams
Alexey Romanchuk
 

Similar to Finding a lost song with Node.js and async iterators (20)

Analyze one year of radio station songs aired with Spark SQL, Spotify, and Da...
Analyze one year of radio station songs aired with Spark SQL, Spotify, and Da...Analyze one year of radio station songs aired with Spark SQL, Spotify, and Da...
Analyze one year of radio station songs aired with Spark SQL, Spotify, and Da...
 
Stream Puzzlers – Traps and Pitfalls in Using Java 8 Streams
Stream Puzzlers – Traps and Pitfalls in Using Java 8 Streams Stream Puzzlers – Traps and Pitfalls in Using Java 8 Streams
Stream Puzzlers – Traps and Pitfalls in Using Java 8 Streams
 
Amazing SQL your ORM can (or can't) do | PGConf EU 2019 | Louise Grandjonc
Amazing SQL your ORM can (or can't) do | PGConf EU 2019 | Louise GrandjoncAmazing SQL your ORM can (or can't) do | PGConf EU 2019 | Louise Grandjonc
Amazing SQL your ORM can (or can't) do | PGConf EU 2019 | Louise Grandjonc
 
Kotlin+MicroProfile: Enseñando trucos de 20 años a un nuevo lenguaje
Kotlin+MicroProfile: Enseñando trucos de 20 años a un nuevo lenguajeKotlin+MicroProfile: Enseñando trucos de 20 años a un nuevo lenguaje
Kotlin+MicroProfile: Enseñando trucos de 20 años a un nuevo lenguaje
 
Criando app Android utilizando Kotlin
Criando app Android utilizando KotlinCriando app Android utilizando Kotlin
Criando app Android utilizando Kotlin
 
À la découverte des Observables - HumanTalks Paris 2017
À la découverte des Observables - HumanTalks Paris 2017À la découverte des Observables - HumanTalks Paris 2017
À la découverte des Observables - HumanTalks Paris 2017
 
Extreme Swift
Extreme SwiftExtreme Swift
Extreme Swift
 
Refactoring to Macros with Clojure
Refactoring to Macros with ClojureRefactoring to Macros with Clojure
Refactoring to Macros with Clojure
 
創作 MusicKit 告白情歌
創作 MusicKit 告白情歌創作 MusicKit 告白情歌
創作 MusicKit 告白情歌
 
Go Concurrency Patterns
Go Concurrency PatternsGo Concurrency Patterns
Go Concurrency Patterns
 
Diagnostics & Debugging webinar
Diagnostics & Debugging webinarDiagnostics & Debugging webinar
Diagnostics & Debugging webinar
 
Data visualization by Kenneth Odoh
Data visualization by Kenneth OdohData visualization by Kenneth Odoh
Data visualization by Kenneth Odoh
 
What can be done with Java, but should better be done with Erlang (@pavlobaron)
What can be done with Java, but should better be done with Erlang (@pavlobaron)What can be done with Java, but should better be done with Erlang (@pavlobaron)
What can be done with Java, but should better be done with Erlang (@pavlobaron)
 
Tuga IT 2018 Summer Edition - The Future of C#
Tuga IT 2018 Summer Edition - The Future of C#Tuga IT 2018 Summer Edition - The Future of C#
Tuga IT 2018 Summer Edition - The Future of C#
 
Reactive stream processing using Akka streams
Reactive stream processing using Akka streams Reactive stream processing using Akka streams
Reactive stream processing using Akka streams
 
Building android apps with kotlin
Building android apps with kotlinBuilding android apps with kotlin
Building android apps with kotlin
 
festival ICT 2013: Solid as diamond: use ruby in an web application penetrati...
festival ICT 2013: Solid as diamond: use ruby in an web application penetrati...festival ICT 2013: Solid as diamond: use ruby in an web application penetrati...
festival ICT 2013: Solid as diamond: use ruby in an web application penetrati...
 
API Development and Scala @ SoundCloud
API Development and Scala @ SoundCloudAPI Development and Scala @ SoundCloud
API Development and Scala @ SoundCloud
 
Writing a REST Interconnection Library in Swift
Writing a REST Interconnection Library in SwiftWriting a REST Interconnection Library in Swift
Writing a REST Interconnection Library in Swift
 
Практическое применения Akka Streams
Практическое применения Akka StreamsПрактическое применения Akka Streams
Практическое применения Akka Streams
 

More from Luciano Mammino

Did you know JavaScript has iterators? DublinJS
Did you know JavaScript has iterators? DublinJSDid you know JavaScript has iterators? DublinJS
Did you know JavaScript has iterators? DublinJS
Luciano Mammino
 
What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...
What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...
What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...
Luciano Mammino
 
Building an invite-only microsite with Next.js & Airtable - ReactJS Milano
Building an invite-only microsite with Next.js & Airtable - ReactJS MilanoBuilding an invite-only microsite with Next.js & Airtable - ReactJS Milano
Building an invite-only microsite with Next.js & Airtable - ReactJS Milano
Luciano Mammino
 
From Node.js to Design Patterns - BuildPiper
From Node.js to Design Patterns - BuildPiperFrom Node.js to Design Patterns - BuildPiper
From Node.js to Design Patterns - BuildPiper
Luciano Mammino
 
Let's build a 0-cost invite-only website with Next.js and Airtable!
Let's build a 0-cost invite-only website with Next.js and Airtable!Let's build a 0-cost invite-only website with Next.js and Airtable!
Let's build a 0-cost invite-only website with Next.js and Airtable!
Luciano Mammino
 
Everything I know about S3 pre-signed URLs
Everything I know about S3 pre-signed URLsEverything I know about S3 pre-signed URLs
Everything I know about S3 pre-signed URLs
Luciano Mammino
 
Serverless for High Performance Computing
Serverless for High Performance ComputingServerless for High Performance Computing
Serverless for High Performance Computing
Luciano Mammino
 
Serverless for High Performance Computing
Serverless for High Performance ComputingServerless for High Performance Computing
Serverless for High Performance Computing
Luciano Mammino
 
JavaScript Iteration Protocols - Workshop NodeConf EU 2022
JavaScript Iteration Protocols - Workshop NodeConf EU 2022JavaScript Iteration Protocols - Workshop NodeConf EU 2022
JavaScript Iteration Protocols - Workshop NodeConf EU 2022
Luciano Mammino
 
Building an invite-only microsite with Next.js & Airtable
Building an invite-only microsite with Next.js & AirtableBuilding an invite-only microsite with Next.js & Airtable
Building an invite-only microsite with Next.js & Airtable
Luciano Mammino
 
Let's take the monolith to the cloud 🚀
Let's take the monolith to the cloud 🚀Let's take the monolith to the cloud 🚀
Let's take the monolith to the cloud 🚀
Luciano Mammino
 
A look inside the European Covid Green Certificate - Rust Dublin
A look inside the European Covid Green Certificate - Rust DublinA look inside the European Covid Green Certificate - Rust Dublin
A look inside the European Covid Green Certificate - Rust Dublin
Luciano Mammino
 
Monoliths to the cloud!
Monoliths to the cloud!Monoliths to the cloud!
Monoliths to the cloud!
Luciano Mammino
 
The senior dev
The senior devThe senior dev
The senior dev
Luciano Mammino
 
Node.js: scalability tips - Azure Dev Community Vijayawada
Node.js: scalability tips - Azure Dev Community VijayawadaNode.js: scalability tips - Azure Dev Community Vijayawada
Node.js: scalability tips - Azure Dev Community Vijayawada
Luciano Mammino
 
A look inside the European Covid Green Certificate (Codemotion 2021)
A look inside the European Covid Green Certificate (Codemotion 2021)A look inside the European Covid Green Certificate (Codemotion 2021)
A look inside the European Covid Green Certificate (Codemotion 2021)
Luciano Mammino
 
AWS Observability Made Simple
AWS Observability Made SimpleAWS Observability Made Simple
AWS Observability Made Simple
Luciano Mammino
 
Semplificare l'observability per progetti Serverless
Semplificare l'observability per progetti ServerlessSemplificare l'observability per progetti Serverless
Semplificare l'observability per progetti Serverless
Luciano Mammino
 
How to send gzipped requests with boto3
How to send gzipped requests with boto3How to send gzipped requests with boto3
How to send gzipped requests with boto3
Luciano Mammino
 
AWS Observability (without the Pain)
AWS Observability (without the Pain)AWS Observability (without the Pain)
AWS Observability (without the Pain)
Luciano Mammino
 

More from Luciano Mammino (20)

Did you know JavaScript has iterators? DublinJS
Did you know JavaScript has iterators? DublinJSDid you know JavaScript has iterators? DublinJS
Did you know JavaScript has iterators? DublinJS
 
What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...
What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...
What I learned by solving 50 Advent of Code challenges in Rust - RustNation U...
 
Building an invite-only microsite with Next.js & Airtable - ReactJS Milano
Building an invite-only microsite with Next.js & Airtable - ReactJS MilanoBuilding an invite-only microsite with Next.js & Airtable - ReactJS Milano
Building an invite-only microsite with Next.js & Airtable - ReactJS Milano
 
From Node.js to Design Patterns - BuildPiper
From Node.js to Design Patterns - BuildPiperFrom Node.js to Design Patterns - BuildPiper
From Node.js to Design Patterns - BuildPiper
 
Let's build a 0-cost invite-only website with Next.js and Airtable!
Let's build a 0-cost invite-only website with Next.js and Airtable!Let's build a 0-cost invite-only website with Next.js and Airtable!
Let's build a 0-cost invite-only website with Next.js and Airtable!
 
Everything I know about S3 pre-signed URLs
Everything I know about S3 pre-signed URLsEverything I know about S3 pre-signed URLs
Everything I know about S3 pre-signed URLs
 
Serverless for High Performance Computing
Serverless for High Performance ComputingServerless for High Performance Computing
Serverless for High Performance Computing
 
Serverless for High Performance Computing
Serverless for High Performance ComputingServerless for High Performance Computing
Serverless for High Performance Computing
 
JavaScript Iteration Protocols - Workshop NodeConf EU 2022
JavaScript Iteration Protocols - Workshop NodeConf EU 2022JavaScript Iteration Protocols - Workshop NodeConf EU 2022
JavaScript Iteration Protocols - Workshop NodeConf EU 2022
 
Building an invite-only microsite with Next.js & Airtable
Building an invite-only microsite with Next.js & AirtableBuilding an invite-only microsite with Next.js & Airtable
Building an invite-only microsite with Next.js & Airtable
 
Let's take the monolith to the cloud 🚀
Let's take the monolith to the cloud 🚀Let's take the monolith to the cloud 🚀
Let's take the monolith to the cloud 🚀
 
A look inside the European Covid Green Certificate - Rust Dublin
A look inside the European Covid Green Certificate - Rust DublinA look inside the European Covid Green Certificate - Rust Dublin
A look inside the European Covid Green Certificate - Rust Dublin
 
Monoliths to the cloud!
Monoliths to the cloud!Monoliths to the cloud!
Monoliths to the cloud!
 
The senior dev
The senior devThe senior dev
The senior dev
 
Node.js: scalability tips - Azure Dev Community Vijayawada
Node.js: scalability tips - Azure Dev Community VijayawadaNode.js: scalability tips - Azure Dev Community Vijayawada
Node.js: scalability tips - Azure Dev Community Vijayawada
 
A look inside the European Covid Green Certificate (Codemotion 2021)
A look inside the European Covid Green Certificate (Codemotion 2021)A look inside the European Covid Green Certificate (Codemotion 2021)
A look inside the European Covid Green Certificate (Codemotion 2021)
 
AWS Observability Made Simple
AWS Observability Made SimpleAWS Observability Made Simple
AWS Observability Made Simple
 
Semplificare l'observability per progetti Serverless
Semplificare l'observability per progetti ServerlessSemplificare l'observability per progetti Serverless
Semplificare l'observability per progetti Serverless
 
How to send gzipped requests with boto3
How to send gzipped requests with boto3How to send gzipped requests with boto3
How to send gzipped requests with boto3
 
AWS Observability (without the Pain)
AWS Observability (without the Pain)AWS Observability (without the Pain)
AWS Observability (without the Pain)
 

Recently uploaded

Mutation Testing for Task-Oriented Chatbots
Mutation Testing for Task-Oriented ChatbotsMutation Testing for Task-Oriented Chatbots
Mutation Testing for Task-Oriented Chatbots
Pablo Gómez Abajo
 
"Frontline Battles with DDoS: Best practices and Lessons Learned", Igor Ivaniuk
"Frontline Battles with DDoS: Best practices and Lessons Learned",  Igor Ivaniuk"Frontline Battles with DDoS: Best practices and Lessons Learned",  Igor Ivaniuk
"Frontline Battles with DDoS: Best practices and Lessons Learned", Igor Ivaniuk
Fwdays
 
Christine's Supplier Sourcing Presentaion.pptx
Christine's Supplier Sourcing Presentaion.pptxChristine's Supplier Sourcing Presentaion.pptx
Christine's Supplier Sourcing Presentaion.pptx
christinelarrosa
 
From Natural Language to Structured Solr Queries using LLMs
From Natural Language to Structured Solr Queries using LLMsFrom Natural Language to Structured Solr Queries using LLMs
From Natural Language to Structured Solr Queries using LLMs
Sease
 
Getting the Most Out of ScyllaDB Monitoring: ShareChat's Tips
Getting the Most Out of ScyllaDB Monitoring: ShareChat's TipsGetting the Most Out of ScyllaDB Monitoring: ShareChat's Tips
Getting the Most Out of ScyllaDB Monitoring: ShareChat's Tips
ScyllaDB
 
JavaLand 2024: Application Development Green Masterplan
JavaLand 2024: Application Development Green MasterplanJavaLand 2024: Application Development Green Masterplan
JavaLand 2024: Application Development Green Masterplan
Miro Wengner
 
GNSS spoofing via SDR (Criptored Talks 2024)
GNSS spoofing via SDR (Criptored Talks 2024)GNSS spoofing via SDR (Criptored Talks 2024)
GNSS spoofing via SDR (Criptored Talks 2024)
Javier Junquera
 
Introducing BoxLang : A new JVM language for productivity and modularity!
Introducing BoxLang : A new JVM language for productivity and modularity!Introducing BoxLang : A new JVM language for productivity and modularity!
Introducing BoxLang : A new JVM language for productivity and modularity!
Ortus Solutions, Corp
 
[OReilly Superstream] Occupy the Space: A grassroots guide to engineering (an...
[OReilly Superstream] Occupy the Space: A grassroots guide to engineering (an...[OReilly Superstream] Occupy the Space: A grassroots guide to engineering (an...
[OReilly Superstream] Occupy the Space: A grassroots guide to engineering (an...
Jason Yip
 
Biomedical Knowledge Graphs for Data Scientists and Bioinformaticians
Biomedical Knowledge Graphs for Data Scientists and BioinformaticiansBiomedical Knowledge Graphs for Data Scientists and Bioinformaticians
Biomedical Knowledge Graphs for Data Scientists and Bioinformaticians
Neo4j
 
inQuba Webinar Mastering Customer Journey Management with Dr Graham Hill
inQuba Webinar Mastering Customer Journey Management with Dr Graham HillinQuba Webinar Mastering Customer Journey Management with Dr Graham Hill
inQuba Webinar Mastering Customer Journey Management with Dr Graham Hill
LizaNolte
 
LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...
LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...
LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...
DanBrown980551
 
"Scaling RAG Applications to serve millions of users", Kevin Goedecke
"Scaling RAG Applications to serve millions of users",  Kevin Goedecke"Scaling RAG Applications to serve millions of users",  Kevin Goedecke
"Scaling RAG Applications to serve millions of users", Kevin Goedecke
Fwdays
 
Astute Business Solutions | Oracle Cloud Partner |
Astute Business Solutions | Oracle Cloud Partner |Astute Business Solutions | Oracle Cloud Partner |
Astute Business Solutions | Oracle Cloud Partner |
AstuteBusiness
 
GlobalLogic Java Community Webinar #18 “How to Improve Web Application Perfor...
GlobalLogic Java Community Webinar #18 “How to Improve Web Application Perfor...GlobalLogic Java Community Webinar #18 “How to Improve Web Application Perfor...
GlobalLogic Java Community Webinar #18 “How to Improve Web Application Perfor...
GlobalLogic Ukraine
 
Containers & AI - Beauty and the Beast!?!
Containers & AI - Beauty and the Beast!?!Containers & AI - Beauty and the Beast!?!
Containers & AI - Beauty and the Beast!?!
Tobias Schneck
 
PRODUCT LISTING OPTIMIZATION PRESENTATION.pptx
PRODUCT LISTING OPTIMIZATION PRESENTATION.pptxPRODUCT LISTING OPTIMIZATION PRESENTATION.pptx
PRODUCT LISTING OPTIMIZATION PRESENTATION.pptx
christinelarrosa
 
Leveraging the Graph for Clinical Trials and Standards
Leveraging the Graph for Clinical Trials and StandardsLeveraging the Graph for Clinical Trials and Standards
Leveraging the Graph for Clinical Trials and Standards
Neo4j
 
Principle of conventional tomography-Bibash Shahi ppt..pptx
Principle of conventional tomography-Bibash Shahi ppt..pptxPrinciple of conventional tomography-Bibash Shahi ppt..pptx
Principle of conventional tomography-Bibash Shahi ppt..pptx
BibashShahi
 
Christine's Product Research Presentation.pptx
Christine's Product Research Presentation.pptxChristine's Product Research Presentation.pptx
Christine's Product Research Presentation.pptx
christinelarrosa
 

Recently uploaded (20)

Mutation Testing for Task-Oriented Chatbots
Mutation Testing for Task-Oriented ChatbotsMutation Testing for Task-Oriented Chatbots
Mutation Testing for Task-Oriented Chatbots
 
"Frontline Battles with DDoS: Best practices and Lessons Learned", Igor Ivaniuk
"Frontline Battles with DDoS: Best practices and Lessons Learned",  Igor Ivaniuk"Frontline Battles with DDoS: Best practices and Lessons Learned",  Igor Ivaniuk
"Frontline Battles with DDoS: Best practices and Lessons Learned", Igor Ivaniuk
 
Christine's Supplier Sourcing Presentaion.pptx
Christine's Supplier Sourcing Presentaion.pptxChristine's Supplier Sourcing Presentaion.pptx
Christine's Supplier Sourcing Presentaion.pptx
 
From Natural Language to Structured Solr Queries using LLMs
From Natural Language to Structured Solr Queries using LLMsFrom Natural Language to Structured Solr Queries using LLMs
From Natural Language to Structured Solr Queries using LLMs
 
Getting the Most Out of ScyllaDB Monitoring: ShareChat's Tips
Getting the Most Out of ScyllaDB Monitoring: ShareChat's TipsGetting the Most Out of ScyllaDB Monitoring: ShareChat's Tips
Getting the Most Out of ScyllaDB Monitoring: ShareChat's Tips
 
JavaLand 2024: Application Development Green Masterplan
JavaLand 2024: Application Development Green MasterplanJavaLand 2024: Application Development Green Masterplan
JavaLand 2024: Application Development Green Masterplan
 
GNSS spoofing via SDR (Criptored Talks 2024)
GNSS spoofing via SDR (Criptored Talks 2024)GNSS spoofing via SDR (Criptored Talks 2024)
GNSS spoofing via SDR (Criptored Talks 2024)
 
Introducing BoxLang : A new JVM language for productivity and modularity!
Introducing BoxLang : A new JVM language for productivity and modularity!Introducing BoxLang : A new JVM language for productivity and modularity!
Introducing BoxLang : A new JVM language for productivity and modularity!
 
[OReilly Superstream] Occupy the Space: A grassroots guide to engineering (an...
[OReilly Superstream] Occupy the Space: A grassroots guide to engineering (an...[OReilly Superstream] Occupy the Space: A grassroots guide to engineering (an...
[OReilly Superstream] Occupy the Space: A grassroots guide to engineering (an...
 
Biomedical Knowledge Graphs for Data Scientists and Bioinformaticians
Biomedical Knowledge Graphs for Data Scientists and BioinformaticiansBiomedical Knowledge Graphs for Data Scientists and Bioinformaticians
Biomedical Knowledge Graphs for Data Scientists and Bioinformaticians
 
inQuba Webinar Mastering Customer Journey Management with Dr Graham Hill
inQuba Webinar Mastering Customer Journey Management with Dr Graham HillinQuba Webinar Mastering Customer Journey Management with Dr Graham Hill
inQuba Webinar Mastering Customer Journey Management with Dr Graham Hill
 
LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...
LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...
LF Energy Webinar: Carbon Data Specifications: Mechanisms to Improve Data Acc...
 
"Scaling RAG Applications to serve millions of users", Kevin Goedecke
"Scaling RAG Applications to serve millions of users",  Kevin Goedecke"Scaling RAG Applications to serve millions of users",  Kevin Goedecke
"Scaling RAG Applications to serve millions of users", Kevin Goedecke
 
Astute Business Solutions | Oracle Cloud Partner |
Astute Business Solutions | Oracle Cloud Partner |Astute Business Solutions | Oracle Cloud Partner |
Astute Business Solutions | Oracle Cloud Partner |
 
GlobalLogic Java Community Webinar #18 “How to Improve Web Application Perfor...
GlobalLogic Java Community Webinar #18 “How to Improve Web Application Perfor...GlobalLogic Java Community Webinar #18 “How to Improve Web Application Perfor...
GlobalLogic Java Community Webinar #18 “How to Improve Web Application Perfor...
 
Containers & AI - Beauty and the Beast!?!
Containers & AI - Beauty and the Beast!?!Containers & AI - Beauty and the Beast!?!
Containers & AI - Beauty and the Beast!?!
 
PRODUCT LISTING OPTIMIZATION PRESENTATION.pptx
PRODUCT LISTING OPTIMIZATION PRESENTATION.pptxPRODUCT LISTING OPTIMIZATION PRESENTATION.pptx
PRODUCT LISTING OPTIMIZATION PRESENTATION.pptx
 
Leveraging the Graph for Clinical Trials and Standards
Leveraging the Graph for Clinical Trials and StandardsLeveraging the Graph for Clinical Trials and Standards
Leveraging the Graph for Clinical Trials and Standards
 
Principle of conventional tomography-Bibash Shahi ppt..pptx
Principle of conventional tomography-Bibash Shahi ppt..pptxPrinciple of conventional tomography-Bibash Shahi ppt..pptx
Principle of conventional tomography-Bibash Shahi ppt..pptx
 
Christine's Product Research Presentation.pptx
Christine's Product Research Presentation.pptxChristine's Product Research Presentation.pptx
Christine's Product Research Presentation.pptx
 

Finding a lost song with Node.js and async iterators

  • 1. Finding a lost song with Node.js & async iterators Luciano Mammino ( ) @loige Dublin, Node.js Meetup 2021-04-29 loige.link/async-it 1
  • 3. Photo by  on Darius Bashar Unsplash  A random song you haven't listened to in years pops into your head... 3
  • 4. It doesn't matter what you do all day... It keeps coming back to you! Photo by on Attentie Attentie Unsplash 4
  • 5. And now you want to listen to it! Photo by on Volodymyr Hryshchenko Unsplash 5
  • 6. But, what if you can't remember the title or the author?! Photo by on Tachina Lee Unsplash 6
  • 7. THERE MUST BE A WAY TO REMEMBER! Photo by on Marius Niveri Unsplash 7
  • 8. Today, I'll tell you how I solved this problem using - Last.fm API - Node.js - Async Iterators Photo by on Quinton Coetzee Unsplash 8
  • 9. Let me introduce myself first... 9
  • 10. Let me introduce myself first... I'm Luciano ( 🍕🍝) 👋 9
  • 11. Let me introduce myself first... I'm Luciano ( 🍕🍝) 👋 Senior Architect @ fourTheorem (Dublin ) 9
  • 12. Let me introduce myself first... I'm Luciano ( 🍕🍝) 👋 Senior Architect @ fourTheorem (Dublin ) nodejsdp.link Co-Author of Node.js Design Patterns  👉 9
  • 13. Let me introduce myself first... I'm Luciano ( 🍕🍝) 👋 Senior Architect @ fourTheorem (Dublin ) nodejsdp.link Co-Author of Node.js Design Patterns  👉 20% eBook discount on Packt 20NODEDUBLIN 9
  • 14. Let me introduce myself first... I'm Luciano ( 🍕🍝) 👋 Senior Architect @ fourTheorem (Dublin ) nodejsdp.link Co-Author of Node.js Design Patterns  👉 Connect with me:     (blog)   (twitter)   (github) loige.co @loige lmammino 20% eBook discount on Packt 20NODEDUBLIN 9
  • 15. We are business focused technologists that deliver.  |  | Accelerated Serverless AI as a Service Platform Modernisation Do you want to ? work with us loige 10
  • 16. There was this song in my mind... loige 11
  • 17. I could only remember some random parts and the word "dark" (probably in the title) loige 12
  • 18. 13
  • 19. 14
  • 21. Luciano - scrobbling since 12 Feb 2007 loige 15
  • 22. Luciano - scrobbling since 12 Feb 2007 loige ~250k scrobbles... that song must be there! 15
  • 24. loige ~5k pages of history &  no search functionality! 😓 16
  • 25. loige But there's an API! https://www.last.fm/api 17
  • 27. loige Let's give it a shot curl "http://ws.audioscrobbler.com/2.0/? method=user.getrecenttracks&user=loige&api_key =${API_KEY}&format=json" | jq . 19
  • 29. It works! 🥳 Let's convert this to JavaScript loige 21
  • 30. import querystring from 'querystring' import axios from 'axios' const query = querystring.stringify({ method: 'user.getrecenttracks', user: 'loige', api_key: process.env.API_KEY, format: 'json' }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) console.log(response.data) loige 22
  • 32. loige We are getting a "paginated" response with 50 tracks per page 23
  • 33. loige We are getting a "paginated" response with 50 tracks per page but there are 51 here! 🤔 23
  • 34. loige We are getting a "paginated" response with 50 tracks per page but there are 51 here! 🤔 (let's ignore this for now...) 23
  • 35. loige We are getting a "paginated" response with 50 tracks per page but there are 51 here! 🤔 How do we fetch the next pages? (let's ignore this for now...) 23
  • 37. let page = 1 while (true) { const query = querystring.stringify({ method: 'user.getrecenttracks', user: 'loige', api_key: process.env.API_KEY, format: 'json', page }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) console.log(response.data) if (page === Number(response.data.recenttracks['@attr'].totalPages)) { break // it's the last page! } loige 25
  • 38. let page = 1 while (true) { const query = querystring.stringify({ method: 'user.getrecenttracks', user: 'loige', api_key: process.env.API_KEY, format: 'json', page }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) console.log(response.data) if (page === Number(response.data.recenttracks['@attr'].totalPages)) { break // it's the last page! } loige 25
  • 39. let page = 1 while (true) { const query = querystring.stringify({ method: 'user.getrecenttracks', user: 'loige', api_key: process.env.API_KEY, format: 'json', page }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) console.log(response.data) if (page === Number(response.data.recenttracks['@attr'].totalPages)) { break // it's the last page! } loige 25
  • 40. let page = 1 while (true) { const query = querystring.stringify({ method: 'user.getrecenttracks', user: 'loige', api_key: process.env.API_KEY, format: 'json', page }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) console.log(response.data) if (page === Number(response.data.recenttracks['@attr'].totalPages)) { break // it's the last page! } loige 25
  • 41. let page = 1 while (true) { const query = querystring.stringify({ method: 'user.getrecenttracks', user: 'loige', api_key: process.env.API_KEY, format: 'json', page }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) console.log(response.data) if (page === Number(response.data.recenttracks['@attr'].totalPages)) { break // it's the last page! } loige 25
  • 42. let page = 1 while (true) { const query = querystring.stringify({ method: 'user.getrecenttracks', user: 'loige', api_key: process.env.API_KEY, format: 'json', page }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) console.log(response.data) if (page === Number(response.data.recenttracks['@attr'].totalPages)) { break // it's the last page! } loige 25
  • 45. Seems good! Let's look at the tracks... loige 27
  • 46. // ... for (const track of response.data.recenttracks.track) { console.log( track.date?.['#text'], `${track.artist['#text']} - ${track.name}` ) } console.log('--- end page ---') // ... loige 28
  • 48. loige * Note that page size here is 10 tracks per page 29
  • 49. loige * Note that page size here is 10 tracks per page Every page has a song with undefined time... This is the song I am currently listening to! It appears at the top of every page. 29
  • 50. loige * Note that page size here is 10 tracks per page Sometimes there are duplicated tracks between pages... 😨 29
  • 51. The "sliding windows" problem 😩 loige 30
  • 53. loige ... tracks (newest to oldest) 31 Page1 Page2
  • 54. loige ... tracks (newest to oldest) 31 Page1 Page2 ...
  • 55. loige ... tracks (newest to oldest) 31 Page1 Page2 ... new track
  • 56. loige ... tracks (newest to oldest) 31 Page1 Page2 ... Page1 Page2 new track
  • 57. loige ... tracks (newest to oldest) 31 Page1 Page2 ... Page1 Page2 new track moved from page 1 to page 2
  • 59. Time based windows 😎 loige 33
  • 60. loige ...* tracks (newest to oldest) 34 * we are done when we get an empty page (or num pages is 1)
  • 61. loige ...* tracks (newest to oldest) 34 Page1 * we are done when we get an empty page (or num pages is 1)
  • 62. loige ...* tracks (newest to oldest) 34 Page1 t1 * we are done when we get an empty page (or num pages is 1)
  • 63. loige ...* tracks (newest to oldest) 34 Page1 before t1 (page 1 "to" t1) t1 * we are done when we get an empty page (or num pages is 1)
  • 64. loige ...* tracks (newest to oldest) 34 Page1 before t1 (page 1 "to" t1) t1 t2 * we are done when we get an empty page (or num pages is 1)
  • 65. loige ...* tracks (newest to oldest) 34 Page1 before t1 (page 1 "to" t1) t1 t2 before t2 (page 1 "to" t2) * we are done when we get an empty page (or num pages is 1)
  • 66. let to while (true) { const query = querystring.stringify({ method: 'user.getrecenttracks', user: 'loige', api_key: process.env.API_KEY, format: 'json', limit: '10', to }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) const tracks = response.data.recenttracks.track console.log( `--- ↓ page to ${to}`, `remaining pages: ${response.data.recenttracks['@attr'].totalPages} ---` ) for (const track of tracks) { console.log(track.date?.uts, `${track.artist['#text']} - ${track.name}`) } if (response.data.recenttracks['@attr'].totalPages <= 1) { break // it's the last page! } const lastTrackInPage = tracks[tracks.length - 1] to = lastTrackInPage.date.uts } loige 35
  • 67. let to while (true) { const query = querystring.stringify({ method: 'user.getrecenttracks', user: 'loige', api_key: process.env.API_KEY, format: 'json', limit: '10', to }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) const tracks = response.data.recenttracks.track console.log( `--- ↓ page to ${to}`, `remaining pages: ${response.data.recenttracks['@attr'].totalPages} ---` ) for (const track of tracks) { console.log(track.date?.uts, `${track.artist['#text']} - ${track.name}`) } if (response.data.recenttracks['@attr'].totalPages <= 1) { break // it's the last page! } const lastTrackInPage = tracks[tracks.length - 1] to = lastTrackInPage.date.uts } loige 35
  • 68. let to while (true) { const query = querystring.stringify({ method: 'user.getrecenttracks', user: 'loige', api_key: process.env.API_KEY, format: 'json', limit: '10', to }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) const tracks = response.data.recenttracks.track console.log( `--- ↓ page to ${to}`, `remaining pages: ${response.data.recenttracks['@attr'].totalPages} ---` ) for (const track of tracks) { console.log(track.date?.uts, `${track.artist['#text']} - ${track.name}`) } if (response.data.recenttracks['@attr'].totalPages <= 1) { break // it's the last page! } const lastTrackInPage = tracks[tracks.length - 1] to = lastTrackInPage.date.uts } loige 35
  • 69. let to while (true) { const query = querystring.stringify({ method: 'user.getrecenttracks', user: 'loige', api_key: process.env.API_KEY, format: 'json', limit: '10', to }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) const tracks = response.data.recenttracks.track console.log( `--- ↓ page to ${to}`, `remaining pages: ${response.data.recenttracks['@attr'].totalPages} ---` ) for (const track of tracks) { console.log(track.date?.uts, `${track.artist['#text']} - ${track.name}`) } if (response.data.recenttracks['@attr'].totalPages <= 1) { break // it's the last page! } const lastTrackInPage = tracks[tracks.length - 1] to = lastTrackInPage.date.uts } loige 35
  • 70. let to while (true) { const query = querystring.stringify({ method: 'user.getrecenttracks', user: 'loige', api_key: process.env.API_KEY, format: 'json', limit: '10', to }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) const tracks = response.data.recenttracks.track console.log( `--- ↓ page to ${to}`, `remaining pages: ${response.data.recenttracks['@attr'].totalPages} ---` ) for (const track of tracks) { console.log(track.date?.uts, `${track.artist['#text']} - ${track.name}`) } if (response.data.recenttracks['@attr'].totalPages <= 1) { break // it's the last page! } const lastTrackInPage = tracks[tracks.length - 1] to = lastTrackInPage.date.uts } loige 35
  • 72. loige The track of the last timestamp becomes the boundary for the next page 36
  • 73. We have a working solution! 🎉 Can we generalise it? loige 37
  • 74. We know how to iterate over every page/track. How do we expose this information? loige 38
  • 75. const reader = LastFmRecentTracks({ apikey: process.env.API_KEY, user: 'loige' }) // callbacks reader.readPages( (page) => { /* ... */ }, // on page (err) => { /* ... */} // on completion (or error) ) loige 39
  • 76. const reader = LastFmRecentTracks({ apikey: process.env.API_KEY, user: 'loige' }) // event emitter reader.read() reader.on('page', (page) => { /* ... */ }) reader.on('completed', (err) => { /* ... */ }) loige 40
  • 77. const reader = LastFmRecentTracks({ apikey: process.env.API_KEY, user: 'loige' }) // streams <3 reader.pipe(/* transform or writable stream here */) reader.on('end', () => { /* ... */ }) reader.on('error', () => { /* ... */ }) loige 41
  • 78. import { pipeline } from 'stream' const reader = LastFmRecentTracks({ apikey: process.env.API_KEY, user: 'loige' }) // streams pipeline <3 <3 pipeline( reader, yourProcessingStream, (err) => { // handle completion or err } ) loige 42
  • 79. const reader = LastFmRecentTracks({ apikey: process.env.API_KEY, user: 'loige' }) // ASYNC ITERATORS! for await (const page of reader) { /* ... */ } // ... do more stuff when all the data is consumed loige 43
  • 80. const reader = LastFmRecentTracks({ apikey: process.env.API_KEY, user: 'loige' }) // ASYNC ITERATORS WITH ERROR HANDLING! try { for await (const page of reader) { /* ... */ } } catch (err) { // handle errors } // ... do more stuff when all the data is consumed loige 44
  • 81. How can we build an async iterator? 🧐 loige 45
  • 82. Meet the iteration protocols! loige loige.co/javascript-iterator-patterns 46
  • 83. The iterator protocol An object is an iterator if it has a next() method. Every time you call it, it returns an object with the keys done (boolean) and value. loige 47
  • 84. function createCountdown (from) { let nextVal = from return { next () { if (nextVal < 0) { return { done: true } } return { done: false, value: nextVal-- } } } } loige 48
  • 85. function createCountdown (from) { let nextVal = from return { next () { if (nextVal < 0) { return { done: true } } return { done: false, value: nextVal-- } } } } loige 48
  • 86. function createCountdown (from) { let nextVal = from return { next () { if (nextVal < 0) { return { done: true } } return { done: false, value: nextVal-- } } } } loige 48
  • 87. function createCountdown (from) { let nextVal = from return { next () { if (nextVal < 0) { return { done: true } } return { done: false, value: nextVal-- } } } } loige 48
  • 88. function createCountdown (from) { let nextVal = from return { next () { if (nextVal < 0) { return { done: true } } return { done: false, value: nextVal-- } } } } loige 48
  • 89. const countdown = createCountdown(3) console.log(countdown.next()) // { done: false, value: 3 } console.log(countdown.next()) // { done: false, value: 2 } console.log(countdown.next()) // { done: false, value: 1 } console.log(countdown.next()) // { done: false, value: 0 } console.log(countdown.next()) // { done: true } loige 49
  • 91. function * createCountdown (from) { for (let i = from; i >= 0; i--) { yield i } } loige 51
  • 92. function * createCountdown (from) { for (let i = from; i >= 0; i--) { yield i } } loige 51
  • 93. function * createCountdown (from) { for (let i = from; i >= 0; i--) { yield i } } loige 51
  • 94. const countdown = createCountdown(3) console.log(countdown.next()) // { done: false, value: 3 } console.log(countdown.next()) // { done: false, value: 2 } console.log(countdown.next()) // { done: false, value: 1 } console.log(countdown.next()) // { done: false, value: 0 } console.log(countdown.next()) // { done: true, value: undefined } loige 52
  • 95. The iterable protocol An object is iterable if it implements the @@iterator* method, a zero-argument function that returns an iterator. loige *Symbol.iterator 53
  • 96. function createCountdown (from) { let nextVal = from return { [Symbol.iterator]: () => ({ next () { if (nextVal < 0) { return { done: true } } return { done: false, value: nextVal-- } } }) } } loige 54
  • 97. function createCountdown (from) { let nextVal = from return { [Symbol.iterator]: () => ({ next () { if (nextVal < 0) { return { done: true } } return { done: false, value: nextVal-- } } }) } } loige 54
  • 98. function createCountdown (from) { let nextVal = from return { [Symbol.iterator]: () => ({ next () { if (nextVal < 0) { return { done: true } } return { done: false, value: nextVal-- } } }) } } loige 54
  • 99. function createCountdown (from) { return { [Symbol.iterator]: function * () { for (let i = from; i >= 0; i--) { yield i } } } } loige 55
  • 100. const countdown = createCountdown(3) for (const value of countdown) { console.log(value) } // 3 // 2 // 1 // 0 loige 56
  • 101. OK. So far this is all synchronous iteration. What about async? 🙄 loige 57
  • 102. The async iterator protocol An object is an async iterator if it has a next() method. Every time you call it, it returns a promise that resolves to an object with the keys done (boolean) and value. loige 58
  • 103. import { setTimeout } from 'timers/promises' function createAsyncCountdown (from, delay = 1000) { let nextVal = from return { async next () { await setTimeout(delay) if (nextVal < 0) { return { done: true } } return { done: false, value: nextVal-- } } } } loige 59
  • 104. import { setTimeout } from 'timers/promises' function createAsyncCountdown (from, delay = 1000) { let nextVal = from return { async next () { await setTimeout(delay) if (nextVal < 0) { return { done: true } } return { done: false, value: nextVal-- } } } } loige 59
  • 105. import { setTimeout } from 'timers/promises' function createAsyncCountdown (from, delay = 1000) { let nextVal = from return { async next () { await setTimeout(delay) if (nextVal < 0) { return { done: true } } return { done: false, value: nextVal-- } } } } loige 59
  • 106. const countdown = createAsyncCountdown(3) console.log(await countdown.next()) // { done: false, value: 3 } console.log(await countdown.next()) // { done: false, value: 2 } console.log(await countdown.next()) // { done: false, value: 1 } console.log(await countdown.next()) // { done: false, value: 0 } console.log(await countdown.next()) // { done: true } loige 60
  • 107. const countdown = createAsyncCountdown(3) console.log(await countdown.next()) // { done: false, value: 3 } console.log(await countdown.next()) // { done: false, value: 2 } console.log(await countdown.next()) // { done: false, value: 1 } console.log(await countdown.next()) // { done: false, value: 0 } console.log(await countdown.next()) // { done: true } loige 60
  • 109. import { setTimeout } from 'timers/promises' // async generators "produce" async iterators! async function * createAsyncCountdown (from, delay = 1000) { for (let i = from; i >= 0; i--) { await setTimeout(delay) yield i } } loige 62
  • 110. import { setTimeout } from 'timers/promises' // async generators "produce" async iterators! async function * createAsyncCountdown (from, delay = 1000) { for (let i = from; i >= 0; i--) { await setTimeout(delay) yield i } } loige 62
  • 111. import { setTimeout } from 'timers/promises' // async generators "produce" async iterators! async function * createAsyncCountdown (from, delay = 1000) { for (let i = from; i >= 0; i--) { await setTimeout(delay) yield i } } loige 62
  • 112. The async iterable protocol An object is an async iterable if it implements the @@asyncIterator* method, a zero-argument function that returns an async iterator. loige *Symbol.asyncIterator 63
  • 113. import { setTimeout } from 'timers/promises' function createAsyncCountdown (from, delay = 1000) { return { [Symbol.asyncIterator]: async function * () { for (let i = from; i >= 0; i--) { await setTimeout(delay) yield i } } } } loige 64
  • 114. import { setTimeout } from 'timers/promises' function createAsyncCountdown (from, delay = 1000) { return { [Symbol.asyncIterator]: async function * () { for (let i = from; i >= 0; i--) { await setTimeout(delay) yield i } } } } loige 64
  • 115. const countdown = createAsyncCountdown(3) for await (const value of countdown) { console.log(value) } loige 65
  • 116. const countdown = createAsyncCountdown(3) for await (const value of countdown) { console.log(value) } loige 65
  • 117. Now we know how to make our LastFmRecentTracks an Async Iterable 🤩 loige 66
  • 118. function createLastFmRecentTracks (apiKey, user) { return { [Symbol.asyncIterator]: async function * () { let to while (true) { const query = querystring.stringify({ method: 'user.getrecenttracks', user, api_key: apiKey, format: 'json', to }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) const tracks = response.data.recenttracks.track yield tracks if (response.data.recenttracks['@attr'].totalPages <= 1) { break // it's the last page! } const lastTrackInPage = tracks[tracks.length - 1] to = lastTrackInPage.date.uts } } } } loige 67
  • 119. function createLastFmRecentTracks (apiKey, user) { return { [Symbol.asyncIterator]: async function * () { let to while (true) { const query = querystring.stringify({ method: 'user.getrecenttracks', user, api_key: apiKey, format: 'json', to }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) const tracks = response.data.recenttracks.track yield tracks if (response.data.recenttracks['@attr'].totalPages <= 1) { break // it's the last page! } const lastTrackInPage = tracks[tracks.length - 1] to = lastTrackInPage.date.uts } } } } loige 67
  • 120. function createLastFmRecentTracks (apiKey, user) { return { [Symbol.asyncIterator]: async function * () { let to while (true) { const query = querystring.stringify({ method: 'user.getrecenttracks', user, api_key: apiKey, format: 'json', to }) const url = `https://ws.audioscrobbler.com/2.0/?${query}` const response = await axios.get(url) const tracks = response.data.recenttracks.track yield tracks if (response.data.recenttracks['@attr'].totalPages <= 1) { break // it's the last page! } const lastTrackInPage = tracks[tracks.length - 1] to = lastTrackInPage.date.uts } } } } loige 67
  • 121. const recentTracks = createLastFmRecentTracks( process.env.API_KEY, 'loige' ) for await (const page of recentTracks) { console.log(page) } loige 68
  • 122. Let's search for all the songs that contain the word "dark" in their title! 🧐 loige 69
  • 123. async function main () { const recentTracks = createLastFmRecentTracks( process.env.API_KEY, 'loige' ) for await (const page of recentTracks) { for (const track of page) { if (track.name.toLowerCase().includes('dark')) { console.log(`${track.artist['#text']} - ${track.name}`) } } } } loige 70
  • 125. loige OMG! This is the song! 😱 ...from 8 years ago! 71
  • 126. For a more serious package that allows you to fetch data from Last.fm: loige npm install scrobbles 72
  • 127. Cover picture by on Thanks to Jacek Spera, , , ,   for reviews and suggestions. Rod Long Unsplash @eoins @pelger @gbinside @ManuEomm    -   loige.link/async-it loige.link/async-it-code for await (const _ of createAsyncCountdown(1_000_000)) { console.log("THANK YOU! 😍") } loige nodejsdp.link 20% eBook discount on Packt 20NODEDUBLIN 73