appcache/promises/indexeddb presentation 2014-09-20
DESCRIPTION
A brief overview of HTML Application Cache, Javascript Promises, and IndexedDB as they relate to the creation of an offline testing interface for an online testing web application. Some code samples and links to further reading.TRANSCRIPT
Appcache, Promises, and IndexedDB
examn is a web application for online testing. One day we decided it needed a way to work offline, too. This is a story about three of the tools that helped it get there.
Offline Testing Project Requirements
As much as possible, the interface must function offline.● Authenticate users.● Download tests to be taken later.● Upload responses for grading.● Enforce time limits and security codes for tests
that have them.● Be compatible with existing test questions.● Don’t expose questions to people not currently
taking a test.● Don’t expose answers at all.
The Tools We Chose
● Offline Page Loading: HTML5 Appcacheo Basically the only way to point a browser at a
webpage while offline and have it load.● Offline Data Storage: IndexedDB
o With WebSQL deprecated, this looks like the future of client-side databases.
● General Humanity: Promiseso IndexedDB is powerful but not user-friendly. I
made a wrapper for it, and the wrapper uses Promises.
Appcache Manifest
<html manifest=”path/to/manifest.file”>
Simple Sample
Appcache Life Cycle
Load page from cache.
Request manifest.
Update cache.
Request manifest.
Set status, fire event.
applicationCache.update();
(Listeners?)
Zzzz….
● The cached version of every file will be loaded first whether you’re online or not. You have to provide a way for the page to update itself when there are changes.
● You have to list every file. Every script, every stylesheet, every image, everything.o Even the ones you don’t want to make
available offline. (That’s what the NETWORK section is for.)
● Fallback resources don’t know why you got sent there.
● There’s no way to tell the browser which files need updating. Any manifest change == full cache download.
Manifest Gotchas
● Manifests can be generated on the fly. (Content-Type: text/cache-manifest)
● Use a version number or date in a comment to force cache updates.
● Any page that references the manifest is implicitly included.● Putting * in NETWORK reverses the default-deny behavior of
Appcache wrt non-cached files.● View your cached files at chrome://appcache-internals or
about:cache.
Manifest Tricks
Manifest Gotchas 2: Gotcha Harder● Don’t list the manifest in the manifest.● Don’t use wildcards in the CACHE section.● Server-side caching schemes that rely on changing urls (query
strings, etc) must be reflected in the manifest.● Manifests that change during load are treated as an error, so
don’t just put a timestamp in a comment.
Keeping The App Up to Date
● Check applicationCache.status on ready.o applicationCache.UPDATEREADY >>
need to reload.● Set an updateready listener.● Periodically call
applicationCache.update().● On updateready, inform the user there’s
an update and let them decide when to reload.o No modals!
● Also looks for .OBSOLETE.
examn.offline’s manifest.php
● Loop over a hardcoded array of file paths to be included.o Check file modification times.o CSS files are parsed looking for url() paths,
which are added to the end of the array.● Include the most recent file
modification time in the output as a comment.
(It’s also cached, based on the modify time of the site’s root folder, so we don’t have to do all of this every time.)
JavaScript Promises
Old and Busted:Request/Listener
New Hotness:Promises
Function returns a request object, listeners are added to that object.
Function returns a Promise object, listeners are added via method chaining.
Discussion question: Is the “Old and Busted”/”New Hotness” categorization scheme itself now Old and/or Busted? If so, what is the new New Hotness?
Promises Versus Requests
Request/Listener● Events can fire many times.● If the event has already
happened, the listener will not fire at time of assignment.
● Not chainable.● Arbitrarily long list of
fireable events.
Promise● Promises resolve only once.● Already-resolved promises
call the appropriate functions immediately.
● Chainable.● Outcomes limited to resolve
and reject.
Promise Chaining
If first() returns a Promise, the rest of the chain will be applied to it, as if you had writtenfirst().then(second)....
Otherwise second() will be called immediately with the return value of first().
Catch calls will receive any Promise rejections or uncaught exceptions from any upstream call.
promiseRequest(params).then(first).then(second).catch(err1).then(third).catch(err2).then(finally);
Basic Promise Creation
By convention, the parameter given to reject() should always be an Error.
Advanced Promise Topics
● Let pa be an array of Promises.o Promise.all(pa) rejects when any of pa
rejects, or resolves when all of pa have resolved.
o Promise.race(pa) resolves or rejects with the first of pa to resolve or reject.
● Promise.resolve(a) and Promise.reject(a) return Promises that always resolve or reject with a.o If a is a thenable, Promise.resolve(a)
instead casts it to a Promise.● getPromise().then(yay,boo) ==
getPromise().then(yay).catch(boo)
IndexedDB Vocabulary
Asynchronous. Object-oriented. Transactional. Key/Value.
domain
database
objectStore
key
key
key
key
object
object
object
object
index
key
key
key
key
value
value
value
value
cursors
ranges
IndexedDB Strengths/Weaknesses
IndexedDB is good at:● Polymorphism● Transactions● Error handling● Queries that can
be expressed as a range on an index
IndexedDB is bad at*:● Encouraging
readable code● Joins● Other kinds of
queries
*: completely incapable of
IndexedDB Creation ExampleAside from success and failure, indexedDB.open() can fire an upgradeneeded event when the requested version is higher than the existing version (or there is no existing version).The only place you can create or modify objectStores is inside an onupgradeneeded listener.
IndexedDB Transaction Examples
IndexedDB Cursor/Range Example
To operate on an index other than the primary key:var index = objectStore.index(indexName);index.openCursor();
Other IDBKeyRange types:● lowerBound()● upperBound()● only()
DevTools and IndexedDB Data
This is the IndexedDB section of Chrome’s Resources tab.
The tree shows databases, objectStores, and indices.
(Extensions are available to view IndexedDB data in Firefox/Firebug.)
The ALLOFEindexedDB Wrapper
Why use a wrapper?● Do I have to answer
that? You saw that code.
Okay, then why make your own wrapper?● Needed multiple
objectStore support.● Wanted to provide
some Ext-like methods, e.g. each(), getBy().
Methods● add● addBatch● each● get● getBy● put● putBatch● remove● removeBy
Examples 2: ALLOFEindexedDB
All of the previous IndexedDB examples redone with ALLOFEindexedDB. 65 lines -> 32 lines.
The examn.offline Schema
App DB● Users
o key: user_ido index: username
(unique)
● Fileso key: test_queue_id_patho index: test_queue_ido index: filename
Report DB(one per user)
● Test Queueso key: test_queue_id
● Test HTMLo key: test_queue_ido index: test_ido index: test_version_id
● Test Responseso key:
queue_test_questiono index: queue_test_pageo index: test_queue_ido index: submitted
What about Compatibility?
For this project we had the luxury of not having to worry about that, but here’s where you can use these things:● Appcache
o IE 10+, FF 3.5+, Chrome 4+, Safari 4+, Opera 10.6+
● Promises (native)o IE no, FF 29+, Chrome 33+, Safari 8+, Opera
20+o There is a polyfill at promisejs.org.
● IndexedDBo IE 10/11 partial, FF 16+, Chrome 24+, Safari
8+, Opera 15+
(compatibility data from caniuse.com.)
Further Reading
● Appcacheo html5rocks.com/en/tutorials/appcache/beginner/o alistapart.com/article/application-cache-is-a-douchebag
● Promiseso promisejs.orgo html5rocks.com/en/tutorials/es6/promises/
● IndexedDBo developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API