Mailspring/internal_packages/worker-sync/lib/nylas-sync-worker.coffee

194 lines
5.4 KiB
CoffeeScript

_ = require 'underscore'
{Actions, DatabaseStore} = require 'nylas-exports'
NylasLongConnection = require './nylas-long-connection'
INITIAL_PAGE_SIZE = 30
MAX_PAGE_SIZE = 250
# BackoffTimer is a small helper class that wraps setTimeout. It fires the function
# you provide at a regular interval, but backs off each time you call `backoff`.
#
class BackoffTimer
constructor: (@fn) ->
@reset()
cancel: =>
clearTimeout(@_timeout) if @_timeout
@_timeout = null
reset: =>
@cancel()
@_delay = 20 * 1000
backoff: =>
@_delay = Math.min(@_delay * 1.4, 5 * 1000 * 60) # Cap at 5 minutes
if not atom.inSpecMode()
console.log("Backing off after sync failure. Will retry in #{Math.floor(@_delay / 1000)} seconds.")
start: =>
clearTimeout(@_timeout) if @_timeout
@_timeout = setTimeout =>
@_timeout = null
@fn()
, @_delay
module.exports =
class NylasSyncWorker
constructor: (api, account) ->
@_api = api
@_account = account
@_terminated = false
@_connection = new NylasLongConnection(api, account.id)
@_resumeTimer = new BackoffTimer =>
# indirection needed so resumeFetches can be spied on
@resumeFetches()
@_unlisten = Actions.retryInitialSync.listen(@_onRetryInitialSync, @)
@_state = null
DatabaseStore.findJSONObject("NylasSyncWorker:#{@_account.id}").then (json) =>
@_state = json ? {}
for model, modelState of @_state
modelState.busy = false
@resumeFetches()
@
account: ->
@_account
connection: ->
@_connection
state: ->
@_state
busy: ->
return false unless @_state
for key, state of @_state
if state.busy
return true
false
start: ->
@_resumeTimer.start()
@_connection.start()
@resumeFetches()
cleanup: ->
@_unlisten?()
@_resumeTimer.cancel()
@_connection.end()
@_terminated = true
@
resumeFetches: =>
return unless @_state
# Stop the timer. If one or more network requests fails during the fetch process
# we'll backoff and restart the timer.
@_resumeTimer.cancel()
@fetchCollection('threads')
if @_account.usesLabels()
@fetchCollection('labels', {initialPageSize: 1000})
if @_account.usesFolders()
@fetchCollection('folders', {initialPageSize: 1000})
@fetchCollection('drafts')
@fetchCollection('contacts')
@fetchCollection('calendars')
@fetchCollection('events')
fetchCollection: (model, options = {}) ->
return unless @_state
return if @_state[model]?.complete and not options.force?
return if @_state[model]?.busy
@_state[model] =
complete: false
error: null
busy: true
count: 0
fetched: 0
@writeState()
@fetchCollectionCount(model)
@fetchCollectionPage(model, {
limit: options.initialPageSize ? INITIAL_PAGE_SIZE,
offset: 0
})
fetchCollectionCount: (model) ->
@_api.makeRequest
accountId: @_account.id
path: "/#{model}"
returnsModel: false
qs:
view: 'count'
success: (response) =>
return if @_terminated
@updateTransferState(model, count: response.count)
error: (err) =>
return if @_terminated
@_resumeTimer.backoff()
@_resumeTimer.start()
fetchCollectionPage: (model, params = {}) ->
requestStartTime = Date.now()
requestOptions =
error: (err) =>
return if @_terminated
@_fetchCollectionPageError(model, err)
success: (json) =>
return if @_terminated
if model in ["labels", "folders"] and @_hasNoInbox(json)
@_fetchCollectionPageError(model, "No inbox in #{model}")
return
lastReceivedIndex = params.offset + json.length
if json.length is params.limit
nextParams = _.extend({}, params, {offset: lastReceivedIndex})
nextParams.limit = Math.min(Math.round(params.limit * 1.5), MAX_PAGE_SIZE)
nextDelay = Math.max(0, 1000 - (Date.now() - requestStartTime))
setTimeout(( => @fetchCollectionPage(model, nextParams)), nextDelay)
@updateTransferState(model, {fetched: lastReceivedIndex})
else
@updateTransferState(model, {fetched: lastReceivedIndex, busy: false, complete: true})
if model is 'threads'
@_api.getThreads(@_account.id, params, requestOptions)
else
@_api.getCollection(@_account.id, model, params, requestOptions)
# It's occasionally possible for the NylasAPI's labels or folders
# endpoint to not return an "inbox" label. Since that's a core part of
# the app and it doesn't function without it, keep retrying until we see
# it.
_hasNoInbox: (json) ->
return not _.any(json, (obj) -> obj.name is "inbox")
_fetchCollectionPageError: (model, err) ->
@_resumeTimer.backoff()
@_resumeTimer.start()
@updateTransferState(model, {busy: false, complete: false, error: err.toString()})
updateTransferState: (model, {busy, error, complete, fetched, count}) ->
@_state[model] = _.defaults({busy, error, complete, fetched, count}, @_state[model])
@writeState()
writeState: ->
@_writeState ?= _.debounce =>
DatabaseStore.persistJSONObject("NylasSyncWorker:#{@_account.id}", @_state)
,100
@_writeState()
_onRetryInitialSync: =>
@resumeFetches()
NylasSyncWorker.BackoffTimer = BackoffTimer
NylasSyncWorker.INITIAL_PAGE_SIZE = INITIAL_PAGE_SIZE
NylasSyncWorker.MAX_PAGE_SIZE = MAX_PAGE_SIZE