source: www.flickr.com/photos/stephendann/80558500

Why Does Offline Matter?

  • In the US we generally have good connectivity, but not always
  • New markets for cloud apps
  • Globally connectivity varies
  • Unavailable servers

Why Does Offline Matter To CURE?

Hydrocephalus

The Problem: 29 countries

The Solution: "Offline First" EMR

Rules for Survival

Rules
  • Live off the grid in a fortified shelter
  • Store your Supplies
  • Have adequate weaponry
  • Have an escape plan... and follow it
source: www.flickr.com/photos/stephendann/80558500

Live off the grid: Application Cache

source: www.flickr.com/photos/jrimages/7688229244

Live off the grid: Application Cache

        <html manifest="manifest.appcache">
            
        CACHE MANIFEST
        #version 1.0
        
        CACHE:
        #files required for offline        
        /css/mycss.min.css
        /images/lol_cat.png
        /js/myjs.min.js
        
        NETWORK:
        #fetch everything else from the network when online
        *
            

Live off the grid: Application Cache

        <html manifest="manifest.appcache.php">
            
        <?php
        function autoVer($filename) {
            //return filename with modified timestamp
        }?>
        CACHE MANIFEST
        #version 1.0  <?php autoVer('js/app-all.js'); ?>

        CACHE:
        #files required for offline
        /css/mycss.min.css
        /images/lol_cat.png
        <?php autoVer('js/app-all.js'); ?>
                
        NETWORK:
        #fetch everything else from the network when online
        *
            

Application Cache Browser Support

source: caniuse.com/#feat=offline-apps
github.com/mastahyeti/browserstats

Store Supplies: IndexedDB

source: www.flickr.com/photos/24218656@N03/5934567119
ASYNC all the things

Store Supplies: IndexedDB

<script>
window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
var myDB, dbVersion = 1, openRequest = window.indexedDB.open('MyDatabase', dbVersion);
openRequest.onerror = function(errorEvent) {
    //Async response, something didn't work
}
openRequest.onsuccess = function(successEvent) {    
    myDB = successEvent.target.result; //Async response, grab the database from the request.
    doStuffAfterOpen(); //Database is ready for use
}
openRequest.onupgradeneeded = function(upgradeEvent) { //Version change!!
    myDB = upgradeEvent.target.result;
    //Define database structure
    var newObjectStore = myDB.createObjectStore('lolcats', { keyPath: 'id' });
    newObjectStore.createIndex('nameidx', 'name', { unique: true });
    newObjectStore.createIndex('categoryidx', 'category');
}
</script>
            

Store Supplies: IndexedDB

<script>
function doStuffAfterOpen() { //This will only work after the async call to open the DB    
    var transaction = myDB.transaction('lolcats','readwrite');
    var lolStore = transaction.objectStore('lolcats');
    var addRequest = lolStore.add({
        id: 1, name: 'Caturday', category: 'Lazy kitty',
        url: 'http://cheezburger.com/6677232128'
    });
    addRequest.onsuccess = function(successEvent) {//Async response, yes you can haz cheezburger
    };
    addRequest.onerror = function(errorEvent) {//Async response, no cheezburger for you
    };
    var getRequest = lolStore.get(1); //Get by id
    getRequest.onsuccess = function(successEvent) { //Async response        
        console.log("Funny kitty here: " + getRequest.result.url);
    };
    getRequest.onerror = function(errorEvent) {//Async response, no funny kitties here};
}
</script>
            

Store Supplies: IndexedDB

<script>
var index = lolStore.index('nameidx'),
    keyRange = IDBKeyRange.bound('C', 'H'), //Search for names between C - H
    idxRequest = index.openCursor(keyRange);

idxRequest.onsuccess = function(successEvent) { //Async response 
    var cursor = successEvent.target.result;
    if (cursor) {
        console.log("Using key range C - H, Found kitty with name of:"+cursor.key);                  
        cursor["continue"]();
    }
};
index = lolStore.index('categoryidx');
keyRange = IDBKeyRange.only('Cat beard'); //Search for specific index value of Cat beard
idxRequest = index.openCursor(keyRange);
idxRequest.onsuccess = function(successEvent) {//Found cat beards!!!!};
</script>
            

Store Supplies: IndexedDB

Chrome Developer Tools

IndexedDB Browser Support

source: http://caniuse.com/#feat=indexeddb
github.com/mastahyeti/browserstats

IndexedDB Browser Support with Polyfill

source: http://caniuse.com/#feat=indexeddb
github.com/mastahyeti/browserstats

Adequate Weaponry: FileSystem API

source: www.flickr.com/photos/hyperxp/7473652752/

Adequate Weaponry: FileSystem API

var fileSystem, size = 1024*1024; //1MB
function handleError(errorEvent) { 
    console.log('Error with filesystem API'); //Async error handler
}
function openFileSystem() {
    window.requestFileSystem(PERSISTENT, size, handleOpenFileSystem, handleError);
}
function handleOpenFileSystem(newFileSystem) {
    fileSystem = newFileSystem;
    //Save file from <input type="file" name="fileinput" id="fileinput">
    var fileToSave = document.querySelector('#fileinput').files[0];
    fileSystem.root.getFile(fileToSave.name, {create: true, exclusive: true}, function(fileEntry) {        
        fileEntry.createWriter(function(fileWriter) {
            fileWriter.write(fileToSave);
            console.log("file available at:"+fileEntry.toURL());
        }, handleError);
    }, handleError);    
}
navigator.webkitPersistentStorage.requestQuota(PERSISTENT, size, openFileSystem, handleError);
            

Adequate Weaponry: FileSystem API

<script>
function getFileAsDataURL(fileSystem) {
  //First get the file entry representing the call (async response)
  fileSystem.root.getFile('lol_cat.jpg', {}, function(fileEntry) {
    //Next get the actual file (async response)      
    fileEntry.file(function(fileToRead) {
        //Next read the file as dataURL(async response)      
       var reader = new FileReader()
       reader.onloadend = function(readerEvent) {           
         var dataURL = readerEvent.target.result;         
       };
       reader.readAsDataURL(fileToRead);
    }, handleError);
  }, handleError);
}
window.requestFileSystem(PERSISTENT, size, getFileAsDataURL, handleError);
</script>
            

FileSystem Browser Support

source: caniuse.com/#feat=offline-apps
github.com/mastahyeti/browserstats

FileSystem Browser Support: Polyfill

FileSystem Browser Support: Polyfill

An Escape Plan

  • Trust no one
  • Know your surroundings
  • Monitor your exits
  • Think on your feet
source: www.flickr.com/photos/impactmatt/5158159260

An Escape Plan: navigator.onLine

Trust No One
  • window.navigator.onLine

    "Returns false if the user agent is
    definitely offline (disconnected from the
    network). Returns true if the user agent
    might be online.
    The events online and
    offline are fired when the value of this
    attribute changes."

  • *Might* be online??
  • XHR is our friend
  • window.online event gives a hint of
    online availability

An Escape Plan: navigator.onLine

//Initialize connection status to navigator.onLine property.
var onlineStatus = navigator.onLine;
$( window ).on( "online", function( event ) {
  onlineStatus = true;  //Persist offline data to the server
});
$( window ).on( "offline", function( event ) {
  onlineStatus = false;
});
$.ajax({
  cache: false, dataType: "json", url: "/save", timeout: 500, type: "POST",
  data: {"foo": "bar"},
  error: function( jqXHR, textStatus, errorThrown ) {         
    onlineStatus = false; //The ajax call failed, therefore the server is offline to us.
  },
  success: function( data, textStatus, jqXHR ) {    
    onlineStatus = true; //The ajax call succeeded, therefore the server is online to us. 
  }
});
            

An Escape Plan: New Relic

source: www.flickr.com/photos/samsamcardiff/7326744462/

An Escape Plan: New Relic Browser Traces

source: www.flickr.com/photos/samsamcardiff/7326744462/

An Escape Plan: New Relic Key Transactions

source: rpm.newrelic.com

An Escape Plan: Think on Your Feet

BEFORE:
[31-Jul-2013 12:48:19 UTC] Time to sync for Zambia was: 21.884245872498
[31-Jul-2013 12:49:42 UTC] Time to sync for Zambia was: 78.699893951416
[31-Jul-2013 12:50:00 UTC] Time to sync for Zambia was: 15.489302873611

AFTER:
[31-Jul-2013 13:45:53 UTC] Time to sync for Zambia was: 7.3927249908447
[31-Jul-2013 13:45:59 UTC] Time to sync for Zambia was: 3.7005159854889
[31-Jul-2013 13:46:01 UTC] Time to sync for Zambia was: 1.2956280708313            
            

Thank You!