diff --git a/src/flux/stores/contact-store.coffee b/src/flux/stores/contact-store.coffee index 951a89d82..c18045dee 100644 --- a/src/flux/stores/contact-store.coffee +++ b/src/flux/stores/contact-store.coffee @@ -1,3 +1,5 @@ +fs = require 'fs' +path = require 'path' Reflux = require 'reflux' Actions = require '../actions' Contact = require '../models/contact' @@ -8,9 +10,124 @@ DatabaseStore = require './database-store' NamespaceStore = require './namespace-store' _ = require 'underscore' +NylasAPI = require '../nylas-api' {Listener, Publisher} = require '../modules/reflux-coffee' CoffeeHelpers = require '../coffee-helpers' +### +The JSONCache class exposes a simple API for maintaining a local cache of data +in a JSON file that needs to be refreshed periodically. Using JSONCache is a good +idea because it handles a file errors and JSON parsing errors gracefully. + +To use the JSONCache class, subclass it and implement `refreshValue`, which +should compute a new JSON value and return it via the callback: + +``` +refreshValue: (callback) -> + NylasAPI.makeRequest(...).then (values) -> + callback(values) +``` + +If you do not wish to refresh the value, do not call the callback. + +When you create an instance of a JSONCache, you need to provide several settings: + +- `localPath`: path on disk to keep the cache + +- `version`: a version number. If the local cache has a different version number + it will be thrown out. Useful if you want to change the format of the data + stored in the cache. + +- `maxAge`: the maximum age of the local cache before it should be refreshed. + +### +class JSONCache + @include: CoffeeHelpers.includeModule + + @include Publisher + + constructor: ({@localPath, @version, @maxAge}) -> + @_value = null + @readLocal() + + detatch: => + clearInterval(@_interval) if @_interval + + value: -> + @_value + + reset: -> + fs.unlink @localPath, (err) -> + console.error(err) + @_value = null + + readLocal: => + fs.exists @localPath, (exists) => + return @refresh() unless exists + fs.readFile @localPath, (err, contents) => + return @refresh() unless contents and not err + try + json = JSON.parse(contents) + if json.version isnt @version + throw new Error("Outdated schema") + if not json.time + throw new Error("No fetch time present") + @_value = json.value + @trigger() + + age = (new Date).getTime() - json.time + if age > @maxAge + @refresh() + else + setTimeout(@refresh, @maxAge - age) + + catch err + console.error(err) + @reset() + @refresh() + + writeLocal: => + json = + version: @version + time: (new Date).getTime() + value: @_value + fs.writeFile(@localPath, JSON.stringify(json)) + + refresh: => + clearInterval(@_interval) if @_interval + @_interval = setInterval(@refresh, @maxAge) + + @refreshValue (newValue) => + @_value = newValue + @writeLocal() + @trigger() + + refreshValue: (callback) => + throw new Error("Subclasses should override this method.") + + +class RankingsJSONCache extends JSONCache + + refreshValue: (callback) => + return unless atom.isMainWindow() + + nsid = NamespaceStore.current()?.id + return unless nsid + NylasAPI.makeRequest + path: "/n/#{nsid}/contacts/rankings" + returnsModel: false + .then (json) => + # Check that the current namespace is still the one we requested data for + return unless nsid is NamespaceStore.current()?.id + # Convert rankings into the format needed for quick lookup + rankings = {} + for [email, rank] in json + rankings[email.toLowerCase()] = rank + callback(rankings) + .catch (err) => + console.warn("Request for Contact Rankings failed. #{err}") + + ### Public: ContactStore maintains an in-memory cache of the user's address book, making it easy to build autocompletion functionality and resolve @@ -36,8 +153,15 @@ class ContactStore extends NylasStore constructor: -> @_contactCache = [] @_namespaceId = null + + @_rankingsCache = new RankingsJSONCache + localPath: path.join(atom.getConfigDirPath(), 'contact-rankings.json') + maxAge: 60 * 60 * 1000 * 24 # one day + version: 1 + @listenTo DatabaseStore, @_onDatabaseChanged @listenTo NamespaceStore, @_onNamespaceChanged + @listenTo @_rankingsCache, @_sortContactsCacheWithRankings @_refreshCache() @@ -113,15 +237,19 @@ class ContactStore extends NylasStore detected __refreshCache: => - new Promise (resolve, reject) => - DatabaseStore.findAll(Contact) - .then (contacts=[]) => - @_contactCache = contacts - @trigger() - resolve() - .catch (err) -> - console.warn("Request for Contacts failed. #{err}") - _refreshCache: _.debounce(ContactStore::__refreshCache, 20) + DatabaseStore.findAll(Contact).then (contacts=[]) => + @_contactCache = contacts + @_sortContactsCacheWithRankings() + @trigger() + .catch (err) => + console.warn("Request for Contacts failed. #{err}") + _refreshCache: _.debounce(ContactStore::__refreshCache, 100) + + _sortContactsCacheWithRankings: => + rankings = @_rankingsCache.value() + return unless rankings + @_contactCache = _.sortBy @_contactCache, (contact) => + - (rankings[contact.email.toLowerCase()] ? 0) / 1 _onDatabaseChanged: (change) => return unless change?.objectClass is Contact.name @@ -129,6 +257,7 @@ class ContactStore extends NylasStore _resetCache: => @_contactCache = [] + @_rankingsCache.reset() @trigger(@) _onNamespaceChanged: => @@ -136,6 +265,7 @@ class ContactStore extends NylasStore @_namespaceId = NamespaceStore.current()?.id if @_namespaceId + @_rankingsCache.refresh() @_refreshCache() else @_resetCache()