fix(drafts): Draft syncing completely disabled to reduce code complexity

Summary:
fix(streaming): Reconnect every 30 seconds, always

Never accept drafts via any API source

fix(attachments): Fix for changes to open API

Get rid of shouldAbort, just let tasks decide what to do in cleanup

Never let SaveDraftTask run while another SaveDraftTask for same draft is running

Never used IPC

Ignore destroy draft 404

Moving draft store proxy to draft store level

Only block on older saves

Replace SaveDraftTask with SyncbackDraftTask, do saving directly from proxy

Never sync back ;-)

Fix specs

Alter SendDraftTask so that it can send an unsaved draft

Test Plan: Run tests

Reviewers: evan

Reviewed By: evan

Differential Revision: https://review.inboxapp.com/D1245
This commit is contained in:
Ben Gotow 2015-03-02 11:23:35 -08:00
parent 71e216ddff
commit e1fc34a562
20 changed files with 251 additions and 299 deletions

View file

@ -2,11 +2,11 @@ React = require 'react'
_ = require 'underscore-plus' _ = require 'underscore-plus'
{Actions, {Actions,
DraftStore,
FileUploadStore, FileUploadStore,
ComponentRegistry} = require 'inbox-exports' ComponentRegistry} = require 'inbox-exports'
FileUploads = require './file-uploads.cjsx' FileUploads = require './file-uploads.cjsx'
DraftStoreProxy = require './draft-store-proxy'
ContenteditableToolbar = require './contenteditable-toolbar.cjsx' ContenteditableToolbar = require './contenteditable-toolbar.cjsx'
ContenteditableComponent = require './contenteditable-component.cjsx' ContenteditableComponent = require './contenteditable-component.cjsx'
ParticipantsTextField = require './participants-text-field.cjsx' ParticipantsTextField = require './participants-text-field.cjsx'
@ -67,7 +67,9 @@ ComposerView = React.createClass
@_prepareForDraft() @_prepareForDraft()
_prepareForDraft: -> _prepareForDraft: ->
@_proxy = new DraftStoreProxy(@props.localId) @_proxy = DraftStore.sessionForLocalId(@props.localId)
if @_proxy.draft()
@_onDraftChanged()
@unlisteners = [] @unlisteners = []
@unlisteners.push @_proxy.listen(@_onDraftChanged) @unlisteners.push @_proxy.listen(@_onDraftChanged)
@ -256,7 +258,6 @@ ComposerView = React.createClass
@_sendDraft({force: true}) @_sendDraft({force: true})
return return
@_proxy.changes.commit()
Actions.sendDraft(@props.localId) Actions.sendDraft(@props.localId)
_destroyDraft: -> _destroyDraft: ->

View file

@ -119,7 +119,6 @@ describe "TaskQueue", ->
taskToDie = makeRemoteFailed(new TaskSubclassA()) taskToDie = makeRemoteFailed(new TaskSubclassA())
spyOn(TaskQueue, "dequeue").andCallThrough() spyOn(TaskQueue, "dequeue").andCallThrough()
spyOn(taskToDie, "abort")
TaskQueue._queue = [taskToDie, @remoteFailed] TaskQueue._queue = [taskToDie, @remoteFailed]
TaskQueue.enqueue(new KillsTaskA()) TaskQueue.enqueue(new KillsTaskA())
@ -127,7 +126,6 @@ describe "TaskQueue", ->
expect(TaskQueue._queue.length).toBe 2 expect(TaskQueue._queue.length).toBe 2
expect(TaskQueue.dequeue).toHaveBeenCalledWith(taskToDie, silent: true) expect(TaskQueue.dequeue).toHaveBeenCalledWith(taskToDie, silent: true)
expect(TaskQueue.dequeue.calls.length).toBe 1 expect(TaskQueue.dequeue.calls.length).toBe 1
expect(taskToDie.abort).toHaveBeenCalled()
describe "dequeue", -> describe "dequeue", ->
beforeEach -> beforeEach ->
@ -147,37 +145,11 @@ describe "TaskQueue", ->
it "throws an error if the task isn't found", -> it "throws an error if the task isn't found", ->
expect( -> TaskQueue.dequeue("bad")).toThrow() expect( -> TaskQueue.dequeue("bad")).toThrow()
it "doesn't abort unstarted tasks", -> it "calls cleanup on dequeued 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") spyOn(@remoteStarted, "cleanup")
TaskQueue.dequeue(@remoteStarted, silent: true) TaskQueue.dequeue(@remoteStarted, silent: true)
expect(@remoteStarted.cleanup).toHaveBeenCalled() 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", -> it "moves it from the queue", ->
TaskQueue.dequeue(@remoteStarted, silent: true) TaskQueue.dequeue(@remoteStarted, silent: true)
expect(TaskQueue._queue.length).toBe 3 expect(TaskQueue._queue.length).toBe 3

View file

@ -81,18 +81,38 @@ describe "FileUploadTask", ->
expect(options.method).toBe('POST') expect(options.method).toBe('POST')
expect(options.formData.file.value).toBe("Read Stream") expect(options.formData.file.value).toBe("Read Stream")
it "can abort the upload with the full file path", ->
spyOn(@task, "_getBytesUploaded").andReturn(100)
waitsForPromise => @task.performRemote().then =>
@task.abort()
expect(@req.abort).toHaveBeenCalled()
data = _.extend uploadData,
state: "aborted"
bytesUploaded: 100
expect(Actions.uploadStateChanged).toHaveBeenCalledWith(data)
it "notifies when the file successfully uploaded", -> it "notifies when the file successfully uploaded", ->
spyOn(@task, "_completedNotification").andReturn(100) spyOn(@task, "_completedNotification").andReturn(100)
waitsForPromise => @task.performRemote().then => waitsForPromise => @task.performRemote().then =>
file = (new File).fromJSON(fileJSON) file = (new File).fromJSON(fileJSON)
expect(@task._completedNotification).toHaveBeenCalledWith(file) expect(@task._completedNotification).toHaveBeenCalledWith(file)
describe "cleanup", ->
it "should not do anything if the request has finished", ->
req = jasmine.createSpyObj('req', ['abort'])
reqSuccess = null
spyOn(atom.inbox, 'makeRequest').andCallFake (reqParams) ->
reqSuccess = reqParams.success
req
@task.performRemote()
reqSuccess([fileJSON])
@task.cleanup()
expect(req.abort).not.toHaveBeenCalled()
it "should cancel the request if it's in flight", ->
req = jasmine.createSpyObj('req', ['abort'])
spyOn(atom.inbox, 'makeRequest').andCallFake (reqParams) -> req
spyOn(Actions, "uploadStateChanged")
@task.performRemote()
@task.cleanup()
expect(req.abort).toHaveBeenCalled()
data = _.extend uploadData,
state: "aborted"
bytesUploaded: 0
expect(Actions.uploadStateChanged).toHaveBeenCalledWith(data)

View file

@ -1,5 +1,5 @@
Actions = require '../../src/flux/actions' Actions = require '../../src/flux/actions'
SaveDraftTask = require '../../src/flux/tasks/save-draft' SyncbackDraftTask = require '../../src/flux/tasks/syncback-draft'
SendDraftTask = require '../../src/flux/tasks/send-draft' SendDraftTask = require '../../src/flux/tasks/send-draft'
DatabaseStore = require '../../src/flux/stores/database-store' DatabaseStore = require '../../src/flux/stores/database-store'
{generateTempId} = require '../../src/flux/models/utils' {generateTempId} = require '../../src/flux/models/utils'
@ -9,7 +9,7 @@ _ = require 'underscore-plus'
describe "SendDraftTask", -> describe "SendDraftTask", ->
describe "shouldWaitForTask", -> describe "shouldWaitForTask", ->
it "should return any SaveDraftTasks for the same draft", -> it "should return any SyncbackDraftTasks for the same draft", ->
@draftA = new Message @draftA = new Message
version: '1' version: '1'
id: '1233123AEDF1' id: '1233123AEDF1'
@ -30,8 +30,8 @@ describe "SendDraftTask", ->
name: 'Dummy' name: 'Dummy'
email: 'dummy@inboxapp.com' email: 'dummy@inboxapp.com'
@saveA = new SaveDraftTask('localid-A') @saveA = new SyncbackDraftTask('localid-A')
@saveB = new SaveDraftTask('localid-B') @saveB = new SyncbackDraftTask('localid-B')
@sendA = new SendDraftTask('localid-A') @sendA = new SendDraftTask('localid-A')
expect(@sendA.shouldWaitForTask(@saveA)).toBe(true) expect(@sendA.shouldWaitForTask(@saveA)).toBe(true)
@ -40,8 +40,8 @@ describe "SendDraftTask", ->
beforeEach -> beforeEach ->
TaskQueue._queue = [] TaskQueue._queue = []
TaskQueue._completed = [] TaskQueue._completed = []
@saveTask = new SaveDraftTask('localid-A') @saveTask = new SyncbackDraftTask('localid-A')
@saveTaskB = new SaveDraftTask('localid-B') @saveTaskB = new SyncbackDraftTask('localid-B')
@sendTask = new SendDraftTask('localid-A') @sendTask = new SendDraftTask('localid-A')
@tasks = [@saveTask, @saveTaskB, @sendTask] @tasks = [@saveTask, @saveTaskB, @sendTask]
@ -124,13 +124,33 @@ describe "SendDraftTask", ->
expect(options.path).toBe("/n/#{@draft.namespaceId}/send") expect(options.path).toBe("/n/#{@draft.namespaceId}/send")
expect(options.method).toBe('POST') expect(options.method).toBe('POST')
it "should send the draft ID and version", -> describe "when the draft has been saved", ->
waitsForPromise => it "should send the draft ID and version", ->
@task.performRemote().then => waitsForPromise =>
expect(atom.inbox.makeRequest.calls.length).toBe(1) @task.performRemote().then =>
options = atom.inbox.makeRequest.mostRecentCall.args[0] expect(atom.inbox.makeRequest.calls.length).toBe(1)
expect(options.body.version).toBe(@draft.version) options = atom.inbox.makeRequest.mostRecentCall.args[0]
expect(options.body.draft_id).toBe(@draft.id) expect(options.body.version).toBe(@draft.version)
expect(options.body.draft_id).toBe(@draft.id)
describe "when the draft has not been saved", ->
beforeEach ->
@draft = new Message
id: generateTempId()
namespaceId: 'A12ADE'
subject: 'New Draft'
draft: true
to:
name: 'Dummy'
email: 'dummy@inboxapp.com'
@task = new SendDraftTask(@draft)
it "should send the draft JSON", ->
waitsForPromise =>
@task.performRemote().then =>
expect(atom.inbox.makeRequest.calls.length).toBe(1)
options = atom.inbox.makeRequest.mostRecentCall.args[0]
expect(options.body).toEqual(@draft.toJSON())
it "should pass returnsModel:true so that the draft is saved to the data store when returned", -> it "should pass returnsModel:true so that the draft is saved to the data store when returned", ->
waitsForPromise => waitsForPromise =>

View file

@ -9,7 +9,7 @@ Contact = require '../../src/flux/models/contact'
DatabaseStore = require '../../src/flux/stores/database-store' DatabaseStore = require '../../src/flux/stores/database-store'
TaskQueue = require '../../src/flux/stores/task-queue' TaskQueue = require '../../src/flux/stores/task-queue'
SaveDraftTask = require '../../src/flux/tasks/save-draft' SyncbackDraftTask = require '../../src/flux/tasks/syncback-draft'
inboxError = inboxError =
message: "No draft with public id bvn4aydxuyqlbmzowh4wraysg", message: "No draft with public id bvn4aydxuyqlbmzowh4wraysg",
@ -33,7 +33,7 @@ testData =
localDraft = new Message _.extend {}, testData, {id: "local-id"} localDraft = new Message _.extend {}, testData, {id: "local-id"}
remoteDraft = new Message _.extend {}, testData, {id: "remoteid1234"} remoteDraft = new Message _.extend {}, testData, {id: "remoteid1234"}
describe "SaveDraftTask", -> describe "SyncbackDraftTask", ->
beforeEach -> beforeEach ->
spyOn(DatabaseStore, "findByLocalId").andCallFake (klass, localId) -> spyOn(DatabaseStore, "findByLocalId").andCallFake (klass, localId) ->
if localId is "localDraftId" then Promise.resolve(localDraft) if localId is "localDraftId" then Promise.resolve(localDraft)
@ -46,53 +46,19 @@ describe "SaveDraftTask", ->
spyOn(DatabaseStore, "swapModel").andCallFake -> spyOn(DatabaseStore, "swapModel").andCallFake ->
Promise.resolve() Promise.resolve()
describe "performLocal", ->
it "rejects if it isn't constructed with a draftLocalId", ->
task = new SaveDraftTask
waitsForPromise =>
task.performLocal().catch (error) ->
expect(error.message).toBeDefined()
it "does nothing if there are no new changes", ->
task = new SaveDraftTask("localDraftId")
waitsForPromise =>
task.performLocal().then ->
expect(DatabaseStore.persistModel).not.toHaveBeenCalled()
it "persists to the Database if there are new changes", ->
task = new SaveDraftTask("localDraftId", body: "test body")
waitsForPromise =>
task.performLocal().then ->
expect(DatabaseStore.persistModel).toHaveBeenCalled()
newBody = DatabaseStore.persistModel.calls[0].args[0].body
expect(newBody).toBe "test body"
it "does nothing if no draft can be found in the db", ->
task = new SaveDraftTask("missingDraftId")
waitsForPromise =>
task.performLocal().then ->
expect(DatabaseStore.persistModel).not.toHaveBeenCalled()
describe "performRemote", -> describe "performRemote", ->
beforeEach -> beforeEach ->
spyOn(atom.inbox, 'makeRequest').andCallFake (opts) -> spyOn(atom.inbox, 'makeRequest').andCallFake (opts) ->
opts.success(remoteDraft.toJSON()) if opts.success opts.success(remoteDraft.toJSON()) if opts.success
it "does nothing if localOnly is set to true", ->
task = new SaveDraftTask("localDraftId", {}, localOnly: true)
waitsForPromise =>
task.performRemote().then ->
expect(DatabaseStore.findByLocalId).not.toHaveBeenCalled()
expect(atom.inbox.makeRequest).not.toHaveBeenCalled()
it "does nothing if no draft can be found in the db", -> it "does nothing if no draft can be found in the db", ->
task = new SaveDraftTask("missingDraftId") task = new SyncbackDraftTask("missingDraftId")
waitsForPromise => waitsForPromise =>
task.performRemote().then -> task.performRemote().then ->
expect(atom.inbox.makeRequest).not.toHaveBeenCalled() expect(atom.inbox.makeRequest).not.toHaveBeenCalled()
it "should start an API request with the Message JSON", -> it "should start an API request with the Message JSON", ->
task = new SaveDraftTask("localDraftId") task = new SyncbackDraftTask("localDraftId")
waitsForPromise => waitsForPromise =>
task.performRemote().then -> task.performRemote().then ->
expect(atom.inbox.makeRequest).toHaveBeenCalled() expect(atom.inbox.makeRequest).toHaveBeenCalled()
@ -100,7 +66,7 @@ describe "SaveDraftTask", ->
expect(reqBody.subject).toEqual testData.subject expect(reqBody.subject).toEqual testData.subject
it "should do a PUT when the draft has already been saved", -> it "should do a PUT when the draft has already been saved", ->
task = new SaveDraftTask("remoteDraftId") task = new SyncbackDraftTask("remoteDraftId")
waitsForPromise => waitsForPromise =>
task.performRemote().then -> task.performRemote().then ->
expect(atom.inbox.makeRequest).toHaveBeenCalled() expect(atom.inbox.makeRequest).toHaveBeenCalled()
@ -109,7 +75,7 @@ describe "SaveDraftTask", ->
expect(options.method).toBe('PUT') expect(options.method).toBe('PUT')
it "should do a POST when the draft is unsaved", -> it "should do a POST when the draft is unsaved", ->
task = new SaveDraftTask("localDraftId") task = new SyncbackDraftTask("localDraftId")
waitsForPromise => waitsForPromise =>
task.performRemote().then -> task.performRemote().then ->
expect(atom.inbox.makeRequest).toHaveBeenCalled() expect(atom.inbox.makeRequest).toHaveBeenCalled()
@ -118,7 +84,7 @@ describe "SaveDraftTask", ->
expect(options.method).toBe('POST') expect(options.method).toBe('POST')
it "should pass returnsModel:false so that the draft can be manually removed/added to the database, accounting for its ID change", -> it "should pass returnsModel:false so that the draft can be manually removed/added to the database, accounting for its ID change", ->
task = new SaveDraftTask("localDraftId") task = new SyncbackDraftTask("localDraftId")
waitsForPromise => waitsForPromise =>
task.performRemote().then -> task.performRemote().then ->
expect(atom.inbox.makeRequest).toHaveBeenCalled() expect(atom.inbox.makeRequest).toHaveBeenCalled()
@ -126,14 +92,14 @@ describe "SaveDraftTask", ->
expect(options.returnsModel).toBe(false) expect(options.returnsModel).toBe(false)
it "should swap the ids if we got a new one from the DB", -> it "should swap the ids if we got a new one from the DB", ->
task = new SaveDraftTask("localDraftId") task = new SyncbackDraftTask("localDraftId")
waitsForPromise => waitsForPromise =>
task.performRemote().then -> task.performRemote().then ->
expect(DatabaseStore.swapModel).toHaveBeenCalled() expect(DatabaseStore.swapModel).toHaveBeenCalled()
expect(DatabaseStore.persistModel).not.toHaveBeenCalled() expect(DatabaseStore.persistModel).not.toHaveBeenCalled()
it "should not swap the ids if we're using a persisted one", -> it "should not swap the ids if we're using a persisted one", ->
task = new SaveDraftTask("remoteDraftId") task = new SyncbackDraftTask("remoteDraftId")
waitsForPromise => waitsForPromise =>
task.performRemote().then -> task.performRemote().then ->
expect(DatabaseStore.swapModel).not.toHaveBeenCalled() expect(DatabaseStore.swapModel).not.toHaveBeenCalled()
@ -146,7 +112,7 @@ describe "SaveDraftTask", ->
opts.error(testError(opts)) if opts.error opts.error(testError(opts)) if opts.error
it "resets the id", -> it "resets the id", ->
task = new SaveDraftTask("remoteDraftId") task = new SyncbackDraftTask("remoteDraftId")
task.onAPIError(testError({})) task.onAPIError(testError({}))
waitsFor -> waitsFor ->
DatabaseStore.swapModel.calls.length > 0 DatabaseStore.swapModel.calls.length > 0

View file

@ -757,6 +757,11 @@ class Atom extends Model
app.emit('will-exit') app.emit('will-exit')
remote.process.exit(status) remote.process.exit(status)
showOpenDialog: (options, callback) ->
parentWindow = if process.platform is 'darwin' then null else @getCurrentWindow()
dialog = remote.require('dialog')
dialog.showOpenDialog(parentWindow, options, callback)
showSaveDialog: (defaultPath, callback) -> showSaveDialog: (defaultPath, callback) ->
parentWindow = if process.platform is 'darwin' then null else @getCurrentWindow() parentWindow = if process.platform is 'darwin' then null else @getCurrentWindow()
dialog = remote.require('dialog') dialog = remote.require('dialog')

View file

@ -215,9 +215,6 @@ class AtomApplication
@on 'application:quit', => @on 'application:quit', =>
@quitting = true @quitting = true
app.quit() app.quit()
@on 'application:open-file-to-window', -> @promptForPath({type: 'file', to_window: true})
@on 'application:open-dev', -> @promptForPath(devMode: true)
@on 'application:open-safe', -> @promptForPath(safeMode: true)
@on 'application:inspect', ({x,y, atomWindow}) -> @on 'application:inspect', ({x,y, atomWindow}) ->
atomWindow ?= @focusedWindow() atomWindow ?= @focusedWindow()
atomWindow?.browserWindow.inspectElement(x, y) atomWindow?.browserWindow.inspectElement(x, y)

View file

@ -1,4 +1,3 @@
ipc = require 'ipc'
Reflux = require 'reflux' Reflux = require 'reflux'
# These actions are rebroadcast through the ActionBridge to all # These actions are rebroadcast through the ActionBridge to all
@ -46,9 +45,6 @@ windowActions = [
# Fired when a dialog is opened and a file is selected # Fired when a dialog is opened and a file is selected
"clearDeveloperConsole", "clearDeveloperConsole",
"openPathsSelected",
"savePathSelected",
# Actions for Selection State # Actions for Selection State
"selectNamespaceId", "selectNamespaceId",
"selectThreadId", "selectThreadId",
@ -87,7 +83,7 @@ windowActions = [
# Some file actions only need to be processed in their current window # Some file actions only need to be processed in their current window
"attachFile", "attachFile",
"abortUpload", "abortUpload",
"persistUploadedFile", # This touches the DB, should only be in main window "attachFileComplete",
"removeFile", "removeFile",
"fetchAndOpenFile", "fetchAndOpenFile",
"fetchAndSaveFile", "fetchAndSaveFile",
@ -106,10 +102,4 @@ Actions.windowActions = windowActions
Actions.mainWindowActions = mainWindowActions Actions.mainWindowActions = mainWindowActions
Actions.globalActions = globalActions Actions.globalActions = globalActions
ipc.on "paths-to-open", (pathsToOpen=[]) ->
Actions.openPathsSelected(pathsToOpen)
ipc.on "save-file-selected", (savePath) ->
Actions.savePathSelected(savePath)
module.exports = Actions module.exports = Actions

View file

@ -193,11 +193,11 @@ class InboxAPI
Thread = require './models/thread' Thread = require './models/thread'
return @_shouldAcceptModelIfNewer(Thread, model) return @_shouldAcceptModelIfNewer(Thread, model)
# For some reason, we occasionally get a delta with: # For the time being, we never accept drafts from the server. This single
# delta.object = 'message', delta.attributes.object = 'draft' # change ensures that all drafts in the system are authored locally. To
# revert, change back to use _shouldAcceptModelIfNewer
if classname is "draft" or model?.object is "draft" if classname is "draft" or model?.object is "draft"
Message = require './models/message' return Promise.reject()
return @_shouldAcceptModelIfNewer(Message, model)
Promise.resolve() Promise.resolve()

View file

@ -18,7 +18,7 @@ class InboxLongConnection
@_emitter = new Emitter @_emitter = new Emitter
@_state = 'idle' @_state = 'idle'
@_req = null @_req = null
@_reqPingInterval = null @_reqForceReconnectInterval = null
@_buffer = null @_buffer = null
@_deltas = [] @_deltas = []
@ -86,9 +86,11 @@ class InboxLongConnection
@_buffer = bufferJSONs[bufferJSONs.length - 1] @_buffer = bufferJSONs[bufferJSONs.length - 1]
start: -> start: ->
throw (new Error 'Cannot start polling without auth token.') unless @_inbox.APIToken return if @_state is InboxLongConnection.State.Ended
return if @_req return if @_req
throw (new Error 'Cannot start polling without auth token.') unless @_inbox.APIToken
console.log("Long Polling Connection: Starting....") console.log("Long Polling Connection: Starting....")
@withCursor (cursor) => @withCursor (cursor) =>
return if @state is InboxLongConnection.State.Ended return if @state is InboxLongConnection.State.Ended
@ -122,18 +124,22 @@ class InboxLongConnection
req.write("1") req.write("1")
@_req = req @_req = req
@_reqPingInterval = setInterval ->
req.write("1")
,250
retry: -> # Currently we have trouble identifying when the connection has closed.
# Instead of trying to fix that, just reconnect every 30 seconds.
@_reqForceReconnectInterval = setInterval =>
@retry(true)
,30000
retry: (immediate = false) ->
return if @_state is InboxLongConnection.State.Ended return if @_state is InboxLongConnection.State.Ended
@setState(InboxLongConnection.State.Retrying) @setState(InboxLongConnection.State.Retrying)
@cleanup() @cleanup()
startDelay = if immediate then 0 else 10000
setTimeout => setTimeout =>
@start() @start()
, 10000 , startDelay
end: -> end: ->
console.log("Long Polling Connection: Closed.") console.log("Long Polling Connection: Closed.")
@ -141,8 +147,8 @@ class InboxLongConnection
@cleanup() @cleanup()
cleanup: -> cleanup: ->
clearInterval(@_reqPingInterval) if @_reqPingInterval clearInterval(@_reqForceReconnectInterval) if @_reqForceReconnectInterval
@_reqPingInterval = null @_reqForceReconnectInterval = null
if @_req if @_req
@_req.end() @_req.end()
@_req.abort() @_req.abort()

View file

@ -16,7 +16,7 @@ utils =
SalesforceContact = require './salesforce-contact' SalesforceContact = require './salesforce-contact'
SalesforceTask = require './salesforce-task' SalesforceTask = require './salesforce-task'
SaveDraftTask = require '../tasks/save-draft' SyncbackDraftTask = require '../tasks/syncback-draft'
SendDraftTask = require '../tasks/send-draft' SendDraftTask = require '../tasks/send-draft'
DestroyDraftTask = require '../tasks/destroy-draft' DestroyDraftTask = require '../tasks/destroy-draft'
AddRemoveTagsTask = require '../tasks/add-remove-tags' AddRemoveTagsTask = require '../tasks/add-remove-tags'
@ -43,7 +43,7 @@ utils =
'MarkMessageReadTask': MarkMessageReadTask 'MarkMessageReadTask': MarkMessageReadTask
'AddRemoveTagsTask': AddRemoveTagsTask 'AddRemoveTagsTask': AddRemoveTagsTask
'SendDraftTask': SendDraftTask 'SendDraftTask': SendDraftTask
'SaveDraftTask': SaveDraftTask 'SyncbackDraftTask': SyncbackDraftTask
'DestroyDraftTask': DestroyDraftTask 'DestroyDraftTask': DestroyDraftTask
'FileUploadTask': FileUploadTask 'FileUploadTask': FileUploadTask
} }

View file

@ -1,4 +1,5 @@
{Message, Actions,DraftStore} = require 'inbox-exports' Message = require '../models/message'
Actions = require '../actions'
EventEmitter = require('events').EventEmitter EventEmitter = require('events').EventEmitter
_ = require 'underscore-plus' _ = require 'underscore-plus'
@ -15,7 +16,11 @@ _ = require 'underscore-plus'
# #
class DraftChangeSet class DraftChangeSet
constructor: (@localId, @_onChange) -> constructor: (@localId, @_onChange) ->
@reset()
reset: ->
@_pending = {} @_pending = {}
clearTimeout(@_timer) if @_timer
@_timer = null @_timer = null
add: (changes, immediate) => add: (changes, immediate) =>
@ -28,9 +33,12 @@ class DraftChangeSet
@_timer = setTimeout(@commit, 5000) @_timer = setTimeout(@commit, 5000)
commit: => commit: =>
@_pending.localId = @localId return unless Object.keys(@_pending).length > 0
if Object.keys(@_pending).length > 1
Actions.saveDraft(@_pending) DatabaseStore = require './database-store'
DatabaseStore.findByLocalId(Message, @localId).then (draft) =>
draft = @applyToModel(draft)
DatabaseStore.persistModel(draft)
@_pending = {} @_pending = {}
applyToModel: (model) => applyToModel: (model) =>
@ -51,30 +59,44 @@ module.exports =
class DraftStoreProxy class DraftStoreProxy
constructor: (@draftLocalId) -> constructor: (@draftLocalId) ->
DraftStore = require './draft-store'
@unlisteners = [] @unlisteners = []
@unlisteners.push DraftStore.listen(@_onDraftChanged, @) @unlisteners.push DraftStore.listen(@_onDraftChanged, @)
@unlisteners.push Actions.didSwapModel.listen(@_onDraftSwapped, @) @unlisteners.push Actions.didSwapModel.listen(@_onDraftSwapped, @)
@_emitter = new EventEmitter() @_emitter = new EventEmitter()
@_draft = false @_draft = false
@_reloadDraft() @_draftPromise = null
@changes = new DraftChangeSet @draftLocalId, => @changes = new DraftChangeSet @draftLocalId, =>
@_emitter.emit('trigger') @_emitter.emit('trigger')
@prepare()
draft: -> draft: ->
@changes.applyToModel(@_draft) @changes.applyToModel(@_draft)
@_draft @_draft
prepare: ->
@_draftPromise ?= new Promise (resolve, reject) =>
DatabaseStore = require './database-store'
DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) =>
@_draft = draft
@_emitter.emit('trigger')
resolve(@)
.catch(reject)
@_draftPromise
listen: (callback, bindContext) -> listen: (callback, bindContext) ->
eventHandler = (args) -> eventHandler = (args) ->
callback.apply(bindContext, args) callback.apply(bindContext, args)
@_emitter.addListener('trigger', eventHandler) @_emitter.addListener('trigger', eventHandler)
return => return =>
@_emitter.removeListener('trigger', eventHandler) @_emitter.removeListener('trigger', eventHandler)
if @_emitter.listeners('trigger').length == 0
# Unlink ourselves from the stores/actions we were listening to cleanup: ->
# so that we can be garbage collected # Unlink ourselves from the stores/actions we were listening to
unlisten() for unlisten in @unlisteners # so that we can be garbage collected
unlisten() for unlisten in @unlisteners
_onDraftChanged: (change) -> _onDraftChanged: (change) ->
# We don't accept changes unless our draft object is loaded # We don't accept changes unless our draft object is loaded
@ -93,12 +115,3 @@ class DraftStoreProxy
if change.oldModel.id is @_draft.id if change.oldModel.id is @_draft.id
@_draft = change.newModel @_draft = change.newModel
@_emitter.emit('trigger') @_emitter.emit('trigger')
_reloadDraft: ->
promise = DraftStore.findByLocalId(@draftLocalId)
promise.catch (err) ->
console.log(err)
promise.then (draft) =>
@_draft = draft
@_emitter.emit('trigger')

View file

@ -2,10 +2,10 @@ _ = require 'underscore-plus'
moment = require 'moment' moment = require 'moment'
Reflux = require 'reflux' Reflux = require 'reflux'
DraftStoreProxy = require './draft-store-proxy'
DatabaseStore = require './database-store' DatabaseStore = require './database-store'
NamespaceStore = require './namespace-store' NamespaceStore = require './namespace-store'
SaveDraftTask = require '../tasks/save-draft'
SendDraftTask = require '../tasks/send-draft' SendDraftTask = require '../tasks/send-draft'
DestroyDraftTask = require '../tasks/destroy-draft' DestroyDraftTask = require '../tasks/destroy-draft'
@ -21,6 +21,7 @@ Actions = require '../actions'
# #
# Remember that a "Draft" is actually just a "Message" with draft: true. # Remember that a "Draft" is actually just a "Message" with draft: true.
# #
module.exports = module.exports =
DraftStore = Reflux.createStore DraftStore = Reflux.createStore
init: -> init: ->
@ -32,18 +33,19 @@ DraftStore = Reflux.createStore
@listenTo Actions.composePopoutDraft, @_onComposePopoutDraft @listenTo Actions.composePopoutDraft, @_onComposePopoutDraft
@listenTo Actions.composeNewBlankDraft, @_onComposeNewBlankDraft @listenTo Actions.composeNewBlankDraft, @_onComposeNewBlankDraft
@listenTo Actions.saveDraft, @_onSaveDraft
@listenTo Actions.sendDraft, @_onSendDraft @listenTo Actions.sendDraft, @_onSendDraft
@listenTo Actions.destroyDraft, @_onDestroyDraft @listenTo Actions.destroyDraft, @_onDestroyDraft
@listenTo Actions.removeFile, @_onRemoveFile @listenTo Actions.removeFile, @_onRemoveFile
@listenTo Actions.persistUploadedFile, @_onFileUploaded @listenTo Actions.attachFileComplete, @_onAttachFileComplete
@_draftSessions = {}
######### PUBLIC ####################################################### ######### PUBLIC #######################################################
# Returns a promise # Returns a promise
findByLocalId: (localId) -> sessionForLocalId: (localId) ->
DatabaseStore.findByLocalId(Message, localId) @_draftSessions[localId] ?= new DraftStoreProxy(localId)
@_draftSessions[localId]
########### PRIVATE #################################################### ########### PRIVATE ####################################################
@ -127,55 +129,30 @@ DraftStore = Reflux.createStore
atom.displayComposer(draftLocalId) atom.displayComposer(draftLocalId)
_onDestroyDraft: (draftLocalId) -> _onDestroyDraft: (draftLocalId) ->
# Immediately reset any pending changes so no saves occur
@_draftSessions[draftLocalId]?.changes.reset()
delete @_draftSessions[draftLocalId]
# Queue the task to destroy the draft
Actions.queueTask(new DestroyDraftTask(draftLocalId)) Actions.queueTask(new DestroyDraftTask(draftLocalId))
atom.close() if atom.state.mode is "composer" atom.close() if atom.state.mode is "composer"
_onSaveDraft: (paramsWithLocalId) ->
params = _.clone(paramsWithLocalId)
draftLocalId = params.localId
if (not draftLocalId?) then throw new Error("Must call saveDraft with a localId")
delete params.localId
if _.size(params) > 0
task = new SaveDraftTask(draftLocalId, params)
Actions.queueTask(task)
_onSendDraft: (draftLocalId) -> _onSendDraft: (draftLocalId) ->
Actions.queueTask(new SendDraftTask(draftLocalId)) # Immediately save any pending changes so we don't save after sending
atom.close() if atom.state.mode is "composer" save = @_draftSessions[draftLocalId]?.changes.commit() ? Promise.resolve()
save.then ->
# Queue the task to send the draft
Actions.queueTask(new SendDraftTask(draftLocalId))
atom.close() if atom.state.mode is "composer"
_findDraft: (draftLocalId) -> _onAttachFileComplete: ({file, messageLocalId}) ->
new Promise (resolve, reject) -> @sessionForLocalId(messageLocalId).prepare().then (proxy) ->
DatabaseStore.findByLocalId(Message, draftLocalId) files = proxy.draft().files ? []
.then (draft) -> files.push(file)
if not draft? then reject("Can't find draft with id #{draftLocalId}") proxy.changes.add({files}, true)
else resolve(draft)
.catch (error) -> reject(error)
# Receives:
# file: - A `File` object
# uploadData:
# messageLocalId
# filePath
# fileSize
# fileName
# bytesUploaded
# state - one of "started" "progress" "completed" "aborted" "failed"
_onFileUploaded: ({file, uploadData}) ->
@_findDraft(uploadData.messageLocalId)
.then (draft) ->
draft.files ?= []
draft.files.push(file)
DatabaseStore.persistModel(draft)
Actions.queueTask(new SaveDraftTask(uploadData.messageLocalId))
.catch (error) -> console.error(error, error.stack)
_onRemoveFile: ({file, messageLocalId}) -> _onRemoveFile: ({file, messageLocalId}) ->
@_findDraft(messageLocalId) @sessionForLocalId(messageLocalId).prepare().then (proxy) ->
.then (draft) -> files = proxy.draft().files ? []
draft.files ?= [] files = _.reject files, (f) -> f.id is file.id
draft.files = _.reject draft.files, (f) -> f.id is file.id proxy.changes.add({files}, true)
DatabaseStore.persistModel(draft)
Actions.queueTask(new SaveDraftTask(uploadData.messageLocalId))
.catch (error) -> console.error(error, error.stack)

View file

@ -35,17 +35,14 @@ FileUploadStore = Reflux.createStore
_onAttachFile: ({messageLocalId}) -> _onAttachFile: ({messageLocalId}) ->
@_verifyId(messageLocalId) @_verifyId(messageLocalId)
unlistenOpen = Actions.openPathsSelected.listen (pathsToOpen=[]) -> # When the dialog closes, it triggers `Actions.pathsToOpen`
unlistenOpen?() atom.showOpenDialog {properties: ['openFile', 'multiSelections']}, (pathsToOpen) ->
pathsToOpen = [pathsToOpen] if _.isString(pathsToOpen) pathsToOpen = [pathsToOpen] if _.isString(pathsToOpen)
for path in pathsToOpen for path in pathsToOpen
# When this task runs, we expect to hear `uploadStateChanged` # When this task runs, we expect to hear `uploadStateChanged`
# Actions. # Actions.
Actions.queueTask(new FileUploadTask(path, messageLocalId)) Actions.queueTask(new FileUploadTask(path, messageLocalId))
# When the dialog closes, it triggers `Actions.pathsToOpen`
ipc.send('command', 'application:open-file-to-window')
# Receives: # Receives:
# uploadData: # uploadData:
# messageLocalId - The localId of the message (draft) we're uploading to # messageLocalId - The localId of the message (draft) we're uploading to

View file

@ -55,7 +55,6 @@ TaskQueue = Reflux.createStore
throw new Error("You must queue a `Task` object") throw new Error("You must queue a `Task` object")
@_initializeTask(task) @_initializeTask(task)
@_dequeueObsoleteTasks(task) @_dequeueObsoleteTasks(task)
@_queue.push(task) @_queue.push(task)
@_update() if not silent @_update() if not silent
@ -63,10 +62,7 @@ TaskQueue = Reflux.createStore
dequeue: (taskOrId={}, {silent}={}) -> dequeue: (taskOrId={}, {silent}={}) ->
task = @_parseArgs(taskOrId) task = @_parseArgs(taskOrId)
task.abort() if @_shouldAbort(task)
task.queueState.isProcessing = false task.queueState.isProcessing = false
task.cleanup() task.cleanup()
@_queue.splice(@_queue.indexOf(task), 1) @_queue.splice(@_queue.indexOf(task), 1)
@ -116,14 +112,15 @@ TaskQueue = Reflux.createStore
@_processQueue() @_processQueue()
_dequeueObsoleteTasks: (task) -> _dequeueObsoleteTasks: (task) ->
for otherTask in @_queue for otherTask in @_queue by -1
if otherTask? and task.shouldDequeueOtherTask(otherTask) # Do not interrupt tasks which are currently processing
continue if otherTask.queueState.isProcessing
# Do not remove ourselves from the queue
continue if otherTask is task
# Dequeue tasks which our new task indicates it makes obsolete
if task.shouldDequeueOtherTask(otherTask)
@dequeue(otherTask, silent: true) @dequeue(otherTask, silent: true)
_shouldAbort: (task) ->
task.queueState.isProcessing or
(task.queueState.performedLocal and not task.queueState.performedRemote)
_taskIsBlocked: (task) -> _taskIsBlocked: (task) ->
_.any @_queue, (otherTask) -> _.any @_queue, (otherTask) ->
task.shouldWaitForTask(otherTask) and task isnt otherTask task.shouldWaitForTask(otherTask) and task isnt otherTask

View file

@ -3,7 +3,7 @@ Message = require '../models/message'
DatabaseStore = require '../stores/database-store' DatabaseStore = require '../stores/database-store'
Actions = require '../actions' Actions = require '../actions'
SaveDraftTask = require './save-draft' SyncbackDraftTask = require './syncback-draft'
SendDraftTask = require './send-draft' SendDraftTask = require './send-draft'
FileUploadTask = require './file-upload-task' FileUploadTask = require './file-upload-task'
@ -12,12 +12,12 @@ class DestroyDraftTask extends Task
constructor: (@draftLocalId) -> super constructor: (@draftLocalId) -> super
shouldDequeueOtherTask: (other) -> shouldDequeueOtherTask: (other) ->
(other instanceof SaveDraftTask and other.draftLocalId is @draftLocalId) or (other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId) or
(other instanceof SendDraftTask and other.draftLocalId is @draftLocalId) or (other instanceof SendDraftTask and other.draftLocalId is @draftLocalId) or
(other instanceof FileUploadTask and other.draftLocalId is @draftLocalId) (other instanceof FileUploadTask and other.draftLocalId is @draftLocalId)
shouldWaitForTask: (other) -> shouldWaitForTask: (other) ->
(other instanceof SaveDraftTask and other.draftLocalId is @draftLocalId) (other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId)
performLocal: -> performLocal: ->
new Promise (resolve, reject) => new Promise (resolve, reject) =>
@ -25,8 +25,8 @@ class DestroyDraftTask extends Task
return reject(new Error("Attempt to call DestroyDraftTask.performLocal without @draftLocalId")) return reject(new Error("Attempt to call DestroyDraftTask.performLocal without @draftLocalId"))
DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) => DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) =>
DatabaseStore.unpersistModel(draft).then(resolve)
@draft = draft @draft = draft
DatabaseStore.unpersistModel(draft).then(resolve)
performRemote: -> performRemote: ->
new Promise (resolve, reject) => new Promise (resolve, reject) =>
@ -47,7 +47,7 @@ class DestroyDraftTask extends Task
onAPIError: (apiError) -> onAPIError: (apiError) ->
inboxMsg = apiError.body?.message ? "" inboxMsg = apiError.body?.message ? ""
if inboxMsg.indexOf("No draft found") >= 0 if apiError.statusCode is 404
# Draft has already been deleted, this is not really an error # Draft has already been deleted, this is not really an error
return true return true
else if inboxMsg.indexOf("is not a draft") >= 0 else if inboxMsg.indexOf("is not a draft") >= 0

View file

@ -11,8 +11,8 @@ DatabaseStore = require '../stores/database-store'
class FileUploadTask extends Task class FileUploadTask extends Task
constructor: (@filePath, @messageLocalId) -> constructor: (@filePath, @messageLocalId) ->
@progress = null # The progress checking timer.
super super
@progress = null # The progress checking timer.
performLocal: -> performLocal: ->
return Promise.reject(new Error("Must pass an absolute path to upload")) unless @filePath?.length return Promise.reject(new Error("Must pass an absolute path to upload")) unless @filePath?.length
@ -30,50 +30,57 @@ class FileUploadTask extends Task
json: false json: false
returnsModel: true returnsModel: true
formData: @_formData() formData: @_formData()
success: (json) => @_onUploadSuccess(json, resolve)
error: reject error: reject
success: (json) =>
# The Inbox API returns the file json wrapped in an array
file = (new File).fromJSON(json[0])
Actions.uploadStateChanged @_uploadData("completed")
@_completedNotification(file)
clearInterval(@progress)
@req = null
resolve()
@progress = setInterval => @progress = setInterval =>
Actions.uploadStateChanged(@_uploadData("progress")) Actions.uploadStateChanged(@_uploadData("progress"))
, 250 , 250
abort: -> cleanup: ->
@req?.abort() super
clearInterval(@progress)
Actions.uploadStateChanged(@_uploadData("aborted"))
setTimeout => # If the request is still in progress, notify observers that
Actions.fileAborted(@_uploadData("aborted")) # we've failed.
, 1000 # To see the aborted state for a little bit if @req
@req.abort()
clearInterval(@progress)
Actions.uploadStateChanged(@_uploadData("aborted"))
setTimeout =>
# To see the aborted state for a little bit
Actions.fileAborted(@_uploadData("aborted"))
, 1000
onAPIError: (apiError) ->
@_rollbackLocal()
onOtherError: (otherError) ->
@_rollbackLocal()
onTimeoutError: ->
# Do nothing. It could take a while.
Promise.resolve() Promise.resolve()
onAPIError: (apiError) -> @_rollbackLocal()
onOtherError: (otherError) -> @_rollbackLocal()
onTimeoutError: -> Promise.resolve() # Do nothing. It could take a while.
onOfflineError: (offlineError) -> onOfflineError: (offlineError) ->
msg = "You can't upload a file while you're offline." msg = "You can't upload a file while you're offline."
@_rollbackLocal(msg) @_rollbackLocal(msg)
_rollbackLocal: (msg) -> _rollbackLocal: (msg) ->
clearInterval(@progress) clearInterval(@progress)
@req = null
msg ?= "There was a problem uploading this file. Please try again later." msg ?= "There was a problem uploading this file. Please try again later."
Actions.postNotification({message: msg, type: "error"}) Actions.postNotification({message: msg, type: "error"})
Actions.uploadStateChanged @_uploadData("failed") Actions.uploadStateChanged @_uploadData("failed")
_onUploadSuccess: (json, taskCallback) ->
clearInterval(@progress)
# The Inbox API returns the file json wrapped in an array
file = (new File).fromJSON(json[0])
Actions.uploadStateChanged @_uploadData("completed")
@_completedNotification(file)
taskCallback()
# The `persistUploadFile` action affects the Database and should only be # The `persistUploadFile` action affects the Database and should only be
# heard in the main window. # heard in the main window.
# #
@ -81,11 +88,8 @@ class FileUploadTask extends Task
# composers) that the file has finished uploading. # composers) that the file has finished uploading.
_completedNotification: (file) -> _completedNotification: (file) ->
setTimeout => setTimeout =>
Actions.persistUploadedFile Actions.attachFileComplete({file, @messageLocalId})
file: file Actions.fileUploaded(uploadData: @_uploadData("completed"))
uploadData: @_uploadData("completed")
Actions.fileUploaded
uploadData: @_uploadData("completed")
, 1000 # To see the success state for a little bit , 1000 # To see the success state for a little bit
_formData: -> _formData: ->

View file

@ -4,7 +4,7 @@ Actions = require '../actions'
DatabaseStore = require '../stores/database-store' DatabaseStore = require '../stores/database-store'
Message = require '../models/message' Message = require '../models/message'
Task = require './task' Task = require './task'
SaveDraftTask = require './save-draft' SyncbackDraftTask = require './syncback-draft'
module.exports = module.exports =
class SendDraftTask extends Task class SendDraftTask extends Task
@ -15,7 +15,7 @@ class SendDraftTask extends Task
other instanceof SendDraftTask and other.draftLocalId is @draftLocalId other instanceof SendDraftTask and other.draftLocalId is @draftLocalId
shouldWaitForTask: (other) -> shouldWaitForTask: (other) ->
other instanceof SaveDraftTask and other.draftLocalId is @draftLocalId other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId
performLocal: -> performLocal: ->
# When we send drafts, we don't update anything in the app until # When we send drafts, we don't update anything in the app until
@ -31,15 +31,19 @@ class SendDraftTask extends Task
# recent draft version # recent draft version
DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) -> DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) ->
# The draft may have been deleted by another task. Nothing we can do. # The draft may have been deleted by another task. Nothing we can do.
return reject(new Error("We couldn't find the saved draft. Please try again in a couple seconds")) unless draft return reject(new Error("We couldn't find the saved draft.")) unless draft
return reject(new Error("Cannot send draft that is not saved!")) unless draft.isSaved()
if draft.isSaved()
body =
draft_id: draft.id
version: draft.version
else
body = draft.toJSON()
atom.inbox.makeRequest atom.inbox.makeRequest
path: "/n/#{draft.namespaceId}/send" path: "/n/#{draft.namespaceId}/send"
method: 'POST' method: 'POST'
body: body: body
draft_id: draft.id
version: draft.version
returnsModel: true returnsModel: true
success: -> success: ->
atom.playSound('mail_sent.ogg') atom.playSound('mail_sent.ogg')
@ -49,15 +53,15 @@ class SendDraftTask extends Task
.catch(reject) .catch(reject)
onAPIError: -> onAPIError: ->
msg = "Our server is having problems. Your messages has NOT been sent" msg = "Our server is having problems. Your message has not been sent."
@notifyErrorMessage(msg) @notifyErrorMessage(msg)
onOtherError: -> onOtherError: ->
msg = "We had a serious issue while sending. Your messages has NOT been sent" msg = "We had a serious issue while sending. Your message has not been sent."
@notifyErrorMessage(msg) @notifyErrorMessage(msg)
onTimeoutError: -> onTimeoutError: ->
msg = "The server is taking an abnormally long time to respond. Your messages has NOT been sent" msg = "The server is taking an abnormally long time to respond. Your message has not been sent."
@notifyErrorMessage(msg) @notifyErrorMessage(msg)
onOfflineError: -> onOfflineError: ->

View file

@ -9,48 +9,33 @@ Message = require '../models/message'
FileUploadTask = require './file-upload-task' FileUploadTask = require './file-upload-task'
# MutateDraftTask
module.exports = module.exports =
class SaveDraftTask extends Task class SyncbackDraftTask extends Task
constructor: (@draftLocalId, @changes={}, {@localOnly}={}) -> constructor: (@draftLocalId) ->
@_saveAttempts = 0
@queuedAt = Date.now()
super super
@_saveAttempts = 0
# 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.
shouldDequeueOtherTask: (other) -> shouldDequeueOtherTask: (other) ->
other instanceof SaveDraftTask and other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId and other.creationDate < @creationDate
other.draftLocalId is @draftLocalId and
other.queuedAt < @queuedAt # other is an older task.
shouldWaitForTask: (other) -> shouldWaitForTask: (other) ->
other instanceof FileUploadTask and other.draftLocalId is @draftLocalId other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId and other.creationDate < @creationDate
performLocal: -> new Promise (resolve, reject) => performLocal: ->
if not @draftLocalId? # SyncbackDraftTask does not do anything locally. You should persist your changes
errMsg = "Attempt to call FileUploadTask.performLocal without @draftLocalId" # to the local database directly or using a DraftStoreProxy, and then queue a
return reject(new Error(errMsg)) # SyncbackDraftTask to send those changes to the server.
if not @draftLocalId?
DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) => errMsg = "Attempt to call FileUploadTask.performLocal without @draftLocalId"
if not draft? Promise.reject(new Error(errMsg))
# This can happen if a save draft task is queued after it has been else
# destroyed. Nothing we can really do about it, so ignore this. Promise.resolve()
resolve()
else if _.size(@changes) is 0
resolve()
else
updatedDraft = @_applyChangesToDraft(draft, @changes)
DatabaseStore.persistModel(updatedDraft).then(resolve)
.catch(reject)
performRemote: -> performRemote: ->
if @localOnly then return Promise.resolve()
new Promise (resolve, reject) => new Promise (resolve, reject) =>
# Fetch the latest draft data to make sure we make the request with the most
# recent draft version
DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) => DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) =>
# The draft may have been deleted by another task. Nothing we can do. # The draft may have been deleted by another task. Nothing we can do.
return resolve() unless draft return resolve() unless draft
@ -74,11 +59,11 @@ class SaveDraftTask extends Task
body: body body: body
returnsModel: false returnsModel: false
success: (json) => success: (json) =>
newDraft = (new Message).fromJSON(json) if json.id != initialId
if newDraft.id != initialId newDraft = (new Message).fromJSON(json)
DatabaseStore.swapModel(oldModel: draft, newModel: newDraft, localId: @draftLocalId).then(resolve) DatabaseStore.swapModel(oldModel: draft, newModel: newDraft, localId: @draftLocalId).then(resolve)
else else
DatabaseStore.persistModel(newDraft).then(resolve) DatabaseStore.persistModel(draft).then(resolve)
error: reject error: reject
onAPIError: (apiError) -> onAPIError: (apiError) ->
@ -139,7 +124,3 @@ class SaveDraftTask extends Task
@notifyErrorMessage(msg) @notifyErrorMessage(msg)
_applyChangesToDraft: (draft, changes={}) ->
for key, definition of draft.attributes()
draft[key] = changes[key] if changes[key]?
return draft

View file

@ -39,7 +39,9 @@ Actions = require '../actions'
class Task class Task
## These are commonly overridden ## ## These are commonly overridden ##
constructor: -> @id = generateTempId() constructor: ->
@id = generateTempId()
@creationDate = new Date()
performLocal: -> Promise.resolve() performLocal: -> Promise.resolve()