Upcoming SlideShare
×

# Persistent Memoization with HTML5 indexedDB and jQuery Promises

1,312
-1

Published on

Published in: Technology
1 Like
Statistics
Notes
• Full Name
Comment goes here.

Are you sure you want to Yes No
• Be the first to comment

Views
Total Views
1,312
On Slideshare
0
From Embeds
0
Number of Embeds
0
Actions
Shares
0
4
0
Likes
1
Embeds 0
No embeds

No notes for slide

### Persistent Memoization with HTML5 indexedDB and jQuery Promises

1. 1. R a y B e l l i s@ r a y b e l l i sj Q u e r y U K – 2 0 1 3 / 0 4 / 1 91Persistent Memoization usingHTML5 indexedDB and Promises
2. 2. 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 ofrecalculating it.”Avoiding:  Expensive calculations  Repeated AJAX calls…
3. 3. Memoization Example Implementation3\$.memoize = function(factory, ctx) { var cache = {}; return function(key) { if (!(key in cache)) { cache[key] = factory.call(ctx, key); } return cache[key]; }; };
4. 4. Usage #1 – Expensive Calculations4// recursive Fibonacci – O(~1.6^n) !! var fib = function(n) { return (n < 2) ? n : fib(n – 1) + fib(n – 2); } // wrap it fib = \$.memoize(fib); The results of recursive calls are delivered from the cacheinstead of being recalculated.The algorithm improves from O(~1.6^n) to O(n) for firstrun, and O(1) for previously calculated values of “n”.
5. 5. Usage #2 – Repeated AJAX Calls5// 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 var memoGeo = \$.memoize(getGeo); memoGeo(“192.168.1.1”).done(function(data) { ... }); Repeated calls to the wrapped function for the sameinput return the same promise, and thus the same result.
6. 6. Usage #2 – Repeated AJAX Calls6// 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 var memoGeo = \$.memoize(getGeo); memoGeo(“192.168.1.1”).done(function(data) { ... }); Repeated calls to the wrapped function for the sameinput return the same promise, and thus the same result.How could I cache results between sessions?
7. 7. HTML5 “indexedDB” to the Rescue7  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”
8. 8. Database Versioning8\$.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 }
9. 9. 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 }
10. 10. … are so 2010!10  jQuery Promises  Introduced in jQuery 1.5  Incredibly useful for asynchronous event handling  Rich API  \$.when()  .done()  .then()  etc
11. 11. 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... };
12. 12. Usage12\$.indexedDB("indexed", store).done(function(db) { // use "db" here ... });
13. 13. Getting Back to Memoization13  One Database – avoids naming collisions  One object store per memoized function  Use Promises for consistency with other jQueryasync operationsNo, I didn’t figure all this out in advance!
14. 14. Code Walkthrough14\$.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(); }; };
15. 15. 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(); }; };
16. 16. 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(); }; };
17. 17. 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(); }; };
18. 18. 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(); }; };
19. 19. 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(); }; };
20. 20. 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(); }; };
21. 21. 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(); }; };
22. 22. and asynchronously resolves the Promise22\$.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(); }; };
23. 23. 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(); }; };
24. 24. Persistent Memoization Usage24// 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 previouslyobtained results, even between browser sessions!