feat(contact-ranking): Use /contacts/rankings in the Contact Store

Summary:
- New JSONCache class hides logic around saving/loading rankings from a file
- Contacts are sorted whenever they're loaded so the runtime of searchContacts is still O(limit * n)

Test Plan: Run tests

Reviewers: evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D1793
This commit is contained in:
Ben Gotow 2015-07-30 18:31:25 -07:00
parent 43f9b0b8c3
commit d551fd37f2

View file

@ -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()