Instantly share code, notes, and snippets.
Created
August 3, 2015 16:47
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save thomaswilburn/a32231133077374c3c3e to your computer and use it in GitHub Desktop.
Evercookie implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
A minimal evercookie implementation for window.name, cookies, localstorage, and IDB. | |
Each get/set function takes a key/value pair as arguments. It also has a clear() method | |
that will reset storage, which is useful during testing. | |
*/ | |
// This module uses ES6 promises, which are shimmed in other browsers | |
var Promise = require("es6-promise").Promise; | |
/* | |
window.name will prevent clearing the data as long as the tab remains open, even in Chrome. | |
We parse and cache it on page load. | |
*/ | |
var tabStorage = window.name; | |
try { | |
tabStorage = JSON.parse(tabStorage); | |
} catch (_) { | |
tabStorage = {}; | |
} | |
var tab = function(key, value) { | |
if (value) { | |
tabStorage[key] = value; | |
window.name = JSON.stringify(tabStorage); | |
return Promise.resolve(); | |
} else { | |
return Promise.resolve(tabStorage[key]); | |
} | |
}; | |
tab.clear = () => window.name = ""; | |
//Parse and cache cookies on page load | |
var cookies = {}; | |
var pairs = document.cookie.split(";"); | |
pairs.forEach(function(pair) { | |
var split = pair.split("="); | |
var value = decodeURI(split[1]); | |
try { | |
value = JSON.parse(value); | |
} catch(_) { } | |
cookies[split[0].trim()] = value; | |
}); | |
var cookie = function(key, value) { | |
if (value) { | |
value = encodeURI(JSON.stringify(value)); | |
document.cookie = `${key}=${value}; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT`; | |
cookies[key] = value; | |
return Promise.resolve(); | |
} else { | |
return Promise.resolve(cookies[key]); | |
} | |
}; | |
cookie.clear = (key) => document.cookie = `${key}=;path=/;expires=Thu, 1 Jan 1970 00:00:00 GMT`; | |
/* | |
The indexedDB implementation taken from Whitman at: https://github.com/thomaswilburn/whitman | |
It returns promises for get/set, which is why this module works the way it does. | |
There's a ready promise on the DB object, which is used to defer access until | |
the database is initialized. In retrospect, the API should do that internally and | |
save the user some line noise, but I didn't think of it at the time. | |
*/ | |
var Database = require("./idb"); | |
var db = new Database("evercookie", 1, function() { | |
db.createStore("cookies", { | |
key: "key", | |
autoincrement: false | |
}); | |
}); | |
var idb = function(key, value) { | |
if (value) { | |
return db.ready.then(_ => db.put("cookies", { key: key, value: value})); | |
} else { | |
return db.ready.then(_ => db.get("cookies", key)).then(result => result ? result.value : null); | |
} | |
}; | |
idb.clear = () => db.ready.then(() => db.clear("cookies")); | |
//Promise-based wrapper around localstorage | |
var localS = function(key, value) { | |
if (value) { | |
window.localStorage.setItem(key, encodeURI(JSON.stringify(value))); | |
return Promise.resolve(); | |
} else { | |
var result = decodeURI(window.localStorage.getItem(key)); | |
try { | |
result = JSON.parse(result); | |
} catch(_) {} | |
return Promise.resolve(result); | |
} | |
}; | |
localS.clear = window.localStorage.clear.bind(window.localStorage); | |
//all storage methods are accessed via this array | |
var methods = [tab, cookie, idb, localS]; | |
var facade = { | |
access: function(key, value) { | |
//if we're setting values, call each method and then resolve | |
if (value) { | |
methods.forEach(m => m(key, value)); | |
return Promise.resolve(); | |
} | |
//otherwise, we're getting values, so return an unresolved Promise | |
return new Promise(function(ok, fail) { | |
//call get on all storage methods | |
var promises = methods.map(m => m(key)); | |
//don't use Promise.all(), because IDB may crash | |
var i = 0; | |
/* | |
The next three functions work together to check on our promises | |
asynchronously. skip() moves onto the next item, and is always | |
attached to the rejection handler of each promise. check() is | |
attached to the resolution handler--if the storage method doesn't | |
find a value, it calls skip(), but if it does, we call found(), | |
which first sets the value in all the other storage locations | |
(hence the evercookie persistence) and then resolves the access() | |
promise with the stored value. | |
*/ | |
var found = function(value, at) { | |
//persist elsewhere once you find it in one place | |
methods.forEach(function(m, i) { | |
if (i != at) m(key, value); | |
}); | |
//return back out through the main promise | |
ok(value); | |
}; | |
var skip = function() { | |
if (!promises[++i]) return ok(null); | |
promises[i].then(check, skip); | |
}; | |
var check = function(value) { | |
if (value) return found(value, i); | |
skip(); | |
}; | |
promises[0].then(check, skip); | |
}); | |
}, | |
wipe: function(key) { | |
methods.forEach(m => m.clear(key)); | |
} | |
}; | |
module.exports = facade; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment