diff --git a/package.json b/package.json index d555e075b..4f3eff261 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "optimist": "0.4.0", "pathwatcher": "^4.4.0", "property-accessors": "^1", + "promise-queue": "2.1.1", "q": "^1.0.1", "raven": "0.7.2", "react": "^0.13.2", diff --git a/spec/stores/database-store-spec.coffee b/spec/stores/database-store-spec.coffee index c943a31d5..47d4126fc 100644 --- a/spec/stores/database-store-spec.coffee +++ b/spec/stores/database-store-spec.coffee @@ -343,13 +343,22 @@ describe "DatabaseStore", -> expect(@performed[1].query).toBe "TEST" expect(@performed[2].query).toBe "COMMIT" - it "resolves, but doesn't fire a commit on failure", -> + it "preserves resolved values", -> + waitsForPromise => + DatabaseStore.atomically( => + DatabaseStore._query("TEST") + return Promise.resolve("myValue") + ).then (myValue) => + expect(myValue).toBe "myValue" + + it "always fires a COMMIT, even if the promise fails", -> waitsForPromise => DatabaseStore.atomically( => throw new Error("BOOO") ).catch => - expect(@performed.length).toBe 1 + expect(@performed.length).toBe 2 expect(@performed[0].query).toBe "BEGIN EXCLUSIVE TRANSACTION" + expect(@performed[1].query).toBe "COMMIT" it "can be called multiple times and get queued", -> waitsForPromise => @@ -366,6 +375,64 @@ describe "DatabaseStore", -> expect(@performed[4].query).toBe "BEGIN EXCLUSIVE TRANSACTION" expect(@performed[5].query).toBe "COMMIT" + it "carries on if one of them fails, but still calls the COMMIT for the failed block", -> + caughtError = false + DatabaseStore.atomically( => DatabaseStore._query("ONE") ) + DatabaseStore.atomically( => throw new Error("fail") ).catch -> + caughtError = true + DatabaseStore.atomically( => DatabaseStore._query("THREE") ) + advanceClock(100) + expect(@performed.length).toBe 8 + expect(@performed[0].query).toBe "BEGIN EXCLUSIVE TRANSACTION" + expect(@performed[1].query).toBe "ONE" + expect(@performed[2].query).toBe "COMMIT" + expect(@performed[3].query).toBe "BEGIN EXCLUSIVE TRANSACTION" + expect(@performed[4].query).toBe "COMMIT" + expect(@performed[5].query).toBe "BEGIN EXCLUSIVE TRANSACTION" + expect(@performed[6].query).toBe "THREE" + expect(@performed[7].query).toBe "COMMIT" + expect(caughtError).toBe true + + it "is actually running in series and blocks on never-finishing specs", -> + resolver = null + DatabaseStore.atomically( -> ) + advanceClock(100) + expect(@performed.length).toBe 2 + expect(@performed[0].query).toBe "BEGIN EXCLUSIVE TRANSACTION" + expect(@performed[1].query).toBe "COMMIT" + DatabaseStore.atomically( -> new Promise (resolve, reject) -> resolver = resolve) + advanceClock(100) + blockedPromiseDone = false + DatabaseStore.atomically( -> ).then => + blockedPromiseDone = true + advanceClock(100) + expect(@performed.length).toBe 3 + expect(@performed[2].query).toBe "BEGIN EXCLUSIVE TRANSACTION" + expect(blockedPromiseDone).toBe false + + # Now that we've made our assertion about blocking, we need to clean up + # our test and actually resolve that blocked promise now, otherwise + # remaining tests won't run properly. + advanceClock(100) + resolver() + advanceClock(100) + expect(blockedPromiseDone).toBe true + advanceClock(100) + + it "can be called multiple times and preserve return values", -> + waitsForPromise => + v1 = null + v2 = null + v3 = null + Promise.all([ + DatabaseStore.atomically( -> "a" ).then (val) -> v1 = val + DatabaseStore.atomically( -> "b" ).then (val) -> v2 = val + DatabaseStore.atomically( -> "c" ).then (val) -> v3 = val + ]).then => + expect(v1).toBe "a" + expect(v2).toBe "b" + expect(v3).toBe "c" + it "can be called multiple times and get queued", -> waitsForPromise => DatabaseStore.atomically( -> ) diff --git a/src/apm-wrapper.coffee b/src/apm-wrapper.coffee index 11ea5b970..86ca9f0d0 100644 --- a/src/apm-wrapper.coffee +++ b/src/apm-wrapper.coffee @@ -210,7 +210,6 @@ class APMWrapper if code is 0 callback?() else - atom.packages.activatePackage(name) if activateOnFailure error = new Error(errorMessage) error.stdout = stdout error.stderr = stderr diff --git a/src/flux/stores/database-store.coffee b/src/flux/stores/database-store.coffee index 507df5945..f21a2520c 100644 --- a/src/flux/stores/database-store.coffee +++ b/src/flux/stores/database-store.coffee @@ -8,6 +8,7 @@ Utils = require '../models/utils' Actions = require '../actions' ModelQuery = require '../models/query' NylasStore = require '../../global/nylas-store' +PromiseQueue = require 'promise-queue' DatabaseSetupQueryBuilder = require './database-setup-query-builder' PriorityUICoordinator = require '../../priority-ui-coordinator' @@ -468,18 +469,18 @@ class DatabaseStore extends NylasStore Promise.resolve(data) atomically: (fn) => - @_atomicPromise ?= Promise.resolve() - @_atomicPromise = @_atomicPromise.then => - @_atomically(fn) - return @_atomicPromise + maxConcurrent = 1 + maxQueue = Infinity + @_promiseQueue ?= new PromiseQueue(maxConcurrent, maxQueue) + return @_promiseQueue.add(=> @_atomically(fn)) _atomically: (fn) -> @_query("BEGIN EXCLUSIVE TRANSACTION") .then => fn() - .then (val) => - @_query("COMMIT").then => - Promise.resolve(val) + .finally (val) => @_query("COMMIT") + # NOTE: The value that `fn` resolves to is propagated all the way back to + # the originally caller of `atomically` ######################################################################## ########################### PRIVATE METHODS ############################