From 8b22b94ab1613f523be01572787a13e3d49c50fd Mon Sep 17 00:00:00 2001 From: Brock Whittaker Date: Tue, 27 Dec 2016 17:14:31 -0800 Subject: [PATCH] Add a LocalStorage wrapper for Zulip. This is a wrapper that allows for versioning and migrations with localStorage along with safe storage of data that respects data types. --- .eslintrc.json | 1 + static/js/localstorage.js | 146 +++++++++++++++++++++++++++++++++++++- 2 files changed, 145 insertions(+), 2 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index afd1dfa56c..caddec431f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -87,6 +87,7 @@ "fenced_code": false, "echo": false, "localstorage": false, + "localStorage": false, "current_msg_list": true, "home_msg_list": false, "pm_list": false, diff --git a/static/js/localstorage.js b/static/js/localstorage.js index 4efda616af..a6305e4435 100644 --- a/static/js/localstorage.js +++ b/static/js/localstorage.js @@ -1,6 +1,149 @@ var localstorage = (function () { -var exports = {}; +var ls = { + // parse JSON without throwing an error. + parseJSON: function (str) { + try { + return JSON.parse(str); + } catch (err) { + return undefined; + } + }, + + // check if the datestamp is from before now and if so return true. + isExpired: function (stamp) { + return new Date(stamp) < new Date(); + }, + + // return the localStorage key that is bound to a version of a key. + formGetter: function (version, name) { + return "ls__" + version + "__" + name; + }, + + // create a formData object to put in the data, a signature that it was + // created with this library, and when it expires (if ever). + formData: function (data, expires) { + return { + data: data, + __valid: true, + expires: new Date().getTime() + expires, + }; + }, + + getData: function (version, name) { + var key = this.formGetter(version, name); + var data = localStorage.getItem(key); + data = ls.parseJSON(data); + + if (data) { + if (data.__valid) { + // JSON forms of data with `Infinity` turns into `null`, + // so if null then it hasn't expired since nothing was specified. + if (!ls.isExpired(data.expires) || data.expires === null) { + return data; + } + } + } + }, + + // set the wrapped version of the data into localStorage. + setData: function (version, name, data, expires) { + var key = this.formGetter(version, name); + var val = this.formData(data, expires); + + localStorage.setItem(key, JSON.stringify(val)); + }, + + // remove the key from localStorage and from memory. + removeData: function (version, name) { + var key = this.formGetter(version, name); + + localStorage.removeItem(key); + }, + + // migrate from an older version of a data src to a newer one with a + // specified callback function. + migrate: function (name, v1, v2, callback) { + var old = this.getData(v1, name); + this.removeData(v1, name); + + if (old && old.__valid) { + var data = callback(old.data); + this.setData(v2, name, data, Infinity); + + return data; + } + }, +}; + +// return a new function instance that has instance-scoped variables. +var exports = function () { + var _data = { + VERSION: 1, + expires: Infinity, + expiresIsGlobal: false, + }; + + var prototype = { + // `expires` should be a Number that represents the number of ms from + // now that this should expire in. + // this allows for it to either be set only once or permanently. + setExpiry: function (expires, isGlobal) { + _data.expires = expires; + _data.expiresIsGlobal = isGlobal || false; + + return this; + }, + + get: function (name) { + var data = ls.getData(_data.VERSION, name); + + if (data) { + return data.data; + } + }, + + set: function (name, data) { + if (typeof _data.VERSION !== "undefined") { + ls.setData(_data.VERSION, name, data, _data.expires); + + // if the expires attribute was not set as a global, then + // make sure to return it back to Infinity to not impose + // constraints on the next key. + if (!_data.expiresIsGlobal) { + _data.expires = Infinity; + } + + return true; + } + + return false; + }, + + // remove a key with a given version. + remove: function (name) { + ls.removeData(_data.VERSION, name); + }, + + migrate: function (name, v1, v2, callback) { + return ls.migrate(name, v1, v2, callback); + }, + }; + + // set a new master version for the LocalStorage instance. + Object.defineProperty(prototype, "version", { + get: function () { + return _data.VERSION; + }, + set: function (version) { + _data.VERSION = version; + + return prototype; + }, + }); + + return prototype; +}; var warned_of_localstorage = false; @@ -17,7 +160,6 @@ exports.supported = function supports_localstorage() { }; return exports; - }()); if (typeof module !== 'undefined') {