ducky/web
14:275a83e4c02e Browse Files
Persist session data to localStorage. Create a helper library that figures out whether to write to chrome.storage.local or window.localStorage, and unifies their two APIs. Update the Me model to use the getOrFetch method for the profiles collection when retrieving the user's profile. This, unfortunately, makes it an async call (because we may need to fetch data from the server), so we can no longer have it be a derived property, which is a shame. It instead must just be the me.profile() function. Separate out the logic to determine when an access token expires, and turn it into the tokenExpires function. Fill the writeToCache placeholder with the logic to store the current session in either window.localStorage or chrome.storage.local, whichever is the more appropriate, using the helper library. Create the load helper function that will attempt to read session data from localStorage or chrome.storage.local, whichever the library decides is available, and updates the session based on it. Implement the logout function, which just uses the helper library to remove the session data from window.localStorage or chrome.storage.local. We should also be resetting the app.me variable, however. Create a debouncedWriteToCache function that will only write to the cache once every 250 ms, to avoid rushes on the cache. When instantiating our app.me variable, load it in from localStorage or chrome.storage.local if we can. Also, listen for changes to app.me, and persist them to chrome.storage.local or localStorage.
src/helpers/local-storage.js src/main.js src/models/me.js
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/src/helpers/local-storage.js Tue Jun 30 01:33:23 2015 -0400 1.3 @@ -0,0 +1,111 @@ 1.4 +import log from 'andlog' 1.5 + 1.6 +const useChromeStorage = chrome && chrome.storage && chrome.storage.local 1.7 +const hasLocalStorage = useChromeStorage || localStorage 1.8 + 1.9 +if (useChromeStorage) { 1.10 + log.info('Using chrome local storage') 1.11 +} else if (hasLocalStorage) { 1.12 + log.info('Using HTML5 local storage') 1.13 +} else { 1.14 + log.error('No acceptable local storage option found.') 1.15 +} 1.16 + 1.17 +const get = (keys) => { 1.18 + const single = !(keys instanceof Array) 1.19 + if (single) { 1.20 + keys = [keys] 1.21 + } 1.22 + log.info('Retrieving', keys, 'from local storage') 1.23 + return new Promise((resolve, reject) => { 1.24 + if (useChromeStorage) { 1.25 + chrome.storage.local.get(keys, (results) => { 1.26 + if (runtime && runtime.lastError) { 1.27 + reject(runtime.lastError) 1.28 + log.error('Error retrieving', keys, 'from local storage:', runtime.lastError) 1.29 + return 1.30 + } 1.31 + if (single) { 1.32 + results = results[keys[0]] 1.33 + } 1.34 + log.info('Retrieved', results, 'from local storage when asking for', keys) 1.35 + resolve(results) 1.36 + }) 1.37 + } else if (hasLocalStorage) { 1.38 + let results = {} 1.39 + keys.forEach((key) => { 1.40 + results[key] = localStorage.getItem(key) 1.41 + }) 1.42 + if (single) { 1.43 + results = results[keys[0]] 1.44 + } 1.45 + log.info('Retrieved', results, 'from local storage when asking for', keys) 1.46 + resolve(results) 1.47 + } else { 1.48 + log.error('No valid local storage options') 1.49 + reject('No valid local storage options') 1.50 + } 1.51 + }) 1.52 +} 1.53 + 1.54 +const set = (key, value) => { 1.55 + log.info('Setting', key, 'to', value, 'in local storage') 1.56 + if (useChromeStorage) { 1.57 + chrome.storage.local.set(key, value, () => { 1.58 + if (runtime && runtime.lastError) { 1.59 + console.error('Error storing data in local storage:', runtime.lastError) 1.60 + } 1.61 + }) 1.62 + } else if (hasLocalStorage) { 1.63 + localStorage.setItem(key, value) 1.64 + } else { 1.65 + console.error('No valid local storage options') 1.66 + } 1.67 + log.info('Set', key, 'to', value, 'in local storage') 1.68 +} 1.69 + 1.70 +const remove = (keys) => { 1.71 + if(!(keys instanceof Array)) { 1.72 + keys = [keys] 1.73 + } 1.74 + log.info('Removing', keys, 'from local storage') 1.75 + if (useChromeStorage) { 1.76 + chrome.storage.local.remove(keys, () => { 1.77 + if (runtime && runtime.lastError) { 1.78 + console.error('Error removing data from local storage:', runtime.lastError) 1.79 + } 1.80 + }) 1.81 + } else if (hasLocalStorage) { 1.82 + keys.forEach((key) => { 1.83 + localStorage.removeItem(key) 1.84 + }) 1.85 + } else { 1.86 + console.error('No valid local storage options') 1.87 + } 1.88 + log.info('Removed', keys, 'from local storage') 1.89 +} 1.90 + 1.91 +const clear = () => { 1.92 + log.info('Clearing local storage') 1.93 + if (useChromeStorage) { 1.94 + chrome.storage.local.clear(() => { 1.95 + if (runtime && runtime.lastError) { 1.96 + console.error('Error clearing data in local storage:', runtime.lastError) 1.97 + } 1.98 + }) 1.99 + } else if (hasLocalStorage) { 1.100 + localStorage.clear() 1.101 + } else { 1.102 + console.error('No valid local storage options') 1.103 + } 1.104 + log.info('Cleared local storage') 1.105 +} 1.106 + 1.107 +const storage = { 1.108 + get: get, 1.109 + set: set, 1.110 + remove: remove, 1.111 + clear: clear 1.112 +} 1.113 + 1.114 +export default storage
2.1 --- a/src/main.js Tue Jun 30 01:25:20 2015 -0400 2.2 +++ b/src/main.js Tue Jun 30 01:33:23 2015 -0400 2.3 @@ -8,7 +8,8 @@ 2.4 window.app = app.extend({ 2.5 init () { 2.6 this.profiles = new Profiles() 2.7 - this.me = new Me() 2.8 + this.me = new Me().load() 2.9 + this.me.on('change', this.me.debouncedWriteToCache) 2.10 this.router = new Router() 2.11 this.router.history.start({ pushState: true }) 2.12 }
3.1 --- a/src/models/me.js Tue Jun 30 01:25:20 2015 -0400 3.2 +++ b/src/models/me.js Tue Jun 30 01:33:23 2015 -0400 3.3 @@ -4,6 +4,8 @@ 3.4 import config from '../config' 3.5 import isObject from 'lodash.isobject' 3.6 import jwtDecode from 'jwt-decode' 3.7 +import localStore from '../helpers/local-storage' 3.8 +import debounce from 'lodash.debounce' 3.9 3.10 export default Model.extend({ 3.11 url: config.urlBase + '/token', 3.12 @@ -19,24 +21,37 @@ 3.13 refresh_token: 'string', 3.14 expires_in: 'int', 3.15 token_created: 'date', 3.16 - name: 'string', 3.17 profileID: 'string', 3.18 }, 3.19 3.20 + profile() { 3.21 + return new Promise((resolve, reject) => { 3.22 + app.profiles.getOrFetch(this.profileID, (err, model) => { 3.23 + if (err) { 3.24 + reject(err) 3.25 + } else { 3.26 + resolve(model) 3.27 + } 3.28 + }) 3.29 + }) 3.30 + }, 3.31 + 3.32 derived: { 3.33 loggedIn () { 3.34 return !!this.access_token 3.35 }, 3.36 + tokenExpires () { 3.37 + let d = this.token_created 3.38 + d.setSeconds(d.getSeconds() + this.expires_in) 3.39 + return d 3.40 + }, 3.41 needsRefresh () { 3.42 - let d = this.token_created 3.43 - return !!this.refresh_token && (new Date() >= d.setSeconds(d.getSeconds() + this.expires_in - 900)) 3.44 + return !!this.refresh_token && (new Date() >= this.tokenExpires) 3.45 }, 3.46 - profile: { 3.47 - deps: ['profileID'], 3.48 - fn () { 3.49 - return app.profiles.get(this.profileID) 3.50 - }, 3.51 - }, 3.52 + }, 3.53 + 3.54 + initialize() { 3.55 + this.debouncedWriteToCache = debounce(this.writeToCache, 250) 3.56 }, 3.57 3.58 login (email, password) { 3.59 @@ -54,7 +69,6 @@ 3.60 } 3.61 let serverAttrs = moc.parse(resp, options) 3.62 serverAttrs.token_created = new Date() 3.63 - console.log(serverAttrs) 3.64 if (options.wait) serverAttrs = assign({}, serverAttrs) 3.65 if (isObject(serverAttrs) && !moc.set(serverAttrs, options)) { 3.66 return false 3.67 @@ -70,11 +84,25 @@ 3.68 }, 3.69 3.70 writeToCache () { 3.71 - // TODO: write this to chrome.storage.local 3.72 + const data = JSON.stringify(this) 3.73 + localStore.set('me', data) 3.74 + }, 3.75 + 3.76 + load () { 3.77 + let moc = this 3.78 + localStore.get('me').catch((err) => { 3.79 + console.error(err) 3.80 + }).then((resp) => { 3.81 + if (resp) { 3.82 + const loaded = this.parse(JSON.parse(resp)) 3.83 + moc.set(loaded, {silent: true}) 3.84 + } 3.85 + }) 3.86 + return this 3.87 }, 3.88 3.89 logout () { 3.90 - // TODO: clear all cached data 3.91 + localStore.remove('me') 3.92 }, 3.93 3.94 })