stdlib/promise.rb
# {Promise} is used to help structure asynchronous code.
#
# It is available in the Opal standard library, and can be required in any Opal
# application:
#
# require 'promise'
#
# ## Basic Usage
#
# Promises are created and returned as objects with the assumption that they
# will eventually be resolved or rejected, but never both. A {Promise} has
# a {#then} and {#fail} method (or one of their aliases) that can be used to
# register a block that gets called once resolved or rejected.
#
# promise = Promise.new
#
# promise.then {
# puts "resolved!"
# }.fail {
# puts "rejected!"
# }
#
# # some time later
# promise.resolve
#
# # => "resolved!"
#
# It is important to remember that a promise can only be resolved or rejected
# once, so the block will only ever be called once (or not at all).
#
# ## Resolving Promises
#
# To resolve a promise, means to inform the {Promise} that it has succeeded
# or evaluated to a useful value. {#resolve} can be passed a value which is
# then passed into the block handler:
#
# def get_json
# promise = Promise.new
#
# HTTP.get("some_url") do |req|
# promise.resolve req.json
# end
#
# promise
# end
#
# get_json.then do |json|
# puts "got some JSON from server"
# end
#
# ## Rejecting Promises
#
# Promises are also designed to handle error cases, or situations where an
# outcome is not as expected. Taking the previous example, we can also pass
# a value to a {#reject} call, which passes that object to the registered
# {#fail} handler:
#
# def get_json
# promise = Promise.new
#
# HTTP.get("some_url") do |req|
# if req.ok?
# promise.resolve req.json
# else
# promise.reject req
# end
#
# promise
# end
#
# get_json.then {
# # ...
# }.fail { |req|
# puts "it went wrong: #{req.message}"
# }
#
# ## Chaining Promises
#
# Promises become even more useful when chained together. Each {#then} or
# {#fail} call returns a new {Promise} which can be used to chain more and more
# handlers together.
#
# promise.then { wait_for_something }.then { do_something_else }
#
# Rejections are propagated through the entire chain, so a "catch all" handler
# can be attached at the end of the tail:
#
# promise.then { ... }.then { ... }.fail { ... }
#
# ## Composing Promises
#
# {Promise.when} can be used to wait for more than one promise to resolve (or
# reject). Using the previous example, we could request two different json
# requests and wait for both to finish:
#
# Promise.when(get_json, get_json2).then |first, second|
# puts "got two json payloads: #{first}, #{second}"
# end
#
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)
end
attr_reader :error, :prev, :next
def initialize(action = {})
@action = action
@realized = false
@exception = false
@value = nil
@error = nil
@delayed = false
@prev = nil
@next = []
end
def value
if Promise === @value
@value.value
else
@value
end
end
def act?
@action.key?(:success) || @action.key?(:always)
end
def action
@action.keys
end
def exception?
@exception
end
def realized?
@realized != false
end
def resolved?
@realized == :resolve
end
def rejected?
@realized == :reject
end
def ^(promise)
promise << self
self >> promise
promise
end
def <<(promise)
@prev = promise
self
end
def >>(promise)
@next << promise
if exception?
promise.reject(@delayed[0])
elsif resolved?
promise.resolve(@delayed ? @delayed[0] : value)
elsif rejected?
if !@action.key?(:failure) || Promise === (@delayed ? @delayed[0] : @error)
promise.reject(@delayed ? @delayed[0] : error)
elsif promise.action.include?(:always)
promise.reject(@delayed ? @delayed[0] : error)
end
end
self
end
def resolve(value = nil)
if realized?
raise ArgumentError, 'the promise has already been realized'
end
if Promise === value
return (value << @prev) ^ self
end
begin
block = @action[:success] || @action[:always]
if block
value = block.call(value)
end
resolve!(value)
rescue Exception => e
exception!(e)
end
self
end
def resolve!(value)
@realized = :resolve
@value = value
if @next.any?
@next.each { |p| p.resolve(value) }
else
@delayed = [value]
end
end
def reject(value = nil)
if realized?
raise ArgumentError, 'the promise has already been realized'
end
if Promise === value
return (value << @prev) ^ self
end
begin
block = @action[:failure] || @action[:always]
if block
value = block.call(value)
end
if @action.key?(:always)
resolve!(value)
else
reject!(value)
end
rescue Exception => e
exception!(e)
end
self
end
def reject!(value)
@realized = :reject
@error = value
if @next.any?
@next.each { |p| p.reject(value) }
else
@delayed = [value]
end
end
def exception!(error)
@exception = true
reject!(error)
end
def then(&block)
self ^ Promise.new(success: block)
end
def then!(&block)
there_can_be_only_one!
self.then(&block)
end
def fail(&block)
self ^ Promise.new(failure: block)
end
def fail!(&block)
there_can_be_only_one!
fail(&block)
end
def always(&block)
self ^ Promise.new(always: block)
end
def always!(&block)
there_can_be_only_one!
always(&block)
end
def trace(depth = nil, &block)
self ^ Trace.new(depth, block)
end
def trace!(*args, &block)
there_can_be_only_one!
trace(*args, &block)
end
def there_can_be_only_one!
if @next.any?
raise ArgumentError, 'a promise has already been chained'
end
end
def inspect
result = "#<#{self.class}(#{object_id})"
if @next.any?
result += " >> #{@next.inspect}"
end
result += if realized?
": #{(@value || @error).inspect}>"
else
'>'
end
result
end
def to_v2
v2 = PromiseV2.new
self.then { |i| v2.resolve(i) }.rescue { |i| v2.reject(i) }
v2
end
# PromiseV1 is not a native construct, we must convert it to a v2 promise
alias await to_v2
alias catch fail
alias catch! fail!
alias do then
alias do! then!
alias ensure always
alias ensure! always!
alias finally always
alias finally! always!
alias rescue fail
alias rescue! fail!
alias to_n to_v2
alias to_v1 itself
class Trace < self
def self.it(promise)
current = []
if promise.act? || promise.prev.nil?
current.push(promise.value)
end
prev = promise.prev
if prev
current.concat(it(prev))
else
current
end
end
def initialize(depth, block)
@depth = depth
super success: proc {
trace = Trace.it(self).reverse
trace.pop
if depth && depth <= trace.length
trace.shift(trace.length - depth)
end
block.call(*trace)
}
end
end
class When < self
def initialize(promises = [])
super()
@wait = []
promises.each do |promise|
wait promise
end
end
def each(&block)
raise ArgumentError, 'no block given' unless block
self.then do |values|
values.each(&block)
end
end
def collect(&block)
raise ArgumentError, 'no block given' unless block
self.then do |values|
When.new(values.map(&block))
end
end
def inject(*args, &block)
self.then do |values|
values.reduce(*args, &block)
end
end
def wait(promise)
unless Promise === promise
promise = Promise.value(promise)
end
if promise.act?
promise = promise.then
end
@wait << promise
promise.always do
try if @next.any?
end
self
end
def >>(*)
super.tap do
try
end
end
def try
if @wait.all?(&:realized?)
promise = @wait.find(&:rejected?)
if promise
reject(promise.error)
else
resolve(@wait.map(&:value))
end
end
end
alias map collect
alias reduce inject
alias and wait
end
end
PromiseV1 = Promise