opal 1.2.0.beta1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -1
- data/CHANGELOG.md +53 -0
- data/UNRELEASED.md +5 -1
- data/lib/opal/parser/patch.rb +10 -0
- data/lib/opal/rewriter.rb +2 -0
- data/lib/opal/rewriters/pattern_matching.rb +287 -0
- data/lib/opal/version.rb +1 -1
- data/opal/corelib/constants.rb +2 -2
- data/opal/corelib/hash.rb +9 -0
- data/opal/corelib/pattern_matching.rb +159 -0
- data/opal/opal/full.rb +1 -0
- data/spec/filters/bugs/array.rb +0 -1
- data/spec/filters/bugs/hash.rb +1 -6
- data/spec/filters/bugs/language.rb +21 -76
- data/spec/filters/bugs/string.rb +1 -0
- data/spec/filters/bugs/struct.rb +0 -10
- data/spec/filters/unsupported/refinements.rb +3 -0
- data/spec/opal/core/language/pattern_matching_spec.rb +124 -0
- data/stdlib/promise/v1.rb +1 -0
- data/stdlib/promise/v2.rb +386 -0
- data/tasks/releasing.rake +1 -1
- data/tasks/testing.rake +0 -1
- data/test/opal/promisev2/test_always.rb +63 -0
- data/test/opal/promisev2/test_error.rb +16 -0
- data/test/opal/promisev2/test_rescue.rb +59 -0
- data/test/opal/promisev2/test_then.rb +90 -0
- data/test/opal/promisev2/test_trace.rb +52 -0
- data/test/opal/promisev2/test_value.rb +16 -0
- data/test/opal/promisev2/test_when.rb +35 -0
- metadata +24 -8
@@ -0,0 +1 @@
|
|
1
|
+
require 'promise'
|
@@ -0,0 +1,386 @@
|
|
1
|
+
# {Promise} is used to help structure asynchronous code.
|
2
|
+
#
|
3
|
+
# It is available in the Opal standard library, and can be required in any Opal
|
4
|
+
# application:
|
5
|
+
#
|
6
|
+
# require 'promise/v2'
|
7
|
+
#
|
8
|
+
# ## Basic Usage
|
9
|
+
#
|
10
|
+
# Promises are created and returned as objects with the assumption that they
|
11
|
+
# will eventually be resolved or rejected, but never both. A {Promise} has
|
12
|
+
# a {#then} and {#fail} method (or one of their aliases) that can be used to
|
13
|
+
# register a block that gets called once resolved or rejected.
|
14
|
+
#
|
15
|
+
# promise = PromiseV2.new
|
16
|
+
#
|
17
|
+
# promise.then {
|
18
|
+
# puts "resolved!"
|
19
|
+
# }.fail {
|
20
|
+
# puts "rejected!"
|
21
|
+
# }
|
22
|
+
#
|
23
|
+
# # some time later
|
24
|
+
# promise.resolve
|
25
|
+
#
|
26
|
+
# # => "resolved!"
|
27
|
+
#
|
28
|
+
# It is important to remember that a promise can only be resolved or rejected
|
29
|
+
# once, so the block will only ever be called once (or not at all).
|
30
|
+
#
|
31
|
+
# ## Resolving Promises
|
32
|
+
#
|
33
|
+
# To resolve a promise, means to inform the {Promise} that it has succeeded
|
34
|
+
# or evaluated to a useful value. {#resolve} can be passed a value which is
|
35
|
+
# then passed into the block handler:
|
36
|
+
#
|
37
|
+
# def get_json
|
38
|
+
# promise = PromiseV2.new
|
39
|
+
#
|
40
|
+
# HTTP.get("some_url") do |req|
|
41
|
+
# promise.resolve req.json
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# promise
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# get_json.then do |json|
|
48
|
+
# puts "got some JSON from server"
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# ## Rejecting Promises
|
52
|
+
#
|
53
|
+
# Promises are also designed to handle error cases, or situations where an
|
54
|
+
# outcome is not as expected. Taking the previous example, we can also pass
|
55
|
+
# a value to a {#reject} call, which passes that object to the registered
|
56
|
+
# {#fail} handler:
|
57
|
+
#
|
58
|
+
# def get_json
|
59
|
+
# promise = PromiseV2.new
|
60
|
+
#
|
61
|
+
# HTTP.get("some_url") do |req|
|
62
|
+
# if req.ok?
|
63
|
+
# promise.resolve req.json
|
64
|
+
# else
|
65
|
+
# promise.reject req
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# promise
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# get_json.then {
|
72
|
+
# # ...
|
73
|
+
# }.fail { |req|
|
74
|
+
# puts "it went wrong: #{req.message}"
|
75
|
+
# }
|
76
|
+
#
|
77
|
+
# ## Chaining Promises
|
78
|
+
#
|
79
|
+
# Promises become even more useful when chained together. Each {#then} or
|
80
|
+
# {#fail} call returns a new {PromiseV2} which can be used to chain more and more
|
81
|
+
# handlers together.
|
82
|
+
#
|
83
|
+
# promise.then { wait_for_something }.then { do_something_else }
|
84
|
+
#
|
85
|
+
# Rejections are propagated through the entire chain, so a "catch all" handler
|
86
|
+
# can be attached at the end of the tail:
|
87
|
+
#
|
88
|
+
# promise.then { ... }.then { ... }.fail { ... }
|
89
|
+
#
|
90
|
+
# ## Composing Promises
|
91
|
+
#
|
92
|
+
# {PromiseV2.when} can be used to wait for more than one promise to resolve (or
|
93
|
+
# reject). Using the previous example, we could request two different json
|
94
|
+
# requests and wait for both to finish:
|
95
|
+
#
|
96
|
+
# PromiseV2.when(get_json, get_json2).then |first, second|
|
97
|
+
# puts "got two json payloads: #{first}, #{second}"
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
|
101
|
+
warn 'PromiseV2 is a technology preview, which means it may change its behavior ' \
|
102
|
+
'in the future until this warning is removed. If you are interested in this part, ' \
|
103
|
+
'please make sure you track the async/await/promises tag on Opal issues: ' \
|
104
|
+
'https://github.com/opal/opal/issues?q=label%3Aasync%2Fawait%2Fpromises'
|
105
|
+
|
106
|
+
class PromiseV2 < `Promise`
|
107
|
+
class << self
|
108
|
+
def allocate
|
109
|
+
ok, fail = nil, nil
|
110
|
+
|
111
|
+
prom = `new self.$$constructor(function(_ok, _fail) { #{ok} = _ok; #{fail} = _fail; })`
|
112
|
+
prom.instance_variable_set(:@type, :opal)
|
113
|
+
prom.instance_variable_set(:@resolve_proc, ok)
|
114
|
+
prom.instance_variable_set(:@reject_proc, fail)
|
115
|
+
prom
|
116
|
+
end
|
117
|
+
|
118
|
+
def when(*promises)
|
119
|
+
promises = Array(promises.length == 1 ? promises.first : promises)
|
120
|
+
`Promise.all(#{promises})`.tap do |prom|
|
121
|
+
prom.instance_variable_set(:@type, :when)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
alias all when
|
126
|
+
|
127
|
+
def all_resolved(*promises)
|
128
|
+
promises = Array(promises.length == 1 ? promises.first : promises)
|
129
|
+
`Promise.allResolved(#{promises})`.tap do |prom|
|
130
|
+
prom.instance_variable_set(:@type, :all_resolved)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def any(*promises)
|
135
|
+
promises = Array(promises.length == 1 ? promises.first : promises)
|
136
|
+
`Promise.any(#{promises})`.tap do |prom|
|
137
|
+
prom.instance_variable_set(:@type, :any)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def race(*promises)
|
142
|
+
promises = Array(promises.length == 1 ? promises.first : promises)
|
143
|
+
`Promise.race(#{promises})`.tap do |prom|
|
144
|
+
prom.instance_variable_set(:@type, :race)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def resolve(value = nil)
|
149
|
+
`Promise.resolve(#{value})`.tap do |prom|
|
150
|
+
prom.instance_variable_set(:@type, :resolve)
|
151
|
+
prom.instance_variable_set(:@realized, :resolve)
|
152
|
+
prom.instance_variable_set(:@value, value)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
alias value resolve
|
156
|
+
|
157
|
+
def reject(value = nil)
|
158
|
+
`Promise.reject(#{value})`.tap do |prom|
|
159
|
+
prom.instance_variable_set(:@type, :reject)
|
160
|
+
prom.instance_variable_set(:@realized, :reject)
|
161
|
+
prom.instance_variable_set(:@value, value)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
alias error reject
|
165
|
+
end
|
166
|
+
|
167
|
+
attr_reader :prev, :next
|
168
|
+
|
169
|
+
# Is this promise native to JavaScript? This means, that methods like resolve
|
170
|
+
# or reject won't be available.
|
171
|
+
def native?
|
172
|
+
@type != :opal
|
173
|
+
end
|
174
|
+
|
175
|
+
# Raise an exception when a non-JS-native method is called on a JS-native promise
|
176
|
+
def nativity_check!
|
177
|
+
raise ArgumentError, 'this promise is native to JavaScript' if native?
|
178
|
+
end
|
179
|
+
|
180
|
+
# Raise an exception when a non-JS-native method is called on a JS-native promise
|
181
|
+
# but permits some typed promises
|
182
|
+
def light_nativity_check!
|
183
|
+
return if %i[reject resolve trace always fail then].include? @type
|
184
|
+
raise ArgumentError, 'this promise is native to JavaScript' if native?
|
185
|
+
end
|
186
|
+
|
187
|
+
# Allow only one chain to be present, as needed by the previous implementation.
|
188
|
+
# This isn't a strict check - it's always possible on the JS side to chain a
|
189
|
+
# given block.
|
190
|
+
def there_can_be_only_one!
|
191
|
+
raise ArgumentError, 'a promise has already been chained' if @next && @next.any?
|
192
|
+
end
|
193
|
+
|
194
|
+
def gen_tracing_proc(passing, &block)
|
195
|
+
proc do |i|
|
196
|
+
res = passing.call(i)
|
197
|
+
yield(res)
|
198
|
+
res
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def resolve(value = nil)
|
203
|
+
nativity_check!
|
204
|
+
raise ArgumentError, 'this promise was already resolved' if @realized
|
205
|
+
@value = value
|
206
|
+
@realized = :resolve
|
207
|
+
@resolve_proc.call(value)
|
208
|
+
self
|
209
|
+
end
|
210
|
+
alias resolve! resolve
|
211
|
+
|
212
|
+
def reject(value = nil)
|
213
|
+
nativity_check!
|
214
|
+
raise ArgumentError, 'this promise was already resolved' if @realized
|
215
|
+
@value = value
|
216
|
+
@realized = :reject
|
217
|
+
@reject_proc.call(value)
|
218
|
+
self
|
219
|
+
end
|
220
|
+
alias reject! reject
|
221
|
+
|
222
|
+
def then(&block)
|
223
|
+
prom = nil
|
224
|
+
blk = gen_tracing_proc(block) do |val|
|
225
|
+
prom.instance_variable_set(:@realized, :resolve)
|
226
|
+
prom.instance_variable_set(:@value, val)
|
227
|
+
end
|
228
|
+
prom = `self.then(#{blk})`
|
229
|
+
prom.instance_variable_set(:@prev, self)
|
230
|
+
prom.instance_variable_set(:@type, :then)
|
231
|
+
(@next ||= []) << prom
|
232
|
+
prom
|
233
|
+
end
|
234
|
+
|
235
|
+
def then!(&block)
|
236
|
+
there_can_be_only_one!
|
237
|
+
self.then(&block)
|
238
|
+
end
|
239
|
+
|
240
|
+
alias do then
|
241
|
+
alias do! then!
|
242
|
+
|
243
|
+
def fail(&block)
|
244
|
+
prom = nil
|
245
|
+
blk = gen_tracing_proc(block) do |val|
|
246
|
+
prom.instance_variable_set(:@realized, :resolve)
|
247
|
+
prom.instance_variable_set(:@value, val)
|
248
|
+
end
|
249
|
+
prom = `self.catch(#{blk})`
|
250
|
+
prom.instance_variable_set(:@prev, self)
|
251
|
+
prom.instance_variable_set(:@type, :fail)
|
252
|
+
(@next ||= []) << prom
|
253
|
+
prom
|
254
|
+
end
|
255
|
+
|
256
|
+
def fail!(&block)
|
257
|
+
there_can_be_only_one!
|
258
|
+
fail(&block)
|
259
|
+
end
|
260
|
+
|
261
|
+
alias rescue fail
|
262
|
+
alias catch fail
|
263
|
+
alias rescue! fail!
|
264
|
+
alias catch! fail!
|
265
|
+
|
266
|
+
def always(&block)
|
267
|
+
prom = nil
|
268
|
+
blk = gen_tracing_proc(block) do |val|
|
269
|
+
prom.instance_variable_set(:@realized, :resolve)
|
270
|
+
prom.instance_variable_set(:@value, val)
|
271
|
+
end
|
272
|
+
prom = `self.finally(#{blk})`
|
273
|
+
prom.instance_variable_set(:@prev, self)
|
274
|
+
prom.instance_variable_set(:@type, :always)
|
275
|
+
(@next ||= []) << prom
|
276
|
+
prom
|
277
|
+
end
|
278
|
+
|
279
|
+
def always!(&block)
|
280
|
+
there_can_be_only_one!
|
281
|
+
always(&block)
|
282
|
+
end
|
283
|
+
|
284
|
+
alias finally always
|
285
|
+
alias ensure always
|
286
|
+
alias finally! always!
|
287
|
+
alias ensure! always!
|
288
|
+
|
289
|
+
def trace(depth = nil, &block)
|
290
|
+
prom = self.then do
|
291
|
+
values = []
|
292
|
+
prom = self
|
293
|
+
while prom && (!depth || depth > 0)
|
294
|
+
val = nil
|
295
|
+
begin
|
296
|
+
val = prom.value
|
297
|
+
rescue ArgumentError
|
298
|
+
val = :native
|
299
|
+
end
|
300
|
+
values.unshift(val)
|
301
|
+
depth -= 1 if depth
|
302
|
+
prom = prom.prev
|
303
|
+
end
|
304
|
+
yield(*values)
|
305
|
+
end
|
306
|
+
|
307
|
+
prom.instance_variable_set(:@type, :trace)
|
308
|
+
prom
|
309
|
+
end
|
310
|
+
|
311
|
+
def trace!(*args, &block)
|
312
|
+
there_can_be_only_one!
|
313
|
+
trace(*args, &block)
|
314
|
+
end
|
315
|
+
|
316
|
+
def resolved?
|
317
|
+
light_nativity_check!
|
318
|
+
@realized == :resolve
|
319
|
+
end
|
320
|
+
|
321
|
+
def rejected?
|
322
|
+
light_nativity_check!
|
323
|
+
@realized == :reject
|
324
|
+
end
|
325
|
+
|
326
|
+
def realized?
|
327
|
+
light_nativity_check!
|
328
|
+
!@realized.nil?
|
329
|
+
end
|
330
|
+
|
331
|
+
def value
|
332
|
+
if resolved?
|
333
|
+
if PromiseV2 === @value
|
334
|
+
@value.value
|
335
|
+
else
|
336
|
+
@value
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def error
|
342
|
+
light_nativity_check!
|
343
|
+
@value if rejected?
|
344
|
+
end
|
345
|
+
|
346
|
+
def and(*promises)
|
347
|
+
promises = promises.map do |i|
|
348
|
+
if PromiseV2 === i
|
349
|
+
i
|
350
|
+
else
|
351
|
+
PromiseV2.value(i)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
PromiseV2.when(self, *promises).then do |a, *b|
|
355
|
+
[*a, *b]
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def initialize(&block)
|
360
|
+
yield self if block_given?
|
361
|
+
end
|
362
|
+
|
363
|
+
alias to_n itself
|
364
|
+
|
365
|
+
def inspect
|
366
|
+
result = "#<#{self.class}"
|
367
|
+
|
368
|
+
if @type
|
369
|
+
result += ":#{@type}" unless %i[opal resolve reject].include? @type
|
370
|
+
else
|
371
|
+
result += ':native'
|
372
|
+
end
|
373
|
+
|
374
|
+
result += ":#{@realized}" if @realized
|
375
|
+
result += "(#{object_id})"
|
376
|
+
|
377
|
+
if @next && @next.any?
|
378
|
+
result += " >> #{@next.inspect}"
|
379
|
+
end
|
380
|
+
|
381
|
+
result += ": #{@value.inspect}" if @value
|
382
|
+
result += '>'
|
383
|
+
|
384
|
+
result
|
385
|
+
end
|
386
|
+
end
|
data/tasks/releasing.rake
CHANGED
@@ -49,7 +49,7 @@ CHANGELOG_HEADING = <<~MARKDOWN
|
|
49
49
|
- **Security** to invite users to upgrade in case of vulnerabilities.
|
50
50
|
MARKDOWN
|
51
51
|
|
52
|
-
desc "Update CHANGELOG.md
|
52
|
+
desc "Update CHANGELOG.md using info from published GitHub releases (the first unreleased section is preserved)"
|
53
53
|
task :changelog do
|
54
54
|
changelog_path = "#{__dir__}/../CHANGELOG.md"
|
55
55
|
unreleased_path = "#{__dir__}/../UNRELEASED.md"
|
data/tasks/testing.rake
CHANGED
@@ -467,4 +467,3 @@ task :test_all => [:rspec, :mspec, :minitest]
|
|
467
467
|
task(:cruby_tests) { warn "The task 'cruby_tests' has been renamed to 'minitest_cruby_nodejs'."; exit 1 }
|
468
468
|
task(:test_cruby) { warn "The task 'test_cruby' has been renamed to 'minitest_cruby_nodejs'."; exit 1 }
|
469
469
|
task(:test_nodejs) { warn "The task 'test_nodejs' has been renamed to 'minitest_node_nodejs'."; exit 1 }
|
470
|
-
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'promise/v2'
|
3
|
+
|
4
|
+
class TestPromiseAlways < Test::Unit::TestCase
|
5
|
+
def test_calls_the_block_when_it_was_resolved
|
6
|
+
x = 42
|
7
|
+
PromiseV2.value(23)
|
8
|
+
.then { |v| x = v }
|
9
|
+
.always { |v| x = 2 }
|
10
|
+
.then { assert_equal(x, 2) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_calls_the_block_when_it_was_rejected
|
14
|
+
x = 42
|
15
|
+
PromiseV2.error(23)
|
16
|
+
.rescue { |v| x = v }
|
17
|
+
.always { |v| x = 2 }
|
18
|
+
.always { assert_equal(x, 2) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_acts_as_resolved
|
22
|
+
x = 42
|
23
|
+
PromiseV2.error(23)
|
24
|
+
.rescue { |v| x = v }
|
25
|
+
.always { x = 2 }
|
26
|
+
.then { x = 3 }
|
27
|
+
.always { assert_equal(x, 3) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_can_be_called_multiple_times_on_resolved_promises
|
31
|
+
p = PromiseV2.value(2)
|
32
|
+
x = 1
|
33
|
+
ps = []
|
34
|
+
ps << p.then { x += 1 }
|
35
|
+
ps << p.fail { x += 2 }
|
36
|
+
ps << p.always { x += 3 }
|
37
|
+
|
38
|
+
PromiseV2.when(ps).always do
|
39
|
+
assert_equal(x, 5)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_can_be_called_multiple_times_on_rejected_promises
|
44
|
+
p = PromiseV2.error(2)
|
45
|
+
x = 1
|
46
|
+
ps = []
|
47
|
+
ps << p.then { x += 1 }.fail{}
|
48
|
+
ps << p.fail { x += 2 }
|
49
|
+
ps << p.always { x += 3 }.fail{}
|
50
|
+
|
51
|
+
PromiseV2.when(ps).then do
|
52
|
+
assert_equal(x, 6)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_raises_with_alwaysB_if_a_promise_has_already_been_chained
|
57
|
+
p = PromiseV2.new
|
58
|
+
|
59
|
+
p.then! {}
|
60
|
+
|
61
|
+
assert_raise(ArgumentError) { p.always! {} }
|
62
|
+
end
|
63
|
+
end
|