Persistent Memoization with HTML5 indexedDB and jQuery Promises

26
Ray Bellis @raybellis jQuery UK – 2013/04/19 1 Persistent Memoization using HTML5 indexedDB and Promises

Transcript of Persistent Memoization with HTML5 indexedDB and jQuery Promises

Page 1: Persistent Memoization with HTML5 indexedDB and jQuery Promises

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

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

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

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

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

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

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

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

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

… 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

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

Usage 12

$.indexedDB("indexed", store).done(function(db) { // use "db" here ...});

Page 13: Persistent Memoization with HTML5 indexedDB and jQuery Promises

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

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

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

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

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

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

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

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

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

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

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

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

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

Questions? 26