Softshake - Offline applications

Preview:

Citation preview

Offline applications

Jérôme Van Der Linden - 28/10/2016

AT AW AD

AnyWhere ?

AnyWhere ?

AnyWhere ?

Offline applications 5 questions to ask before creating an offline application

Question #1 What can I do offline ?

READ

CREATE

UPDATE

UPDATE, SURE ?

DELETE

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

Few kilobytes…

Few megabytes

Hundred of megabytes

(maybe few giga)

Several gigabytes (or many more) ?

Storage Solutions

Application Cache<html manifest="/cache.manifest"> ... </html>

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

Application Cache<html manifest="/cache.manifest"> ... </html>

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

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

Service Workers (Cache API)

44+40+

https://jakearchibald.github.io/isserviceworkerready/

27+

Future of upcoming web development ?

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

Web SQLvar 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 = '<p>Log message created and row inserted.</p>'; document.querySelector('#status').innerHTML = msg; });

db.transaction(function (tx) { tx.executeSql('SELECT * FROM LOGS', [], function (tx, results) { var len = results.rows.length, i; msg = "<p>Found rows: " + len + "</p>"; document.querySelector('#status').innerHTML += msg; for (i = 0; i < len; i++) { msg = "<p><b>" + results.rows.item(i).log + "</b></p>";

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

}, null); });

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 = '<p>Log message created and row inserted.</p>'; document.querySelector('#status').innerHTML = msg; });

db.transaction(function (tx) { tx.executeSql('SELECT * FROM LOGS', [], function (tx, results) { var len = results.rows.length, i; msg = "<p>Found rows: " + len + "</p>"; document.querySelector('#status').innerHTML += msg; for (i = 0; i < len; i++) { msg = "<p><b>" + results.rows.item(i).log + "</b></p>";

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

}, null); });

Web SQL

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

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

IndexedDBvar 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

IndexedDB

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

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

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)

IndexedDB wrappers

• db.js • joqular • TaffyDB

• localForage • IDBWrapper • YDN

IndexedDB

16+24+ 15+ 10+

8+ 4.4+

Google Gears

HTML 5 Storage Limitations

Quotas

50 %

33 %

20 %

20 %

Free disk space

Space browser can use

Space application (domain) can use

Quotas

Users

https://storage.spec.whatwg.org/ https://developers.google.com/web/updates/2016/06/persistent-storage

if (navigator.storage && navigator.storage.persist) navigator.storage.persist().then(granted => { if (granted) alert("Storage will not be cleared except by explicit user action"); else alert("Storage may be cleared by the UA under storage pressure."); });

if (navigator.storage && navigator.storage.persist) navigator.storage.persisted().then(persistent=>{ if (persistent) console.log("Storage will not be cleared except by explicit user action"); else console.log("Storage may be cleared by the UA under storage pressure."); });

Persistent storage

55+

Question #3 How to handle offline-online

synchronization ?

CONFLICTS

Basic Resolution : based on timestamp « Last version win »

Optimistic lock

Source : Patterns of Enterprise Application Architecture - Martin Fowler

System transaction boundaries

Business transaction boundaries

Pessimistic lock

Source : Patterns of Enterprise Application Architecture - Martin Fowler

System transaction boundaries

Business transaction boundaries

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!

kinto.jsvar 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

kinto.jsvar 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

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)

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

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

Question #4

How to communicate with users ?

Inform the user …

Save Save locally

Send Send when online

… or not

Outbox (1) Send

Do no display errors !

Do not load indefinitelyyyyyyyyyy

Do not display an empty screen

Handling conflicts

Question #5 Do I really need offline ?

(2001) (2009) (2020)

« 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

ATAWAD

Unfortunately NOT

AnyWhere !

User Experience matters !

Thank you

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