Mailspring/internal_packages/worker-sync/lib/nylas-long-connection.coffee
Ben Gotow 9d995ded67 feat(work): Create the "Work" window, move TaskQueue, Nylas sync workers
Summary:
Move sync workers and Edgehill token checks to work window

Move the task queue and database setup to the work window

Move ContactStore background refresh to work window

Store the task queue in the database

WIP

The TaskQueue now puts tasks in the database instead of in a file, which also means it can be observed

Move all delta sync and initial sync to a package, make NylasSyncStore which exposes read-only sync state

DraftStore no longer reads task status. Once you set the "sending" bit on a draft, it never gets unset. But that's fine actually.

If your package lists windowTypes, you *only* get loaded in those windowTypes. If you specify no windowTypes, you get loaded in the root window.

This means that onboarding, worker-ui, worker-sync, etc. no longer get loaded into the main window

ActivitySidebar has a special little store that observes the task queue since it's no longer in the window

Move "toggle component regions" / "toggle react remote" to the Developer menu

Move sync worker specs, update draft store specs to not rely on TaskQueue at all

Test Plan: Run existing tests, all pass

Reviewers: dillon, evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D1936
2015-08-27 16:39:40 -07:00

176 lines
5 KiB
CoffeeScript

{Emitter} = require 'event-kit'
url = require 'url'
_ = require 'underscore'
class NylasLongConnection
@State =
Idle: 'idle'
Ended: 'ended'
Connecting: 'connecting'
Connected: 'connected'
Retrying: 'retrying'
constructor: (api, accountId) ->
@_api = api
@_accountId = accountId
@_cursorKey = "nylas.#{@_accountId}.cursor"
@_emitter = new Emitter
@_state = 'idle'
@_req = null
@_reqForceReconnectInterval = null
@_buffer = null
@_deltas = []
@_flushDeltasDebounced = _.debounce =>
@_emitter.emit('deltas-stopped-arriving', @_deltas)
@_deltas = []
, 1000
@
accountId: ->
@_accountId
hasCursor: ->
!!atom.config.get(@_cursorKey)
withCursor: (callback) ->
cursor = atom.config.get(@_cursorKey)
return callback(cursor) if cursor
stamp = Math.round(new Date().getTime() / 1000.0)
@_api.makeRequest
path: "/delta/generate_cursor"
accountId: @_accountId
method: 'POST'
body: { "start": stamp }
success: (json) =>
@setCursor(json['cursor'])
callback(json['cursor'])
console.log("Retrieved cursor #{json['cursor']} from \
`generate_cursor` with timestamp: #{stamp}")
setCursor: (cursor) ->
atom.config.set(@_cursorKey, cursor)
state: ->
@state
setState: (state) ->
@_state = state
@_emitter.emit('state-change', state)
onStateChange: (callback) ->
@_emitter.on('state-change', callback)
onDeltas: (callback) ->
@_emitter.on('deltas-stopped-arriving', callback)
onProcessBuffer: =>
bufferJSONs = @_buffer.split('\n')
bufferCursor = null
# We can't parse the last block - we don't know whether we've
# received the entire delta or only part of it. Wait
# until we have more.
@_buffer = bufferJSONs.pop()
for deltaJSON in bufferJSONs
continue if deltaJSON.length is 0
delta = null
try
delta = JSON.parse(deltaJSON)
catch e
console.log("#{deltaJSON} could not be parsed as JSON.", e)
if delta
throw (new Error 'Received delta with no cursor!') unless delta.cursor
@_deltas.push(delta)
@_flushDeltasDebounced()
bufferCursor = delta.cursor
# Note: setCursor is slow and saves to disk, so we do it once at the end
if bufferCursor
@setCursor(bufferCursor)
start: ->
token = @_api.accessTokenForAccountId(@_accountId)
return if not token?
return if @_state is NylasLongConnection.State.Ended
return if @_req
@withCursor (cursor) =>
return if @state is NylasLongConnection.State.Ended
console.log("Long Polling Connection: Starting for account #{@_accountId}, token #{token}, with cursor #{cursor}")
options = url.parse("#{@_api.APIRoot}/delta/streaming?cursor=#{cursor}&exclude_types=event&exclude_folders=false")
options.auth = "#{token}:"
if @_api.APIRoot.indexOf('https') is -1
lib = require 'http'
else
options.port = 443
lib = require 'https'
req = lib.request options, (res) =>
if res.statusCode isnt 200
res.on 'data', (chunk) =>
if chunk.toString().indexOf('Invalid cursor') > 0
console.log('Long Polling Connection: Cursor is invalid. Need to blow away local cache.')
# TODO THIS!
else
@retry()
return
@_buffer = ''
processBufferThrottled = _.throttle(@onProcessBuffer, 400, {leading: false})
res.setEncoding('utf8')
res.on 'close', => @retry()
res.on 'data', (chunk) =>
# Ignore redundant newlines sent as pings. Want to avoid
# calls to @onProcessBuffer that contain no actual updates
return if chunk is '\n' and (@_buffer.length is 0 or @_buffer[-1] is '\n')
@_buffer += chunk
processBufferThrottled()
req.setTimeout(60*60*1000)
req.setSocketKeepAlive(true)
req.on 'error', => @retry()
req.on 'socket', (socket) =>
@setState(NylasLongConnection.State.Connecting)
socket.on 'connect', =>
@setState(NylasLongConnection.State.Connected)
req.write("1")
@_req = req
# Currently we have trouble identifying when the connection has closed.
# Instead of trying to fix that, just reconnect every 120 seconds.
@_reqForceReconnectInterval = setInterval =>
@retry(true)
,30000
retry: (immediate = false) ->
return if @_state is NylasLongConnection.State.Ended
@setState(NylasLongConnection.State.Retrying)
@cleanup()
startDelay = if immediate then 0 else 10000
setTimeout =>
@start()
, startDelay
end: ->
console.log("Long Polling Connection: Closed.")
@setState(NylasLongConnection.State.Ended)
@cleanup()
cleanup: ->
clearInterval(@_reqForceReconnectInterval) if @_reqForceReconnectInterval
@_reqForceReconnectInterval = null
@_buffer = ''
if @_req
@_req.end()
@_req.abort()
@_req = null
module.exports = NylasLongConnection