Class: Async::Promise

Inherits:
Variable
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initialize(on_resolve = nil, on_reject = nil) ⇒ Promise

Returns a new instance of Promise.

Parameters:

  • on_resolve ((value) => next_value_or_promise, nil) (defaults to: nil)

    the function to call when the promise is resolved with a value.

  • on_reject ((reason) => next_value_or_promise, nil) (defaults to: nil)

    the function to call when the promise is rejected (either manually or due to a raised error).



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

Parameters:

  • promises (Array<[T, Promise<T>]>) (defaults to: [])

    Provide all of the input T or Promise<T> to wait for, in order to resolve.



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.

Parameters:

  • promises (Array<[T, Promise<T>]>)

    Provide all of the input Promise<T> to wait for, in order to resolve.



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

Parameters:

  • reason (String, StandardError) (defaults to: "Promise error")

    Give the reason for rejection.



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

Parameters:

  • value (T, Promise<T>) (defaults to: nil)

    Generic value of type ‘T`.



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.

Parameters:

  • resolve_in (Float, nil) (defaults to: nil)

    The time in seconds to wait before resolving with keyword-value ‘resolve` (if provided).

  • reject_in (Float, nil) (defaults to: nil)

    The time in seconds to wait before rejecting with keyword-reason ‘reject` (if provided).

  • resolve (Any) (defaults to: nil)

    The value to resolve with after ‘resolve_in` seconds.

  • reject (String, StandardError) (defaults to: "Promise timeout")

    The reason to reject after ‘reject_in` seconds.



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_resolveObject

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_waitObject

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.

Parameters:

  • on_reject ((reason) => next_value_or_promise, nil) (defaults to: nil)

    the function to call when the promise is rejected (either manually or due to a raised error).

Returns:

  • (Promise)

    returns a new promise, so that multiple [then] and [catch] calls can be chained.



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.

Parameters:

  • reason (String, StandardError) (defaults to: "Promise error")

    The error to pass on to the next series of dependant promises.



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.

Parameters:

  • value (T, Promise<T>) (defaults to: nil)

    Generic value of type ‘T`.



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.

Returns:

  • ("pending", "fulfilled", "rejected")

    Represents the current state of this Promise node.



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.

Parameters:

  • on_resolve ((value) => next_value_or_promise, nil) (defaults to: nil)

    the function to call when the promise is resolved with a value.

  • on_reject ((reason) => next_value_or_promise, nil) (defaults to: nil)

    the function to call when the promise is rejected (either manually or due to a raised error).

Returns:

  • (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

#waitObject

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.

Raises:

  • (String, StandardError)

    The error/reason for the rejection of this Promise.



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