persistent memoization with html5 indexeddb and jquery promises
TRANSCRIPT
![Page 1: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/1.jpg)
R a y B e l l i s @ r a y b e l l i s
j Q u e r y U K – 2 0 1 3 / 0 4 / 1 9
1
Persistent Memoization using HTML5 indexedDB and Promises
![Page 2: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/2.jpg)
What is Memoization? 2
“Automatic caching of a pure function’s return value, so that a subsequent call with the same parameter(s) obtains the return value from a cache instead of recalculating it.”
Avoiding: Expensive calculations Repeated AJAX calls…
![Page 3: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/3.jpg)
Memoization Example Implementation 3
$.memoize = function(factory, ctx) { var cache = {}; return function(key) { if (!(key in cache)) { cache[key] = factory.call(ctx, key); } return cache[key]; };};
![Page 4: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/4.jpg)
Usage #1 – Expensive Calculations 4
// recursive Fibonacci – O(~1.6^n) !!var fib = function(n) { return (n < 2) ? n : fib(n – 1) + fib(n – 2);}
// wrap itfib = $.memoize(fib);
The results of recursive calls are delivered from the cache instead of being recalculated.
The algorithm improves from O(~1.6^n) to O(n) for first run, and O(1) for previously calculated values of “n”.
![Page 5: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/5.jpg)
Usage #2 – Repeated AJAX Calls 5
// AJAX function – returns a “Promise”// expensive to call – may even cost real money!function getGeo(ip) { return $.getJSON(url, {ip: ip});}
// create a wrapped versionvar memoGeo = $.memoize(getGeo);
memoGeo(“192.168.1.1”).done(function(data) { ...});
Repeated calls to the wrapped function for the same input return the same promise, and thus the same result.
![Page 6: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/6.jpg)
Usage #2 – Repeated AJAX Calls 6
// AJAX function – returns a “Promise”// expensive to call – may even cost real money!function getGeo(ip) { return $.getJSON(url, {ip: ip});}
// create a wrapped versionvar memoGeo = $.memoize(getGeo);
memoGeo(“192.168.1.1”).done(function(data) { ...});
Repeated calls to the wrapped function for the same input return the same promise, and thus the same result.
How could I cache results between sessions?
![Page 7: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/7.jpg)
HTML5 “indexedDB” to the Rescue 7
Key/Value Store Values may be Objects
localStorage only allows Strings
Databases are origin specific (CORS) Multiple tables (“object stores”) per Database Asynchronous API
Sync API exists but may be deprecated by W3C Schema changes require “Database Versioning”
![Page 8: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/8.jpg)
Database Versioning 8
$.indexedDB = function(dbname, store) { var version; // initially undefined
(function retry() { var request; if (typeof version === "undefined") { request = indexedDB.open(dbname); // open latest version } else { request = indexedDB.open(dbname, version) // or open specific version number }
request.onsuccess = function(ev) { var db = ev.target.result; if (!db.objectStoreNames.contains(store)) { // if the store is missing version = db.version + 1; // increment version number db.close(); // close the DB retry(); // and open it again – NB: recursion! } else { // use the database here ... } };
request.onupgradeneeded = function(ev) { var db = ev.target.result; db.createObjectStore(store); // create new table }; })(); // invoke immediately}
![Page 9: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/9.jpg)
Callbacks… 9
$.indexedDB = function(dbname, store, callback) { var version; // initially undefined
(function retry() { var request; if (typeof version === "undefined") { request = indexedDB.open(dbname); // open latest version } else { request = indexedDB.open(dbname, version) // or open specific version number }
request.onsuccess = function(ev) { var db = ev.target.result; if (!db.objectStoreNames.contains(store)) { // if the store is missing version = db.version + 1; // increment version number db.close(); // close the DB retry(); // and open it again – NB: recursion! } else { // use the database here callback(db); } };
request.onupgradeneeded = function(ev) { var db = ev.target.result; db.createObjectStore(store); // create new table }; })(); // invoke immediately}
![Page 10: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/10.jpg)
… are so 2010! 10
jQuery Promises Introduced in jQuery 1.5 Incredibly useful for asynchronous event handling Rich API
$.when() .done() .then() etc
![Page 11: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/11.jpg)
Let’s ditch those callbacks! 11
$.indexedDB = function(dbname, store) { var def = $.Deferred(); // I promise to return ... var version;
(function retry() { var request; if (typeof version === "undefined") { request = indexedDB.open(dbname); } else { request = indexedDB.open(dbname, version); }
request.onsuccess = function(ev) { var db = ev.target.result; if (!db.objectStoreNames.contains(store)) { version = db.version + 1; db.close(); retry(); } else { // use the database here def.resolve(db); // Tell the caller she can use the DB now } };
request.onupgradeneeded = function(ev) { var db = ev.target.result; db.createObjectStore(store); }; })();
return def.promise(); // I really do promise...};
![Page 12: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/12.jpg)
Usage 12
$.indexedDB("indexed", store).done(function(db) { // use "db" here ...});
![Page 13: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/13.jpg)
Getting Back to Memoization 13
One Database – avoids naming collisions One object store per memoized function Use Promises for consistency with other jQuery
async operations
No, I didn’t figure all this out in advance!
![Page 14: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/14.jpg)
Code Walkthrough 14
$.memoizeForever = function(factory, store, keyPath, ctx) { var idb = $.indexedDB("indexed", store, keyPath); return function(key) { var def = $.Deferred(); idb.done(function(db) { db.transaction(store).objectStore(store).get(key).onsuccess = function(ev) { if (typeof ev.target.result === "undefined") { $.when(factory.call(ctx, key)).done(function(data) { db.transaction(store, "readwrite").objectStore(store) .add(data).onsuccess = function() { def.resolve(data); }; }).fail(def.reject); } else { def.resolve(ev.target.result); } }; }); return def.promise(); };};
![Page 15: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/15.jpg)
We need to return a function… 15
$.memoizeForever = function(factory, store, keyPath, ctx) { var idb = $.indexedDB("indexed", store, keyPath); return function(key) { var def = $.Deferred(); idb.done(function(db) { db.transaction(store).objectStore(store).get(key).onsuccess = function(ev) { if (typeof ev.target.result === "undefined") { $.when(factory.call(ctx, key)).done(function(data) { db.transaction(store, "readwrite").objectStore(store) .add(data).onsuccess = function() { def.resolve(data); }; }).fail(def.reject); } else { def.resolve(ev.target.result); } }; }); return def.promise(); };};
![Page 16: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/16.jpg)
that returns a Promise… 16
$.memoizeForever = function(factory, store, keyPath, ctx) { var idb = $.indexedDB("indexed", store, keyPath); return function(key) { var def = $.Deferred(); idb.done(function(db) { db.transaction(store).objectStore(store).get(key).onsuccess = function(ev) { if (typeof ev.target.result === "undefined") { $.when(factory.call(ctx, key)).done(function(data) { db.transaction(store, "readwrite").objectStore(store) .add(data).onsuccess = function() { def.resolve(data); }; }).fail(def.reject); } else { def.resolve(ev.target.result); } }; }); return def.promise(); };};
![Page 17: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/17.jpg)
and requires a DB connection… 17
$.memoizeForever = function(factory, store, keyPath, ctx) { var idb = $.indexedDB("indexed", store, keyPath); return function(key) { var def = $.Deferred(); idb.done(function(db) { db.transaction(store).objectStore(store).get(key).onsuccess = function(ev) { if (typeof ev.target.result === "undefined") { $.when(factory.call(ctx, key)).done(function(data) { db.transaction(store, "readwrite").objectStore(store) .add(data).onsuccess = function() { def.resolve(data); }; }).fail(def.reject); } else { def.resolve(ev.target.result); } }; }); return def.promise(); };};
![Page 18: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/18.jpg)
that looks up the key… 18
$.memoizeForever = function(factory, store, keyPath, ctx) { var idb = $.indexedDB("indexed", store, keyPath); return function(key) { var def = $.Deferred(); idb.done(function(db) { db.transaction(store).objectStore(store).get(key).onsuccess = function(ev) { if (typeof ev.target.result === "undefined") { $.when(factory.call(ctx, key)).done(function(data) { db.transaction(store, "readwrite").objectStore(store) .add(data).onsuccess = function() { def.resolve(data); }; }).fail(def.reject); } else { def.resolve(ev.target.result); } }; }); return def.promise(); };};
![Page 19: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/19.jpg)
and if found, resolves the Promise… 19
$.memoizeForever = function(factory, store, keyPath, ctx) { var idb = $.indexedDB("indexed", store, keyPath); return function(key) { var def = $.Deferred(); idb.done(function(db) { db.transaction(store).objectStore(store).get(key).onsuccess = function(ev) { if (typeof ev.target.result === "undefined") { $.when(factory.call(ctx, key)).done(function(data) { db.transaction(store, "readwrite").objectStore(store) .add(data).onsuccess = function() { def.resolve(data); }; }).fail(def.reject); } else { def.resolve(ev.target.result); } }; }); return def.promise(); };};
![Page 20: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/20.jpg)
otherwise, calls the original function… 20
$.memoizeForever = function(factory, store, keyPath, ctx) { var idb = $.indexedDB("indexed", store, keyPath); return function(key) { var def = $.Deferred(); idb.done(function(db) { db.transaction(store).objectStore(store).get(key).onsuccess = function(ev) { if (typeof ev.target.result === "undefined") { $.when(factory.call(ctx, key)).done(function(data) { db.transaction(store, "readwrite").objectStore(store) .add(data).onsuccess = function() { def.resolve(data); }; }).fail(def.reject); } else { def.resolve(ev.target.result); } }; }); return def.promise(); };};
![Page 21: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/21.jpg)
and $.when .done, stores it in the DB… 21
$.memoizeForever = function(factory, store, keyPath, ctx) { var idb = $.indexedDB("indexed", store, keyPath); return function(key) { var def = $.Deferred(); idb.done(function(db) { db.transaction(store).objectStore(store).get(key).onsuccess = function(ev) { if (typeof ev.target.result === "undefined") { $.when(factory.call(ctx, key)).done(function(data) { db.transaction(store, "readwrite").objectStore(store) .add(data).onsuccess = function() { def.resolve(data); }; }).fail(def.reject); } else { def.resolve(ev.target.result); } }; }); return def.promise(); };};
![Page 22: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/22.jpg)
and asynchronously resolves the Promise 22
$.memoizeForever = function(factory, store, keyPath, ctx) { var idb = $.indexedDB("indexed", store, keyPath); return function(key) { var def = $.Deferred(); idb.done(function(db) { db.transaction(store).objectStore(store).get(key).onsuccess = function(ev) { if (typeof ev.target.result === "undefined") { $.when(factory.call(ctx, key)).done(function(data) { db.transaction(store, "readwrite").objectStore(store) .add(data).onsuccess = function() { def.resolve(data); }; }).fail(def.reject); } else { def.resolve(ev.target.result); } }; }); return def.promise(); };};
![Page 23: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/23.jpg)
if it can… 23
$.memoizeForever = function(factory, store, keyPath, ctx) { var idb = $.indexedDB("indexed", store, keyPath); return function(key) { var def = $.Deferred(); idb.done(function(db) { db.transaction(store).objectStore(store).get(key).onsuccess = function(ev) { if (typeof ev.target.result === "undefined") { $.when(factory.call(ctx, key)).done(function(data) { db.transaction(store, "readwrite").objectStore(store) .add(data).onsuccess = function() { def.resolve(data); }; }).fail(def.reject); } else { def.resolve(ev.target.result); } }; }); return def.promise(); };};
![Page 24: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/24.jpg)
Persistent Memoization Usage 24
// AJAX function – returns a “Promise”// expensive to call – may even cost real money!function getGeo(ip) { return $.getJSON(url, {ip: ip});}
// create a wrapped version// Object store name is "geoip" and JSON path to key is "ip"var memoGeo = $.memoizeForever(getGeo, "geoip", "ip");
memoGeo("192.168.1.1”).done(function(data) { ...});
Now, repeated calls to the function return previously obtained results, even between browser sessions!
![Page 25: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/25.jpg)
Download 25
Source available at:
https://gist.github.com/raybellis/5254306#file-jquery-memoize-js
![Page 26: Persistent Memoization with HTML5 indexedDB and jQuery Promises](https://reader033.vdocuments.us/reader033/viewer/2022042607/5560b70cd8b42af93b8b4b77/html5/thumbnails/26.jpg)
Questions? 26