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

Serving Data From Browsers

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

Serving Data From Browsers

In this talk, we'll cover how service workers can be used to gather, process & generate content in the browser. I'll show a practical use case of using this to drive web-based visualisation of geo-location data.

Avatar for Ben Foxall

Ben Foxall

April 27, 2016
Tweet

More Decks by Ben Foxall

Other Decks in Technology

Transcript

  1. WHAT WE’RE TALKING ABOUT: MY DATA ➡ ‘ AS A

    USER, I WANT TO HAVE MY DATA IN A CSV DOCUMENT
  2. export BEARER=MY_TOKEN_FROM_THE_CONSOLE curl https://api.runkeeper.com/fitnessActivities?pageSize=100 -H "Authorization: Bearer $BEARER" > page1.json

    curl https://api.runkeeper.com/fitnessActivities?pageSize=100&page=2 -H "Authorization: Bearer $BEARER" > page2.json curl https://api.runkeeper.com/fitnessActivities?pageSize=100&page=3 -H "Authorization: Bearer $BEARER" > page3.json curl https://api.runkeeper.com/fitnessActivities?pageSize=100&page=4 -H "Authorization: Bearer $BEARER" > page4.json curl https://api.runkeeper.com/fitnessActivities?pageSize=100&page=5 -H "Authorization: Bearer $BEARER" > page5.json jq '.items' page*.json | grep uri | sed 's/^.*"\///g' | sed 's/"//g' > urls.txt mkdir fitnessActivities while read p; do curl https://api.runkeeper.com/$p -H "Authorization: Bearer $BEARER" > $p.json done < urls.txt
  3. A ONE FUNCTION JAVASCRIPT API const lastFM = params =>

    fetch("http://ws.audioscrobbler.com/2.0/?"+ "method=user.getrecenttracks&format=json"+ "&api_key=974a5ebc077564f72bd639d122479d4b&"+ params) .then(r => r.json())
  4. WHAT REQUESTS DO WE NEED FOR A USER? const requestsForUser

    = username => lastFM(`user=${username}`) .then(_find_number_of_pages_) .then(number => Array.from({length: number - 1}, (_,i) => `user=${username}&page=${i+1}` ) )
  5. WHAT REQUESTS DO WE NEED FOR A USER? requestsForUser("benjaminf") //

    resolves to: [ "user=benjaminf&page=1", "user=benjaminf&page=2", "user=benjaminf&page=3", "user=benjaminf&page=4", … ]
  6. …ANOTHER WAY // pull out a row of keys const

    row = (keys, obj) => keys.map(k => obj[k]) // create a csv row from an array const csv = array => array.map(item => typeof(item) === 'string' ? item.replace(/[\",]/g,'') : item ).join(',') + "\n" /* csv(row(["a","b","n"],{b: "\"bar\"", c: "yeah", a: "foo", n: 42}) > "foo,bar,42" */
  7. ALL TOGETHER const process = response => response.tracks.map(track => csv(track,

    ["artist", "name"]) ) const dataForUser = username => requestsForUser(username) .then(requests => Promise.all( requests.map(lastFM) ) .then(parts => parts.join("")) )
  8. ALL TOGETHER dataForUser("benjaminf") .then( csv => { const link =

    document.createElement("a") link.href = _to_data_uri(csv, "text/csv") link.innerText = "Download csv" document.body.appendChild(link) })
  9. const blob = new Blob(["a,b,c\n", "d,e,f"],{type : "text/csv"}) const url

    = window.URL.createObjectURL(blob) // > "blob:…/41f8da4d-…-dbdc62106843" const a = document.createElement("a") a.textContent = a.href = url document.body.appendChild(a)
  10. const process = response => response.tracks.map(track => csv(track, ["artist", "name"])

    ) const dataForUser = username => requestsForUser(username) .then(requests => Promise.all( requests.map( req => lastFM(req).then(process) ) ) .then(parts => parts.join("")) )
  11. const process = response => new Blob(response.tracks.map(track => csv(track, ["artist",

    "name"]) )) const dataForUser = username => requestsForUser(username) .then(requests => Promise.all( requests.map( req => lastFM(req).then(process) ) ) .then(parts => new Blob(parts, {type : "text/csv"})) )
  12. dataForUser("benjaminf") .then( csv => { const link = document.createElement("a") link.href

    = _to_data_uri(csv, "text/csv") link.innerText = "Download csv" document.body.appendChild(link) })
  13. dataForUser("benjaminf") .then( csv => { const link = document.createElement("a") link.href

    = URL.createObjectURL(csv) link.innerText = "Download csv" document.body.appendChild(link) })
  14. 1. REQUEST > 2 SAVE > 3. PROCESS > 4.

    CACHE > 5. SERVE* (RUNKEEPER)
  15. IndexedDB (via Dexie) var db = new Dexie("Runkeeper") db.version(1).stores({ activities:

    "&uri" }) db.open(); // on request fetch('data' + uri, { credentials: 'include' }) .then(res => res.json() ) .then(data => db.activities.put(data))
  16. SUMMARY RESPONSE function summaryResponse() { console.time('build summary') var keys =

    ['uri', 'duration', 'type', 'start_time', 'total_distance', 'climb', 'total_calories']; var rows = [keys]; return db .activities .reverse() .each(function(activity){ rows.push( row(keys, activity) ) }) .then(function(){ console.timeEnd('build summary'); var data = new Blob(rows.map(function(row){ return csv(row) + '\n'; })); return new Response( data, { headers: { 'Content-Type': 'text/csv' } }); }) }
  17. function respond(event, generator){ event.respondWith( caches.match(event.request) .then(cached => { if(cached) return

    cached else return respondAndCache() }) ) function respondAndCache(){ return generator() .then((response) => { var responseToCache = response.clone(); caches.open(CACHE_NAME) .then((cache) => cache.put(event.request, responseToCache); ) return response; }) } }
  18. WIN: MULTIPLE VIEWS self.addEventListener('fetch', event => { if(event.request.url.match(/sw\/summary\.csv$/)) respond(event, summaryResponse)

    if(event.request.url.match(/sw\/distances\.csv$/)) respond(event, distancesResponse) if(event.request.url.match(/sw\/paths\.csv$/)) respond(event, pathsResponse) if(event.request.url.match(/sw\/geo\.json$/)) respond(event, geoJSONResponse) if(event.request.url.match(/sw\/geo\.simple\.json$/)) respond(event, geoJSONResponseSimple) if(event.request.url.match(/sw\/binary\.path\.b$/)) respond(event, binaryPathResponse) })