diff --git a/CHANGELOG.md b/CHANGELOG.md index 34fa2b4..1fd353c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # deferrable_gratification changes +## v1.1.0 +- Add `DG.all_successes` to asynchronously perform a set of operations, and + either succeed when all operations succeed, or fail with the first failure. + ## v1.0.0 - Remove the `Fluent` module as the same syntax is available in `EventMachine` 1.0.3. diff --git a/lib/deferrable_gratification/combinators.rb b/lib/deferrable_gratification/combinators.rb index 54982f4..133feac 100644 --- a/lib/deferrable_gratification/combinators.rb +++ b/lib/deferrable_gratification/combinators.rb @@ -264,6 +264,28 @@ def join_successes(*operations) Join::Successes.setup!(*operations) end + # Combinator that waits for the supplied asynchronous operations + # to succeed or fail, then succeeds with the results of all those + # operations that were successful. + # + # This Deferrable will fail if any of the operations fail. It will either + # succeed with all the operations or fail with the first failure. + # + # The successful results are guaranteed to be in the same order as the + # operations were passed in (which may _not_ be the same as the + # chronological order in which they succeeded). + # + # @param [*Deferrable] *operations deferred statuses of asynchronous + # operations to wait for. + # + # @return [Deferrable] a deferred status that will either succeed after + # all the +operations+ have succeeded or fail after the first failed + # operation; its callbacks will be passed an +Enumerable+ containing + # the results of those operations that succeeded. + def all_successes(*operations) + Join::AllSuccesses.setup!(*operations) + end + # Combinator that waits for any of the supplied asynchronous operations # to succeed, and succeeds with the result of the first (chronologically) # to do so. diff --git a/lib/deferrable_gratification/combinators/join.rb b/lib/deferrable_gratification/combinators/join.rb index 5211b19..dc427c9 100644 --- a/lib/deferrable_gratification/combinators/join.rb +++ b/lib/deferrable_gratification/combinators/join.rb @@ -73,6 +73,33 @@ def finish end end + # Combinator that waits for the supplied asynchronous operations + # to succeed or fail, then succeeds with the results of all those + # operations that were successful. + # + # This Deferrable will fail if any of the operations fail. It will either + # succeed with all the operations or fail with the first failure. + # + # The successful results are guaranteed to be in the same order as the + # operations were passed in (which may _not_ be the same as the + # chronological order in which they succeeded). + # + # You probably want to call {ClassMethods#all_successes} rather than + # using this class directly. + class AllSuccesses < Join + private + def done? + failures.length > 0 || all_completed? + end + + def finish + if failures.length > 0 + fail(failures.first) + else + succeed(successes) + end + end + end # Combinator that waits for any of the supplied asynchronous operations # to succeed, and succeeds with the result of the first (chronologically) diff --git a/lib/deferrable_gratification/version.rb b/lib/deferrable_gratification/version.rb index 61d9b96..1be3db5 100644 --- a/lib/deferrable_gratification/version.rb +++ b/lib/deferrable_gratification/version.rb @@ -1,3 +1,3 @@ module DeferrableGratification - VERSION = '1.0.0' + VERSION = '1.1.0' end diff --git a/spec/deferrable_gratification/combinators_spec.rb b/spec/deferrable_gratification/combinators_spec.rb index bd85ece..cb5c311 100644 --- a/spec/deferrable_gratification/combinators_spec.rb +++ b/spec/deferrable_gratification/combinators_spec.rb @@ -464,6 +464,67 @@ def bind!() first_query.bind! {|id| raise "id #{id} not authorised" } end end end + describe '.all_successes' do + describe 'DG.all_successes()' do + subject { DG.all_successes() } + it { should succeed_with [] } + end + + describe 'DG.all_successes(first, second)' do + let(:first) { EM::DefaultDeferrable.new } + let(:second) { EM::DefaultDeferrable.new } + subject { DG.all_successes(first, second) } + + it 'should not succeed or fail' do + subject.should_not succeed_with_anything + subject.should_not fail_with_anything + end + + describe 'after first succeeds with :one' do + before { first.succeed :one } + + it 'should not succeed or fail' do + subject.should_not succeed_with_anything + subject.should_not fail_with_anything + end + + describe 'after second succeeds with :two' do + before { second.succeed :two } + + it { should succeed_with [:one, :two] } + end + + describe 'after second fails' do + before { second.fail RuntimeError.new('oops') } + + it { should fail_with('oops') } + end + end + + describe 'after both fail' do + before do + first.fail RuntimeError.new('oops 1') + second.fail RuntimeError.new('oops 2') + end + + it { should fail_with('oops 1') } + end + + describe 'preserving order of operations' do + describe 'if second succeeds before first does' do + subject do + DG.all_successes(first, second).tap do |successes| + second.succeed :two + first.succeed :one + end + end + it 'should still succeed with [:one, :two]' do + subject.should succeed_with [:one, :two] + end + end + end + end + end describe '.join_first_success' do describe 'DG.join_first_success()' do