Class: Scarpe::Promise
- Inherits:
-
Object
- Object
- Scarpe::Promise
- Includes:
- Shoes::Log
- Defined in:
- lib/scarpe/components/promises.rb
Overview
Scarpe::Promise is a promises library, but one with no form of built-in concurrency. Instead, promise callbacks are executed synchronously. Even execution is usually synchronous, but can also be handled manually for forms of execution not controlled in Ruby (like Webview.)
Funny thing… We need promises as an API concept since we have a JS event loop doing its thing, and we need to respond to actions that it takes. But there’s not really a Ruby implementation of Promises without an attached form of concurrency. So here we are, writing our own :-/
In theory you could probably write some kind of “no-op thread pool” for the ruby-concurrency gem, pass it manually to every promise we created and then raise an exception any time we tried to do something in the background. That’s probably more code than writing our own, though, and we’d be fighting it constantly.
This class is inspired by concurrent-ruby [Promise](ruby-concurrency.github.io/concurrent-ruby/1.1.5/Concurrent/Promise.html) which is inspired by Javascript Promises, which is what we actually need for our use case. We can’t easily tell when our WebView begins processing our request, which removes the :processing state. This can be used for executing JS, but also generally waiting on events.
We don’t fully control ordering here, so it is conceivable that a child waiting on a parent can be randomly fulfilled, even if we didn’t expect it. We don’t consider that an error. Similarly, we’ll call on_scheduled callbacks if a promise is fulfilled, even though we never explicitly scheduled it. If a promise is rejected without ever being scheduled, we won’t call those callbacks.
Constant Summary collapse
- PROMISE_STATES =
The unscheduled promise state means it’s waiting on a parent promise that hasn’t completed yet. The pending state means it’s waiting to execute. Fulfilled means it has completed successfully and returned a value, while rejected means it has failed, normally producing a reason.
[:unscheduled, :pending, :fulfilled, :rejected]
Instance Attribute Summary collapse
-
#parents ⇒ Object
readonly
The parent promises of this promise, sometimes an empty array.
-
#reason ⇒ Object
readonly
If the promise is rejected, this is the reason, sometimes an exception.
-
#returned_value ⇒ Object
readonly
If the promise is fulfilled, this is the value returned.
-
#state ⇒ Object
readonly
The state of the promise, which should be one of PROMISE_STATES.
Class Method Summary collapse
-
.fulfilled(return_val = nil, parents: [], &block) ⇒ Object
Create a promise and then instantly fulfill it.
-
.rejected(reason = nil, parents: []) ⇒ Object
Create a promise and then instantly reject it.
Instance Method Summary collapse
-
#complete? ⇒ Boolean
Return true if the Promise is either fulfilled or rejected.
-
#fulfilled!(value = nil) ⇒ Object
Fulfill the promise, setting the returned_value to value.
-
#fulfilled? ⇒ Boolean
Return true if the promise is already fulfilled.
-
#initialize(state: nil, parents: []) { ... } ⇒ Promise
constructor
The Promise.new method, along with all the various handlers, are pretty raw.
-
#inspect ⇒ Object
An inspect method to give slightly smaller output, for ease of reading in irb.
-
#on_fulfilled { ... } ⇒ Scarpe::Promise
Register a handler to be called when the promise is fulfilled.
-
#on_rejected { ... } ⇒ Scarpe::Promise
Register a handler to be called when the promise is rejected.
-
#on_scheduled { ... } ⇒ Scarpe::Promise
Register a handler to be called when the promise is scheduled.
-
#rejected!(reason = nil) ⇒ Object
Reject the promise, setting the reason to reason.
-
#rejected? ⇒ Boolean
Return true if the promise is already rejected.
-
#then(&block) ⇒ Object
Create a new promise with this promise as a parent.
-
#to_execute(&block) ⇒ Object
These promises are mostly designed for external execution.
Constructor Details
#initialize(state: nil, parents: []) { ... } ⇒ Promise
The Promise.new method, along with all the various handlers, are pretty raw. They’ll do what promises do, but they’re not the prettiest. However, they ensure that guarantees are made and so on, so they’re great as plumbing under the syntactic sugar above.
Note that the state passed in may not be the actual initial state. If a parent is rejected, the state will become rejected. If no parents are waiting or failed then a state of nil or :unscheduled will become :pending.
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/scarpe/components/promises.rb', line 99 def initialize(state: nil, parents: [], &scheduler) log_init("Promise") # These are as initially specified, and effectively immutable @state = state @parents = parents # These are what we're waiting on, and will be # removed as time goes forward. @waiting_on = parents.select { |p| !p.complete? } @on_fulfilled = [] @on_rejected = [] @on_scheduled = [] @scheduler = scheduler @executor = nil @returned_value = nil @reason = nil if complete? # Did we start out already fulfilled or rejected? # If so, we can skip a lot of fiddly checking. # Don't need a scheduler or to care about parents # or anything. @waiting_on = [] @scheduler = nil elsif @parents.any? { |p| p.state == :rejected } @state = :rejected @waiting_on = [] @scheduler = nil elsif @state == :pending # Did we get an explicit :pending? Then we don't need # to schedule ourselves, or care about the scheduler # in general. @scheduler = nil elsif @state.nil? || @state == :unscheduled # If no state was given or we're unscheduled, we'll # wait until our parents have all completed to # schedule ourselves. if @waiting_on.empty? # No further dependencies, we can schedule ourselves @state = :pending # We have no on_scheduled handlers yet, but this will # call and clear the scheduler. call_handlers_for(:pending) else # We're still waiting on somebody, no scheduling yet @state = :unscheduled @waiting_on.each do |dep| dep.on_fulfilled { parent_fulfilled!(dep) } dep.on_rejected { parent_rejected!(dep) } end end end end |
Instance Attribute Details
#parents ⇒ Object (readonly)
The parent promises of this promise, sometimes an empty array
47 48 49 |
# File 'lib/scarpe/components/promises.rb', line 47 def parents @parents end |
#reason ⇒ Object (readonly)
If the promise is rejected, this is the reason, sometimes an exception
53 54 55 |
# File 'lib/scarpe/components/promises.rb', line 53 def reason @reason end |
#returned_value ⇒ Object (readonly)
If the promise is fulfilled, this is the value returned
50 51 52 |
# File 'lib/scarpe/components/promises.rb', line 50 def returned_value @returned_value end |
#state ⇒ Object (readonly)
The state of the promise, which should be one of PROMISE_STATES
44 45 46 |
# File 'lib/scarpe/components/promises.rb', line 44 def state @state end |
Class Method Details
.fulfilled(return_val = nil, parents: [], &block) ⇒ Object
Create a promise and then instantly fulfill it.
56 57 58 59 60 |
# File 'lib/scarpe/components/promises.rb', line 56 def self.fulfilled(return_val = nil, parents: [], &block) p = Promise.new(parents: parents, &block) p.fulfilled!(return_val) p end |
Instance Method Details
#complete? ⇒ Boolean
Return true if the Promise is either fulfilled or rejected.
159 160 161 |
# File 'lib/scarpe/components/promises.rb', line 159 def complete? @state == :fulfilled || @state == :rejected end |
#fulfilled!(value = nil) ⇒ Object
Fulfill the promise, setting the returned_value to value
70 71 72 |
# File 'lib/scarpe/components/promises.rb', line 70 def fulfilled!(value = nil) set_state(:fulfilled, value) end |
#fulfilled? ⇒ Boolean
Return true if the promise is already fulfilled.
166 167 168 |
# File 'lib/scarpe/components/promises.rb', line 166 def fulfilled? @state == :fulfilled end |
#inspect ⇒ Object
An inspect method to give slightly smaller output, for ease of reading in irb
178 179 180 181 182 183 184 185 186 |
# File 'lib/scarpe/components/promises.rb', line 178 def inspect "#<Scarpe::Promise:#{object_id} " + "@state=#{@state.inspect} @parents=#{@parents.inspect} " + "@waiting_on=#{@waiting_on.inspect} @on_fulfilled=#{@on_fulfilled.size} " + "@on_rejected=#{@on_rejected.size} @on_scheduled=#{@on_scheduled.size} " + "@scheduler=#{@scheduler ? "Y" : "N"} @executor=#{@executor ? "Y" : "N"} " + "@returned_value=#{@returned_value.inspect} @reason=#{@reason.inspect}" + ">" end |
#on_fulfilled { ... } ⇒ Scarpe::Promise
Register a handler to be called when the promise is fulfilled. If called on a fulfilled promise, the handler will be called immediately.
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 |
# File 'lib/scarpe/components/promises.rb', line 390 def on_fulfilled(&handler) unless handler raise Shoes::Errors::InvalidAttributeValueError, "You must pass a block to on_fulfilled!" end case @state when :fulfilled handler.call(*@parents.map(&:returned_value)) when :pending, :unscheduled @on_fulfilled << handler when :rejected # Do nothing end self end |
#on_rejected { ... } ⇒ Scarpe::Promise
Register a handler to be called when the promise is rejected. If called on a rejected promise, the handler will be called immediately.
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 |
# File 'lib/scarpe/components/promises.rb', line 412 def on_rejected(&handler) unless handler raise Shoes::Errors::InvalidAttributeValueError, "You must pass a block to on_rejected!" end case @state when :rejected handler.call(*@parents.map(&:returned_value)) when :pending, :unscheduled @on_rejected << handler when :fulfilled # Do nothing end self end |
#on_scheduled { ... } ⇒ Scarpe::Promise
Register a handler to be called when the promise is scheduled. If called on a promise that was scheduled earlier, the handler will be called immediately.
435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 |
# File 'lib/scarpe/components/promises.rb', line 435 def on_scheduled(&handler) unless handler raise Shoes::Errors::InvalidAttributeValueError, "You must pass a block to on_scheduled!" end # Add a pending handler or call it now case @state when :fulfilled, :pending handler.call(*@parents.map(&:returned_value)) when :unscheduled @on_scheduled << handler when :rejected # Do nothing end self end |
#rejected!(reason = nil) ⇒ Object
Reject the promise, setting the reason to reason
75 76 77 |
# File 'lib/scarpe/components/promises.rb', line 75 def rejected!(reason = nil) set_state(:rejected, reason) end |
#rejected? ⇒ Boolean
Return true if the promise is already rejected.
173 174 175 |
# File 'lib/scarpe/components/promises.rb', line 173 def rejected? @state == :rejected end |
#then(&block) ⇒ Object
Create a new promise with this promise as a parent. It runs the specified code in block when scheduled.
81 82 83 |
# File 'lib/scarpe/components/promises.rb', line 81 def then(&block) Promise.new(parents: [self], &block) end |
#to_execute(&block) ⇒ Object
These promises are mostly designed for external execution. You could put together your own thread-pool, or use RPC, a WebView, a database or similar source of external calculation. But in many cases it’s reasonable to execute locally. In those cases, you can register an executor which will be called when the promise is ready to execute but has not yet done so. Registering an executor on a promise that is already fulfilled is an error. Registering an executor on a promise that has already rejected is a no-op.
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/scarpe/components/promises.rb', line 197 def to_execute(&block) case @state when :fulfilled # Should this be a no-op instead? raise Scarpe::NoOperationError, "Registering an executor on an already fulfilled promise means it will never run!" when :rejected return when :unscheduled @executor = block # save for later when :pending @executor = block call_executor else raise Scarpe::InternalError, "Internal error, illegal state!" end self end |