Skip to content

Commit

Permalink
Merge pull request #464 from opal/promise
Browse files Browse the repository at this point in the history
Implement promises
  • Loading branch information
meh committed Dec 25, 2013
2 parents abb78c1 + 86a79c9 commit 9b67f08
Show file tree
Hide file tree
Showing 7 changed files with 392 additions and 0 deletions.
35 changes: 35 additions & 0 deletions spec/opal/stdlib/promise/collect_spec.rb
@@ -0,0 +1,35 @@
require 'promise'

describe 'Promise#collect' do
it 'calls the block with all the previous results' do
x = 42

Promise.value(1).then { 2 }.then { 3 }.collect {|a, b, c|
x = a + b + c
}

x.should == 6
end

it 'calls the then after the collect' do
x = 42

Promise.value(1).then { 2 }.then { 3 }.collect {|a, b, c|
a + b + c
}.then { |v| x = v }

x.should == 6
end

it 'works after a when' do
x = 42

Promise.value(1).then {
Promise.when Promise.value(2), Promise.value(3)
}.collect {|a, b|
x = a + b[0] + b[1]
}

x.should == 6
end
end
15 changes: 15 additions & 0 deletions spec/opal/stdlib/promise/error_spec.rb
@@ -0,0 +1,15 @@
require 'promise'

describe 'Promise.error' do
it 'rejects the promise with the given error' do
Promise.error(23).error.should == 23
end

it 'marks the promise as realized' do
Promise.error(23).realized?.should be_true
end

it 'marks the promise as rejected' do
Promise.error(23).rejected?.should be_true
end
end
35 changes: 35 additions & 0 deletions spec/opal/stdlib/promise/rescue_spec.rb
@@ -0,0 +1,35 @@
require 'promise'

describe 'Promise#rescue' do
it 'calls the block when the promise has already been rejected' do
x = 42
Promise.error(23).rescue { |v| x = v }
x.should == 23
end

it 'calls the block when the promise is rejected' do
a = Promise.new
x = 42

a.rescue { |v| x = v }
a.reject(23)

x.should == 23
end

it 'does not call then blocks when the promise is rejected' do
x = 42
y = 23

Promise.error(23).then { y = 42 }.rescue { |v| x = v }

x.should == 23
y.should == 23
end

it 'does not call subsequent rescue blocks' do
x = 42
Promise.error(23).rescue { |v| x = v }.rescue { x = 42 }
x.should == 23
end
end
38 changes: 38 additions & 0 deletions spec/opal/stdlib/promise/then_spec.rb
@@ -0,0 +1,38 @@
require 'promise'

describe 'Promise#then' do
it 'calls the block when the promise has already been resolved' do
x = 42
Promise.value(23).then { |v| x = v }
x.should == 23
end

it 'calls the block when the promise is resolved' do
a = Promise.new
x = 42

a.then { |v| x = v }
a.resolve(23)

x.should == 23
end

it 'works with multiple chains' do
x = 42
Promise.value(2).then { |v| v * 2 }.then { |v| v * 4 }.then { |v| x = v }
x.should == 16
end

it 'works when a block returns a promise' do
a = Promise.new
b = Promise.new

x = 42
a.then { b }.then { |v| x = v }

a.resolve(42)
b.resolve(23)

x.should == 23
end
end
15 changes: 15 additions & 0 deletions spec/opal/stdlib/promise/value_spec.rb
@@ -0,0 +1,15 @@
require 'promise'

describe 'Promise.value' do
it 'resolves the promise with the given value' do
Promise.value(23).value.should == 23
end

it 'marks the promise as realized' do
Promise.value(23).realized?.should be_true
end

it 'marks the promise as resolved' do
Promise.value(23).resolved?.should be_true
end
end
19 changes: 19 additions & 0 deletions spec/opal/stdlib/promise/when_spec.rb
@@ -0,0 +1,19 @@
require 'promise'

describe 'Promise.when' do
it 'calls the block with all promises results' do
a = Promise.new
b = Promise.new

x = 42

Promise.when(a, b).then {|y, z|
x = y + z
}

a.resolve(1)
b.resolve(2)

x.should == 3
end
end
235 changes: 235 additions & 0 deletions stdlib/promise.rb
@@ -0,0 +1,235 @@
class Promise
def self.value(value)
new.resolve(value)
end

def self.error(value)
new.reject(value)
end

def self.when(*promises)
When.new(promises.flatten)
end

attr_reader :value, :error, :prev, :next

def initialize(success = nil, failure = nil)
@success = success
@failure = failure

@realized = nil
@value = nil
@error = nil
@delayed = nil

@prev = nil
@next = nil
end

def act?
@success != nil
end

def realized?
@realized != nil
end

def resolved?
@realized == :value
end

def rejected?
@realized == :error
end

def ^(promise)
promise << self
self >> promise

promise
end

def <<(promise)
@prev = promise

self
end

def >>(promise)
@next = promise

if resolved?
promise.resolve(@delayed || value)
elsif rejected? && (!@failure || Promise === (@delayed || @error))
promise.reject(@delayed || error)
end

self
end

def resolve(value)
if realized?
raise ArgumentError, 'the promise has already been realized'
end

if Promise === value
value << @prev

return value ^ self
end

@realized = :value
@value = value

if @success
value = @success.call(value)
end

if @next
@next.resolve(value)
else
@delayed = value
end

self
end

def reject(value)
if realized?
raise ArgumentError, 'the promise has already been realized'
end

if Promise === value
value << @prev

return value ^ self
end

@realized = :error
@error = value

if @failure
value = @failure.call(value)

if Promise === value
if @next
@next.reject(value)
else
@delayed = value
end
end
else
if @next
@next.reject(value)
else
@delayed = value
end
end

self
end

def then(&block)
self ^ Promise.new(block)
end

alias do then

def fail(&block)
self ^ Promise.new(nil, block)
end

alias rescue fail
alias catch fail

def always(&block)
self ^ Promise.new(block, block)
end

alias finally always
alias ensure always

def collect(&block)
self ^ Collect.new(block)
end

def inspect
result = "#<#{self.class}(#{object_id})"

if @next
result += " >> #{@next.inspect}"
end

if realized?
result += ": #{(@value || @error).inspect}>"
else
result += ">"
end

result
end

class Collect < self
def self.it(promise)
unless promise.realized?
raise ArgumentError, "the promise hasn't been realized"
end

current = promise.act? ? [promise.value] : []

if prev = promise.prev
current.concat(it(prev))
else
current
end
end

def initialize(block)
super -> {
block.call(*Collect.it(self).reverse)
}
end
end

class When < self
def initialize(promises = [])
super()

@wait = []

promises.each {|promise|
wait promise
}
end

def wait(promise)
@wait << promise

promise.always {
try if @next
}

self
end

def >>(*)
super.tap {
try
}
end

def try
if @wait.all?(&:realized?)
if promise = @wait.find(&:rejected?)
reject(promise.error)
else
resolve(@wait.map(&:value))
end
end
end

def and(&block)
wait Promise.new(block)
end
end
end

0 comments on commit 9b67f08

Please sign in to comment.