persistent memoization with html5 indexeddb and jquery promises
TRANSCRIPT
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
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…
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]; };};
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”.
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.
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?
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”
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}
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}
… are so 2010! 10
jQuery Promises Introduced in jQuery 1.5 Incredibly useful for asynchronous event handling Rich API
$.when() .done() .then() etc
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...};
Usage 12
$.indexedDB("indexed", store).done(function(db) { // use "db" here ...});
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!
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(); };};
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(); };};
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(); };};
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(); };};
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(); };};
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(); };};
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(); };};
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(); };};
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(); };};
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(); };};
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!
Download 25
Source available at:
https://gist.github.com/raybellis/5254306#file-jquery-memoize-js
Questions? 26