Persistent Memoization with HTML5 indexedDB and jQuery Promises

Post on 24-May-2015

1.078 views 0 download

Tags:

Transcript of 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

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