mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-21 07:46:06 +08:00
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:
parent
c952ea3b12
commit
132263c38c
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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: ->
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
400
spec-inbox/stores/task-queue-spec.coffee
Normal file
400
spec-inbox/stores/task-queue-spec.coffee
Normal 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
|
|
@ -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()
|
||||
|
|
@ -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 ->
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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", ->
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()]
|
||||
|
|
|
@ -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)
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
179
src/flux/stores/task-queue.coffee
Normal file
179
src/flux/stores/task-queue.coffee
Normal 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)
|
|
@ -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...)
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue