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

Serving Data From Browsers

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.

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) })