Slide 1

Slide 1 text

Offline applications BOF OCTO CH - JVA@OCTO.COM - 20/07/2016

Slide 2

Slide 2 text

AT AW AD

Slide 3

Slide 3 text

AnyWhere ?

Slide 4

Slide 4 text

AnyWhere ?

Slide 5

Slide 5 text

AnyWhere ?

Slide 6

Slide 6 text

Offline applications 5 questions to ask before creating an offline application

Slide 7

Slide 7 text

Question #1 What can I do offline ?

Slide 8

Slide 8 text

READ

Slide 9

Slide 9 text

CREATE

Slide 10

Slide 10 text

UPDATE

Slide 11

Slide 11 text

UPDATE, SURE ?

Slide 12

Slide 12 text

DELETE

Slide 13

Slide 13 text

Question #2 How much data is it and where can i store it ?

Slide 14

Slide 14 text

Few kilobytes…

Slide 15

Slide 15 text

Few megabytes Hundred of megabytes (maybe few giga)

Slide 16

Slide 16 text

Several gigabytes ?

Slide 17

Slide 17 text

Storage Solutions

Slide 18

Slide 18 text

Application Cache ... CACHE MANIFEST # Explicitly cached CACHE: /favicon.ico page.html stylesheet.css images/logo.png scripts/main.js http://cdn.example.com/scripts/main.js # Resources that require the user to be online. NETWORK: * # static.html will be served if main.py is inaccessible # offline.jpg will be served in place of all images in images/large/ FALLBACK: /main.py /static.html images/large/ images/offline.jpg cache.manifest index.html http://www.html5rocks.com/en/tutorials/appcache/beginner/ http://alistapart.com/article/application-cache-is-a-douchebag

Slide 19

Slide 19 text

Application Cache ... CACHE MANIFEST # Explicitly cached CACHE: /favicon.ico page.html stylesheet.css images/logo.png scripts/main.js http://cdn.example.com/scripts/main.js # Resources that require the user to be online. NETWORK: * # static.html will be served if main.py is inaccessible # offline.jpg will be served in place of all images in images/large/ FALLBACK: /main.py /static.html images/large/ images/offline.jpg cache.manifest index.html http://www.html5rocks.com/en/tutorials/appcache/beginner/ http://alistapart.com/article/application-cache-is-a-douchebag

Slide 20

Slide 20 text

Service Workers (Cache API) this.addEventListener('install', function(event) { event.waitUntil( caches.open('v1').then(function(cache) { return cache.addAll([ '/sw-test/', '/sw-test/index.html', '/sw-test/style.css', '/sw-test/app.js', '/sw-test/star-wars-logo.jpg', '/sw-test/gallery/', '/sw-test/gallery/myLittleVader.jpg' ]); }) ); }); 2. Installation of Service Worker if ('serviceWorker' in navigator) { navigator.serviceWorker.register(‘/sw.js') .then(function(registration) { // Registration was successful }).catch(function(err) { // registration failed :( }); } 1. Registration of Service Worker self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { // Cache hit - return response if (response) { return response; } var fetchRequest = event.request.clone(); return fetch(fetchRequest).then( function(response) { if (!response || response.status !== 200) { return response; } var responseToCache = response.clone(); caches.open('v1').then(function(cache) { cache.put(event.request, responseToCache); }); return response; } ); }) ); }); 3 . Fetch and Cache requests

Slide 21

Slide 21 text

Service Workers (Cache API) 44+ 40+ https://jakearchibald.github.io/isserviceworkerready/ 27+

Slide 22

Slide 22 text

Web storage (local / session) if (('localStorage' in window) && window['localStorage'] !== null) { localStorage.setItem(key, value); } if (key in localStorage) { var value = localStorage.getItem(key); } 1. Store data 2. Retrieve data if (key in localStorage) { localStorage.removeItem(key); } localStorage.clear(); 3. Remove data / clear

Slide 23

Slide 23 text

Web SQL var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024); var msg; db.transaction(function (tx) { tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique, log)'); tx.executeSql('INSERT INTO LOGS (id, log) VALUES (1, "foobar")'); tx.executeSql('INSERT INTO LOGS (id, log) VALUES (2, "logmsg")'); msg = '

Log message created and row inserted.

'; document.querySelector('#status').innerHTML = msg; }); db.transaction(function (tx) { tx.executeSql('SELECT * FROM LOGS', [], function (tx, results) { var len = results.rows.length, i; msg = "

Found rows: " + len + "

"; document.querySelector('#status').innerHTML += msg; for (i = 0; i < len; i++) { msg = "

" + results.rows.item(i).log + "

"; document.querySelector('#status').innerHTML += msg; } }, null); });

Slide 24

Slide 24 text

var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024); var msg; db.transaction(function (tx) { tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique, log)'); tx.executeSql('INSERT INTO LOGS (id, log) VALUES (1, "foobar")'); tx.executeSql('INSERT INTO LOGS (id, log) VALUES (2, "logmsg")'); msg = '

Log message created and row inserted.

'; document.querySelector('#status').innerHTML = msg; }); db.transaction(function (tx) { tx.executeSql('SELECT * FROM LOGS', [], function (tx, results) { var len = results.rows.length, i; msg = "

Found rows: " + len + "

"; document.querySelector('#status').innerHTML += msg; for (i = 0; i < len; i++) { msg = "

" + results.rows.item(i).log + "

"; document.querySelector('#status').innerHTML += msg; } }, null); }); Web SQL

Slide 25

Slide 25 text

function onInitFs(fs) { fs.root.getFile('log.txt', {}, function(fileEntry) { // Get a File object representing the file, // then use FileReader to read its contents. fileEntry.file(function(file) { var reader = new FileReader(); reader.onloadend = function(e) { var txtArea = document.createElement('textarea'); txtArea.value = this.result; document.body.appendChild(txtArea); }; reader.readAsText(file); }, errorHandler); }, errorHandler); } window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler); FileSystem API

Slide 26

Slide 26 text

function onInitFs(fs) { fs.root.getFile('log.txt', {}, function(fileEntry) { // Get a File object representing the file, // then use FileReader to read its contents. fileEntry.file(function(file) { var reader = new FileReader(); reader.onloadend = function(e) { var txtArea = document.createElement('textarea'); txtArea.value = this.result; document.body.appendChild(txtArea); }; reader.readAsText(file); }, errorHandler); }, errorHandler); } window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler); FileSystem API

Slide 27

Slide 27 text

IndexedDB var db; function openDb() { var req = indexedDB.open(DB_NAME, DB_VERSION); req.onsuccess = function (evt) { db = this.result; }; req.onerror = function (evt) { console.error("openDb:", evt.target.errorCode); }; req.onupgradeneeded = function (evt) { var store = evt.currentTarget.result.createObjectStore( DB_STORE_NAME, { keyPath: 'id', autoIncrement: true }); store.createIndex('title', 'title', { unique: false }); store.createIndex('isbn', 'isbn', { unique: true }); }; } 1. Open Database

Slide 28

Slide 28 text

IndexedDB var var tx = db.transaction(DB_STORE_NAME, 'readwrite'); var store = tx.objectStore(DB_STORE_NAME); var obj = { isbn: ‘0062316095’, title: ‘Sapiens: A Brief History of Humankind’, year: 2015 }; var req; try { req = store.add(obj); } catch (e) { // ... } req.onsuccess = function (evt) { console.log("Insertion in DB successful"); // ... }; req.onerror = function() { console.error("Insert error", this.error); // ... }; 2. Insert data

Slide 29

Slide 29 text

IndexedDB var var tx = db.transaction(DB_STORE_NAME, 'readonly'); var store = tx.objectStore(DB_STORE_NAME); var req = store.openCursor(); req.onsuccess = function (evt) { var cursor = evt.target.result; if (cursor) { alert(cursor.value.title); cursor.continue(); } }; 3. Retrieve data (cursor) var var tx = db.transaction(DB_STORE_NAME, 'readonly'); var store = tx.objectStore(DB_STORE_NAME); var req = store.get(42); req.onsuccess = function (evt) { var object = evt.target.result; alert(object.title); }; 3. Retrieve data (one item)

Slide 30

Slide 30 text

IndexedDB var var tx = db.transaction(DB_STORE_NAME, 'readonly'); var store = tx.objectStore(DB_STORE_NAME); var index = store.index(‘title’); var req = index.get(‘Sapiens: A Brief History of Humankind’); req.onsuccess = function (evt) { var result = evt.target.result; if (result) { // ... } }; 3. Retrieve data (index)

Slide 31

Slide 31 text

IndexedDB wrappers • db.js • joqular • TaffyDB • localForage • IDBWrapper • YDN

Slide 32

Slide 32 text

IndexedDB 16+ 24+ 15+ 10+ 8+ 4.4+

Slide 33

Slide 33 text

Google Gears

Slide 34

Slide 34 text

HTML 5 Storage Limitations

Slide 35

Slide 35 text

Quotas 50 % 33 % 20 % 20 % Free disk space Space browser can use Space application can use

Slide 36

Slide 36 text

Quotas

Slide 37

Slide 37 text

Users

Slide 38

Slide 38 text

Users

Slide 39

Slide 39 text

Question #3 How to handle offline-online synchronization ?

Slide 40

Slide 40 text

CONFLICTS

Slide 41

Slide 41 text

Basic Resolution : based on timestamp « Last version win »

Slide 42

Slide 42 text

Optimistic lock Source : Patterns of Enterprise Application Architecture - Martin Fowler System transaction boundaries Business transaction boundaries

Slide 43

Slide 43 text

Pessimistic lock Source : Patterns of Enterprise Application Architecture - Martin Fowler System transaction boundaries Business transaction boundaries

Slide 44

Slide 44 text

Theory is when you know everything but nothing works. Practice is when everything works but no one knows why. In our lab, theory and practice are combined: nothing works and no one knows why!

Slide 45

Slide 45 text

No content

Slide 46

Slide 46 text

kinto.js var db = new Kinto(); var todos = db.collection(‘todos’); todos.create({ title: ‘buy some bread’), finished : false }) .then(function(res){…}) .catch(function(err){…}) todos.list().then(function(res) { renderTodos(res.data); }) .catch(function(err) {…}); todos.update(todo) .then(function(res) {…}) .catch(function(err) {…}); Create, Read, Update, Delete using IndexedDB todos.delete(todo.id) .then(function(res) {…}) .catch(function(err) {…}); var syncOptions = { remote: "https://host/kintoapi", headers: {Authorization: …} }; todos.sync(syncOptions) .then(function(res){…}) .catch(function(err){…}) Synchronize with remote

Slide 47

Slide 47 text

kinto.js var syncOptions = { remote: "https://host/kintoapi", headers: {Authorization: …} }; todos.sync(syncOptions) .then(function(res){…}) .catch(function(err){…}) { "ok": true, "lastModified": 1434617181458, "errors": [], "created": [], // created locally "updated": [], // updated locally "deleted": [], // deleted locally "published": [ // published remotely { "last_modified": 1434617181458, "done": false, "id": "7ca54d89-479a-4201-8494", "title": "buy some bread", "_status": "synced" } ], "conflicts": [], "skipped": [] } { "ok": true, "lastModified": 1434617181458, "errors": [], "created": [], // created locally "updated": [], // updated locally "deleted": [], // deleted locally "published": [], // published remotely "conflicts": [ { "type": "incoming", // or outgoing "local": { "last_modified": 1434619634577, "done": true, "id": "7ca54d89-479a-4201-8494", "title": "buy some bread", "_status": "updated" }, "remote": { "last_modified": 1434619745465, "done": false, "id": "7ca54d89-479a-4201-8494", "title": "buy some bread and wine" } } ], "skipped": [] } OK Conflicts

Slide 48

Slide 48 text

kinto.js { "ok": true, "lastModified": 1434617181458, "errors": [], "created": [], // created locally "updated": [], // updated locally "deleted": [], // deleted locally "published": [], // published remotely "conflicts": [ { "type": "incoming", // or outgoing "local": { "last_modified": 1434619634577, "done": true, "id": "7ca54d89-479a-4201-8494", "title": "buy some bread", "_status": "updated" }, "remote": { "last_modified": 1434619745465, "done": false, "id": "7ca54d89-479a-4201-8494", "title": "buy some bread and wine" } } ], "skipped": [] } Conflicts todos.sync(syncOptions) .then(function(res){ if (res.conflicts.length) { return handleConflicts(res.conflicts); } }) .catch(function(err){…}); function handleConflicts(conflicts) { return Promise.all(conflicts.map(function(conflict) { return todos.resolve(conflict, conflict.remote); })) .then(function() { todos.sync(syncOptions); }); } Choose your way to solve the conflict: • Choose remote or local version • Choose according last_modified • Pick the good fields (need to provide 3-way-merge screen)

Slide 49

Slide 49 text

var db = new PouchDB(‘todos’); db.post({ // can use ‘put’ with an _id title: ‘buy some bread’), finished : false }) .then(function(res){…}) .catch(function(err){…}) db.get(‘mysuperid’).then(function(todo) { // return an object with auto // generated ‘_rev’ field // update the full doc (with _rev) todo.finished = true; db.put(todo); // remove the full doc (with _rev) db.remove(todo); }) .catch(function(err) {…}); Create, Read, Update, Delete using IndexedDB var localDB = new PouchDB(‘todos’); // Remote CouchDB var remoteDB = new PouchDB(‘http://host/todos’); localDB.replicate.to(remoteDB); localDB.replicate.from(remoteDB); // or localDB.sync(remoteDB, { live: true, retry: true }).on('change', function (change) { // something changed! }).on('paused', function (info) { // replication was paused, // usually because of a lost connection }).on('active', function (info) { // replication was resumed }).on('error', function (err) { // unhandled error (shouldn't happen) }); Synchronize with remote

Slide 50

Slide 50 text

var myDoc = { _id: 'someid', _rev: '1-somerev' }; db.put(myDoc).then(function () { // success }).catch(function (err) { if (err.name === 'conflict') { // conflict! Handle it! } else { // some other error } }); Immediate conflict : error 409 _rev: ‘1-revabc’ _rev: ‘1-revabc’ _rev: ‘2-revcde’ _rev: ‘2-revjkl’ _rev: ‘1-revabc’ _rev: ‘2-revjkl’ _rev: ‘2-revcde’ db.get('someid', {conflicts: true}) .then(function (doc) { // do something with the object }).catch(function (err) { // handle any errors }); { "_id": "someid", "_rev": "2-revjkl", "_conflicts": ["2-revcde"] } ==> Eventual conflict ==> remove the bad one, merge, … it’s up to you

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

Question #4 How to communicate with users ?

Slide 53

Slide 53 text

Inform the user … Save Save locally Send Send when online

Slide 54

Slide 54 text

… or not Outbox (1) Send

Slide 55

Slide 55 text

Do no display errors !

Slide 56

Slide 56 text

Do not load indefinitelyyyyyyyyyy

Slide 57

Slide 57 text

Do not display an empty screen

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

Handling conflicts

Slide 60

Slide 60 text

Question #5 Do I really need offline ?

Slide 61

Slide 61 text

(2001) (2009) (2020)

Slide 62

Slide 62 text

No content

Slide 63

Slide 63 text

« You are not on a f*cking plane and if you are, it doesn’t matter » - David Heinemeier Hansson (2007) https://signalvnoise.com/posts/347-youre-not-on-a-fucking-plane-and-if-you-are-it-doesnt-matter

Slide 64

Slide 64 text

ATAWAD Unfortunately NOT AnyWhere !

Slide 65

Slide 65 text

User Experience matters !

Slide 66

Slide 66 text

Thank you

Slide 67

Slide 67 text

Bibliography • http://diveintohtml5.info/offline.html • https://github.com/pazguille/offline-first • https://jakearchibald.com/2014/offline-cookbook/ • https://github.com/offlinefirst/research/blob/master/links.md • http://www.html5rocks.com/en/tutorials/offline/whats-offline/ • http://offlinefirst.org/ • http://fr.slideshare.net/MarcelKalveram/offline-first-the-painless-way • https://developer.mozilla.org/en-US/Apps/Fundamentals/Offline • https://uxdesign.cc/offline-93c2f8396124#.97njk8o5m • https://www.ibm.com/developerworks/community/blogs/worklight/entry/ offline_patterns?lang=en • http://apress.jensimmons.com/v5/pro-html5-programming/ch12.html • http://alistapart.com/article/offline-first • http://alistapart.com/article/application-cache-is-a-douchebag • https://logbook.hanno.co/offline-first-matters-developers-know/ • https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/ Using_Service_Workers • https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API • https://developer.chrome.com/apps/offline_storage • http://martinfowler.com/eaaCatalog/index.html • http://offlinestat.es/ • http://caniuse.com/ Jake Archibald