Class: Async::Promise
- Inherits:
-
Variable
- Object
- Variable
- Async::Promise
- Defined in:
- lib/async/promise.rb
Overview
and which child Promise nodes to propagate the output values of the resolver/rejector to.
Constant Summary collapse
- VERSION =
"0.1.1"
Class Method Summary collapse
-
.all(promises = []) ⇒ Object
Create a new Promise that resolves when all of its input promises have been resolved, and rejects when any single input promise is rejected.
-
.race(promises) ⇒ Object
Create a new Promise that either rejects or resolves based on whichever encompassing promise settles first.
-
.reject(reason = "Promise error") ⇒ Object
Create a new Promise that is already rejected with the provided [reason].
-
.resolve(value = nil) ⇒ Object
Create a new Promise that is already resolved with the provided [value] of type ‘T`.
-
.timeout(resolve_in = nil, reject_in = nil, resolve: nil, reject: "Promise timeout") ⇒ Object
Create a promise that either resolves or rejects after a given timeout.
Instance Method Summary collapse
-
#async_resolve ⇒ Object
rename the ‘Async::Variable.resolve` instance method to `async_resolve`, since we will be using the same method name for our own logic of resolving values.
-
#async_wait ⇒ Object
rename the ‘Async::Variable.wait` instance method to `async_wait`, since we will need to tap into the waiting process to raise any errors that may have occurred during the process.
-
#catch(on_reject = nil) ⇒ Promise
A catch method is supposed to rescue any rejections that are made by the parent promise.
-
#initialize(on_resolve = nil, on_reject = nil) ⇒ Promise
constructor
A new instance of Promise.
-
#reject(reason = "Promise error") ⇒ Object
Reject the value of this Promise node with an optional error reason.
-
#resolve(value = nil) ⇒ Object
Resolve the value of this Promise node.
-
#status ⇒ "pending", ...
Get the status of the this Promise.
-
#then(on_resolve = nil, on_reject = nil) ⇒ Promise
Returns a new promise, so that multiple [then] and [catch] calls can be chained.
-
#wait ⇒ Object
Wait for the Promise to be resolved, or rejected.
Constructor Details
#initialize(on_resolve = nil, on_reject = nil) ⇒ Promise
Returns a new instance of Promise.
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/async/promise.rb', line 17 def initialize(on_resolve = nil, on_reject = nil) super() # @type ["pending", "fulfilled", "rejected"] Represents the current state of this Promise node. @status = "pending" # @type [Array<Promise>] An array of child [Promise] nodes that will be notified when this promise resolves. # the resulting structure is a tree-like, and we shall traverse them in DFS (depth first search). @children = [] # @type [Any] the value of type `T` that will be assigned to this node when it resolves. # this value is kept purely for the sake of providing latecomer-then calls with a resolve value (if the `@status` was "fulfilled") # a latecomer is when a call to the [then] or [catch] method is made after the promise has already been "fulfilled". @value = nil # @type [String, StandardError] the error reason that will be assigned to this node when it is rejected. # this value is kept purely for the sake of providing latecomer-then calls with a rejection reason (if the `@status` was "rejected") # a latecomer is when a call to the [then] or [catch] method is made after the promise has already been "rejected". @reason = nil @on_resolve = on_resolve @on_reject = on_reject end |
Class Method Details
.all(promises = []) ⇒ Object
Create a new Promise that resolves when all of its input promises have been resolved, and rejects when any single input promise is rejected. return [Promise<Array<T>>] Returns a Promise which, when resolved, contains the array of all resolved values. TODO: create unit test
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/async/promise.rb', line 61 def self.all(promises = []) # we must return early on if no promises we given, since nothing will then resolve the new_promise. return self.resolve([]) if promises.empty? # next, we make sure that all values within the `promises` array are actual `Promise`s. the ones that aren't, are converted to a resolved promise. promises = promises.map { |p| p.is_a?(Promise) ? p : self.resolve(p) } resolved_values = [] remaining_promises = promises.length new_promise = new() # The following may not be the prettiest implementation. TODO: consider if you can use a Array.map for this function promises.each_with_index { |promise, index| promise.then(->(value) { resolved_values[index] = value remaining_promises -= 1 if remaining_promises == 0 new_promise.resolve(resolved_values) end }, ->(reason) { # if there is any rejected dependency promise, we should immediately reject our new_promise # note that this is somewhat of a error-racing scenario, since the new promise will be rejected due to the first error it encounters. # i.e. its order can vary from time to time, possibly resulting in different kinds of rejection reasons new_promise.reject(reason) }) } new_promise end |
.race(promises) ⇒ Object
Create a new Promise that either rejects or resolves based on whichever encompassing promise settles first. The value it either resolves or rejects with, will be inherited by the promise that wins the race. return [Promise<Array<T>>] Returns a Promise which, when resolved, contains the array of all resolved values.
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/async/promise.rb', line 94 def self.race(promises) # if any of the promises happens to be a regular value (i.e. not an Promise), then we will just return that (whichever one that we encounter first) promises.each { |p| unless p.is_a?(Promise) return self.resolve(p) end } new_promise = new() # in the racing condition, there is no need to check if `new_promise` was already settled, because the `resolve` and `reject` methods already ignore requests made after the resolve/reject. # thus it is ok for our each loop to mindlessly and rapidly call `new_promise.resolve` and `new_promise.reject` with no consequences (might affect performance? probably just a micro optimization). promises.each { |promise| promise.then( ->(value) { new_promise.resolve(value) }, ->(reason) { new_promise.reject(reason) }, ) } new_promise end |
.reject(reason = "Promise error") ⇒ Object
Create a new Promise that is already rejected with the provided [reason]. WARNING: Do not pass a ‘nil` as the reason, because it will break the error handling logic, since it will seem to it like there was no error. return [Promise<nil>] The newly created (and rejected) promise is returned. TODO: create unit test, and maybe reduce the amount of pre-resolved rejects you create in your tests through the use of this
51 52 53 54 55 |
# File 'lib/async/promise.rb', line 51 def self.reject(reason = "Promise error") new_promise = new() new_promise.reject(reason) new_promise end |
.resolve(value = nil) ⇒ Object
Create a new Promise that is already resolved with the provided [value] of type ‘T`. return [Promise<T>] The newly created (and resolved) promise is returned. TODO: create unit test, and maybe reduce the amount of pre-resolved promises you create in your tests through the use of this
40 41 42 43 44 |
# File 'lib/async/promise.rb', line 40 def self.resolve(value = nil) new_promise = new() new_promise.resolve(value) new_promise end |
.timeout(resolve_in = nil, reject_in = nil, resolve: nil, reject: "Promise timeout") ⇒ Object
Create a promise that either resolves or rejects after a given timeout.
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/async/promise.rb', line 118 def self.timeout(resolve_in = nil, reject_in = nil, resolve: nil, reject: "Promise timeout") new_promise = new # if both timers are `nil`, then we will just return a never resolving promise (at least not from here. but it can be resolved externally) return new_promise if resolve_in.nil? && reject_in.nil? Async do # determine the shorter timeout and take the action of either resolving or rejecting after the timeout if (reject_in.nil?) || ((not resolve_in.nil?) && (resolve_in <= reject_in)) sleep(resolve_in) new_promise.resolve(resolve) else sleep(reject_in) new_promise.reject(reject) end end new_promise end |
Instance Method Details
#async_resolve ⇒ Object
rename the ‘Async::Variable.resolve` instance method to `async_resolve`, since we will be using the same method name for our own logic of resolving values.
12 |
# File 'lib/async/promise.rb', line 12 alias_method :async_resolve, :resolve |
#async_wait ⇒ Object
rename the ‘Async::Variable.wait` instance method to `async_wait`, since we will need to tap into the waiting process to raise any errors that may have occurred during the process.
13 |
# File 'lib/async/promise.rb', line 13 alias_method :async_wait, :wait |
#catch(on_reject = nil) ⇒ Promise
A catch method is supposed to rescue any rejections that are made by the parent promise. it is syntactically equivalent to a ‘self.then(nil, on_reject)` call.
226 227 228 |
# File 'lib/async/promise.rb', line 226 def catch(on_reject = nil) self.then(nil, on_reject) end |
#reject(reason = "Promise error") ⇒ Object
Reject the value of this Promise node with an optional error reason. WARNING: Do not pass a ‘nil` as the reason, because it will break the error handling logic, since it will seem to it like there was no error. return [void] nothing is returned.
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/async/promise.rb', line 171 def reject(reason = "Promise error") return nil if @status != "pending" # if the promise is already fulfilled or rejected, return immediately Async do |task| # since there has been an error, we must call the `@on_reject` method to see if it handles it appropriately (by not raising another error and giving a return value). # if there is no `on_reject` available, we will just have to continue with the error propagation to the children. if @on_reject.nil? # no rejection handler exists, thus the children must bear the responsibility of handling the error self.handle_imminent_reject(reason) else # an `@on_reject` handler exists, so lets see if it can reolve the current error with a value. new_handled_value = nil begin new_handled_value = @on_reject.call(reason) rescue => new_error_reason # a new error occurred in the `@on_reject` handler, resulting in no resolvable value. # we must now pass on the responsibility of handling it to the children. self.handle_imminent_reject(new_error_reason) else # the `@on_reject` function handled the error appropriately and returned a value, so we may now resolve the children with that new value. self.handle_imminent_resolve(new_handled_value) end end end nil end |
#resolve(value = nil) ⇒ Object
Resolve the value of this Promise node. return [void] nothing is returned.
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/async/promise.rb', line 138 def resolve(value = nil) return nil if @status != "pending" # if the promise is already fulfilled or rejected, return immediately Async do |task| if value.is_a?(Promise) # if the provided value itself is a promise, then this (self) promise will need to become dependant on it. value.then( ->(resolved_value) { self.resolve(resolved_value); resolved_value }, ->(rejection_reason) { self.reject(rejection_reason); rejection_reason }, ) else # otherwise, since we have an actual resolved value at hand, we may now pass it onto the dependent children. begin value = @on_resolve.nil? \ ? value : @on_resolve.call(value) # it's ok if `@on_resolve` returns another promise object, because the children will then lach on to its promise when their `resolve` method is called. rescue => error_reason # some uncaught error occurred while running the `@on_resolve` function. we should now reject this (self) promise, and pass the responsibility of handling to the children (if any). self.handle_imminent_reject(error_reason) else # no errors occurred after running the `@on_resolve` function. we may now resolve the children. self.handle_imminent_resolve(value) end end end nil end |
#status ⇒ "pending", ...
Get the status of the this Promise. This should be used for debugging purposes only. your application logic MUST NOT depend on it, at all.
246 247 248 |
# File 'lib/async/promise.rb', line 246 def status @status end |
#then(on_resolve = nil, on_reject = nil) ⇒ Promise
Returns a new promise, so that multiple [then] and [catch] calls can be chained.
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/async/promise.rb', line 202 def then(on_resolve = nil, on_reject = nil) chainable_promise = self.class.new(on_resolve, on_reject) Async do |task| case @status when "pending" # add the new promise as a child to the currently pending promise. once this promise resolves, the new child will be notified. @children << chainable_promise when "fulfilled" # this promise has already been fulfilled, so the child must be notified of the resolved value immediately. chainable_promise.resolve(@value) when "rejected" # this promise has already been rejected, so the child must be notified of the rejection reason immediately. chainable_promise.reject(@reason) end end chainable_promise end |
#wait ⇒ Object
Wait for the Promise to be resolved, or rejected. If the Promise was rejected, and you wait for it, then it will raise an error, which you will have to rescue externally.
234 235 236 237 238 239 240 241 |
# File 'lib/async/promise.rb', line 234 def wait value = self.async_wait() # if an error had existed for this promise, then we shall raise it now. raise @reason unless @reason.nil? # if `value` is a promise object, then we will have to await for it to resolve return value.wait() if value.is_a?(Promise) value end |