fix(contacts): move contact rank fetching to sync workers, refactor

Summary:
Fixes bug where contact ranking was not being fetched, and refactors the refreshing
of contact ranks. Moves periodic refreshing of the database-stored ranks to the sync
workers so it occurs in the background, once per account. Refactors JSON cache code
accordingly.

Test Plan: manual

Reviewers: evan, bengotow

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2137
This commit is contained in:
Drew Regitsky 2015-10-09 12:40:36 -07:00
parent 7000c58645
commit 0332d7264e
5 changed files with 130 additions and 107 deletions

View file

@ -0,0 +1,30 @@
RefreshingJSONCache = require './refreshing-json-cache'
{NylasAPI} = require 'nylas-exports'
# Stores contact rankings
class ContactRankingsCache extends RefreshingJSONCache
constructor: (accountId) ->
@_accountId = accountId
super({
key: "ContactRankingsFor#{accountId}",
version: 1,
refreshInterval: 60 * 60 * 1000 * 24 # one day
})
fetchData: (callback) =>
NylasAPI.makeRequest
accountId: @_accountId
path: "/contacts/rankings"
returnsModel: false
.then (json) =>
# 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 for account #{@_accountId}. #{err}")
module.exports = ContactRankingsCache

View file

@ -1,6 +1,7 @@
_ = require 'underscore'
{Actions, DatabaseStore} = require 'nylas-exports'
NylasLongConnection = require './nylas-long-connection'
ContactRankingsCache = require './contact-rankings-cache'
INITIAL_PAGE_SIZE = 30
MAX_PAGE_SIZE = 250
@ -42,6 +43,7 @@ class NylasSyncWorker
@_terminated = false
@_connection = new NylasLongConnection(api, account.id)
@_refreshingCaches = [new ContactRankingsCache(account.id)]
@_resumeTimer = new BackoffTimer =>
# indirection needed so resumeFetches can be spied on
@resumeFetches()
@ -76,12 +78,14 @@ class NylasSyncWorker
start: ->
@_resumeTimer.start()
@_connection.start()
@_refreshingCaches.map (c) -> c.start()
@resumeFetches()
cleanup: ->
@_unlisten?()
@_resumeTimer.cancel()
@_connection.end()
@_refreshingCaches.map (c) -> c.end()
@_terminated = true
@

View file

@ -0,0 +1,53 @@
_ = require 'underscore'
{NylasStore, DatabaseStore} = require 'nylas-exports'
class RefreshingJSONCache
constructor: ({@key, @version, @refreshInterval}) ->
@_timeoutId = null
start: ->
# Clear any scheduled actions
@end()
# Look up existing data from db
DatabaseStore.findJSONObject(@key).then (json) =>
# Refresh immediately if json is missing or version is outdated. Otherwise,
# compute next refresh time and schedule
timeUntilRefresh = 0
if json? and json.version is @version
timeUntilRefresh = Math.max(0, @refreshInterval - (Date.now() - json.time))
@_timeoutId = setTimeout(@refresh, timeUntilRefresh)
reset: ->
# Clear db value, turn off any scheduled actions
DatabaseStore.persistJSONObject(@key, {})
@end()
end: ->
# Turn off any scheduled actions
clearInterval(@_timeoutId) if @_timeoutId
@_timeoutId = null
refresh: =>
# Set up next refresh call
clearTimeout(@_timeoutId) if @_timeoutId
@_timeoutId = setTimeout(@refresh, @refreshInterval)
# Call fetch data function, save it to the database
@fetchData (newValue) =>
DatabaseStore.persistJSONObject(@key, {
version: @version
time: Date.now()
value: newValue
})
fetchData: (callback) =>
throw new Error("Subclasses should override this method.")
module.exports = RefreshingJSONCache

View file

@ -0,0 +1,39 @@
NylasStore = require 'nylas-store'
DatabaseStore = require './database-store'
AccountStore = require './account-store'
class ContactRankingStore extends NylasStore
constructor: ->
@listenTo DatabaseStore, @_onDatabaseChanged
@listenTo AccountStore, @_onAccountChanged
@_value = null
@_accountId = null
@_refresh()
_onDatabaseChanged: (change) =>
if change.objectClass is 'JSONObject' and change.objects[0].key is "ContactRankingsFor#{@_accountId}"
@_value = change.objects[0].json.value
@trigger()
_onAccountChanged: =>
@_refresh()
@reset()
@trigger()
value: ->
@_value
reset: ->
@_value = null
_refresh: =>
return if @_accountId is AccountStore.current()?.id
@_accountId = AccountStore.current()?.id
DatabaseStore.findJSONObject("ContactRankingsFor#{@_accountId}").then (json) =>
@_value = if json? then json.value else null
@trigger()
module.exports = new ContactRankingStore()

View file

@ -8,112 +8,11 @@ NylasStore = require 'nylas-store'
RegExpUtils = require '../../regexp-utils'
DatabaseStore = require './database-store'
AccountStore = require './account-store'
ContactRankingStore = require './contact-ranking-store'
_ = require 'underscore'
NylasAPI = require '../nylas-api'
{Listener, Publisher} = require '../modules/reflux-coffee'
CoffeeHelpers = require '../coffee-helpers'
WindowBridge = require '../../window-bridge'
###
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:
- `key`: A unique key identifying this object.
- `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: ({@key, @version, @maxAge}) ->
@_value = null
DatabaseStore.findJSONObject(@key).then (json) =>
return @refresh() unless json
return @refresh() unless json.version is @version
@_value = json.value
@trigger()
age = (new Date).getTime() - json.time
if age > @maxAge
@refresh()
else
setTimeout(@refresh, @maxAge - age)
value: ->
@_value
reset: ->
DatabaseStore.persistJSONObject(@key, {})
clearInterval(@_interval) if @_interval
@_interval = null
@_value = null
refresh: =>
clearInterval(@_interval) if @_interval
@_interval = setInterval(@refresh, @maxAge)
@refreshValue (newValue) =>
@_value = newValue
DatabaseStore.persistJSONObject(@key, {
version: @version
time: (new Date).getTime()
value: @_value
})
@trigger()
refreshValue: (callback) =>
throw new Error("Subclasses should override this method.")
class RankingsJSONCache extends JSONCache
constructor: ->
super(key: 'RankingsJSONCache', version: 1, maxAge: 60 * 60 * 1000 * 24)
refreshValue: (callback) =>
return unless atom.isMainWindow()
accountId = AccountStore.current()?.id
return unless accountId
NylasAPI.makeRequest
accountId: accountId
path: "/contacts/rankings"
returnsModel: false
.then (json) =>
# Check that the current account is still the one we requested data for
return unless accountId is AccountStore.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
@ -141,10 +40,9 @@ class ContactStore extends NylasStore
@_contactCache = []
@_accountId = null
@_rankingsCache = new RankingsJSONCache()
@listenTo DatabaseStore, @_onDatabaseChanged
@listenTo AccountStore, @_onAccountChanged
@listenTo @_rankingsCache, @_sortContactsCacheWithRankings
@listenTo ContactRankingStore, @_sortContactsCacheWithRankings
@_accountId = AccountStore.current()?.id
@_refreshCache()
@ -276,7 +174,7 @@ class ContactStore extends NylasStore
_refreshCache: _.debounce(ContactStore::__refreshCache, 100)
_sortContactsCacheWithRankings: =>
rankings = @_rankingsCache.value()
rankings = ContactRankingStore.value()
return unless rankings
@_contactCache = _.sortBy @_contactCache, (contact) =>
- (rankings[contact.email.toLowerCase()] ? 0) / 1
@ -287,7 +185,7 @@ class ContactStore extends NylasStore
_resetCache: =>
@_contactCache = []
@_rankingsCache.reset()
ContactRankingStore.reset()
@trigger(@)
_onAccountChanged: =>
@ -295,7 +193,6 @@ class ContactStore extends NylasStore
@_accountId = AccountStore.current()?.id
if @_accountId
@_rankingsCache.refresh()
@_refreshCache()
else
@_resetCache()