mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-27 10:28:31 +08:00
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:
parent
43f9b0b8c3
commit
d551fd37f2
1 changed files with 139 additions and 9 deletions
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue