stdlib/promise/v2.rb
# backtick_javascript: true
# {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/v2'
#
# ## 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 = PromiseV2.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 = PromiseV2.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 = PromiseV2.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 {PromiseV2} 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
#
# {PromiseV2.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:
#
# PromiseV2.when(get_json, get_json2).then |first, second|
# puts "got two json payloads: #{first}, #{second}"
# end
#
class PromiseV2 < `Promise`
class << self
def allocate
ok, fail = nil, nil
prom = `new self.$$constructor(function(_ok, _fail) { #{ok} = _ok; #{fail} = _fail; })`
prom.instance_variable_set(:@type, :opal)
prom.instance_variable_set(:@resolve_proc, ok)
prom.instance_variable_set(:@reject_proc, fail)
prom
end
def when(*promises)
promises = Array(promises.length == 1 ? promises.first : promises)
`Promise.all(#{promises})`.tap do |prom|
prom.instance_variable_set(:@type, :when)
end
end
def all_resolved(*promises)
promises = Array(promises.length == 1 ? promises.first : promises)
`Promise.allResolved(#{promises})`.tap do |prom|
prom.instance_variable_set(:@type, :all_resolved)
end
end
def any(*promises)
promises = Array(promises.length == 1 ? promises.first : promises)
`Promise.any(#{promises})`.tap do |prom|
prom.instance_variable_set(:@type, :any)
end
end
def race(*promises)
promises = Array(promises.length == 1 ? promises.first : promises)
`Promise.race(#{promises})`.tap do |prom|
prom.instance_variable_set(:@type, :race)
end
end
def resolve(value = nil)
`Promise.resolve(#{value})`.tap do |prom|
prom.instance_variable_set(:@type, :resolve)
prom.instance_variable_set(:@realized, :resolve)
prom.instance_variable_set(:@value_set, true)
prom.instance_variable_set(:@value, value)
end
end
def reject(value = nil)
`Promise.reject(#{value})`.tap do |prom|
prom.instance_variable_set(:@type, :reject)
prom.instance_variable_set(:@realized, :reject)
prom.instance_variable_set(:@value_set, true)
prom.instance_variable_set(:@value, value)
end
end
alias all when
alias error reject
alias value resolve
end
attr_reader :prev, :next
# Is this promise native to JavaScript? This means, that methods like resolve
# or reject won't be available.
def native?
@type != :opal
end
# Raise an exception when a non-JS-native method is called on a JS-native promise
def nativity_check!
raise ArgumentError, 'this promise is native to JavaScript' if native?
end
# Raise an exception when a non-JS-native method is called on a JS-native promise
# but permits some typed promises
def light_nativity_check!
return if %i[reject resolve trace always fail then].include? @type
raise ArgumentError, 'this promise is native to JavaScript' if native?
end
# Allow only one chain to be present, as needed by the previous implementation.
# This isn't a strict check - it's always possible on the JS side to chain a
# given block.
def there_can_be_only_one!
raise ArgumentError, 'a promise has already been chained' if @next && @next.any?
end
def gen_tracing_proc(passing, &block)
proc do |i|
res = passing.call(i)
yield(res)
res
end
end
def resolve(value = nil)
nativity_check!
raise ArgumentError, 'this promise was already resolved' if @realized
@value_set = true
@value = value
@realized = :resolve
@resolve_proc.call(value)
self
end
def reject(value = nil)
nativity_check!
raise ArgumentError, 'this promise was already resolved' if @realized
@value_set = true
@value = value
@realized = :reject
@reject_proc.call(value)
self
end
def then(&block)
prom = nil
blk = gen_tracing_proc(block) do |val|
prom.instance_variable_set(:@realized, :resolve)
prom.instance_variable_set(:@value_set, true)
prom.instance_variable_set(:@value, val)
end
prom = `self.then(#{blk})`
prom.instance_variable_set(:@prev, self)
prom.instance_variable_set(:@type, :then)
(@next ||= []) << prom
prom
end
def then!(&block)
there_can_be_only_one!
self.then(&block)
end
def fail(&block)
prom = nil
blk = gen_tracing_proc(block) do |val|
prom.instance_variable_set(:@realized, :resolve)
prom.instance_variable_set(:@value_set, true)
prom.instance_variable_set(:@value, val)
end
prom = `self.catch(#{blk})`
prom.instance_variable_set(:@prev, self)
prom.instance_variable_set(:@type, :fail)
(@next ||= []) << prom
prom
end
def fail!(&block)
there_can_be_only_one!
fail(&block)
end
def always(&block)
prom = nil
blk = gen_tracing_proc(block) do |val|
prom.instance_variable_set(:@realized, :resolve)
prom.instance_variable_set(:@value_set, true)
prom.instance_variable_set(:@value, val)
end
prom = `self.finally(function() { return blk(self.$value_internal()); })`
prom.instance_variable_set(:@prev, self)
prom.instance_variable_set(:@type, :always)
(@next ||= []) << prom
prom
end
def always!(&block)
there_can_be_only_one!
always(&block)
end
def trace(depth = nil, &block)
prom = self.then do
values = []
prom = self
while prom && (!depth || depth > 0)
val = nil
begin
val = prom.value
rescue ArgumentError
val = :native
end
values.unshift(val)
depth -= 1 if depth
prom = prom.prev
end
yield(*values)
end
prom.instance_variable_set(:@type, :trace)
prom
end
def trace!(*args, &block)
there_can_be_only_one!
trace(*args, &block)
end
def resolved?
light_nativity_check!
@realized == :resolve
end
def rejected?
light_nativity_check!
@realized == :reject
end
def realized?
light_nativity_check!
!@realized.nil?
end
def value
if resolved?
value_internal
end
end
def error
light_nativity_check!
@value if rejected?
end
def and(*promises)
promises = promises.map do |i|
if PromiseV2 === i
i
else
PromiseV2.value(i)
end
end
PromiseV2.when(self, *promises).then do |a, *b|
[*a, *b]
end
end
def initialize(&block)
yield self if block_given?
end
def to_v1
v1 = PromiseV1.new
self.then { |i| v1.resolve(i) }.rescue { |i| v1.reject(i) }
v1
end
def inspect
result = "#<#{self.class}"
if @type
result += ":#{@type}" unless %i[opal resolve reject].include? @type
else
result += ':native'
end
result += ":#{@realized}" if @realized
result += "(#{object_id})"
if @next && @next.any?
result += " >> #{@next.inspect}"
end
result += ": #{value.inspect}"
result += '>'
result
end
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 reject! reject
alias rescue fail
alias rescue! fail!
alias resolve! resolve
alias to_n itself
alias to_v2 itself
private
def value_internal
if PromiseV2 === @value
@value.value
elsif @value_set
@value
elsif @prev
@prev.value
end
end
end