fix(long-polling): Close streaming connections, wait for changes to stop

Summary: fix(*): Minor onboarding react issue

Test Plan: Run existing tests

Reviewers: evan

Reviewed By: evan

Differential Revision: https://review.inboxapp.com/D1208
This commit is contained in:
Ben Gotow 2015-02-18 13:59:58 -08:00
parent f97a1bec22
commit b97f607a6e
3 changed files with 61 additions and 25 deletions

View file

@ -125,7 +125,7 @@ ContainerView = React.createClass
<div className="page" key={@state.page}>
<div className="check">
<svg preserveAspectRatio="xMidYMid" width="61" height="52" viewBox="0 0 61 52" className="check-icon">
<path d="M56.560,-0.010 C37.498,10.892 26.831,26.198 20.617,33.101 C20.617,33.101 5.398,23.373 5.398,23.373 C5.398,23.373 0.010,29.051 0.010,29.051 C0.010,29.051 24.973,51.981 24.973,51.981 C29.501,41.166 42.502,21.583 60.003,6.565 C60.003,6.565 56.560,-0.010 56.560,-0.010 Z" id="path-1" class="cls-2" fill-rule="evenodd"/>
<path d="M56.560,-0.010 C37.498,10.892 26.831,26.198 20.617,33.101 C20.617,33.101 5.398,23.373 5.398,23.373 C5.398,23.373 0.010,29.051 0.010,29.051 C0.010,29.051 24.973,51.981 24.973,51.981 C29.501,41.166 42.502,21.583 60.003,6.565 C60.003,6.565 56.560,-0.010 56.560,-0.010 Z" id="path-1" className="cls-2" fill-rule="evenodd"/>
</svg>
</div>
</div>

View file

@ -17,6 +17,8 @@ class InboxAPI
@
_onConfigChanged: =>
prev = {@APIToken, @AppID, @APIRoot}
@APIToken = atom.config.get('inbox.token')
env = atom.config.get('inbox.env')
if env in ['production']
@ -26,7 +28,9 @@ class InboxAPI
@AppID = '54miogmnotxuo5st254trcmb9'
@APIRoot = 'https://api-staging.inboxapp.com'
if @APIToken && (atom.state.mode == 'editor')
current = {@APIToken, @AppID, @APIRoot}
if atom.state.mode is 'editor' and not _.isEqual(prev, current)
@makeRequest
path: "/n"
returnsModel: true
@ -35,13 +39,21 @@ class InboxAPI
error: =>
@_startLongPolling()
_stopLongPolling: ->
for namespace, connection of @APILongConnections
connection.end()
@APILongConnections = {}
_startLongPolling: ->
return unless atom.state.mode == 'editor'
return if atom.getLoadSettings().isSpec
DatabaseStore = require './stores/database-store'
Namespace = require './models/namespace'
DatabaseStore.findAll(Namespace).then (namespaces) =>
@_stopLongPolling()
namespaces.forEach (namespace) =>
connection = new InboxLongConnection(@, namespace.id)
@APILongConnections[namespace.id] = connection
@ -54,10 +66,13 @@ class InboxAPI
Actions.longPollStateChanged(state)
if state == InboxLongConnection.State.Connected
Actions.restartTaskQueue()
connection.onDelta (delta) =>
@_handleLongPollingChange(namespace.id, delta)
connection.onDeltas (deltas) =>
@_handleDeltas(namespace.id, deltas)
Actions.restartTaskQueue()
connection.start()
.catch (error) -> console.error(error)
# Delegates to node's request object.
@ -95,22 +110,35 @@ class InboxAPI
@_handleModelResponse(body) if options.returnsModel
options.success(body) if options.success
_handleLongPollingChange: (namespaceId, delta) ->
return if delta.object == 'contact'
return if delta.object == 'event'
_handleDeltas: (namespaceId, deltas) ->
console.log("Processing deltas:")
@_shouldAcceptModel(delta.object, delta.attributes).then =>
if delta.event == 'create'
@_handleModelResponse(delta.attributes)
else if delta.event == 'modify'
@_handleModelResponse(delta.attributes)
else if delta.event == 'delete'
klass = modelClassMap()[delta.object]
return unless klass
DatabaseStore.find(klass, delta.id).then (model) ->
DatabaseStore.unpersistModel(model)
.catch (rejectionReason) ->
console.log("Delta to #{delta.event} a '#{delta.object}' was ignored. #{rejectionReason}", delta)
# Group deltas by object type so we can mutate our local cache efficiently
deltasByObject = {}
deltasDeletions = []
for delta in deltas
if delta.event is 'delete'
deltasDeletions.push(delta)
else if delta.event is 'create' or delta.event is 'modify'
deltasByObject[delta.object] ||= []
deltasByObject[delta.object].push(delta.attributes)
# Remove events and contacts - we don't apply deltas to them
delete deltasByObject['contact']
delete deltasByObject['event']
# Apply all the create / modfiy events by class
for object, items of deltasByObject
console.log(" + #{items.length} #{object}")
@_handleModelResponse(items)
# Apply all of the deletions
for delta in deltasDeletions
console.log(" - 1 #{delta.object} (#{delta.id})")
klass = modelClassMap()[delta.object]
return unless klass
DatabaseStore.find(klass, delta.id).then (model) ->
DatabaseStore.unpersistModel(model) if model
_defaultErrorCallback: (apiError) ->
console.error("Unhandled Inbox API Error:", apiError.message, apiError)

View file

@ -19,6 +19,13 @@ class InboxLongConnection
@_req = null
@_reqPingInterval = null
@_buffer = null
@_deltas = []
@_flushDeltasDebounced = _.debounce =>
@_emitter.emit('deltas-stopped-arriving', @_deltas)
@_deltas = []
, 1000
@
hasCursor: ->
@ -50,8 +57,8 @@ class InboxLongConnection
onStateChange: (callback) ->
@_emitter.on('state-change', callback)
onDelta: (callback) ->
@_emitter.on('delta', callback)
onDeltas: (callback) ->
@_emitter.on('deltas-stopped-arriving', callback)
onProcessBuffer: =>
bufferJSONs = @_buffer.split('\n')
@ -66,12 +73,12 @@ class InboxLongConnection
console.log("#{bufferJSONs[i]} could not be parsed as JSON.", e)
if delta
throw (new Error 'Received delta with no cursor!') unless delta.cursor
@_emitter.emit('delta', delta)
@_deltas.push(delta)
@_flushDeltasDebounced()
bufferCursor = delta.cursor
# Note: setCursor is slow and saves to disk, so we do it once at the end
@setCursor(bufferCursor)
console.log("Long Polling Connection: Processed #{bufferJSONs.length-1} updates")
@_buffer = bufferJSONs[bufferJSONs.length - 1]
start: ->
@ -93,7 +100,7 @@ class InboxLongConnection
return @retry() unless res.statusCode == 200
@_buffer = ''
res.setEncoding('utf8')
processBufferThrottled = _.throttle(@onProcessBuffer, 500, {leading: false})
processBufferThrottled = _.throttle(@onProcessBuffer, 400, {leading: false})
res.on 'close', => @retry()
res.on 'data', (chunk) =>
@_buffer += chunk
@ -120,7 +127,8 @@ class InboxLongConnection
@start()
, 10000
end: =>
end: ->
console.log("Long Polling Connection: Closed.")
@setState(InboxLongConnection.State.Idle)
clearInterval(@_reqPingInterval) if @_reqPingInterval
@_reqPingInterval = null