Skip to content

Instantly share code, notes, and snippets.

@thomaswilburn
Created August 3, 2015 16:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thomaswilburn/a32231133077374c3c3e to your computer and use it in GitHub Desktop.
Save thomaswilburn/a32231133077374c3c3e to your computer and use it in GitHub Desktop.
Evercookie implementation
/*
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