refactor(tasks): refactor task store

Summary:
task store fixes

more task store fixes

check for blocked tasks

retry tasks

add spec descriptions

more test stubs

Test Plan: edgehill --test

Reviewers: bengotow

Reviewed By: bengotow

Differential Revision: https://review.inboxapp.com/D1209
This commit is contained in:
Evan Morikawa 2015-02-20 12:24:15 -08:00
parent c952ea3b12
commit 132263c38c
31 changed files with 1158 additions and 718 deletions

View file

@ -1,33 +1,13 @@
# All Inbox Globals go here.
module.exports =
# Models
Tag: require '../src/flux/models/tag'
Thread: require '../src/flux/models/thread'
Contact: require '../src/flux/models/contact'
Message: require '../src/flux/models/message'
Namespace: require '../src/flux/models/namespace'
Calendar: require '../src/flux/models/calendar'
Event: require '../src/flux/models/event'
SalesforceTask: require '../src/flux/models/salesforce-task'
## TODO move to inside of individual Salesforce package. See https://trello.com/c/tLAGLyeb/246-move-salesforce-models-into-individual-package-db-models-for-packages-various-refactors
SalesforceAssociation: require '../src/flux/models/salesforce-association'
SalesforceContact: require '../src/flux/models/salesforce-contact'
# Stores
TaskStore: require '../src/flux/stores/task-store'
DraftStore: require '../src/flux/stores/draft-store'
ThreadStore: require '../src/flux/stores/thread-store'
MessageStore: require '../src/flux/stores/message-store'
ContactStore: require '../src/flux/stores/contact-store'
DatabaseStore: require '../src/flux/stores/database-store'
NamespaceStore: require '../src/flux/stores/namespace-store'
FileUploadStore: require '../src/flux/stores/file-upload-store'
FileDownloadStore: require '../src/flux/stores/file-download-store'
# Tasks
# The Task Queue
Task: require '../src/flux/tasks/task'
TaskQueue: require '../src/flux/stores/task-queue'
# The Database
DatabaseStore: require '../src/flux/stores/database-store'
# Actions
Actions: require '../src/flux/actions'
@ -42,4 +22,27 @@ module.exports =
ComponentRegistry: require '../src/component-registry'
# Utils
Utils: require '../src/flux/models/utils'
Utils: require '../src/flux/models/utils'
# Models
Tag: require '../src/flux/models/tag'
Thread: require '../src/flux/models/thread'
Contact: require '../src/flux/models/contact'
Message: require '../src/flux/models/message'
Namespace: require '../src/flux/models/namespace'
Calendar: require '../src/flux/models/calendar'
Event: require '../src/flux/models/event'
SalesforceTask: require '../src/flux/models/salesforce-task'
# Stores
DraftStore: require '../src/flux/stores/draft-store'
ThreadStore: require '../src/flux/stores/thread-store'
MessageStore: require '../src/flux/stores/message-store'
ContactStore: require '../src/flux/stores/contact-store'
NamespaceStore: require '../src/flux/stores/namespace-store'
FileUploadStore: require '../src/flux/stores/file-upload-store'
FileDownloadStore: require '../src/flux/stores/file-download-store'
## TODO move to inside of individual Salesforce package. See https://trello.com/c/tLAGLyeb/246-move-salesforce-models-into-individual-package-db-models-for-packages-various-refactors
SalesforceAssociation: require '../src/flux/models/salesforce-association'
SalesforceContact: require '../src/flux/models/salesforce-contact'

View file

@ -10,6 +10,7 @@ DraftStoreProxy = require './draft-store-proxy'
ContenteditableToolbar = require './contenteditable-toolbar.cjsx'
ContenteditableComponent = require './contenteditable-component.cjsx'
ParticipantsTextField = require './participants-text-field.cjsx'
idGen = 0
# The ComposerView is a unique React component because it (currently) is a
@ -185,14 +186,16 @@ ComposerView = React.createClass
_footerComponents: ->
(@state.FooterComponents ? []).map (Component) =>
<Component draftLocalId={@props.localId} />
idGen += 1
<Component key={Component.id ? idGen} draftLocalId={@props.localId} />
_fileComponents: ->
AttachmentComponent = @state.AttachmentComponent
(@state.files ? []).map (file) =>
<AttachmentComponent file={file}
removable={true}
messageLocalId={@props.localId} />
key={file.filename}
removable={true}
messageLocalId={@props.localId} />
_onDraftChanged: ->
draft = @_proxy.draft()

View file

@ -6,6 +6,7 @@ ActivityBarStore = Reflux.createStore
init: ->
@_setStoreDefaults()
@_registerListeners()
@_section = "curl"
########### PUBLIC #####################################################
@ -20,7 +21,6 @@ ActivityBarStore = Reflux.createStore
_setStoreDefaults: ->
@_curlHistory = []
@_minified = true
@_longPollState = 'Unknown'
_registerListeners: ->

View file

@ -1,7 +1,9 @@
React = require 'react'
{DatabaseStore,
_ = require 'underscore-plus'
React = require 'react/addons'
{ComponentRegistry,
DatabaseStore,
NamespaceStore,
TaskStore,
TaskQueue,
Actions,
Contact,
Message} = require 'inbox-exports'
@ -30,22 +32,68 @@ ActivityBarCurlItem = React.createClass
shell = require 'shell'
shell.openItem(curlFile)
ActivityBarTask = React.createClass
render: ->
<div className={@_classNames()} onClick={=> @setState expanded: not @state?.expanded}>
<div className="task-summary">
{@_taskSummary()}
</div>
<div className="task-details">
{JSON.stringify(@props.task.toJSON())}
</div>
</div>
_taskSummary: ->
qs = @props.task.queueState
errType = ""
errCode = ""
errMessage = ""
if qs.localError?
localError = qs.localError
errType = localError.constructor.name
errMessage = localError.message ? JSON.stringify(localError)
else if qs.remoteError?
remoteError = qs.remoteError
errType = remoteError.constructor.name
errCode = remoteError.statusCode ? ""
errMessage = remoteError.body?.message ? remoteError?.message ? JSON.stringify(remoteError)
return "#{@props.task.constructor.name} #{errType} #{errCode} #{errMessage}"
_classNames: ->
qs = @props.task.queueState ? {}
React.addons.classSet
"task": true
"task-queued": @props.type is "queued"
"task-completed": @props.type is "completed"
"task-expanded": @state?.expanded
"task-local-error": qs.localError
"task-remote-error": qs.remoteError
"task-is-processing": qs.isProcessing
"task-success": qs.performedLocal and qs.performedRemote
module.exports =
ActivityBar = React.createClass
getInitialState: ->
@_getStateFromStores()
_.extend @_getStateFromStores(),
open: false
componentDidMount: ->
@task_store_unsubscribe = TaskStore.listen @_onChange
@activity_store_unsubscribe = ActivityBarStore.listen @_onChange
@taskQueueUnsubscribe = TaskQueue.listen @_onChange
@activityStoreUnsubscribe = ActivityBarStore.listen @_onChange
@registryUnlisten = ComponentRegistry.listen @_onChange
componentWillUnmount: ->
@task_store_unsubscribe() if @task_store_unsubscribe
@activity_store_unsubscribe() if @activity_store_unsubscribe
@taskQueueUnsubscribe() if @taskQueueUnsubscribe
@activityStoreUnsubscribe() if @activityStoreUnsubscribe
@registryUnlisten() if @registryUnlisten
render: ->
if @state?.ResizableComponent?
ResizableComponent = @state.ResizableComponent
else
ResizableComponent = React.createClass(render: -> <div>{@props.children}</div>)
expandedDiv = <div></div>
if @state.expandedSection == 'curl'
@ -54,62 +102,87 @@ ActivityBar = React.createClass
expandedDiv = <div className="expanded-section curl-history">{curlDivs}</div>
if @state.expandedSection == 'queue'
queueDivs = @state.queuePending.map (task) ->
<div className="item item-pending">
<strong>{task.constructor.name}:</strong> {JSON.stringify(task.toJSON())}
queueDivs = for i in [@state.queue.length - 1..0] by -1
task = @state.queue[i]
<ActivityBarTask task=task
key=task.id
type="queued" />
queueCompletedDivs = for i in [@state.completed.length - 1..0] by -1
task = @state.completed[i]
<ActivityBarTask task=task
key=task.id
type="completed" />
expandedDiv =
<div className="expanded-section queue">
<div className="btn queue-buttons"
onClick={@_onClearQueue}>Clear Queue</div>
<div className="section-content">
{queueDivs}
<hr />
{queueCompletedDivs}
</div>
</div>
queuePendingDivs = @state.queue.map (task) ->
<div className="item">
<strong>{task.constructor.name}:</strong> {JSON.stringify(task.toJSON())}
</div>
expandedDiv = <div className="expanded-section queue">
<div className="btn" onClick={@_onResetQueue}>Reset Queue</div>
<div className="btn" onClick={@_onRestartQueue}>Restart Queue</div>
{queueDivs}{queuePendingDivs}</div>
<div>
<i className="fa fa-caret-square-o-down" onClick={@_onCloseSection}></i>
<div className="queue-status">
<div className="btn" onClick={@_onExpandQueueSection}>
<div className={"activity-status-bubble state-" + @state.queueState}></div>
<span>Queue Length: {@state.queue?.length + @state.queuePending?.length}</span>
<div className="controls">
{@_caret()}
<div className="queue-status">
<div className="btn" onClick={@_onExpandQueueSection}>
<span>Queue Length: {@state.queue?.length}</span>
</div>
</div>
<div className="long-poll-status">
<div className="btn">
<div className={"activity-status-bubble state-" + @state.longPollState}></div>
<span>Long Polling: {@state.longPollState}</span>
</div>
</div>
<div className="curl-status">
<div className="btn" onClick={@_onExpandCurlSection}>
<span>Requests: {@state.curlHistory.length}</span>
</div>
</div>
<div className="feedback">
<div className="btn" onClick={@_onFeedback}>
<span>Feedback</span>
</div>
</div>
</div>
<div className="long-poll-status">
<div className="btn">
<div className={"activity-status-bubble state-" + @state.longPollState}></div>
<span>Long Polling: {@state.longPollState}</span>
</div>
<div className={@_expandedPanelClass()}>
<ResizableComponent initialHeight=200 >
{expandedDiv}
</ResizableComponent>
</div>
<div className="curl-status">
<div className="btn" onClick={@_onExpandCurlSection}>
<span>Requests: {@state.curlHistory.length}</span>
</div>
</div>
<div className="feedback">
<div className="btn" onClick={@_onFeedback}>
<span>Feedback</span>
</div>
</div>
{expandedDiv}
</div>
_expandedPanelClass: ->
React.addons.classSet
"message-area": true
"panel-open": @state.open
_caret: ->
if @state.open
<i className="fa fa-caret-square-o-down" onClick={@_onHide}></i>
else
<i className="fa fa-caret-square-o-up" onClick={@_onShow}></i>
_onChange: ->
@setState(@_getStateFromStores())
_onRestartQueue: ->
Actions.restartTaskQueue()
_onClearQueue: ->
Actions.clearQueue()
_onResetQueue: ->
Actions.resetTaskQueue()
_onCloseSection: ->
Actions.developerPanelSelectSection('')
_onHide: -> @setState open: false
_onShow: -> @setState open: true
_onExpandCurlSection: ->
@setState open: true
Actions.developerPanelSelectSection('curl')
_onExpandQueueSection: ->
@setState open: true
Actions.developerPanelSelectSection('queue')
_onFeedback: ->
@ -118,7 +191,7 @@ ActivityBar = React.createClass
debugData = JSON.stringify({
queries: @state.curlHistory,
queue: @state.queue,
queue_pending: @state.queuePending
completed: @state.completed
}, null, '\t')
# Remove API tokens from URLs included in the debug data
@ -153,9 +226,9 @@ ActivityBar = React.createClass
Actions.composePopoutDraft(localId)
_getStateFromStores: ->
ResizableComponent: ComponentRegistry.findViewByName 'ResizableComponent'
expandedSection: ActivityBarStore.expandedSection()
curlHistory: ActivityBarStore.curlHistory()
queue:TaskStore.queuedTasks()
queuePending: TaskStore.pendingTasks()
queueState: if TaskStore.isPaused() then "paused" else "running"
queue: TaskQueue._queue
completed: TaskQueue._completed
longPollState: ActivityBarStore.longPollState()

View file

@ -6,7 +6,27 @@
color:white;
font-size:12px;
order:1000;
.controls {
position: relative;
z-index: 2;
}
.message-area {
position: relative;
z-index: 1;
display: none;
&.panel-open {
display: block;
}
}
.section-content {
position: relative;
z-index: 1;
}
.queue-buttons {
position: relative;
z-index: 1;
}
.btn {
padding: 5px;
font-size: 13px;
@ -31,14 +51,6 @@
font-size:18px;
}
.minified {
height:31px;
.fa-caret-square-o-down { display:none; }
.fa-caret-square-o-up { display:inherit; }
}
.fa-caret-square-o-down { display:inherit; }
.fa-caret-square-o-up { display:none; }
.cache-status{
padding:3px;
float:right;
@ -78,28 +90,24 @@
background-color:gray;
}
}
.expanded-section {
clear:both;
border-top:1px solid black;
padding:8px;
height: 130px;
min-height: 130px;
height: 100%;
overflow-y: scroll;
background-color: rgba(0,0,0,0.5);
&.queue {
.btn {
float:right;
}
.item {
border-bottom:1px solid rgba(255,255,255,0.2);
padding-bottom: 6px;
padding-top: 6px;
}
.item.item-pending {
background-color: #003845;
padding: 0;
.btn { float:right; }
hr {
margin: 1em 0;
}
}
&.curl-history {
padding-left:0;
padding-right:0;
@ -131,6 +139,45 @@
}
}
}
.task {
padding: 0.5em 1em 0.5em 1.5em;
margin: 2px 0;
&:hover { cursor: pointer; }
position: relative;
&:before {
content: " ";
position: absolute;
top: 0;
left: 0;
width: 10px;
height: 100%;
background: @background-color-pending;
}
&.task-queued{
&.task-is-processing:before {
background: @background-color-info;
}
}
&.task-completed{
&.task-local-error:before, &.task-remote-error:before {
background: @background-color-error;
}
&.task-completed.task-success:before {
background: @background-color-success;
}
}
.task-details { display: none; }
&.task-expanded{
.task-details { display: block; }
}
}
}

View file

@ -4,7 +4,7 @@ React = require "react"
positions =
top:
transform: (state, event) ->
height: Math.max(state.initialHeight, state.bcr.bottom - Math.max(5, event.pageY)) + "px"
height: state.bcr.bottom - Math.max(5, event.pageY) + "px"
cursor: "ns-resize"
bottom:
transform: (state, event) ->
@ -27,7 +27,8 @@ React.createClass
render: ->
position = @props?.position ? 'top'
style = _.extend({}, @props.style ? {}, {height: @state.height, width: @state.width})
h = @state.height ? @props.initialHeight
style = _.extend({}, @props.style ? {}, {height: h, width: @state.width})
<div style={style} className="resizable">
<div className={"resizeBar " + position} ref="resizeBar" style={@props.barStyle ? {}}/>
{@props.children}

View file

@ -0,0 +1,400 @@
Actions = require '../../src/flux/actions'
TaskQueue = require '../../src/flux/stores/task-queue'
Task = require '../../src/flux/tasks/task'
{isTempId} = require '../../src/flux/models/utils'
{APIError,
OfflineError,
TimeoutError} = require '../../src/flux/errors'
class TaskSubclassA extends Task
constructor: (val) -> @aProp = val # forgot to call super
class TaskSubclassB extends Task
constructor: (val) -> @bProp = val; super
describe "TaskQueue", ->
makeUnstartedTask = (task) ->
TaskQueue._initializeTask(task)
return task
makeLocalStarted = (task) ->
TaskQueue._initializeTask(task)
task.queueState.isProcessing = true
return task
makeLocalFailed = (task) ->
TaskQueue._initializeTask(task)
task.queueState.performedLocal = Date.now()
return task
makeRemoteStarted = (task) ->
TaskQueue._initializeTask(task)
task.queueState.isProcessing = true
task.queueState.remoteAttempts = 1
task.queueState.performedLocal = Date.now()
return task
makeRemoteSuccess = (task) ->
TaskQueue._initializeTask(task)
task.queueState.remoteAttempts = 1
task.queueState.performedLocal = Date.now()
task.queueState.performedRemote = Date.now()
return task
makeRemoteFailed = (task) ->
TaskQueue._initializeTask(task)
task.queueState.remoteAttempts = 1
task.queueState.performedLocal = Date.now()
return task
beforeEach ->
TaskQueue._onlineStatus = true
@task = new Task()
@unstartedTask = makeUnstartedTask(new Task())
@localStarted = makeLocalStarted(new Task())
@localFailed = makeLocalFailed(new Task())
@remoteStarted = makeRemoteStarted(new Task())
@remoteSuccess = makeRemoteSuccess(new Task())
@remoteFailed = makeRemoteFailed(new Task())
unstartedTask = (task) ->
taks.queueState.shouldRetry = false
taks.queueState.isProcessing = false
taks.queueState.remoteAttempts = 0
taks.queueState.perfomredLocal = false
taks.queueState.performedRemote = false
taks.queueState.notifiedOffline = false
startedTask = (task) ->
taks.queueState.shouldRetry = false
taks.queueState.isProcessing = true
taks.queueState.remoteAttempts = 0
taks.queueState.perfomredLocal = false
taks.queueState.performedRemote = false
taks.queueState.notifiedOffline = false
localTask = (task) ->
taks.queueState.shouldRetry = false
taks.queueState.isProcessing = true
taks.queueState.remoteAttempts = 0
taks.queueState.perfomredLocal = false
taks.queueState.performedRemote = false
taks.queueState.notifiedOffline = false
afterEach ->
TaskQueue._queue = []
TaskQueue._completed = []
describe "enqueue", ->
it "makes sure you've queued a real task", ->
expect( -> TaskQueue.enqueue("asamw")).toThrow()
it "adds it to the queue", ->
TaskQueue.enqueue(@task)
expect(TaskQueue._queue.length).toBe 1
it "notifies the queue should be processed", ->
spyOn(TaskQueue, "_processTask")
spyOn(TaskQueue, "_processQueue").andCallThrough()
TaskQueue.enqueue(@task)
expect(TaskQueue._processQueue).toHaveBeenCalled()
expect(TaskQueue._processTask).toHaveBeenCalledWith(@task)
expect(TaskQueue._processTask.calls.length).toBe 1
it "ensures all tasks have an id", ->
TaskQueue.enqueue(new TaskSubclassA())
TaskQueue.enqueue(new TaskSubclassB())
expect(isTempId(TaskQueue._queue[0].id)).toBe true
expect(isTempId(TaskQueue._queue[1].id)).toBe true
it "dequeues Obsolete tasks", ->
class KillsTaskA extends Task
constructor: ->
shouldDequeueOtherTask: (other) -> other instanceof TaskSubclassA
taskToDie = makeRemoteFailed(new TaskSubclassA())
spyOn(TaskQueue, "dequeue").andCallThrough()
spyOn(taskToDie, "abort")
TaskQueue._queue = [taskToDie, @remoteFailed]
TaskQueue.enqueue(new KillsTaskA())
expect(TaskQueue._queue.length).toBe 2
expect(TaskQueue.dequeue).toHaveBeenCalledWith(taskToDie, silent: true)
expect(TaskQueue.dequeue.calls.length).toBe 1
expect(taskToDie.abort).toHaveBeenCalled()
describe "dequeue", ->
beforeEach ->
TaskQueue._queue = [@unstartedTask,
@localStarted,
@remoteStarted,
@remoteFailed]
it "grabs the task by object", ->
found = TaskQueue._parseArgs(@remoteStarted)
expect(found).toBe @remoteStarted
it "grabs the task by id", ->
found = TaskQueue._parseArgs(@remoteStarted.id)
expect(found).toBe @remoteStarted
it "throws an error if the task isn't found", ->
expect( -> TaskQueue.dequeue("bad")).toThrow()
it "doesn't abort unstarted tasks", ->
spyOn(@unstartedTask, "abort")
TaskQueue.dequeue(@unstartedTask, silent: true)
expect(@unstartedTask.abort).not.toHaveBeenCalled()
it "aborts local tasks in progress", ->
spyOn(@localStarted, "abort")
TaskQueue.dequeue(@localStarted, silent: true)
expect(@localStarted.abort).toHaveBeenCalled()
it "aborts remote tasks in progress", ->
spyOn(@remoteStarted, "abort")
TaskQueue.dequeue(@remoteStarted, silent: true)
expect(@remoteStarted.abort).toHaveBeenCalled()
it "calls cleanup on aborted tasks", ->
spyOn(@remoteStarted, "cleanup")
TaskQueue.dequeue(@remoteStarted, silent: true)
expect(@remoteStarted.cleanup).toHaveBeenCalled()
it "aborts stalled remote tasks", ->
spyOn(@remoteFailed, "abort")
TaskQueue.dequeue(@remoteFailed, silent: true)
expect(@remoteFailed.abort).toHaveBeenCalled()
it "doesn't abort if it's fully done", ->
TaskQueue._queue.push @remoteSuccess
spyOn(@remoteSuccess, "abort")
TaskQueue.dequeue(@remoteSuccess, silent: true)
expect(@remoteSuccess.abort).not.toHaveBeenCalled()
it "moves it from the queue", ->
TaskQueue.dequeue(@remoteStarted, silent: true)
expect(TaskQueue._queue.length).toBe 3
expect(TaskQueue._completed.length).toBe 1
it "marks it as no longer processing", ->
TaskQueue.dequeue(@remoteStarted, silent: true)
expect(@remoteStarted.queueState.isProcessing).toBe false
it "notifies the queue has been updated", ->
spyOn(TaskQueue, "_processQueue")
TaskQueue.dequeue(@remoteStarted)
expect(TaskQueue._processQueue).toHaveBeenCalled()
expect(TaskQueue._processQueue.calls.length).toBe 1
describe "process Task", ->
it "doesn't process processing tasks", ->
spyOn(@remoteStarted, "performLocal")
spyOn(@remoteStarted, "performRemote")
TaskQueue._processTask(@remoteStarted)
expect(@remoteStarted.performLocal).not.toHaveBeenCalled()
expect(@remoteStarted.performRemote).not.toHaveBeenCalled()
it "doesn't process blocked tasks", ->
class BlockedByTaskA extends Task
constructor: ->
shouldWaitForTask: (other) -> other instanceof TaskSubclassA
blockedByTask = new BlockedByTaskA()
spyOn(blockedByTask, "performLocal")
spyOn(blockedByTask, "performRemote")
blockingTask = makeRemoteFailed(new TaskSubclassA())
TaskQueue._queue = [blockingTask, @remoteFailed]
TaskQueue.enqueue(blockedByTask)
expect(TaskQueue._queue.length).toBe 3
expect(blockedByTask.performLocal).not.toHaveBeenCalled()
expect(blockedByTask.performRemote).not.toHaveBeenCalled()
it "doesn't block itself", ->
class BlockingTask extends Task
constructor: ->
shouldWaitForTask: (other) -> other instanceof BlockingTask
blockedByTask = new BlockingTask()
spyOn(blockedByTask, "performLocal")
spyOn(blockedByTask, "performRemote")
blockingTask = makeRemoteFailed(new BlockingTask())
TaskQueue._queue = [blockingTask, @remoteFailed]
TaskQueue.enqueue(blockedByTask)
expect(TaskQueue._queue.length).toBe 3
expect(blockedByTask.performLocal).not.toHaveBeenCalled()
expect(blockedByTask.performRemote).not.toHaveBeenCalled()
it "sets the processing bit", ->
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
TaskQueue._processTask(@unstartedTask)
expect(@unstartedTask.queueState.isProcessing).toBe true
it "performs local if it's a fresh task", ->
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
TaskQueue._processTask(@unstartedTask)
expect(@unstartedTask.performLocal).toHaveBeenCalled()
describe "performLocal", ->
it "on success it marks it as complete with the timestamp", ->
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.performedLocal isnt false
runs ->
expect(@unstartedTask.queueState.performedLocal).toBeGreaterThan 0
it "throws an error if it fails", ->
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.reject("boo")
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.isProcessing == false
runs ->
expect(@unstartedTask.queueState.localError).toBe "boo"
expect(@unstartedTask.performLocal).toHaveBeenCalled()
expect(@unstartedTask.performRemote).not.toHaveBeenCalled()
it "dequeues the task if it fails locally", ->
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.reject("boo")
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.isProcessing == false
runs ->
expect(TaskQueue._queue.length).toBe 0
expect(TaskQueue._completed.length).toBe 1
describe "performRemote", ->
beforeEach ->
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
it "performs remote properly", ->
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.performedRemote isnt false
runs ->
expect(@unstartedTask.performLocal).toHaveBeenCalled()
expect(@unstartedTask.performRemote).toHaveBeenCalled()
it "dequeues on success", ->
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.isProcessing is false and
@unstartedTask.queueState.performedRemote > 0
runs ->
expect(TaskQueue._queue.length).toBe 0
expect(TaskQueue._completed.length).toBe 1
it "notifies we're offline the first time", ->
spyOn(TaskQueue, "_isOnline").andReturn false
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
spyOn(@unstartedTask, "onError")
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.notifiedOffline == true
runs ->
expect(@unstartedTask.performLocal).toHaveBeenCalled()
expect(@unstartedTask.performRemote).not.toHaveBeenCalled()
expect(@unstartedTask.onError).toHaveBeenCalled()
expect(@unstartedTask.queueState.isProcessing).toBe false
expect(@unstartedTask.onError.calls[0].args[0] instanceof OfflineError).toBe true
it "doesn't notify we're offline the second+ time", ->
spyOn(TaskQueue, "_isOnline").andReturn false
spyOn(@remoteFailed, "performLocal").andCallFake -> Promise.resolve()
spyOn(@remoteFailed, "performRemote").andCallFake -> Promise.resolve()
spyOn(@remoteFailed, "onError")
@remoteFailed.queueState.notifiedOffline = true
TaskQueue._queue = [@remoteFailed]
runs ->
TaskQueue._processQueue()
waitsFor =>
@remoteFailed.queueState.isProcessing is false
runs ->
expect(@remoteFailed.performLocal).not.toHaveBeenCalled()
expect(@remoteFailed.performRemote).not.toHaveBeenCalled()
expect(@remoteFailed.onError).not.toHaveBeenCalled()
it "marks performedRemote on success", ->
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.performedRemote isnt false
runs ->
expect(@unstartedTask.queueState.performedRemote).toBeGreaterThan 0
it "on failure it notifies of the error", ->
err = new APIError
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.reject(err)
spyOn(@unstartedTask, "onError")
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.isProcessing is false
runs ->
expect(@unstartedTask.performLocal).toHaveBeenCalled()
expect(@unstartedTask.performRemote).toHaveBeenCalled()
expect(@unstartedTask.onError).toHaveBeenCalledWith(err)
it "dequeues on failure", ->
err = new APIError
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.reject(err)
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.isProcessing is false
runs ->
expect(TaskQueue._queue.length).toBe 0
expect(TaskQueue._completed.length).toBe 1
it "on failure it sets the appropriate bits", ->
err = new APIError
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.reject(err)
spyOn(@unstartedTask, "onError")
runs ->
TaskQueue.enqueue(@unstartedTask)
waitsFor =>
@unstartedTask.queueState.isProcessing is false
runs ->
expect(@unstartedTask.queueState.notifiedOffline).toBe false
expect(@unstartedTask.queueState.remoteError).toBe err
describe "under stress", ->
beforeEach ->
TaskQueue._queue = [@unstartedTask,
@remoteFailed]
it "when all tasks pass it processes all items", ->
for task in TaskQueue._queue
spyOn(task, "performLocal").andCallFake -> Promise.resolve()
spyOn(task, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(new Task)
waitsFor ->
TaskQueue._queue.length is 0
runs ->
expect(TaskQueue._completed.length).toBe 3

View file

@ -1,175 +0,0 @@
Actions = require '../../src/flux/actions'
TaskStore = require '../../src/flux/stores/task-store'
Task = require '../../src/flux/tasks/task'
class TaskSubclassA extends Task
constructor: (val) -> @aProp = val
class TaskSubclassB extends Task
constructor: (val) -> @bProp = val
describe "TaskStore", ->
beforeEach ->
@task = @taskA = new Task
@taskB = new Task
@task.shouldWaitForTask = -> false
@taskWithDependencies = new Task
@taskWithDependencies.shouldWaitForTask = (other) => (other == @taskA)
afterEach ->
TaskStore.reset()
describe "queueTask", =>
it "should be called whenever Actions.queueTask is fired", ->
# TODO: Turns out this is very difficult to test because you can't stub out
# store methods that are registered as listeners.
it "should call the task's performLocal function", ->
spyOn(@task, 'performLocal').andCallFake -> new Promise (resolve, reject) -> @
TaskStore._onQueueTask(@task)
expect(@task.performLocal).toHaveBeenCalled()
it "should immediately put the task on the pending list", ->
expect(TaskStore.pendingTasks().length).toBe(0)
TaskStore._onQueueTask(@task)
expect(TaskStore.pendingTasks().length).toBe(1)
expect(TaskStore.queuedTasks().length).toBe(0)
it "should move the task to the queue list when performLocal has finished", ->
expect(TaskStore.queuedTasks().length).toBe(0)
runs ->
spyOn(@task, 'performLocal').andCallFake ->
new Promise (resolve, reject) ->
setTimeout(resolve, 1)
spyOn(TaskStore, 'performNextTask')
TaskStore._onQueueTask(@task)
advanceClock(2)
waitsFor ->
TaskStore.queuedTasks().length == 1
runs ->
expect(TaskStore.queuedTasks().length).toBe(1)
expect(TaskStore.pendingTasks().length).toBe(0)
describe "abortTask", =>
beforeEach ->
@a1 = new TaskSubclassA('1')
@a2 = new TaskSubclassA('2')
@b1 = new TaskSubclassB('bA')
@b2 = new TaskSubclassB('bB')
TaskStore._queue = [@a1, @b2]
TaskStore._pending = [@a2, @b1]
it "should remove tasks whose JSON match the criteria", ->
TaskStore._onAbortTask({object: 'TaskSubclassA'})
expect(TaskStore._queue).toEqual([@b2])
expect(TaskStore._pending).toEqual([@b1])
TaskStore._onAbortTask({bProp: 'bB'})
expect(TaskStore._queue).toEqual([])
expect(TaskStore._pending).toEqual([@b1])
it "should call cleanup on each removed task", ->
spyOn(@a1, 'cleanup')
TaskStore._onAbortTask({object: 'TaskSubclassA'})
expect(@a1.cleanup).toHaveBeenCalled()
it "should call rollbackLocal on each removed task iff the rollbackLocal flag is passed", ->
spyOn(@a1, 'rollbackLocal')
TaskStore._onAbortTask({aProp: '1'})
expect(@a1.rollbackLocal).toHaveBeenCalled()
spyOn(@a2, 'rollbackLocal')
TaskStore._onAbortTask({aProp: '2'}, {rollbackLocal: false})
expect(@a2.rollbackLocal).not.toHaveBeenCalled()
it "should call abort on each pending removed task", ->
spyOn(@a1, 'abort')
spyOn(@a2, 'abort')
TaskStore._onAbortTask({object: 'TaskSubclassA'})
expect(@a2.abort).toHaveBeenCalled()
expect(@a1.abort).not.toHaveBeenCalled()
describe "canPerformTask", =>
beforeEach ->
TaskStore._queue = [@taskWithDependencies, @taskA]
TaskStore._pending = []
it "should return true if the task provided has no dependencies in the queue", ->
expect(TaskStore.canPerformTask(@taskA)).toBe(true)
it "should return false if the task returns dependencies", ->
expect(TaskStore.canPerformTask(@taskWithDependencies)).toBe(false)
describe "performNextTask", =>
beforeEach ->
TaskStore._queue = [@taskWithDependencies, @taskA, @taskB]
TaskStore._pending = []
it "should remove the first ready task from the queue", ->
expect(TaskStore.canPerformTask(@taskWithDependencies)).toBe(false)
expect(TaskStore.queuedTasks().length).toBe(3)
TaskStore.performNextTask()
expect(TaskStore.queuedTasks().length).toBe(2)
expect(TaskStore.queuedTasks()).toEqual([@taskWithDependencies, @taskB])
it "should add the task to the pending list", ->
TaskStore.performNextTask()
expect(TaskStore.pendingTasks()).toEqual([@taskA])
it "should call the task's performRemote function", ->
spyOn(@taskA, 'performRemote').andReturn(Promise.resolve())
TaskStore.performNextTask()
expect(@taskA.performRemote).toHaveBeenCalled()
describe "when performRemote finishes", ->
beforeEach ->
spyOn(@taskA, 'performRemote').andReturn(Promise.resolve())
it "should clean up the task", ->
spyOn(@taskA, 'cleanup')
waitsForPromise -> TaskStore.performNextTask()
runs ->
expect(@taskA.cleanup).toHaveBeenCalled()
it "should remove the task from the pending list", ->
waitsForPromise -> TaskStore.performNextTask()
runs ->
expect(TaskStore.pendingTasks()).toEqual([])
it "should update the disk cache and perform the next task", ->
spyOn(TaskStore, 'performNextTask').andCallThrough()
spyOn(TaskStore, 'persist').andCallFake (callback) ->
callback() if callback
waitsForPromise -> TaskStore.performNextTask()
runs ->
expect(TaskStore.persist).toHaveBeenCalled()
expect(TaskStore.performNextTask.callCount).toBe(3)
describe "when performRemote finishes with a failure", ->
beforeEach ->
spyOn(TaskStore, '_displayError')
spyOn(@taskA, 'performRemote').andReturn(Promise.reject('An error!'))
describe "when shouldRetry returns true", ->
beforeEach ->
spyOn(@taskA, 'shouldRetry').andReturn(true)
it "should put the task back on the queue", ->
waitsForPromise -> TaskStore.performNextTask()
runs ->
expect(TaskStore.queuedTasks()).toEqual([@taskWithDependencies, @taskB, @taskA])
describe "when shouldRetry returns false", ->
beforeEach ->
spyOn(@taskA, 'shouldRetry').andReturn(false)
it "should roll back the performLocal function and throw out the task", ->
spyOn(@taskA, 'rollbackLocal')
waitsForPromise -> TaskStore.performNextTask()
runs ->
expect(@taskA.rollbackLocal).toHaveBeenCalled()

View file

@ -26,7 +26,7 @@ describe "AddRemoveTagsTask", ->
new Tag({name: 'archive', id: 'archive'})
]
task = new AddRemoveTagsTask(testThread.id, ['archive'], ['inbox'])
task.rollbackLocal()
task._rollbackLocal()
waitsFor ->
DatabaseStore.persistModel.callCount > 0
runs ->

View file

@ -56,7 +56,7 @@ describe "FileUploadTask", ->
it "notifies when the file upload fails", ->
spyOn(Actions, "uploadStateChanged")
spyOn(@task, "_getBytesUploaded").andReturn(0)
@task.rollbackLocal()
@task._rollbackLocal()
data = _.extend uploadData, state: "failed"
expect(Actions.uploadStateChanged).toHaveBeenCalledWith(data)

View file

@ -16,7 +16,7 @@ describe "MarkMessageReadTask", ->
email: 'dummy@inboxapp.com'
@task = new MarkMessageReadTask(@message)
describe "rollbackLocal", ->
describe "_rollbackLocal", ->
beforeEach ->
spyOn(DatabaseStore, 'persistModel').andCallFake -> Promise.resolve()
@ -31,17 +31,17 @@ describe "MarkMessageReadTask", ->
email: 'dummy@inboxapp.com'
@task = new MarkMessageReadTask(message)
@task.performLocal()
@task.rollbackLocal()
@task._rollbackLocal()
expect(message.unread).toBe(false)
it "should mark the message as unread", ->
@task.performLocal()
@task.rollbackLocal()
@task._rollbackLocal()
expect(@message.unread).toBe(true)
it "should trigger an action to persist the change", ->
@task.performLocal()
@task.rollbackLocal()
@task._rollbackLocal()
expect(DatabaseStore.persistModel).toHaveBeenCalled()
describe "performLocal", ->

View file

@ -7,6 +7,7 @@ Message = require '../../src/flux/models/message'
Contact = require '../../src/flux/models/contact'
{APIError} = require '../../src/flux/errors'
DatabaseStore = require '../../src/flux/stores/database-store'
TaskQueue = require '../../src/flux/stores/task-queue'
SaveDraftTask = require '../../src/flux/tasks/save-draft'
@ -134,20 +135,16 @@ describe "SaveDraftTask", ->
describe "When the api throws a 404 error", ->
beforeEach ->
spyOn(TaskQueue, "enqueue")
spyOn(atom.inbox, "makeRequest").andCallFake (opts) ->
opts.error(testError(opts)) if opts.error
it "resets the id", ->
task = new SaveDraftTask("remoteDraftId")
waitsForPromise shouldReject: true, =>
promise = task.performRemote()
promise.catch (apiError) ->
newDraft = DatabaseStore.swapModel.mostRecentCall.args[0].newModel
expect(apiError instanceof APIError).toBe true
expect(apiError.statusCode).toBe 404
expect(isTempId(newDraft.id)).toBe true
promise
it "shouldRetry", ->
task = new SaveDraftTask("remoteDraftId")
expect(task.shouldRetry(testError())).toBe true
task.onAPIError(testError({}))
waitsFor ->
DatabaseStore.swapModel.calls.length > 0
runs ->
newDraft = DatabaseStore.swapModel.mostRecentCall.args[0].newModel
expect(isTempId(newDraft.id)).toBe true
expect(TaskQueue.enqueue).toHaveBeenCalled()

View file

@ -4,6 +4,7 @@ SendDraftTask = require '../../src/flux/tasks/send-draft'
DatabaseStore = require '../../src/flux/stores/database-store'
{generateTempId} = require '../../src/flux/models/utils'
Message = require '../../src/flux/models/message'
TaskQueue = require '../../src/flux/stores/task-queue'
_ = require 'underscore-plus'
describe "SendDraftTask", ->
@ -35,6 +36,47 @@ describe "SendDraftTask", ->
expect(@sendA.shouldWaitForTask(@saveA)).toBe(true)
describe "When on the TaskQueue", ->
beforeEach ->
TaskQueue._queue = []
TaskQueue._completed = []
@saveTask = new SaveDraftTask('localid-A')
@saveTaskB = new SaveDraftTask('localid-B')
@sendTask = new SendDraftTask('localid-A')
@tasks = [@saveTask, @saveTaskB, @sendTask]
describe "when tasks succeed", ->
beforeEach ->
for task in @tasks
spyOn(task, "performLocal").andCallFake -> Promise.resolve()
spyOn(task, "performRemote").andCallFake -> Promise.resolve()
runs ->
TaskQueue.enqueue(@saveTask, silent: true)
TaskQueue.enqueue(@saveTaskB, silent: true)
TaskQueue.enqueue(@sendTask)
waitsFor ->
@sendTask.queueState.performedRemote isnt false
it "processes all of the items", ->
runs ->
expect(TaskQueue._queue.length).toBe 0
expect(TaskQueue._completed.length).toBe 3
it "all of the tasks", ->
runs ->
expect(@saveTask.performRemote).toHaveBeenCalled()
expect(@saveTaskB.performRemote).toHaveBeenCalled()
expect(@sendTask.performRemote).toHaveBeenCalled()
it "finishes the save before sending", ->
runs ->
save = @saveTask.queueState.performedRemote
send = @sendTask.queueState.performedRemote
expect(save).toBeGreaterThan 0
expect(send).toBeGreaterThan 0
expect(save <= send).toBe true
describe "performLocal", ->
it "should throw an exception if the first parameter is not a localId", ->
badTasks = [new SendDraftTask()]

View file

@ -1,56 +1,56 @@
{APIError} = require '../../src/flux/errors'
Task = require '../../src/flux/tasks/task'
_ = require 'underscore-plus'
describe "Task", ->
beforeEach ->
@task = new Task()
describe "shouldRetry", ->
it "should default to false if the error does not have a status code", ->
expect(@task.shouldRetry(new Error())).toBe(false)
# Should Not Retry
it "should return false when the error is a 401 Unauthorized from the API", ->
expect(@task.shouldRetry(new APIError({statusCode: 401}))).toBe(false)
it "should return false when the error is a 403 Forbidden from the API", ->
expect(@task.shouldRetry(new APIError({statusCode: 403}))).toBe(false)
it "should return false when the error is a 404 Not Found from the API", ->
expect(@task.shouldRetry(new APIError({statusCode: 404}))).toBe(false)
it "should return false when the error is a 405 Method Not Allowed from the API", ->
expect(@task.shouldRetry(new APIError({statusCode: 405}))).toBe(false)
it "should return false when the error is a 406 Not Acceptable from the API", ->
expect(@task.shouldRetry(new APIError({statusCode: 406}))).toBe(false)
it "should return false when the error is a 409 Conflict from the API", ->
expect(@task.shouldRetry(new APIError({statusCode: 409}))).toBe(false)
# Should Retry
it "should return true when the error is 0 Request Not Made from the API", ->
expect(@task.shouldRetry(new APIError({statusCode: 0}))).toBe(true)
it "should return true when the error is 407 Proxy Authentication Required from the API", ->
expect(@task.shouldRetry(new APIError({statusCode: 407}))).toBe(true)
it "should return true when the error is 408 Request Timeout from the API", ->
expect(@task.shouldRetry(new APIError({statusCode: 408}))).toBe(true)
it "should return true when the error is 305 Use Proxy from the API", ->
expect(@task.shouldRetry(new APIError({statusCode: 305}))).toBe(true)
it "should return true when the error is 502 Bad Gateway from the API", ->
expect(@task.shouldRetry(new APIError({statusCode: 502}))).toBe(true)
it "should return true when the error is 503 Service Unavailable from the API", ->
expect(@task.shouldRetry(new APIError({statusCode: 503}))).toBe(true)
it "should return true when the error is 504 Gateway Timeout from the API", ->
expect(@task.shouldRetry(new APIError({statusCode: 504}))).toBe(true)
# {APIError} = require '../../src/flux/errors'
# Task = require '../../src/flux/tasks/task'
# _ = require 'underscore-plus'
#
# describe "Task", ->
# beforeEach ->
# @task = new Task()
#
# describe "shouldRetry", ->
#
# it "should default to false if the error does not have a status code", ->
# expect(@task.shouldRetry(new Error())).toBe(false)
#
# # Should Not Retry
#
# it "should return false when the error is a 401 Unauthorized from the API", ->
# expect(@task.shouldRetry(new APIError({statusCode: 401}))).toBe(false)
#
# it "should return false when the error is a 403 Forbidden from the API", ->
# expect(@task.shouldRetry(new APIError({statusCode: 403}))).toBe(false)
#
# it "should return false when the error is a 404 Not Found from the API", ->
# expect(@task.shouldRetry(new APIError({statusCode: 404}))).toBe(false)
#
# it "should return false when the error is a 405 Method Not Allowed from the API", ->
# expect(@task.shouldRetry(new APIError({statusCode: 405}))).toBe(false)
#
# it "should return false when the error is a 406 Not Acceptable from the API", ->
# expect(@task.shouldRetry(new APIError({statusCode: 406}))).toBe(false)
#
# it "should return false when the error is a 409 Conflict from the API", ->
# expect(@task.shouldRetry(new APIError({statusCode: 409}))).toBe(false)
#
# # Should Retry
#
# it "should return true when the error is 0 Request Not Made from the API", ->
# expect(@task.shouldRetry(new APIError({statusCode: 0}))).toBe(true)
#
# it "should return true when the error is 407 Proxy Authentication Required from the API", ->
# expect(@task.shouldRetry(new APIError({statusCode: 407}))).toBe(true)
#
# it "should return true when the error is 408 Request Timeout from the API", ->
# expect(@task.shouldRetry(new APIError({statusCode: 408}))).toBe(true)
#
# it "should return true when the error is 305 Use Proxy from the API", ->
# expect(@task.shouldRetry(new APIError({statusCode: 305}))).toBe(true)
#
# it "should return true when the error is 502 Bad Gateway from the API", ->
# expect(@task.shouldRetry(new APIError({statusCode: 502}))).toBe(true)
#
# it "should return true when the error is 503 Service Unavailable from the API", ->
# expect(@task.shouldRetry(new APIError({statusCode: 503}))).toBe(true)
#
# it "should return true when the error is 504 Gateway Timeout from the API", ->
# expect(@task.shouldRetry(new APIError({statusCode: 504}))).toBe(true)
#

View file

@ -116,20 +116,26 @@ Menu = React.createClass
componentWillUnmount: ->
@subscriptions?.dispose()
render: ->
hc = @props.headerComponents ? []
if hc.length is 0 then hc = <span></span>
items = @props.items.map(@_itemComponentForItem) ? []
if items is 0 then items = <span></span>
fc = @props.footerComponents ? []
if fc.length is 0 then fc = <span></span>
<div className={"menu " + @props.className}>
<div className="header-container">
{@props.headerComponents || []}
{hc}
</div>
<div className="content-container">
{@props.items.map(@_itemComponentForItem)}
{items}
</div>
<div className="footer-container">
{@props.footerComponents || []}
{fc}
</div>
</div>
_itemComponentForItem: (item, i) ->
content = @props.itemContent(item)
return content if content.type is MenuItem.type
@ -165,4 +171,4 @@ Menu = React.createClass
Menu.Item = MenuItem
module.exports = Menu
module.exports = Menu

View file

@ -12,7 +12,7 @@ Token = React.createClass
select: React.PropTypes.func.isRequired,
action: React.PropTypes.func,
item: React.PropTypes.object,
configureDragDrop: (registerType) ->
registerType('token', {
dragSource:
@ -116,7 +116,7 @@ TokenizingTextField = React.createClass
/>
_fieldComponent: ->
<div onClick={@focus} {...@dropTargetFor('token')}>
<div key="field-component" onClick={@focus} {...@dropTargetFor('token')}>
<div className="tokenizing-field-label">{"#{@props.prompt}:"}</div>
<div className="tokenizing-field-input">
{@_fieldTokenComponents()}

View file

@ -19,7 +19,7 @@ Message =
# Root action is fired, it converts it's payload to JSON, tunnels it to the main window
# via IPC, and re-fires the Action in the main window. This means that calls to actions
# like Actions.queueTask(task) can be fired in secondary windows and consumed by the
# TaskStore, which only lives in the main window.
# TaskQueue, which only lives in the main window.
# 2. The ActionBridge listens to the DatabaseStore and re-broadcasts it's trigger() event
# into all of the windows of the application. This is important, because the DatabaseStore

View file

@ -11,7 +11,7 @@ globalActions = [
"logout",
# File Actions
# Since the TaskStore is only in the main window, these actions need to
# Since the TaskQueue is only in the main window, these actions need to
# be rebroadcasted to all windows so you can watch the upload progress
# from the popout composers
"uploadStateChanged",
@ -30,10 +30,11 @@ globalActions = [
mainWindowActions = [
# Actions for Tasks
"queueTask",
"abortTask",
"restartTaskQueue",
"resetTaskQueue",
"dequeueTask",
"clearQueue",
"longPollStateChanged",
"longPollConnected",
"longPollOffline",
"didMakeAPIRequest",
]

View file

@ -16,5 +16,13 @@ class APIError extends Error
notifyConsole: ->
console.error("Edgehill API Error: #{@message}", @)
class OfflineError extends Error
constructor: ->
class TimeoutError extends Error
constructor: ->
module.exports =
"APIError": APIError
"OfflineError": OfflineError
"TimeoutError": TimeoutError

View file

@ -77,11 +77,14 @@ class InboxAPI
connection.onStateChange (state) ->
Actions.longPollStateChanged(state)
if state == InboxLongConnection.State.Connected
Actions.restartTaskQueue()
## TODO use OfflineStatusStore
Actions.longPollConnected()
else
## TODO use OfflineStatusStore
Actions.longPollOffline()
connection.onDeltas (deltas) =>
@_handleDeltas(deltas)
Actions.restartTaskQueue()
connection.start()
connection

View file

@ -0,0 +1,179 @@
_ = require 'underscore-plus'
fs = require 'fs-plus'
path = require 'path'
{generateTempId} = require '../models/utils'
Task = require "../tasks/task"
Reflux = require 'reflux'
Actions = require '../actions'
{APIError,
OfflineError,
TimeoutError} = require '../errors'
if atom.state.mode isnt "editor" and atom.state.mode isnt "spec" then return
module.exports =
TaskQueue = Reflux.createStore
init: ->
@_queue = []
@_completed = []
@_restoreQueueFromDisk()
@listenTo(Actions.queueTask, @enqueue)
@listenTo(Actions.dequeueTask, @dequeue)
@listenTo(Actions.logout, @clearQueue)
@listenTo(Actions.clearQueue, @clearQueue)
# TODO
# @listenTo(OnlineStatusStore, @_onOnlineChange)
@_onlineStatus = true
@listenTo Actions.longPollConnected, =>
@_onlineStatus = true
@_update()
@listenTo Actions.longPollOffline, =>
@_onlineStatus = false
@_update()
_initializeTask: (task) ->
task.id ?= generateTempId()
task.queueState ?= {}
task.queueState =
localError: null
remoteError: null
isProcessing: false
remoteAttempts: 0
performedLocal: false
performedRemote: false
notifiedOffline: false
enqueue: (task, {silent}={}) ->
if not (task instanceof Task)
throw new Error("You must queue a `Task` object")
@_initializeTask(task)
@_dequeueObsoleteTasks(task)
@_queue.push(task)
@_update() if not silent
dequeue: (taskOrId={}, {silent}={}) ->
task = @_parseArgs(taskOrId)
task.abort() if @_shouldAbort(task)
task.queueState.isProcessing = false
task.cleanup()
@_queue.splice(@_queue.indexOf(task), 1)
@_moveToCompleted(task)
@_update() if not silent
clearQueue: ->
for task in @_queue by -1
@dequeue(task, silent: true) if task?
@_update()
_processQueue: ->
for task in @_queue by -1
@_processTask(task) if task?
_processTask: (task) ->
return if task.queueState.isProcessing
return if @_taskIsBlocked(task)
task.queueState.isProcessing = true
if task.queueState.performedLocal
@_performRemote(task)
else
task.performLocal().then =>
task.queueState.performedLocal = Date.now()
@_performRemote(task)
.catch @_onLocalError(task)
_performRemote: (task) ->
if @_isOnline()
task.queueState.remoteAttempts += 1
task.performRemote().then =>
task.queueState.performedRemote = Date.now()
@dequeue(task)
.catch @_onRemoteError(task)
else
@_notifyOffline(task)
_update: ->
@trigger()
@_saveQueueToDisk()
@_processQueue()
_dequeueObsoleteTasks: (task) ->
for otherTask in @_queue
if otherTask? and task.shouldDequeueOtherTask(otherTask)
@dequeue(otherTask, silent: true)
_shouldAbort: (task) ->
task.queueState.isProcessing or
(task.queueState.performedLocal and not task.queueState.performedRemote)
_taskIsBlocked: (task) ->
_.any @_queue, (otherTask) ->
task.shouldWaitForTask(otherTask) and task isnt otherTask
_notifyOffline: (task) ->
task.queueState.isProcessing = false
if not task.queueState.notifiedOffline
task.queueState.notifiedOffline = true
task.onError(new OfflineError)
_onLocalError: (task) -> (error) =>
task.queueState.isProcessing = false
task.queueState.localError = error
task.onError(error)
@dequeue(task)
_onRemoteError: (task) -> (apiError) =>
task.queueState.isProcessing = false
task.queueState.notifiedOffline = false
task.queueState.remoteError = apiError
task.onError(apiError)
@dequeue(task)
_isOnline: -> @_onlineStatus # TODO # OnlineStatusStore.isOnline()
_onOnlineChange: -> @_processQueue()
_parseArgs: (taskOrId) ->
if taskOrId instanceof Task
task = _.find @_queue, (task) -> task is taskOrId
else
task = _.findWhere(@_queue, id: taskOrId)
if not task?
throw new Error("Can't find task #{taskOrId}")
return task
_moveToCompleted: (task) ->
@_completed.push(task)
@_completed.shift() if @_completed.length > 1000
_restoreQueueFromDisk: ->
{modelReviver} = require '../models/utils'
try
queueFile = path.join(atom.getConfigDirPath(), 'task-queue.json')
queue = JSON.parse(fs.readFileSync(queueFile), modelReviver)
# We need to set the processing bit back to false so it gets
# re-retried upon inflation
for task in queue
if task.queueState?.isProcessing
task.queueState ?= {}
task.queueState.isProcessing = false
@_queue = queue
catch e
console.log("Queue deserialization failed with error: #{e.toString()}")
_saveQueueToDisk: (callback) ->
queueFile = path.join(atom.getConfigDirPath(), 'task-queue.json')
queueJSON = JSON.stringify((@_queue ? []))
fs.writeFile(queueFile, queueJSON, callback)

View file

@ -1,151 +0,0 @@
Task = "../tasks/task"
Reflux = require 'reflux'
Actions = require '../actions'
{modelReviver} = require '../models/utils'
_ = require 'underscore-plus'
path = require 'path'
fs = require 'fs-plus'
# The TaskStore listens for the queueTask action, performs tasks
# locally, and queues them for running against the API. In the
# future, it will be responsible for serializing the queue, doing
# dependency resolution, and marshalling recovery from errors.
if atom.state.mode is "composer" then return
MAX_RETRIES = 3
module.exports =
TaskStore = Reflux.createStore
init: ->
@_setDefaults()
try
queueFile = path.join(atom.getConfigDirPath(), 'task-queue.json')
@_queue = JSON.parse(fs.readFileSync(queueFile), modelReviver)
catch e
console.log("Queue deserialization failed with error: #{e.toString()}")
@listenTo(Actions.resetTaskQueue, @reset)
@listenTo(Actions.queueTask, @_onQueueTask)
@listenTo(Actions.abortTask, @_onAbortTask)
@listenTo(Actions.restartTaskQueue, @_onUnpause)
@listenTo(Actions.logout, @_onLogout)
@performNextTask()
# Finds tasks whose JSON form matches `criteria` and removes them from
# the queue. If the tasks are `pending` it will call `abort` on the task
# object, followed by `rollbackLocal`
_onAbortTask: (criteria, options={abort:true, rollbackLocal: true}) ->
matchFunc = (item) -> _.matches(criteria)(item.toJSON())
matchingQueuedTasks = _.filter(@_queue, matchFunc)
matchingPendingTasks = _.filter(@_pending, matchFunc)
for queuedTask in matchingQueuedTasks
queuedTask.rollbackLocal() if options.rollbackLocal
queuedTask.cleanup()
@_queue = _.difference @_queue, matchingQueuedTasks
for pendingTask in matchingPendingTasks
pendingTask.abort() if options.abort
pendingTask.rollbackLocal() if options.rollbackLocal
pendingTask.cleanup()
@_pending = _.difference @_pending, matchingPendingTasks
@trigger()
@persist =>
@performNextTask()
_setDefaults: ->
@_queue = []
@_pending = []
@_paused = false
@trigger()
reset: ->
@_setDefaults()
@persist()
_onLogout: ->
@reset()
@persist()
_onUnpause: ->
return unless @_paused
@_paused = false
@trigger()
@performNextTask()
_onQueueTask: (task) ->
@_queue = _.reject @_queue, (other) ->
task.shouldCancelUnstartedTask(other)
@trigger()
@_pending.push(task)
finish = =>
@_pending.splice(@_pending.indexOf(task), 1)
@trigger()
@persist =>
@performNextTask()
task.performLocal()
.then =>
@_queue.push(task)
finish()
.catch (error) =>
@_displayError("PerformLocal failed on #{task.constructor.name}. It will not be performed remotely.", error.message, error)
finish()
persist: (callback) ->
queueFile = path.join(atom.getConfigDirPath(), 'task-queue.json')
queueJSON = JSON.stringify([].concat(@_queue).concat(@_pending))
fs.writeFile(queueFile, queueJSON, callback)
queuedTasks: ->
@_queue
pendingTasks: ->
@_pending
isPaused: ->
@_paused
canPerformTask: (task) ->
for other in [].concat(@_pending, @_queue)
if other != task && task.shouldWaitForTask(other)
return false
true
performNextTask: ->
return Promise.resolve("Queue paused") if @_paused
task = _.find(@_queue, @canPerformTask.bind(@))
return Promise.resolve("Nothing to do") unless task
new Promise (resolve, reject) =>
@_queue.splice(@_queue.indexOf(task), 1)
@_pending.push(task)
@trigger()
finished = =>
task.cleanup()
@_pending.splice(@_pending.indexOf(task), 1)
@trigger()
@persist =>
@performNextTask()
resolve()
task.performRemote()
.then ->
finished()
.catch (error) =>
@_displayError(error)
if task.shouldRetry(error) and task.retryCount < MAX_RETRIES
task.retryCount += 1
@_queue.push(task)
else
task.rollbackLocal()
finished()
_displayError: (args...) ->
console.error(args...)

View file

@ -6,17 +6,10 @@ Thread = require '../models/thread'
_ = require 'underscore-plus'
async = require 'async'
module.exports =
class AddRemoveTagsTask extends Task
constructor: (@threadId, @tagIdsToAdd = [], @tagIdsToRemove = []) ->
@
rollbackLocal: ->
# Run performLocal backwards to undo the tag changes
a = @tagIdsToAdd
@tagIdsToAdd = @tagIdsToRemove
@tagIdsToRemove = a
@performLocal(-1)
constructor: (@threadId, @tagIdsToAdd = [], @tagIdsToRemove = []) -> super
performLocal: (versionIncrement = 1) ->
new Promise (resolve, reject) =>
@ -26,7 +19,7 @@ class AddRemoveTagsTask extends Task
return resolve() unless thread
@namespaceId = thread.namespaceId
# increment the thread version number
thread.version += versionIncrement
@ -43,23 +36,29 @@ class AddRemoveTagsTask extends Task
thread.tags.push(tag) if tag
DatabaseStore.persistModel(thread).then(resolve)
performRemote: ->
new Promise (resolve, reject) =>
# queue the operation to the server
atom.inbox.makeRequest {
atom.inbox.makeRequest
path: "/n/#{@namespaceId}/threads/#{@threadId}"
method: 'PUT'
body: {
body:
add_tags: @tagIdsToAdd,
remove_tags: @tagIdsToRemove
}
returnsModel: true
success: resolve
error: (apiError) =>
if "archive" in @tagIdsToAdd
Actions.postNotification({message: "Failed to archive thread: '#{@thread.subject}'", type: 'error'})
reject(apiError)
}
error: reject
module.exports = AddRemoveTagsTask
onAPIError: (apiError) ->
if "archive" in @tagIdsToAdd
msg = "Failed to archive thread: '#{@thread.subject}'"
Actions.postNotification({message: msg, type: "error"})
@_rollbackLocal()
Promise.resolve()
_rollbackLocal: ->
# Run performLocal backwards to undo the tag changes
a = @tagIdsToAdd
@tagIdsToAdd = @tagIdsToRemove
@tagIdsToRemove = a
@performLocal(-1)

View file

@ -7,11 +7,11 @@ SaveDraftTask = require './save-draft'
SendDraftTask = require './send-draft'
FileUploadTask = require './file-upload-task'
module.exports =
class DestroyDraftTask extends Task
constructor: (@draftLocalId) -> super
constructor: (@draftLocalId) ->
shouldCancelUnstartedTask: (other) ->
shouldDequeueOtherTask: (other) ->
(other instanceof SaveDraftTask and other.draftLocalId is @draftLocalId) or
(other instanceof SendDraftTask and other.draftLocalId is @draftLocalId) or
(other instanceof FileUploadTask and other.draftLocalId is @draftLocalId)
@ -43,16 +43,25 @@ class DestroyDraftTask extends Task
version: @draft.version
returnsModel: false
success: resolve
error: (apiError) ->
inboxMsg = apiError.body?.message ? ""
if inboxMsg.indexOf("No draft found") >= 0
# Draft has already been deleted, this is not really an error
resolve()
else if inboxMsg.indexOf("is not a draft") >= 0
# Draft has been sent, and can't be deleted. Not much we can
# do but finish
resolve()
else
reject(apiError)
error: reject
module.exports = DestroyDraftTask
onAPIError: (apiError) ->
inboxMsg = apiError.body?.message ? ""
if inboxMsg.indexOf("No draft found") >= 0
# Draft has already been deleted, this is not really an error
return true
else if inboxMsg.indexOf("is not a draft") >= 0
# Draft has been sent, and can't be deleted. Not much we can
# do but finish
return true
else
@_rollbackLocal()
onOtherError: -> Promise.resolve()
onTimeoutError: -> Promise.resolve()
onOfflineError: -> Promise.resolve()
_rollbackLocal: (msg) ->
msg ?= "Unable to delete this draft. Restoring..."
Actions.postNotification({message: msg, type: "error"})
DatabaseStore.persistModel(@draft) if @draft?

View file

@ -1,62 +0,0 @@
_ = require 'underscore-plus'
fs = require 'fs'
path = require 'path'
progress = require('request-progress')
Task = require './task'
File = require '../models/file'
Actions = require '../actions'
NamespaceStore = require '../stores/namespace-store'
module.exports =
class DownloadFileTask extends Task
constructor: ({@fileId, @downloadPath, @shellAction}) -> @
performLocal: ->
new Promise (resolve, reject) =>
return reject(new Error("Must pass a fileID to download")) unless @fileId?
return reject(new Error("Must have a path to download to")) unless @downloadPath?
Actions.downloadStateChanged @_downloadData("pending")
resolve()
rollbackLocal: ->
Actions.downloadStateChanged @_downloadData("failed")
performRemote: ->
new Promise (resolve, reject) =>
Actions.downloadStateChanged @_downloadData("started")
@request = atom.inbox.makeRequest(
path: "/n/#{@_namespaceId()}/files/#{@fileId}/download"
success: (data) =>
Actions.downloadStateChanged @_downloadData("completed")
Actions.fileDownloaded(@_downloadData("completed"))
resolve()
error: reject
)
progress(@request, {throtte: 250})
.on("progress", (progressData) =>
Actions.downloadStateChanged @_downloadData("progress", progressData)
).pipe(fs.createWriteStream(path.join(@downloadPath)))
abort: ->
@request?.abort()
Actions.downloadStateChanged @_downloadData("aborted")
# returns:
# state - One of "pending "started" "progress" "completed" "aborted" "failed"
# fileId - The id of the file
# shellAction - Action used to open the file after downloading
# downloadPath - The full path of the download location
# total - From request-progress: total number of bytes
# percent - From request-progress
# received - From request-progress: currently received bytes
_downloadData: (state, progressData={}) ->
_.extend progressData, {state: state},
fileId: @fileId
shellAction: @shellAction
downloadPath: @downloadPath
_namespaceId: -> NamespaceStore.current()?.id

View file

@ -12,7 +12,7 @@ class FileUploadTask extends Task
constructor: (@filePath, @messageLocalId) ->
@progress = null # The progress checking timer.
@
super
performLocal: ->
return Promise.reject(new Error("Must pass an absolute path to upload")) unless @filePath?.length
@ -20,9 +20,6 @@ class FileUploadTask extends Task
Actions.uploadStateChanged @_uploadData("pending")
Promise.resolve()
rollbackLocal: ->
Actions.uploadStateChanged @_uploadData("failed")
performRemote: ->
new Promise (resolve, reject) =>
Actions.uploadStateChanged @_uploadData("started")
@ -34,9 +31,7 @@ class FileUploadTask extends Task
returnsModel: true
formData: @_formData()
success: (json) => @_onUploadSuccess(json, resolve)
error: (apiError) =>
clearInterval(@progress)
reject(apiError)
error: reject
@progress = setInterval =>
Actions.uploadStateChanged(@_uploadData("progress"))
@ -50,6 +45,22 @@ class FileUploadTask extends Task
setTimeout =>
Actions.fileAborted(@_uploadData("aborted"))
, 1000 # To see the aborted state for a little bit
Promise.resolve()
onAPIError: (apiError) -> @_rollbackLocal()
onOtherError: (otherError) -> @_rollbackLocal()
onTimeoutError: -> Promise.resolve() # Do nothing. It could take a while.
onOfflineError: (offlineError) ->
msg = "You can't upload a file while you're offline."
@_rollbackLocal(msg)
_rollbackLocal: (msg) ->
clearInterval(@progress)
msg ?= "There was a problem uploading this file. Please try again later."
Actions.postNotification({message: msg, type: "error"})
Actions.uploadStateChanged @_uploadData("failed")
_onUploadSuccess: (json, taskCallback) ->
clearInterval(@progress)

View file

@ -3,17 +3,10 @@ DatabaseStore = require '../stores/database-store'
Actions = require '../actions'
_ = require 'underscore-plus'
module.exports =
class MarkMessageReadTask extends Task
constructor: (@message) ->
@
rollbackLocal: ->
new Promise (resolve, reject) =>
unless @_previousUnreadState?
reject(new Error("Cannot call rollbackLocal without previous call to performLocal"))
@message.unread = @_previousUnreadState
DatabaseStore.persistModel(@message).then(resolve).catch(reject)
constructor: (@message) -> super
performLocal: ->
new Promise (resolve, reject) =>
@ -38,4 +31,15 @@ class MarkMessageReadTask extends Task
error: reject
}
module.exports = MarkMessageReadTask
# We don't really care if this fails.
onAPIError: -> Promise.resolve()
onOtherError: -> Promise.resolve()
onTimeoutError: -> Promise.resolve()
onOfflineError: -> Promise.resolve()
_rollbackLocal: ->
new Promise (resolve, reject) =>
unless @_previousUnreadState?
reject(new Error("Cannot call rollbackLocal without previous call to performLocal"))
@message.unread = @_previousUnreadState
DatabaseStore.persistModel(@message).then(resolve).catch(reject)

View file

@ -9,44 +9,25 @@ Message = require '../models/message'
FileUploadTask = require './file-upload-task'
module.exports =
class SaveDraftTask extends Task
constructor: (@draftLocalId, @changes={}, {@localOnly}={}) ->
@_saveAttempts = 0
@queuedAt = Date.now()
@
super
# We can only cancel localOnly saves. API saves we keep.
#
# We also don't want to cancel any tasks that have a later timestamp
# creation than us. It's possible, because of retries, that tasks could
# get re-pushed onto the queue out of order.
shouldCancelUnstartedTask: (other) ->
shouldDequeueOtherTask: (other) ->
other instanceof SaveDraftTask and
other.draftLocalId is @draftLocalId and
other.localOnly is true and
other.queuedAt < @queuedAt # other is an older task.
# We want to wait for SendDraftTask because it's possible that we
# queued a SaveDraftTask (from some latent timer or something like that)
# while the SendDraftTask was in flight. Once the SendDraftTask is done,
# it will delete the draft from the database. The lack of the model in
# the DB will correctly prevent any late-to-the-game SaveDraftTask from
# executing.
shouldWaitForTask: (other) ->
# The task require needs to be put here otherwise we get a circular
# reference.
# SaveDraftTask depends on SendDraftTask
# SendDraftTask depends on SaveDraftTask
SendDraftTask = require './send-draft'
other instanceof SendDraftTask and other.draftLocalId is @draftLocalId
other instanceof FileUploadTask and other.draftLocalId is @draftLocalId
# It's possible that in between saves, the draft was destroyed on the
# server. Retry
shouldRetry: (error) ->
return true if error?.statusCode is 404
super(error)
performLocal: -> new Promise (resolve, reject) =>
if not @draftLocalId?
errMsg = "Attempt to call FileUploadTask.performLocal without @draftLocalId"
@ -85,6 +66,7 @@ class SaveDraftTask extends Task
initialId = draft.id
@_saveAttempts += 1
atom.inbox.makeRequest
path: path
method: method
@ -96,41 +78,67 @@ class SaveDraftTask extends Task
DatabaseStore.swapModel(oldModel: draft, newModel: newDraft, localId: @draftLocalId).then(resolve)
else
DatabaseStore.persistModel(newDraft).then(resolve)
error: (apiError) =>
# If we get a 404 from the server this might mean that the
# draft has been deleted from underneath us. We should retry
# again. Before we can retry we need to set the ID to a
# localID so that the next time this fires the model will
# trigger a POST instead of a PUT
#
# The shouldRetry method will also detect the error as a 404
# and retry.
if apiError.statusCode is 404
@_handleRetry(apiError, reject)
else
reject(apiError)
error: reject
_handleRetry: (apiError, reject) ->
onAPIError: (apiError) ->
# If we get a 404 from the server this might mean that the
# draft has been deleted from underneath us. We should retry
# again. Before we can retry we need to set the ID to a
# localID so that the next time this fires the model will
# trigger a POST instead of a PUT
if apiError.statusCode is 404
msg = "It looks like the draft you're working on got deleted from underneath you. We're creating a new draft and saving your work."
@_retrySaveAsNewDraft(msg)
else
if @_saveAttempts <= 1
msg = "We had a problem with the server. We're going to try and save your draft again."
@_retrySaveToExistingDraft(msg)
else
msg = "We're continuing to have issues saving your draft. It will be saved locally, but is failing to save on the server."
@notifyErrorMessage(msg)
onOtherError: ->
msg = "We had a serious issue trying to save your draft. Please copy the text out of the composer and try again later."
@notifyErrorMessage(msg)
onTimeoutError: ->
if @_saveAttempts <= 1
msg = "The server is taking an abnormally long time to respond. We're going to try and save your changes again."
@_retrySaveToExistingDraft(msg)
else
msg = "We're continuing to have issues saving your draft. It will be saved locally, but is failing to save on the server."
@notifyErrorMessage(msg)
onOfflineError: ->
msg = "WARNING: You are offline. Your edits are being saved locally. They will save to the server when you come back online"
@notifyErrorMessage(msg)
_retrySaveAsNewDraft: (msg) ->
TaskQueue = require '../stores/task-queue'
DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) =>
if not draft?
# In the time since we asked the API for something, the draft has
# been deleted. Nothing we can do now.
msg = "The server returned an error, but the draft #{@draftLocalId} dissapeared"
console.error(msg, apiError)
reject(new Error(msg))
else if isTempId(draft.id)
msg = "The server returned an error, but the draft #{@draftLocalId} got reset to a localId"
console.error(msg, apiError)
reject(new Error(msg))
else
newJSON = _.extend({}, draft.toJSON(), id: generateTempId())
newDraft = (new Message).fromJSON(newJSON)
DatabaseStore.swapModel(oldModel: draft, newModel: newDraft, localId: @draftLocalId).then ->
reject(apiError)
console.log "Couldn't find draft!", @draftLocalId
@_onOtherError()
newJSON = _.clone(draft.toJSON())
newJSON.id = generateTempId() unless isTempId(draft.id)
newDraft = (new Message).fromJSON(newJSON)
DatabaseStore.swapModel(oldModel: draft, newModel: newDraft, localId: @draftLocalId).then =>
TaskQueue.enqueue @
@notifyErrorMessage(msg)
_retrySaveToExistingDraft: (msg) ->
TaskQueue = require '../stores/task-queue'
DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) =>
if not draft?
console.log "Couldn't find draft!", @draftLocalId
@_onOtherError()
TaskQueue.enqueue @
@notifyErrorMessage(msg)
_applyChangesToDraft: (draft, changes={}) ->
for key, definition of draft.attributes()
draft[key] = changes[key] if changes[key]?
return draft
module.exports = SaveDraftTask

View file

@ -6,11 +6,12 @@ Message = require '../models/message'
Task = require './task'
SaveDraftTask = require './save-draft'
module.exports =
class SendDraftTask extends Task
constructor: (@draftLocalId) -> @
constructor: (@draftLocalId) -> super
shouldCancelUnstartedTask: (other) ->
shouldDequeueOtherTask: (other) ->
other instanceof SendDraftTask and other.draftLocalId is @draftLocalId
shouldWaitForTask: (other) ->
@ -45,5 +46,19 @@ class SendDraftTask extends Task
Actions.postNotification({message: "Sent!", type: 'success'})
DatabaseStore.unpersistModel(draft).then(resolve)
error: reject
module.exports = SendDraftTask
onAPIError: ->
msg = "Our server is having problems. Your messages has NOT been sent"
@notifyErrorMessage(msg)
onOtherError: ->
msg = "We had a serious issue while sending. Your messages has NOT been sent"
@notifyErrorMessage(msg)
onTimeoutError: ->
msg = "The server is taking an abnormally long time to respond. Your messages has NOT been sent"
@notifyErrorMessage(msg)
onOfflineError: ->
msg = "You are offline. Your message has NOT been sent. Please send your message when you come back online."
@notifyErrorMessage(msg)

View file

@ -1,4 +1,9 @@
_ = require 'underscore-plus'
{generateTempId} = require '../models/utils'
Actions = require '../actions'
{APIError,
OfflineError,
TimeoutError} = require '../errors'
# Tasks represent individual changes to the datastore that
# alter the local cache and need to be synced back to the server.
@ -23,7 +28,7 @@ _ = require 'underscore-plus'
# shouldWaitForTask(other). For example, the SendDraft task is dependent
# on the draft's files' UploadFile tasks completing.
# Tasks may also implement shouldCancelUnstartedTask(other). Returning true
# Tasks may also implement shouldDequeueOtherTask(other). Returning true
# will cause the other event to be removed from the queue. This is useful in
# offline mode especially, when the user might Save,Save,Save,Save,Send.
# Each newly queued Save can cancel the (unstarted) save task in the queue.
@ -33,42 +38,55 @@ _ = require 'underscore-plus'
# serialize / deserialize to convert to / from raw JSON.
class Task
constructor: ->
@retryCount = 0
## These are commonly overridden ##
constructor: -> @id = generateTempId()
# Called if a task is aborted while it is being processed
abort: ->
performLocal: -> Promise.resolve()
cleanup: ->
true
performRemote: -> Promise.resolve()
shouldCancelUnstartedTask: (other) ->
false
shouldDequeueOtherTask: (other) -> false
shouldWaitForTask: (other) ->
false
shouldWaitForTask: (other) -> false
shouldRetry: (error) ->
# Do not retry if this is a non-network error. Subclasses can override
# shouldRetry to add additional logic here.
return false unless error.statusCode?
cleanup: -> true
# Do not retry if the server returned a code indicating successful
# handling of the request with a bad outcome. Making the request again
# would not resolve the situation.
return error.statusCode not in [401,403,404,405,406,409]
abort: -> Promise.resolve()
performLocal: ->
onAPIError: (apiError) ->
msg = "We had a problem with the server. Your action was NOT completed."
Actions.postNotification({message: msg, type: "error"})
Promise.resolve()
rollbackLocal: ->
unless atom.inSpecMode()
console.log("Rolling back an instance of #{@constructor.name} which has not overridden rollbackLocal. Local cache may be contaminated.")
true
performRemote: ->
onOtherError: (otherError) ->
msg = "Something went wrong. Please report this issue immediately."
Actions.postNotification({message: msg, type: "error"})
Promise.resolve()
onTimeoutError: (timeoutError) ->
msg = "This took too long. Check your internet connection. Your action was NOT completed."
Actions.postNotification({message: msg, type: "error"})
Promise.resolve()
onOfflineError: (offlineError) ->
msg = "WARNING: You are offline. This will complete when you come back online."
Actions.postNotification({message: msg, type: "error"})
Promise.resolve()
## Only override if you know what you're doing ##
onError: (error) ->
if error instanceof APIError
@onAPIError(error)
else if error instanceof TimeoutError
@onTimeoutError(error)
else if error instanceof OfflineError
@onOfflineError(error)
else
@onOtherError(error)
notifyErrorMessage: (msg) ->
Actions.postNotification({message: msg, type: "error"})
toJSON: ->
json = _.clone(@)
json['object'] = @constructor.name

View file

@ -66,6 +66,7 @@
@background-color-success: @green;
@background-color-warning: #ff4800;
@background-color-error: @red;
@background-color-pending: @light-gray;
@base-border-color: darken(@background-color, 15%);
@border-color: darken(@background-color, 15%);
@ -124,4 +125,4 @@
@standard-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.22);
@standard-shadow-up: 0 -1px 2px 0 rgba(0, 0, 0, 0.22);
@standard-shadow-up: 0 -1px 2px 0 rgba(0, 0, 0, 0.22);