fix(animations): Don't process API data while animations are in-flight

Summary:
Expose the animation coordinator in Nilas-exports, use in more places

Provide a way for queries to individually bypass waiting (messages query when opening thread)

Test Plan: No new tests to see here yet

Reviewers: evan

Reviewed By: evan

Differential Revision: https://review.inboxapp.com/D1376
This commit is contained in:
Ben Gotow 2015-03-30 18:08:38 -07:00
parent 6ebe1118eb
commit c09d6dba77
10 changed files with 163 additions and 55 deletions

View file

@ -37,6 +37,8 @@ module.exports =
# Mixins
UndoManager: require '../src/flux/undo-manager'
PriorityUICoordinator: require '../src/priority-ui-coordinator'
# Stores
DraftStore: require '../src/flux/stores/draft-store'
ThreadStore: require '../src/flux/stores/thread-store'

View file

@ -1,7 +1,7 @@
_ = require 'underscore-plus'
Reflux = require 'reflux'
request = require 'request'
{FocusedContactsStore, NamespaceStore} = require 'inbox-exports'
{FocusedContactsStore, NamespaceStore, PriorityUICoordinator} = require 'inbox-exports'
module.exports =
FullContactStore = Reflux.createStore
@ -55,26 +55,28 @@ FullContactStore = Reflux.createStore
console.log('Fetching Internal Admin Data')
# Swap the url's to see real data
request 'https://admin.inboxapp.com/api/status/accounts', (err, resp, data) =>
if err
@_error = err
else
@_error = null
try
@_accountCache = JSON.parse(data)
catch err
PriorityUICoordinator.settle.then =>
if err
@_error = err
@_accountCache = null
@trigger(@)
else
@_error = null
try
@_accountCache = JSON.parse(data)
catch err
@_error = err
@_accountCache = null
@trigger(@)
# Swap the url's to see real data
request 'https://admin.inboxapp.com/api/status/accounts/applications', (err, resp, data) =>
if err
@_error = err
else
@_error = null
try
@_applicationCache = JSON.parse(data)
catch err
PriorityUICoordinator.settle.then =>
if err
@_error = err
@_applicationCache = null
@trigger(@)
else
@_error = null
try
@_applicationCache = JSON.parse(data)
catch err
@_error = err
@_applicationCache = null
@trigger(@)

View file

@ -1,6 +1,8 @@
React = require 'react'
_ = require 'underscore-plus'
{Actions,ComponentRegistry} = require "inbox-exports"
{Actions,
ComponentRegistry,
PriorityUICoordinator} = require "inbox-exports"
ResizableHandle =
Top:
@ -96,6 +98,10 @@ ResizableRegion = React.createClass
@setState(height: nextProps.initialHeight)
if nextProps.handle.axis is 'horizontal' and nextProps.initialWidth != @props.initialWidth
@setState(width: nextProps.initialWidth)
componentWillUnmount: ->
PriorityUICoordinator.endPriorityTask(@_taskId) if @_taskId
@_taskId = null
_mouseDown: (event) ->
return if event.button != 0
@ -106,6 +112,9 @@ ResizableRegion = React.createClass
event.stopPropagation()
event.preventDefault()
PriorityUICoordinator.endPriorityTask(@_taskId) if @_taskId
@_taskId = PriorityUICoordinator.beginPriorityTask()
_mouseUp: (event) ->
return if event.button != 0
@setState
@ -114,6 +123,9 @@ ResizableRegion = React.createClass
event.stopPropagation()
event.preventDefault()
PriorityUICoordinator.endPriorityTask(@_taskId)
@_taskId = null
_mouseMove: (event) ->
return if !@state.dragging
@setState @props.handle.transform(@state, @props, event)

View file

@ -23,13 +23,13 @@
###
React = require('react/addons')
PriorityUICoordinator = require('../priority-ui-coordinator')
ReactTransitionGroup = React.addons.TransitionGroup
TICK = 17
endEvents = ['webkitTransitionEnd', 'webkitAnimationEnd']
animationSupported = ->
endEvents.length != 0
animationSupported = -> true
###*
# Functions for element class management to replace dependency on jQuery
@ -63,12 +63,31 @@ TimeoutTransitionGroupChild = React.createClass(
className = @props.name + '-' + animationType
activeClassName = className + '-active'
endListener = ->
removeClass node, className
removeClass node, activeClassName
# If you animate back and forth fast enough, you can call `transition`
# before a previous transition has finished. Make sure we cancel the
# old timeout.
if @animationTimeout
clearTimeout(@animationTimeout)
@animationTimeout = null
if @animationTaskId
PriorityUICoordinator.endPriorityTask(@animationTaskId)
@animationTaskId = null
# Block database responses, JSON parsing while we are in flight
@animationTaskId = PriorityUICoordinator.beginPriorityTask()
endListener = =>
removeClass(node, className)
removeClass(node, activeClassName)
# Usually this optional callback is used for informing an owner of
# a leave animation and telling it to remove the child.
finishCallback and finishCallback()
if @animationTaskId
PriorityUICoordinator.endPriorityTask(@animationTaskId)
@animationTaskId = null
@animationTimeout = null
return
if !animationSupported()
@ -78,7 +97,9 @@ TimeoutTransitionGroupChild = React.createClass(
@animationTimeout = setTimeout(endListener, @props.enterTimeout)
else if animationType == 'leave'
@animationTimeout = setTimeout(endListener, @props.leaveTimeout)
addClass node, className
addClass(node, className)
# Need to do this to actually trigger a transition.
@queueClass activeClassName
return
@ -88,11 +109,11 @@ TimeoutTransitionGroupChild = React.createClass(
if !@timeout
@timeout = setTimeout(@flushClassNameQueue, TICK)
return
flushClassNameQueue: ->
if @isMounted()
@classNameQueue.forEach ((name) ->
addClass @getDOMNode(), name
addClass(@getDOMNode(), name)
return
).bind(this)
@classNameQueue.length = 0
@ -101,13 +122,19 @@ TimeoutTransitionGroupChild = React.createClass(
componentWillMount: ->
@classNameQueue = []
@animationTimeout = null
@animationTaskId = null
return
componentWillUnmount: ->
if @timeout
clearTimeout @timeout
clearTimeout(@timeout)
if @animationTimeout
clearTimeout @animationTimeout
clearTimeout(@animationTimeout)
@animationTimeout = null
if @animationTaskId
PriorityUICoordinator.endPriorityTask(@animationTaskId)
@animationTaskId = null
return
componentWillEnter: (done) ->

View file

@ -2,6 +2,7 @@ nodeRequest = require 'request'
Actions = require './actions'
{APIError} = require './errors'
DatabaseStore = require './stores/database-store'
PriorityUICoordinator = require '../priority-ui-coordinator'
{modelFromJSON} = require './models/utils'
async = require 'async'
@ -38,11 +39,12 @@ class EdgehillAPI
options.error ?= @_defaultErrorCallback
nodeRequest options, (error, response, body) ->
Actions.didMakeAPIRequest({request: options, response: response})
if error? or response.statusCode > 299
options.error(new APIError({error:error, response:response, body:body, requestOptions: options}))
else
options.success(body) if options.success
PriorityUICoordinator.settle.then ->
Actions.didMakeAPIRequest({request: options, response: response})
if error? or response.statusCode > 299
options.error(new APIError({error:error, response:response, body:body, requestOptions: options}))
else
options.success(body) if options.success
urlForConnecting: (provider, email_address = '') ->
auth = @getCredentials()

View file

@ -2,6 +2,7 @@ _ = require 'underscore-plus'
request = require 'request'
Actions = require './actions'
{APIError} = require './errors'
PriorityUICoordinator = require '../priority-ui-coordinator'
DatabaseStore = require './stores/database-store'
NamespaceStore = require './stores/namespace-store'
InboxLongConnection = require './inbox-long-connection'
@ -84,7 +85,8 @@ class InboxAPI
Actions.longPollOffline()
connection.onDeltas (deltas) =>
@_handleDeltas(deltas)
PriorityUICoordinator.settle.then =>
@_handleDeltas(deltas)
connection.start()
connection
@ -112,17 +114,18 @@ class InboxAPI
options.error ?= @_defaultErrorCallback
request options, (error, response, body) =>
Actions.didMakeAPIRequest({request: options, response: response})
if error? or response.statusCode > 299
options.error(new APIError({error:error, response:response, body:body}))
else
if _.isString body
try
body = JSON.parse(body)
catch error
options.error(new APIError({error:error, response:response, body:body}))
@_handleModelResponse(body) if options.returnsModel
options.success(body) if options.success
PriorityUICoordinator.settle.then =>
Actions.didMakeAPIRequest({request: options, response: response})
if error? or response.statusCode > 299
options.error(new APIError({error:error, response:response, body:body}))
else
if _.isString body
try
body = JSON.parse(body)
catch error
options.error(new APIError({error:error, response:response, body:body}))
@_handleModelResponse(body) if options.returnsModel
options.success(body) if options.success
_handleDeltas: (deltas) ->
Actions.longPollReceivedRawDeltas(deltas)

View file

@ -8,6 +8,7 @@ class ModelQuery
@_matchers = []
@_orders = []
@_singular = false
@_evaluateImmediately = false
@_includeJoinedData = []
@_count = false
@
@ -61,6 +62,9 @@ class ModelQuery
count: ->
@_count = true
@
evaluateImmediately: ->
@_evaluateImmediately = true
# Query Execution
@ -107,6 +111,9 @@ class ModelQuery
limit += " OFFSET #{@_range.offset}"
"SELECT #{result} FROM `#{@_klass.name}` #{@_whereClause()} #{order} #{limit}"
executeOptions: ->
evaluateImmediately: @_evaluateImmediately
_whereClause: ->
joins = []
@_matchers.forEach (c) =>

View file

@ -6,6 +6,7 @@ Actions = require '../actions'
Model = require '../models/model'
LocalLink = require '../models/local-link'
ModelQuery = require '../models/query'
PriorityUICoordinator = require '../../priority-ui-coordinator'
{AttributeCollection, AttributeJoinedData} = require '../attributes'
{modelFromJSON, modelClassMap, tableNameForJoin, generateTempId, isTempId} = require '../models/utils'
fs = require 'fs-plus'
@ -22,20 +23,28 @@ verboseFilter = (query) ->
class DatabaseProxy
constructor: (@databasePath) ->
@windowId = remote.getCurrentWindow().id
@queryCallbacks = {}
@queryRecords = {}
@queryId = 0
ipc.on 'database-result', ({queryKey, err, result}) =>
@queryCallbacks[queryKey](err, result) if @queryCallbacks[queryKey]
{callback, options} = @queryRecords[queryKey]
console.timeStamp("DB END #{queryKey}. #{result?.length} chars")
delete @queryCallbacks[queryKey]
waits = Promise.resolve()
waits = PriorityUICoordinator.settle unless options.evaluateImmediately
waits.then =>
callback(err, result) if callback
delete @queryRecords[queryKey]
@
query: (query, values, callback) ->
query: (query, values, callback, options) ->
@queryId += 1
queryKey = "#{@windowId}-#{@queryId}"
@queryCallbacks[queryKey] = callback if callback
@queryRecords[queryKey] = {
callback: callback,
options: options
}
console.timeStamp("DB SEND #{queryKey}: #{query}")
console.log(query) if verboseFilter(query)
ipc.send('database-query', {@databasePath, queryKey, query, values})
@ -47,7 +56,7 @@ class DatabasePromiseTransaction
constructor: (@_db, @_resolve, @_reject) ->
@_running = 0
execute: (query, values, querySuccess, queryFailure) ->
execute: (query, values, querySuccess, queryFailure, options = {}) ->
# Wrap any user-provided success callback in one that checks query time
callback = (err, result) =>
if err
@ -66,7 +75,7 @@ class DatabasePromiseTransaction
@_resolve(result)
@_running += 1
@_db.query(query, values || [], callback)
@_db.query(query, values || [], callback, options)
executeInSeries: (queries) ->
async.eachSeries queries
@ -354,7 +363,7 @@ DatabaseStore = Reflux.createStore
run: (modelQuery) ->
@inTransaction {readonly: true}, (tx) ->
tx.execute modelQuery.sql(), []
tx.execute(modelQuery.sql(), [], null, null, modelQuery.executeOptions())
.then (result) ->
Promise.resolve(modelQuery.formatResult(result))

View file

@ -84,6 +84,7 @@ MessageStore = Reflux.createStore
query = DatabaseStore.findAll(Message, threadId: loadedThreadId)
query.include(Message.attributes.body)
query.evaluateImmediately()
query.then (items) =>
localIds = {}
async.each items, (item, callback) ->

View file

@ -0,0 +1,43 @@
{generateTempId} = require './flux/models/utils'
# A small object that keeps track of the current animation state of the
# application. You can use it to defer work until animations have finished.
# Integrated with our fork of TimeoutTransitionGroup
#
# PriorityUICoordinator.settle.then ->
# # Do something expensive
#
class PriorityUICoordinator
constructor: ->
@tasks = {}
@settle = Promise.resolve()
setInterval(( => @detectOrphanedTasks()), 1000)
beginPriorityTask: ->
if Object.keys(@tasks).length is 0
@settle = new Promise (resolve, reject) =>
@settlePromiseResolve = resolve
id = generateTempId()
@tasks[id] = Date.now()
id
endPriorityTask: (id) ->
throw new Error("You must provide a task id to endPriorityTask") unless id
delete @tasks[id]
if Object.keys(@tasks).length is 0
@settlePromiseResolve() if @settlePromiseResolve
@settlePromiseResolve = null
detectOrphanedTasks: ->
now = Date.now()
threshold = 15000 # milliseconds
for id, timestamp of @tasks
if now - timestamp > threshold
console.log("PriorityUICoordinator detected oprhaned priority task lasting #{threshold}ms. Ending.")
@endPriorityTask(id)
busy: ->
Object.keys(@tasks).length > 0
module.exports = new PriorityUICoordinator()