From d3d450105ead30964d288c9499c7460ec8b913d8 Mon Sep 17 00:00:00 2001 From: Drew Regitsky Date: Fri, 9 Oct 2015 12:40:36 -0700 Subject: [PATCH] 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 --- .../lib/contact-rankings-cache.coffee | 30 +++++ .../worker-sync/lib/nylas-sync-worker.coffee | 4 + .../lib/refreshing-json-cache.coffee | 53 +++++++++ src/flux/stores/contact-ranking-store.coffee | 39 ++++++ src/flux/stores/contact-store.coffee | 111 +----------------- 5 files changed, 130 insertions(+), 107 deletions(-) create mode 100644 internal_packages/worker-sync/lib/contact-rankings-cache.coffee create mode 100644 internal_packages/worker-sync/lib/refreshing-json-cache.coffee create mode 100644 src/flux/stores/contact-ranking-store.coffee diff --git a/internal_packages/worker-sync/lib/contact-rankings-cache.coffee b/internal_packages/worker-sync/lib/contact-rankings-cache.coffee new file mode 100644 index 000000000..78c80ed99 --- /dev/null +++ b/internal_packages/worker-sync/lib/contact-rankings-cache.coffee @@ -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 \ No newline at end of file diff --git a/internal_packages/worker-sync/lib/nylas-sync-worker.coffee b/internal_packages/worker-sync/lib/nylas-sync-worker.coffee index 6071fe837..acd2d705e 100644 --- a/internal_packages/worker-sync/lib/nylas-sync-worker.coffee +++ b/internal_packages/worker-sync/lib/nylas-sync-worker.coffee @@ -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 @ diff --git a/internal_packages/worker-sync/lib/refreshing-json-cache.coffee b/internal_packages/worker-sync/lib/refreshing-json-cache.coffee new file mode 100644 index 000000000..a5bade88d --- /dev/null +++ b/internal_packages/worker-sync/lib/refreshing-json-cache.coffee @@ -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 \ No newline at end of file diff --git a/src/flux/stores/contact-ranking-store.coffee b/src/flux/stores/contact-ranking-store.coffee new file mode 100644 index 000000000..f7079e677 --- /dev/null +++ b/src/flux/stores/contact-ranking-store.coffee @@ -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() diff --git a/src/flux/stores/contact-store.coffee b/src/flux/stores/contact-store.coffee index d1146c4a9..16b5ea64f 100644 --- a/src/flux/stores/contact-store.coffee +++ b/src/flux/stores/contact-store.coffee @@ -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()