Skip to content

Commit

Permalink
feat: Fix Promise#sync when mixing Promise classes (#24)
Browse files Browse the repository at this point in the history
Promise now has a source attribute which is used by Promise#wait to
implement Promise#sync, so it can now be used on the result of Promise.all
or promises chained off of it using Promise#then.
  • Loading branch information
dylanahsmith committed Nov 14, 2016
2 parents 3545029 + 9824b00 commit b9fa9c0
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 5 deletions.
4 changes: 2 additions & 2 deletions config/reek.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ NilCheck:
RepeatedConditional:
enabled: true
exclude: []
max_ifs: 3
max_ifs: 4
TooManyInstanceVariables:
enabled: true
exclude: []
Expand All @@ -64,7 +64,7 @@ TooManyStatements:
exclude:
- initialize
- each
max_statements: 5
max_statements: 6
UncommunicativeMethodName:
enabled: true
exclude: []
Expand Down
3 changes: 3 additions & 0 deletions config/rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,6 @@ GuardClause:

Alias:
EnforcedStyle: prefer_alias_method

Metrics/ClassLength:
Enabled: false
25 changes: 22 additions & 3 deletions lib/promise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@

class Promise
Error = Class.new(RuntimeError)
BrokenError = Class.new(Error)

include Promise::Progress

attr_accessor :source
attr_reader :state, :value, :reason

def self.resolve(obj)
Expand Down Expand Up @@ -61,7 +63,10 @@ def rescue(&block)
alias_method :catch, :rescue

def sync
wait if pending?
if pending?
wait
raise BrokenError if pending?
end
raise reason if rejected?
value
end
Expand All @@ -72,6 +77,7 @@ def fulfill(value = nil)
else
dispatch do
@state = :fulfilled
@source = nil
@value = value
end
end
Expand All @@ -81,19 +87,32 @@ def fulfill(value = nil)
def reject(reason = nil)
dispatch do
@state = :rejected
@source = nil
@reason = reason_coercion(reason || Error)
end
end

def defer
yield
# Override to support sync on a promise without a source or to wait
# for deferred callbacks on the source
def wait
while source
saved_source = source
saved_source.wait
break if saved_source.equal?(source)
end
end

protected

# Override to defer calling the callback for Promises/A+ spec compliance
def defer
yield
end

def add_callback(callback)
if pending?
@callbacks << callback
callback.source = self
else
dispatch!(callback)
end
Expand Down
7 changes: 7 additions & 0 deletions lib/promise/callback.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

class Promise
class Callback
attr_accessor :source

def initialize(on_fulfill, on_reject, next_promise)
@on_fulfill = on_fulfill
@on_reject = on_reject
@next_promise = next_promise
@next_promise.source = self
end

def fulfill(value)
Expand All @@ -24,6 +27,10 @@ def reject(reason)
end
end

def wait
source.wait
end

private

def call_block(block, param)
Expand Down
8 changes: 8 additions & 0 deletions lib/promise/group.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class Promise
class Group
attr_accessor :source
attr_reader :promise

def initialize(result_promise, inputs)
Expand All @@ -9,10 +10,17 @@ def initialize(result_promise, inputs)
if @remaining.zero?
promise.fulfill(inputs)
else
promise.source = self
chain_inputs
end
end

def wait
each_promise do |input_promise|
input_promise.wait if input_promise.pending?
end
end

private

def chain_inputs
Expand Down
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

require 'promise'
require_relative 'support/delayed_promise'
require_relative 'support/promise_loader'

require 'awesome_print'
require 'devtools/spec_helper' if Gem.ruby_version >= Gem::Version.new('2.1')
Expand Down
13 changes: 13 additions & 0 deletions spec/support/promise_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class PromiseLoader
def self.lazy_load(promise, &block)
promise.source = new(&block)
end

def initialize(&block)
@block = block
end

def wait
@block.call
end
end
60 changes: 60 additions & 0 deletions spec/unit/promise_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,42 @@
expect(subject).not_to receive(:wait)
expect(subject.sync).to be(value)
end

it 'waits for source by default' do
PromiseLoader.lazy_load(subject) { subject.fulfill(1) }
p2 = subject.then { |v| v + 1 }
expect(p2).to be_pending
expect(p2.sync).to eq(2)
expect(p2.source).to eq(nil)
end

it 'waits for source that is fulfilled with a promise' do
PromiseLoader.lazy_load(subject) { subject.fulfill(1) }
p2 = subject.then do |v|
Promise.new.tap do |p3|
PromiseLoader.lazy_load(p3) { p3.fulfill(v + 1) }
end
end
expect(p2).to be_pending
expect(p2.sync).to eq(2)
expect(p2.source).to eq(nil)
end

it 'waits for source rejection' do
PromiseLoader.lazy_load(subject) { subject.reject(reason) }
p2 = subject.then { |v| v + 1 }
expect { p2.sync }.to raise_error(reason)
expect(p2.source).to eq(nil)
end

it 'raises for promise without a source by default' do
expect { subject.sync }.to raise_error(Promise::BrokenError)
end

it 'raises if source.wait leaves promise pending' do
PromiseLoader.lazy_load(subject) {}
expect { subject.sync }.to raise_error(Promise::BrokenError)
end
end

describe '.resolve' do
Expand Down Expand Up @@ -575,6 +611,30 @@
p1.fulfill(1.0)
expect(result.sync).to eq([1.0, 2])
end

it 'returns a promise that can sync promises of another class' do
p1 = DelayedPromise.new
DelayedPromise.deferred << -> { p1.fulfill('a') }

result = Promise.all([p1, Promise.resolve(:b), 3])

expect(result).to be_pending
expect(result.sync).to eq(['a', :b, 3])
end

it 'sync on result does not call wait on resolved promises' do
p1 = Class.new(Promise) do
def wait
raise 'wait not expected'
end
end.resolve(:one)
p2 = DelayedPromise.new
DelayedPromise.deferred << -> { p2.fulfill(:two) }

result = Promise.all([p1, p2])

expect(result.sync).to eq([:one, :two])
end
end

describe '.map_value' do
Expand Down

0 comments on commit b9fa9c0

Please sign in to comment.