-
-
Notifications
You must be signed in to change notification settings - Fork 329
/
promise.rb
427 lines (351 loc) · 7.9 KB
/
promise.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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
87
88
89
90
91
92
93
94
95
96
97
98
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# {Promise} is used to help structure asynchronous code.
#
# It is available in the Opal standard library, and can be required in any Opal
# application:
#
# require 'promise'
#
# ## Basic Usage
#
# Promises are created and returned as objects with the assumption that they
# will eventually be resolved or rejected, but never both. A {Promise} has
# a {#then} and {#fail} method (or one of their aliases) that can be used to
# register a block that gets called once resolved or rejected.
#
# promise = Promise.new
#
# promise.then {
# puts "resolved!"
# }.fail {
# puts "rejected!"
# }
#
# # some time later
# promise.resolve
#
# # => "resolved!"
#
# It is important to remember that a promise can only be resolved or rejected
# once, so the block will only ever be called once (or not at all).
#
# ## Resolving Promises
#
# To resolve a promise, means to inform the {Promise} that it has succeeded
# or evaluated to a useful value. {#resolve} can be passed a value which is
# then passed into the block handler:
#
# def get_json
# promise = Promise.new
#
# HTTP.get("some_url") do |req|
# promise.resolve req.json
# end
#
# promise
# end
#
# get_json.then do |json|
# puts "got some JSON from server"
# end
#
# ## Rejecting Promises
#
# Promises are also designed to handle error cases, or situations where an
# outcome is not as expected. Taking the previous example, we can also pass
# a value to a {#reject} call, which passes that object to the registered
# {#fail} handler:
#
# def get_json
# promise = Promise.new
#
# HTTP.get("some_url") do |req|
# if req.ok?
# promise.resolve req.json
# else
# promise.reject req
# end
#
# promise
# end
#
# get_json.then {
# # ...
# }.fail { |req|
# puts "it went wrong: #{req.message}"
# }
#
# ## Chaining Promises
#
# Promises become even more useful when chained together. Each {#then} or
# {#fail} call returns a new {Promise} which can be used to chain more and more
# handlers together.
#
# promise.then { wait_for_something }.then { do_something_else }
#
# Rejections are propagated through the entire chain, so a "catch all" handler
# can be attached at the end of the tail:
#
# promise.then { ... }.then { ... }.fail { ... }
#
# ## Composing Promises
#
# {Promise.when} can be used to wait for more than one promise to resolve (or
# reject). Using the previous example, we could request two different json
# requests and wait for both to finish:
#
# Promise.when(get_json, get_json2).then |first, second|
# puts "got two json payloads: #{first}, #{second}"
# end
#
class Promise
def self.value(value)
new.resolve(value)
end
def self.error(value)
new.reject(value)
end
def self.when(*promises)
When.new(promises)
end
attr_reader :error, :prev, :next
def initialize(action = {})
@action = action
@realized = false
@exception = false
@value = nil
@error = nil
@delayed = false
@prev = nil
@next = nil
end
def value
if Promise === @value
@value.value
else
@value
end
end
def act?
@action.has_key?(:success) || @action.has_key?(:always)
end
def action
@action.keys
end
def exception?
@exception
end
def realized?
!!@realized
end
def resolved?
@realized == :resolve
end
def rejected?
@realized == :reject
end
def ^(promise)
promise << self
self >> promise
promise
end
def <<(promise)
@prev = promise
self
end
def >>(promise)
@next = promise
if exception?
promise.reject(@delayed[0])
elsif resolved?
promise.resolve(@delayed ? @delayed[0] : value)
elsif rejected?
if !@action.has_key?(:failure) || Promise === (@delayed ? @delayed[0] : @error)
promise.reject(@delayed ? @delayed[0] : error)
elsif promise.action.include?(:always)
promise.reject(@delayed ? @delayed[0] : error)
end
end
self
end
def resolve(value = nil)
if realized?
raise ArgumentError, 'the promise has already been realized'
end
if Promise === value
return (value << @prev) ^ self
end
begin
if block = @action[:success] || @action[:always]
value = block.call(value)
end
resolve!(value)
rescue Exception => e
exception!(e)
end
self
end
def resolve!(value)
@realized = :resolve
@value = value
if @next
@next.resolve(value)
else
@delayed = [value]
end
end
def reject(value = nil)
if realized?
raise ArgumentError, 'the promise has already been realized'
end
if Promise === value
return (value << @prev) ^ self
end
begin
if block = @action[:failure] || @action[:always]
value = block.call(value)
end
if @action.has_key?(:always)
resolve!(value)
else
reject!(value)
end
rescue Exception => e
exception!(e)
end
self
end
def reject!(value)
@realized = :reject
@error = value
if @next
@next.reject(value)
else
@delayed = [value]
end
end
def exception!(error)
@exception = true
reject!(error)
end
def then(&block)
if @next
raise ArgumentError, 'a promise has already been chained'
end
self ^ Promise.new(success: block)
end
alias do then
def fail(&block)
if @next
raise ArgumentError, 'a promise has already been chained'
end
self ^ Promise.new(failure: block)
end
alias rescue fail
alias catch fail
def always(&block)
if @next
raise ArgumentError, 'a promise has already been chained'
end
self ^ Promise.new(always: block)
end
alias finally always
alias ensure always
def trace(depth = nil, &block)
if @next
raise ArgumentError, 'a promise has already been chained'
end
self ^ Trace.new(depth, block)
end
def inspect
result = "#<#{self.class}(#{object_id})"
if @next
result += " >> #{@next.inspect}"
end
if realized?
result += ": #{(@value || @error).inspect}>"
else
result += ">"
end
result
end
class Trace < self
def self.it(promise)
current = []
if promise.act? || promise.prev.nil?
current.push(promise.value)
end
if prev = promise.prev
current.concat(it(prev))
else
current
end
end
def initialize(depth, block)
@depth = depth
super success: -> {
trace = Trace.it(self).reverse
trace.pop
if depth && depth <= trace.length
trace.shift(trace.length - depth)
end
block.call(*trace)
}
end
end
class When < self
def initialize(promises = [])
super()
@wait = []
promises.each {|promise|
wait promise
}
end
def each(&block)
raise ArgumentError, 'no block given' unless block
self.then {|values|
values.each(&block)
}
end
def collect(&block)
raise ArgumentError, 'no block given' unless block
self.then {|values|
When.new(values.map(&block))
}
end
def inject(*args, &block)
self.then {|values|
values.reduce(*args, &block)
}
end
alias map collect
alias reduce inject
def wait(promise)
unless Promise === promise
promise = Promise.value(promise)
end
if promise.act?
promise = promise.then
end
@wait << promise
promise.always {
try if @next
}
self
end
alias and wait
def >>(*)
super.tap {
try
}
end
def try
if @wait.all?(&:realized?)
if promise = @wait.find(&:rejected?)
reject(promise.error)
else
resolve(@wait.map(&:value))
end
end
end
end
end