Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #464 from opal/promise
Implement promises
- Loading branch information
Showing
7 changed files
with
392 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |