rosarium 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +25 -0
- data/LICENSE +13 -0
- data/README.md +119 -0
- data/lib/rosarium/deferred.rb +25 -0
- data/lib/rosarium/fixed_thread_executor.rb +61 -0
- data/lib/rosarium/promise.rb +111 -0
- data/lib/rosarium/simple_promise.rb +155 -0
- data/lib/rosarium.rb +10 -0
- data/spec/deferred_spec.rb +134 -0
- data/spec/fixed_thread_executor_spec.rb +38 -0
- data/spec/promise_methods_spec.rb +71 -0
- data/spec/promise_static_spec.rb +103 -0
- data/spec/promise_test_helper.rb +39 -0
- metadata +74 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f0c296247c0162ee53783379115ff4a8aaa6a33e
|
4
|
+
data.tar.gz: fcfdc65d25c8610894b0c00087b4a12762c53056
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 36bc6091d46684b63bb142db5b4b638785ea69552f8e00f2377c7dfdbe88be38abcc9d60fe827bca648308c30071f272b58ff7aca87e86679f888527897e4ae9
|
7
|
+
data.tar.gz: 6a6a19090556dfba2defc71ff3c2ac44e884682003df62439148c39b1c546e49f7b4a074276c83362552fc66b1e47984ccac758c7ab0e9116f447fd2fab88e2c
|
data/Gemfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
gem 'rspec', '~> 3.4'
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
GEM
|
2
|
+
specs:
|
3
|
+
diff-lcs (1.2.5)
|
4
|
+
rspec (3.4.0)
|
5
|
+
rspec-core (~> 3.4.0)
|
6
|
+
rspec-expectations (~> 3.4.0)
|
7
|
+
rspec-mocks (~> 3.4.0)
|
8
|
+
rspec-core (3.4.1)
|
9
|
+
rspec-support (~> 3.4.0)
|
10
|
+
rspec-expectations (3.4.0)
|
11
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
12
|
+
rspec-support (~> 3.4.0)
|
13
|
+
rspec-mocks (3.4.0)
|
14
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
15
|
+
rspec-support (~> 3.4.0)
|
16
|
+
rspec-support (3.4.1)
|
17
|
+
|
18
|
+
PLATFORMS
|
19
|
+
ruby
|
20
|
+
|
21
|
+
DEPENDENCIES
|
22
|
+
rspec (~> 3.4)
|
23
|
+
|
24
|
+
BUNDLED WITH
|
25
|
+
1.11.2
|
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2016 Rachel Evans
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
# Rosarium
|
2
|
+
|
3
|
+
A library for implementing Promises - or something like them - in ruby.
|
4
|
+
|
5
|
+
# Why?
|
6
|
+
|
7
|
+
Because I keep hitting bugs and annoying inflexibilities in `concurrent-ruby`,
|
8
|
+
whereas I really enjoy the stability and flexibility of JavaScript's "Q"
|
9
|
+
library (<https://github.com/kriskowal/q/wiki/API-Reference>).
|
10
|
+
|
11
|
+
I'm not expecting anyone but me to use this code at this time. But you're
|
12
|
+
welcome to do so, if you like.
|
13
|
+
|
14
|
+
# Example
|
15
|
+
|
16
|
+
```
|
17
|
+
require 'rosarium'
|
18
|
+
```
|
19
|
+
|
20
|
+
## Static methods for creating promises:
|
21
|
+
|
22
|
+
```
|
23
|
+
# Immediately ready for async execution:
|
24
|
+
promise = Rosarium::Promise.execute { ... }
|
25
|
+
|
26
|
+
# Immediately fulfilled:
|
27
|
+
promise = Rosarium::Promise.resolve(anything_except_a_promise)
|
28
|
+
|
29
|
+
# Immediately rejected:
|
30
|
+
promise = Rosarium::Promise.reject(an_exception)
|
31
|
+
|
32
|
+
# The same promise (returns its argument)
|
33
|
+
a_promise = Rosarium::Promise.resolve(a_promise)
|
34
|
+
|
35
|
+
# Once all promises in the list are fulfilled, then fulfill with a list of
|
36
|
+
# their values. If any promise in the list is rejected, then reject with
|
37
|
+
# the same reason:
|
38
|
+
promise = Rosarium::Promise.all([ promise1, promise2, ... ])
|
39
|
+
|
40
|
+
# Wait for all the promises in the list to become settled (fulfilled or
|
41
|
+
# rejected); then fulfill with the list of promises.
|
42
|
+
promise = Rosarium::Promise.all_settled([ promise1, promise2, ... ])
|
43
|
+
```
|
44
|
+
|
45
|
+
## Deferreds
|
46
|
+
|
47
|
+
```
|
48
|
+
# Create a "deferred":
|
49
|
+
deferred = Rosarium::Promise.defer
|
50
|
+
promise = deferred.promise
|
51
|
+
```
|
52
|
+
|
53
|
+
then later, use the "deferred" to fulfill or reject the promise:
|
54
|
+
|
55
|
+
```
|
56
|
+
# Fulfill:
|
57
|
+
deferred.resolve(anything_except_a_promise)
|
58
|
+
|
59
|
+
# Reject:
|
60
|
+
deferred.reject(an_exception)
|
61
|
+
|
62
|
+
# Fulfill or reject, once the other promise is fulfilled / rejected:
|
63
|
+
deferred.resolve(other_promise)
|
64
|
+
```
|
65
|
+
|
66
|
+
## Methods of promises:
|
67
|
+
|
68
|
+
```
|
69
|
+
# One of: :pending, :resolving, :fulfilled, :rejected.
|
70
|
+
promise.state
|
71
|
+
|
72
|
+
# Wait for the promise to be settled, then return its value (if fulfilled -
|
73
|
+
# note the value may be nil), or nil (if rejected).
|
74
|
+
promise.value
|
75
|
+
|
76
|
+
# Wait for the promise to be settled, then return its reason (if rejected),
|
77
|
+
# or nil (if fulfilled).
|
78
|
+
promise.reason
|
79
|
+
|
80
|
+
# true iff state == :fulfilled
|
81
|
+
promise.fulfilled?
|
82
|
+
|
83
|
+
# true iff state == :rejected
|
84
|
+
promise.rejected?
|
85
|
+
|
86
|
+
# Wait for the promise to be settled, then return its value (if fulfilled),
|
87
|
+
# or raise with the rejection reason (if rejected).
|
88
|
+
promise.value!
|
89
|
+
|
90
|
+
# Wait for the promise to be settled
|
91
|
+
promise.wait
|
92
|
+
```
|
93
|
+
|
94
|
+
Chaining promises together:
|
95
|
+
|
96
|
+
```
|
97
|
+
# Handling promise1 fulfillment:
|
98
|
+
promise2 = promise1.then { |promise1_value| ... }
|
99
|
+
|
100
|
+
# Four different ways of handling promise1 rejection:
|
101
|
+
promise2 = promise1.then(Proc.new { |promise1_reason| ... })
|
102
|
+
promise2 = promise1.rescue { |promise1_reason| ... }
|
103
|
+
promise2 = promise1.catch { |promise1_reason| ... }
|
104
|
+
promise2 = promise1.on_error { |promise1_reason| ... }
|
105
|
+
|
106
|
+
# Handle both fulfillment and rejection:
|
107
|
+
promise2 = promise1.then(Proc.new { |promise1_reason| ... }) { |promise1_value| ... }
|
108
|
+
```
|
109
|
+
|
110
|
+
# Miscellany
|
111
|
+
|
112
|
+
Promise code (every time a ruby block appears in the above examples) is run
|
113
|
+
via a fixed-size thread pool, currently set to 10 threads. Execution order is
|
114
|
+
not defined.
|
115
|
+
|
116
|
+
In comparison to the Promises/A+ spec <https://promisesaplus.com/>, these
|
117
|
+
promises have an extra possible state, `:resolving`. You are encouraged to
|
118
|
+
use `#fulfilled?` and `#rejected?` instead anyway.
|
119
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Rosarium
|
2
|
+
|
3
|
+
class Deferred
|
4
|
+
|
5
|
+
def initialize(promise, resolver, rejecter)
|
6
|
+
@promise = promise
|
7
|
+
@resolver = resolver
|
8
|
+
@rejecter = rejecter
|
9
|
+
end
|
10
|
+
|
11
|
+
def promise
|
12
|
+
@promise
|
13
|
+
end
|
14
|
+
|
15
|
+
def resolve(value)
|
16
|
+
@resolver.call(value)
|
17
|
+
end
|
18
|
+
|
19
|
+
def reject(reason)
|
20
|
+
@rejecter.call(reason)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Rosarium
|
2
|
+
|
3
|
+
class FixedThreadExecutor
|
4
|
+
|
5
|
+
def initialize(max = 1)
|
6
|
+
@max = max
|
7
|
+
@mutex = Mutex.new
|
8
|
+
@waiting = []
|
9
|
+
@executing = 0
|
10
|
+
@threads = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def submit(&block)
|
14
|
+
@mutex.synchronize do
|
15
|
+
@waiting << block
|
16
|
+
if @executing < @max
|
17
|
+
@executing = @executing + 1
|
18
|
+
t = Thread.new { execute_and_count_down }
|
19
|
+
@threads.push t
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def discard
|
25
|
+
@mutex.synchronize { @waiting.clear }
|
26
|
+
end
|
27
|
+
|
28
|
+
def wait_until_idle
|
29
|
+
loop do
|
30
|
+
t = @mutex.synchronize { @threads.shift }
|
31
|
+
t or break
|
32
|
+
t.join
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def execute_and_count_down
|
39
|
+
begin
|
40
|
+
execute
|
41
|
+
ensure
|
42
|
+
@mutex.synchronize do
|
43
|
+
@executing = @executing - 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def execute
|
49
|
+
while true
|
50
|
+
block = @mutex.synchronize { @waiting.shift }
|
51
|
+
block or break
|
52
|
+
begin
|
53
|
+
block.call
|
54
|
+
rescue Exception => e
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Rosarium
|
2
|
+
|
3
|
+
class Promise < SimplePromise
|
4
|
+
|
5
|
+
DEFAULT_ON_FULFILL = Proc.new {|value| value}
|
6
|
+
DEFAULT_ON_REJECT = Proc.new {|reason| raise reason}
|
7
|
+
|
8
|
+
def self.defer
|
9
|
+
new_deferred
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.resolve(value)
|
13
|
+
if value.kind_of? Promise
|
14
|
+
return value
|
15
|
+
end
|
16
|
+
|
17
|
+
deferred = new_deferred
|
18
|
+
deferred.resolve(value)
|
19
|
+
deferred.promise
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.reject(reason)
|
23
|
+
deferred = new_deferred
|
24
|
+
deferred.reject(reason)
|
25
|
+
deferred.promise
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.execute(&block)
|
29
|
+
deferred = new_deferred
|
30
|
+
EXECUTOR.submit do
|
31
|
+
begin
|
32
|
+
deferred.resolve block.call
|
33
|
+
rescue Exception => e
|
34
|
+
deferred.reject e
|
35
|
+
end
|
36
|
+
end
|
37
|
+
deferred.promise
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.all_settled(promises)
|
41
|
+
return resolve([]) if promises.empty?
|
42
|
+
|
43
|
+
deferred = new_deferred
|
44
|
+
promises = promises.dup
|
45
|
+
|
46
|
+
check = Proc.new do
|
47
|
+
if promises.all? {|promise| promise.fulfilled? or promise.rejected?}
|
48
|
+
deferred.resolve promises
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
promises.each do |promise|
|
53
|
+
promise.then(check) { check.call }
|
54
|
+
end
|
55
|
+
|
56
|
+
deferred.promise
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.all(promises)
|
60
|
+
return resolve([]) if promises.empty?
|
61
|
+
|
62
|
+
deferred = new_deferred
|
63
|
+
promises = promises.dup
|
64
|
+
|
65
|
+
do_reject = Proc.new {|reason| deferred.reject reason}
|
66
|
+
do_fulfill = Proc.new do
|
67
|
+
if promises.all?(&:fulfilled?)
|
68
|
+
deferred.resolve(promises.map &:value)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
promises.each do |promise|
|
73
|
+
promise.then(do_reject) { do_fulfill.call }
|
74
|
+
end
|
75
|
+
|
76
|
+
deferred.promise
|
77
|
+
end
|
78
|
+
|
79
|
+
def then(on_rejected = nil, &on_fulfilled)
|
80
|
+
deferred = self.class.new_deferred
|
81
|
+
|
82
|
+
on_fulfilled ||= DEFAULT_ON_FULFILL
|
83
|
+
on_rejected ||= DEFAULT_ON_REJECT
|
84
|
+
|
85
|
+
on_resolution do
|
86
|
+
callback, arg = if fulfilled?
|
87
|
+
[ on_fulfilled, value ]
|
88
|
+
else
|
89
|
+
[ on_rejected, reason ]
|
90
|
+
end
|
91
|
+
|
92
|
+
begin
|
93
|
+
deferred.resolve(callback.call arg)
|
94
|
+
rescue Exception => e
|
95
|
+
deferred.reject e
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
deferred.promise
|
100
|
+
end
|
101
|
+
|
102
|
+
def rescue(&block)
|
103
|
+
self.then(block)
|
104
|
+
end
|
105
|
+
|
106
|
+
alias_method :catch, :rescue
|
107
|
+
alias_method :on_error, :rescue
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module Rosarium
|
2
|
+
|
3
|
+
class SimplePromise
|
4
|
+
|
5
|
+
def self.new_deferred
|
6
|
+
promise = new
|
7
|
+
resolver = promise.method :resolve
|
8
|
+
rejecter = promise.method :reject
|
9
|
+
|
10
|
+
class <<promise
|
11
|
+
undef :resolve
|
12
|
+
undef :reject
|
13
|
+
end
|
14
|
+
|
15
|
+
Deferred.new(promise, resolver, rejecter)
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@state = :pending
|
20
|
+
@mutex = Mutex.new
|
21
|
+
@condition = ConditionVariable.new
|
22
|
+
@on_resolution = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def state
|
26
|
+
synchronized { @state }
|
27
|
+
end
|
28
|
+
|
29
|
+
def value
|
30
|
+
wait
|
31
|
+
synchronized { @value }
|
32
|
+
end
|
33
|
+
|
34
|
+
def reason
|
35
|
+
wait
|
36
|
+
synchronized { @reason }
|
37
|
+
end
|
38
|
+
|
39
|
+
def fulfilled?
|
40
|
+
state == :fulfilled
|
41
|
+
end
|
42
|
+
|
43
|
+
def rejected?
|
44
|
+
state == :rejected
|
45
|
+
end
|
46
|
+
|
47
|
+
def value!
|
48
|
+
wait
|
49
|
+
synchronized do
|
50
|
+
if @state == :rejected
|
51
|
+
raise @reason
|
52
|
+
else
|
53
|
+
@value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def wait
|
59
|
+
on_resolution do
|
60
|
+
@mutex.synchronize { @condition.broadcast }
|
61
|
+
end
|
62
|
+
|
63
|
+
@mutex.synchronize do
|
64
|
+
loop do
|
65
|
+
return if @state == :fulfilled or @state == :rejected
|
66
|
+
@condition.wait @mutex
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def synchronized
|
74
|
+
@mutex.synchronize { yield }
|
75
|
+
end
|
76
|
+
|
77
|
+
public
|
78
|
+
|
79
|
+
def resolve(value)
|
80
|
+
_resolve(value, nil)
|
81
|
+
end
|
82
|
+
|
83
|
+
def reject(reason)
|
84
|
+
raise "reason must be an Exception" unless reason.kind_of? Exception
|
85
|
+
_resolve(nil, reason)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def _resolve(value, reason)
|
91
|
+
callbacks = []
|
92
|
+
add_on_resolution = false
|
93
|
+
|
94
|
+
synchronized do
|
95
|
+
if @state == :pending
|
96
|
+
if value.kind_of? SimplePromise
|
97
|
+
@state = :resolving
|
98
|
+
add_on_resolution = true
|
99
|
+
elsif reason.nil?
|
100
|
+
@value = value
|
101
|
+
@state = :fulfilled
|
102
|
+
callbacks.concat @on_resolution
|
103
|
+
@on_resolution.clear
|
104
|
+
else
|
105
|
+
@reason = reason
|
106
|
+
@state = :rejected
|
107
|
+
callbacks.concat @on_resolution
|
108
|
+
@on_resolution.clear
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
if add_on_resolution
|
114
|
+
value.on_resolution { copy_resolution_from value }
|
115
|
+
end
|
116
|
+
|
117
|
+
callbacks.each {|c| EXECUTOR.submit { c.call } }
|
118
|
+
end
|
119
|
+
|
120
|
+
def copy_resolution_from(other)
|
121
|
+
callbacks = []
|
122
|
+
|
123
|
+
synchronized do
|
124
|
+
if @state == :resolving
|
125
|
+
@value = other.value
|
126
|
+
@reason = other.reason
|
127
|
+
@state = other.state
|
128
|
+
callbacks.concat @on_resolution
|
129
|
+
@on_resolution.clear
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
callbacks.each {|c| EXECUTOR.submit { c.call } }
|
134
|
+
end
|
135
|
+
|
136
|
+
protected
|
137
|
+
|
138
|
+
def on_resolution(&block)
|
139
|
+
immediate = synchronized do
|
140
|
+
if @state == :fulfilled or @state == :rejected
|
141
|
+
true
|
142
|
+
else
|
143
|
+
@on_resolution << block
|
144
|
+
false
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
block.call if immediate
|
149
|
+
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
data/lib/rosarium.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
require "rosarium"
|
2
|
+
require_relative "./promise_test_helper"
|
3
|
+
|
4
|
+
describe "deferred promises" do
|
5
|
+
|
6
|
+
include PromiseTestHelper
|
7
|
+
|
8
|
+
it "creates a pending promise" do
|
9
|
+
deferred = Rosarium::Promise.defer
|
10
|
+
check_pending deferred.promise
|
11
|
+
end
|
12
|
+
|
13
|
+
it "deferred can be fulfilled only once" do
|
14
|
+
deferred = Rosarium::Promise.defer
|
15
|
+
check_pending deferred.promise
|
16
|
+
deferred.resolve 7
|
17
|
+
check_fulfilled deferred.promise, 7
|
18
|
+
deferred.resolve 8
|
19
|
+
check_fulfilled deferred.promise, 7
|
20
|
+
deferred.reject an_error
|
21
|
+
check_fulfilled deferred.promise, 7
|
22
|
+
end
|
23
|
+
|
24
|
+
it "deferred can be rejected only once" do
|
25
|
+
e = an_error
|
26
|
+
deferred = Rosarium::Promise.defer
|
27
|
+
check_pending deferred.promise
|
28
|
+
deferred.reject e
|
29
|
+
check_rejected deferred.promise, e
|
30
|
+
deferred.reject an_error("again")
|
31
|
+
check_rejected deferred.promise, e
|
32
|
+
deferred.resolve 9
|
33
|
+
check_rejected deferred.promise, e
|
34
|
+
end
|
35
|
+
|
36
|
+
it "can only be rejected with an exception" do
|
37
|
+
deferred = Rosarium::Promise.defer
|
38
|
+
check_pending deferred.promise
|
39
|
+
expect { deferred.reject "123" }.to raise_error /reason must be an Exception/
|
40
|
+
check_pending deferred.promise
|
41
|
+
end
|
42
|
+
|
43
|
+
it "can be resolved with an already-fulfilled promise" do
|
44
|
+
d1 = Rosarium::Promise.defer
|
45
|
+
d2 = Rosarium::Promise.defer
|
46
|
+
d2.resolve 7
|
47
|
+
d1.resolve(d2.promise)
|
48
|
+
check_fulfilled d1.promise, 7
|
49
|
+
end
|
50
|
+
|
51
|
+
it "can be resolved with an already-rejected promise" do
|
52
|
+
d1 = Rosarium::Promise.defer
|
53
|
+
d2 = Rosarium::Promise.defer
|
54
|
+
e = an_error
|
55
|
+
d2.reject e
|
56
|
+
d1.resolve(d2.promise)
|
57
|
+
check_rejected d1.promise, e
|
58
|
+
end
|
59
|
+
|
60
|
+
it "can be resolved with a later-fulfilled promise" do
|
61
|
+
d1 = Rosarium::Promise.defer
|
62
|
+
d2 = Rosarium::Promise.defer
|
63
|
+
d1.resolve(d2.promise)
|
64
|
+
check_resolving d1.promise
|
65
|
+
d2.resolve 7
|
66
|
+
d1.promise.wait
|
67
|
+
check_fulfilled d1.promise, 7
|
68
|
+
end
|
69
|
+
|
70
|
+
it "can be resolved with a later-rejected promise" do
|
71
|
+
d1 = Rosarium::Promise.defer
|
72
|
+
d2 = Rosarium::Promise.defer
|
73
|
+
d1.resolve(d2.promise)
|
74
|
+
check_resolving d1.promise
|
75
|
+
e = an_error
|
76
|
+
d2.reject e
|
77
|
+
d1.promise.wait
|
78
|
+
check_rejected d1.promise, e
|
79
|
+
end
|
80
|
+
|
81
|
+
it "waits for a value (fulfilled)" do
|
82
|
+
d = Rosarium::Promise.defer
|
83
|
+
check_pending d.promise
|
84
|
+
Thread.new { sleep 0.1; d.resolve 7 }
|
85
|
+
v = d.promise.value
|
86
|
+
expect(v).to eq(7)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "waits for a value (rejected)" do
|
90
|
+
d = Rosarium::Promise.defer
|
91
|
+
check_pending d.promise
|
92
|
+
Thread.new { sleep 0.1; d.reject an_error }
|
93
|
+
v = d.promise.value
|
94
|
+
expect(v).to eq(nil)
|
95
|
+
expect(d.promise).to be_rejected
|
96
|
+
end
|
97
|
+
|
98
|
+
it "waits for a reason (fulfilled)" do
|
99
|
+
d = Rosarium::Promise.defer
|
100
|
+
check_pending d.promise
|
101
|
+
Thread.new { sleep 0.1; d.resolve 7 }
|
102
|
+
r = d.promise.reason
|
103
|
+
expect(r).to eq(nil)
|
104
|
+
expect(d.promise).to be_fulfilled
|
105
|
+
end
|
106
|
+
|
107
|
+
it "waits for a reason (rejected)" do
|
108
|
+
d = Rosarium::Promise.defer
|
109
|
+
check_pending d.promise
|
110
|
+
e = an_error
|
111
|
+
Thread.new { sleep 0.1; d.reject e }
|
112
|
+
r = d.promise.reason
|
113
|
+
expect(r).to eq(e)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "waits for a value! (fulfilled)" do
|
117
|
+
d = Rosarium::Promise.defer
|
118
|
+
check_pending d.promise
|
119
|
+
Thread.new { sleep 0.1; d.resolve 7 }
|
120
|
+
v = d.promise.value!
|
121
|
+
expect(v).to eq(7)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "waits for a value! (rejected)" do
|
125
|
+
d = Rosarium::Promise.defer
|
126
|
+
check_pending d.promise
|
127
|
+
e = an_error
|
128
|
+
Thread.new { sleep 0.1; d.reject e }
|
129
|
+
expect {
|
130
|
+
d.promise.value!
|
131
|
+
}.to raise_error(e)
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "rosarium"
|
2
|
+
|
3
|
+
describe Rosarium::FixedThreadExecutor do
|
4
|
+
|
5
|
+
it "runs a job" do
|
6
|
+
ex = Rosarium::FixedThreadExecutor.new(1)
|
7
|
+
done = false
|
8
|
+
ex.submit { done = true }
|
9
|
+
ex.wait_until_idle
|
10
|
+
expect(done).to be_truthy
|
11
|
+
end
|
12
|
+
|
13
|
+
it "discards exceptions" do
|
14
|
+
ex = Rosarium::FixedThreadExecutor.new(1)
|
15
|
+
done = false
|
16
|
+
ex.submit { raise "bang" }
|
17
|
+
ex.submit { done = true }
|
18
|
+
ex.wait_until_idle
|
19
|
+
expect(done).to be_truthy
|
20
|
+
end
|
21
|
+
|
22
|
+
it "runs jobs concurrently" do
|
23
|
+
ex = Rosarium::FixedThreadExecutor.new(3)
|
24
|
+
m = Mutex.new
|
25
|
+
done = []
|
26
|
+
3.times do
|
27
|
+
ex.submit do
|
28
|
+
m.synchronize { done << "s" }
|
29
|
+
sleep 0.1
|
30
|
+
m.synchronize { done << "e" }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
ex.wait_until_idle
|
34
|
+
expect(done).to eq(%w[ s s s e e e ])
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "rosarium"
|
2
|
+
require_relative "./promise_test_helper"
|
3
|
+
|
4
|
+
describe Rosarium::Promise do
|
5
|
+
|
6
|
+
include PromiseTestHelper
|
7
|
+
|
8
|
+
# Chaining promises
|
9
|
+
|
10
|
+
it "supports simple 'then'" do
|
11
|
+
deferred = Rosarium::Promise.defer
|
12
|
+
chained = deferred.promise.then {|arg| arg * 2}
|
13
|
+
check_pending chained
|
14
|
+
deferred.resolve 7
|
15
|
+
sleep 0.1
|
16
|
+
check_fulfilled chained, 14
|
17
|
+
end
|
18
|
+
|
19
|
+
it "rejects if 'then' raises an error" do
|
20
|
+
e = an_error
|
21
|
+
deferred = Rosarium::Promise.defer
|
22
|
+
chained = deferred.promise.then { raise e }
|
23
|
+
check_pending chained
|
24
|
+
deferred.resolve 7
|
25
|
+
sleep 0.1
|
26
|
+
check_rejected chained, e
|
27
|
+
end
|
28
|
+
|
29
|
+
it "rejects if the parent rejects" do
|
30
|
+
e = an_error
|
31
|
+
deferred = Rosarium::Promise.defer
|
32
|
+
then_called = false
|
33
|
+
chained = deferred.promise.then { then_called = true }
|
34
|
+
check_pending chained
|
35
|
+
deferred.reject e
|
36
|
+
chained.wait
|
37
|
+
check_rejected chained, e
|
38
|
+
expect(then_called).to be_falsy
|
39
|
+
end
|
40
|
+
|
41
|
+
it "supports then(on_rejected)" do
|
42
|
+
e = an_error
|
43
|
+
e2 = an_error("another")
|
44
|
+
deferred = Rosarium::Promise.defer
|
45
|
+
got_args = nil
|
46
|
+
chained = deferred.promise.then(Proc.new {|*args| got_args = args; raise e2 }) { raise "should never be called" }
|
47
|
+
deferred.reject e
|
48
|
+
chained.wait
|
49
|
+
check_rejected chained, e2
|
50
|
+
expect(got_args).to eq([e])
|
51
|
+
end
|
52
|
+
|
53
|
+
it "on_rejected can cause fulfilled" do
|
54
|
+
deferred = Rosarium::Promise.defer
|
55
|
+
chained = deferred.promise.then(Proc.new {7}) { raise "should never be called" }
|
56
|
+
deferred.reject an_error
|
57
|
+
chained.wait
|
58
|
+
check_fulfilled chained, 7
|
59
|
+
end
|
60
|
+
|
61
|
+
it "supports rescue/catch/on_error" do
|
62
|
+
%i[ rescue catch on_error ].each do |method|
|
63
|
+
deferred = Rosarium::Promise.defer
|
64
|
+
chained = deferred.promise.send(method) { 7 }
|
65
|
+
deferred.reject an_error
|
66
|
+
chained.wait
|
67
|
+
check_fulfilled chained, 7
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require "rosarium"
|
2
|
+
require_relative "./promise_test_helper"
|
3
|
+
|
4
|
+
describe "instantly-resolved promises" do
|
5
|
+
|
6
|
+
include PromiseTestHelper
|
7
|
+
|
8
|
+
it "returns the same promise" do
|
9
|
+
d = Rosarium::Promise.defer
|
10
|
+
t = Rosarium::Promise.resolve d.promise
|
11
|
+
expect(t).to eq(d.promise)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "creates a fulfilled promise" do
|
15
|
+
t = Rosarium::Promise.resolve 7
|
16
|
+
check_fulfilled t, 7
|
17
|
+
expect(t).not_to respond_to(:fulfill)
|
18
|
+
expect(t).not_to respond_to(:reject)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "creates a rejected promise" do
|
22
|
+
e = an_error
|
23
|
+
t = Rosarium::Promise.reject an_error
|
24
|
+
check_rejected t, an_error
|
25
|
+
expect(t).not_to respond_to(:fulfill)
|
26
|
+
expect(t).not_to respond_to(:reject)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "creates an immediately-executable promise" do
|
30
|
+
promise = Rosarium::Promise.execute do
|
31
|
+
sleep 0.1 ; 7
|
32
|
+
end
|
33
|
+
check_pending promise
|
34
|
+
sleep 0.2
|
35
|
+
check_fulfilled promise, 7
|
36
|
+
end
|
37
|
+
|
38
|
+
it "catches errors from the executed block and rejects" do
|
39
|
+
e = RuntimeError.new("bang")
|
40
|
+
promise = Rosarium::Promise.execute do
|
41
|
+
sleep 0.1 ; raise e
|
42
|
+
end
|
43
|
+
check_pending promise
|
44
|
+
sleep 0.2
|
45
|
+
check_rejected promise, e
|
46
|
+
end
|
47
|
+
|
48
|
+
it "supports all_settled (empty)" do
|
49
|
+
promise = Rosarium::Promise.all_settled []
|
50
|
+
check_fulfilled promise, []
|
51
|
+
end
|
52
|
+
|
53
|
+
it "supports all_settled (non-empty)" do
|
54
|
+
d1 = Rosarium::Promise.defer
|
55
|
+
d2 = Rosarium::Promise.defer
|
56
|
+
promise = Rosarium::Promise.all_settled [d1.promise, d2.promise]
|
57
|
+
check_pending promise
|
58
|
+
|
59
|
+
d1.resolve 7
|
60
|
+
check_pending promise
|
61
|
+
|
62
|
+
e = an_error
|
63
|
+
d2.reject e
|
64
|
+
promise.wait
|
65
|
+
check_fulfilled promise, [ d1.promise, d2.promise ]
|
66
|
+
end
|
67
|
+
|
68
|
+
it "supports all (empty)" do
|
69
|
+
promise = Rosarium::Promise.all []
|
70
|
+
check_fulfilled promise, []
|
71
|
+
end
|
72
|
+
|
73
|
+
it "supports all (reject)" do
|
74
|
+
d1 = Rosarium::Promise.defer
|
75
|
+
d2 = Rosarium::Promise.defer
|
76
|
+
d3 = Rosarium::Promise.defer
|
77
|
+
promise = Rosarium::Promise.all [d1.promise, d2.promise, d3.promise]
|
78
|
+
check_pending promise
|
79
|
+
|
80
|
+
d1.resolve 7
|
81
|
+
check_pending promise
|
82
|
+
|
83
|
+
e = an_error
|
84
|
+
d3.reject e
|
85
|
+
promise.wait
|
86
|
+
check_rejected promise, e
|
87
|
+
end
|
88
|
+
|
89
|
+
it "supports all (fulfill)" do
|
90
|
+
d1 = Rosarium::Promise.defer
|
91
|
+
d2 = Rosarium::Promise.defer
|
92
|
+
promise = Rosarium::Promise.all [d1.promise, d2.promise]
|
93
|
+
check_pending promise
|
94
|
+
|
95
|
+
d1.resolve 7
|
96
|
+
check_pending promise
|
97
|
+
|
98
|
+
d2.resolve 8
|
99
|
+
promise.wait
|
100
|
+
check_fulfilled promise, [7,8]
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module PromiseTestHelper
|
2
|
+
|
3
|
+
def check_pending(promise)
|
4
|
+
expect(promise.state).to eq(:pending)
|
5
|
+
expect(promise).not_to be_fulfilled
|
6
|
+
expect(promise).not_to be_rejected
|
7
|
+
# expect(promise.value).to be_nil # should block
|
8
|
+
# expect(promise.reason).to be_nil # should block
|
9
|
+
end
|
10
|
+
|
11
|
+
def check_resolving(promise)
|
12
|
+
expect(promise.state).to eq(:resolving)
|
13
|
+
expect(promise).not_to be_fulfilled
|
14
|
+
expect(promise).not_to be_rejected
|
15
|
+
# expect(promise.value).to be_nil # should block
|
16
|
+
# expect(promise.reason).to be_nil # should block
|
17
|
+
end
|
18
|
+
|
19
|
+
def check_fulfilled(promise, value)
|
20
|
+
expect(promise.state).to eq(:fulfilled)
|
21
|
+
expect(promise).to be_fulfilled
|
22
|
+
expect(promise).not_to be_rejected
|
23
|
+
expect(promise.value).to eq(value)
|
24
|
+
expect(promise.reason).to be_nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def check_rejected(promise, e)
|
28
|
+
expect(promise.state).to eq(:rejected)
|
29
|
+
expect(promise).not_to be_fulfilled
|
30
|
+
expect(promise).to be_rejected
|
31
|
+
expect(promise.value).to eq(nil)
|
32
|
+
expect(promise.reason).to eq(e)
|
33
|
+
end
|
34
|
+
|
35
|
+
def an_error(message = "bang")
|
36
|
+
RuntimeError.new(message)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rosarium
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rachel Evans
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-07-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.4'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.4'
|
27
|
+
description: |2
|
28
|
+
Rosarium implements something that's a bit like Promises,
|
29
|
+
inspired by the stability and ease of use of Q
|
30
|
+
(<https://github.com/kriskowal/q/wiki/API-Reference>).
|
31
|
+
email: git@rve.org.uk
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- Gemfile
|
37
|
+
- Gemfile.lock
|
38
|
+
- LICENSE
|
39
|
+
- README.md
|
40
|
+
- lib/rosarium.rb
|
41
|
+
- lib/rosarium/deferred.rb
|
42
|
+
- lib/rosarium/fixed_thread_executor.rb
|
43
|
+
- lib/rosarium/promise.rb
|
44
|
+
- lib/rosarium/simple_promise.rb
|
45
|
+
- spec/deferred_spec.rb
|
46
|
+
- spec/fixed_thread_executor_spec.rb
|
47
|
+
- spec/promise_methods_spec.rb
|
48
|
+
- spec/promise_static_spec.rb
|
49
|
+
- spec/promise_test_helper.rb
|
50
|
+
homepage: http://rve.org.uk/gems/rosarium
|
51
|
+
licenses:
|
52
|
+
- Apache-2.0
|
53
|
+
metadata: {}
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 2.5.1
|
71
|
+
signing_key:
|
72
|
+
specification_version: 4
|
73
|
+
summary: Promises, or something like them
|
74
|
+
test_files: []
|