mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-22 08:16:09 +08:00
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:
parent
7000c58645
commit
0332d7264e
|
@ -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
|
|
@ -1,6 +1,7 @@
|
||||||
_ = require 'underscore'
|
_ = require 'underscore'
|
||||||
{Actions, DatabaseStore} = require 'nylas-exports'
|
{Actions, DatabaseStore} = require 'nylas-exports'
|
||||||
NylasLongConnection = require './nylas-long-connection'
|
NylasLongConnection = require './nylas-long-connection'
|
||||||
|
ContactRankingsCache = require './contact-rankings-cache'
|
||||||
|
|
||||||
INITIAL_PAGE_SIZE = 30
|
INITIAL_PAGE_SIZE = 30
|
||||||
MAX_PAGE_SIZE = 250
|
MAX_PAGE_SIZE = 250
|
||||||
|
@ -42,6 +43,7 @@ class NylasSyncWorker
|
||||||
|
|
||||||
@_terminated = false
|
@_terminated = false
|
||||||
@_connection = new NylasLongConnection(api, account.id)
|
@_connection = new NylasLongConnection(api, account.id)
|
||||||
|
@_refreshingCaches = [new ContactRankingsCache(account.id)]
|
||||||
@_resumeTimer = new BackoffTimer =>
|
@_resumeTimer = new BackoffTimer =>
|
||||||
# indirection needed so resumeFetches can be spied on
|
# indirection needed so resumeFetches can be spied on
|
||||||
@resumeFetches()
|
@resumeFetches()
|
||||||
|
@ -76,12 +78,14 @@ class NylasSyncWorker
|
||||||
start: ->
|
start: ->
|
||||||
@_resumeTimer.start()
|
@_resumeTimer.start()
|
||||||
@_connection.start()
|
@_connection.start()
|
||||||
|
@_refreshingCaches.map (c) -> c.start()
|
||||||
@resumeFetches()
|
@resumeFetches()
|
||||||
|
|
||||||
cleanup: ->
|
cleanup: ->
|
||||||
@_unlisten?()
|
@_unlisten?()
|
||||||
@_resumeTimer.cancel()
|
@_resumeTimer.cancel()
|
||||||
@_connection.end()
|
@_connection.end()
|
||||||
|
@_refreshingCaches.map (c) -> c.end()
|
||||||
@_terminated = true
|
@_terminated = true
|
||||||
@
|
@
|
||||||
|
|
||||||
|
|
|
@ -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
|
39
src/flux/stores/contact-ranking-store.coffee
Normal file
39
src/flux/stores/contact-ranking-store.coffee
Normal 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()
|
|
@ -8,112 +8,11 @@ NylasStore = require 'nylas-store'
|
||||||
RegExpUtils = require '../../regexp-utils'
|
RegExpUtils = require '../../regexp-utils'
|
||||||
DatabaseStore = require './database-store'
|
DatabaseStore = require './database-store'
|
||||||
AccountStore = require './account-store'
|
AccountStore = require './account-store'
|
||||||
|
ContactRankingStore = require './contact-ranking-store'
|
||||||
_ = require 'underscore'
|
_ = require 'underscore'
|
||||||
|
|
||||||
NylasAPI = require '../nylas-api'
|
|
||||||
{Listener, Publisher} = require '../modules/reflux-coffee'
|
|
||||||
CoffeeHelpers = require '../coffee-helpers'
|
|
||||||
|
|
||||||
WindowBridge = require '../../window-bridge'
|
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
|
Public: ContactStore maintains an in-memory cache of the user's address
|
||||||
book, making it easy to build autocompletion functionality and resolve
|
book, making it easy to build autocompletion functionality and resolve
|
||||||
|
@ -141,10 +40,9 @@ class ContactStore extends NylasStore
|
||||||
@_contactCache = []
|
@_contactCache = []
|
||||||
@_accountId = null
|
@_accountId = null
|
||||||
|
|
||||||
@_rankingsCache = new RankingsJSONCache()
|
|
||||||
@listenTo DatabaseStore, @_onDatabaseChanged
|
@listenTo DatabaseStore, @_onDatabaseChanged
|
||||||
@listenTo AccountStore, @_onAccountChanged
|
@listenTo AccountStore, @_onAccountChanged
|
||||||
@listenTo @_rankingsCache, @_sortContactsCacheWithRankings
|
@listenTo ContactRankingStore, @_sortContactsCacheWithRankings
|
||||||
|
|
||||||
@_accountId = AccountStore.current()?.id
|
@_accountId = AccountStore.current()?.id
|
||||||
@_refreshCache()
|
@_refreshCache()
|
||||||
|
@ -276,7 +174,7 @@ class ContactStore extends NylasStore
|
||||||
_refreshCache: _.debounce(ContactStore::__refreshCache, 100)
|
_refreshCache: _.debounce(ContactStore::__refreshCache, 100)
|
||||||
|
|
||||||
_sortContactsCacheWithRankings: =>
|
_sortContactsCacheWithRankings: =>
|
||||||
rankings = @_rankingsCache.value()
|
rankings = ContactRankingStore.value()
|
||||||
return unless rankings
|
return unless rankings
|
||||||
@_contactCache = _.sortBy @_contactCache, (contact) =>
|
@_contactCache = _.sortBy @_contactCache, (contact) =>
|
||||||
- (rankings[contact.email.toLowerCase()] ? 0) / 1
|
- (rankings[contact.email.toLowerCase()] ? 0) / 1
|
||||||
|
@ -287,7 +185,7 @@ class ContactStore extends NylasStore
|
||||||
|
|
||||||
_resetCache: =>
|
_resetCache: =>
|
||||||
@_contactCache = []
|
@_contactCache = []
|
||||||
@_rankingsCache.reset()
|
ContactRankingStore.reset()
|
||||||
@trigger(@)
|
@trigger(@)
|
||||||
|
|
||||||
_onAccountChanged: =>
|
_onAccountChanged: =>
|
||||||
|
@ -295,7 +193,6 @@ class ContactStore extends NylasStore
|
||||||
@_accountId = AccountStore.current()?.id
|
@_accountId = AccountStore.current()?.id
|
||||||
|
|
||||||
if @_accountId
|
if @_accountId
|
||||||
@_rankingsCache.refresh()
|
|
||||||
@_refreshCache()
|
@_refreshCache()
|
||||||
else
|
else
|
||||||
@_resetCache()
|
@_resetCache()
|
||||||
|
|
Loading…
Reference in a new issue