deferrable_gratification 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -1,64 +1,361 @@
1
1
  # Deferrable Gratification #
2
2
 
3
- ## Purpose ##
3
+ Deferrable Gratification (DG) facilitates asynchronous programming in Ruby, by
4
+ helping create abstractions around complex operations built up from simpler
5
+ ones. It helps make asynchronous code less error-prone and easier to compose.
6
+ It also provides some enhancements to the
7
+ [`Deferrable`](http://eventmachine.rubyforge.org/EventMachine/Deferrable.html)
8
+ API.
4
9
 
5
- Deferrable Gratification (DG) makes evented code less error-prone and easier to
6
- compose, and thus easier to create higher-level abstractions around. It also
7
- enhances the API offered by Ruby Deferrables to make them more pleasant to work
8
- with.
10
+ ## Motivation ##
9
11
 
10
- ## Documentation ##
12
+ Asynchronous programming, as supported in Ruby by
13
+ [EventMachine](http://rubyeventmachine.com/), offers the benefits of (limited)
14
+ concurrency without the complexity of threads. However, it requires a style of
15
+ code that fits awkwardly into Ruby's synchronous semantics.
11
16
 
12
- * [API](http://samstokes.github.com/deferrable_gratification/doc/frames.html)
13
- * [Behaviour specs](http://samstokes.github.com/deferrable_gratification/doc/spec/index.html)
14
- (generated from RSpec code examples)
17
+ A method that performs an asynchronous operation cannot simply return a result:
18
+ instead it must take a callback, which it calls with the eventual result of the
19
+ operation. That method's caller must also now take a callback, and so on up
20
+ the call chain. This means replacing a synchronous library such as
21
+ [Net::HTTP](http://ruby-doc.org/stdlib-1.8.7/libdoc/net/http/rdoc/index.html)
22
+ with an asynchronous library such as
23
+ [em-http-request](https://github.com/igrigorik/em-http-request) can require
24
+ rewriting a surprisingly large part of a codebase.
15
25
 
16
- ## Components ##
26
+ Ruby's block syntax initially seems to support this callback-passing style
27
+ well.
17
28
 
18
- It currently consists of the following components:
29
+ asynchronously_fetch_page do |page|
30
+ # do something with 'page'...
31
+ end
19
32
 
20
- * [`DG::Fluent`](#fluent): fluent (aka chainable) syntax for registering
21
- multiple callbacks and errbacks to the same Deferrable.
33
+ The first problem is that, unlike regular method parameters, Ruby doesn't check
34
+ that the caller remembered to provide a block, making it easy to create bugs by
35
+ forgetting a callback.
22
36
 
23
- * [`DG::Bothback`](#bothback): a `#bothback` method for registering code to
24
- run on either success or failure.
37
+ page = asynchronously_fetch_page
38
+ # returns immediately with no error. 'page' is probably nil.
25
39
 
26
- * [`DG::Combinators`](#combinators): a combinator library for building up
27
- complex asynchronous operations out of simpler ones.
40
+ Similarly the method implementer may forget to pass the callback down to nested
41
+ calls, which can render the whole chain of asynchronous methods unable to
42
+ return a result. (The asynchronous operation might itself check that a
43
+ callback was given, but asynchronous libraries will often not require a
44
+ callback, in case they were invoked only for their side-effects.)
28
45
 
46
+ def first_thing(&callback)
47
+ second_thing(&callback)
48
+ end
49
+ def second_thing(&callback)
50
+ third_thing # oops! no error though.
51
+ end
52
+ def third_thing(&callback)
53
+ compute_answer
54
+ yield 42 if block_given?
55
+ end
29
56
 
30
- <h3 id="fluent"><tt>DG::Fluent</tt></h3>
57
+ first_thing {|answer| puts answer } # never runs
31
58
 
32
- Use JQuery-style fluent syntax for registering several callbacks and
33
- errbacks on the same Deferrable. e.g.
59
+ This is a symptom of a more general problem: only the outermost caller really
60
+ cares about the callback being run, yet every method in the chain must be aware
61
+ of it, which is poor encapsulation.
34
62
 
35
- DeferrableMonkeyShaver.new(monkey).
36
- callback { puts "Monkey is shaved" }.
37
- callback { monkey.entertain! }.
38
- errback {|e| puts "Unable to shave monkey! #{e}" }.
39
- errback {|_| monkey.terminate! }.
40
- shave
63
+ This style also breaks down when the asynchronous operation needs to
64
+ communicate failure: we want to pass in some code to be called on error, but
65
+ Ruby's syntax only allows passing a single block to a method, so callers now
66
+ need to pass in `lambda`s or hashes of `Proc`s, the syntax becomes inconsistent
67
+ and noisy, and readability and maintainability suffer:
41
68
 
42
- <h3 id="bothback"><tt>DG::Bothback</tt></h3>
69
+ def first_thing(errback, &callback)
70
+ do_something
71
+ yield if block_given? # as declared, callback is implicitly optional
72
+ rescue => e
73
+ errback.call(e) # as declared, errback is mandatory
74
+ end
43
75
 
44
- Register code to run on either success or failure: shorthand for calling both
45
- `#callback` and `#errback` with the same code block.
76
+ # Excessive punctuation alert!
77
+ first_thing(lambda {|error| handle_error }) {|result| use_result(result) }
46
78
 
47
- <h3 id="combinators"><tt>DG::Combinators</tt></h3>
79
+ EventMachine offers the
80
+ [Deferrable](http://eventmachine.rubyforge.org/EventMachine/Deferrable.html)
81
+ pattern to communicate results of asynchronous operations in an object-oriented
82
+ style more natural to Ruby. Rather than taking a callback which it must
83
+ remember to call, the method simply returns a `Deferrable` object which
84
+ encapsulates the status of the operation, and promises to update that object at
85
+ a later date. Callers can register callbacks and errbacks on the Deferrable,
86
+ which takes care of calling them when the operation succeeds or fails.
87
+ Intermediate methods in the chain can simply pass the Deferrable on, and only
88
+ code which cares about callbacks need know about them.
48
89
 
49
- Allows building up higher-level asynchronous abstractions by composing simpler
50
- asynchronous operations, without having to manually wire callbacks together
51
- and remember to propagate errors correctly.
90
+ However, asynchronous programming with Deferrables still suffers from two key
91
+ problems: it is difficult to compose multiple operations, and to build up
92
+ complex operations from simpler ones. Below is a method which performs three
93
+ synchronous operations in sequence, each depending on the result of the
94
+ previous, and returns the result of the last operation to the caller:
52
95
 
53
- Motivating example: assume we have an asynchronous database API `DB.query`
54
- which returns a Deferrable to communicate when the query finishes. (See
55
- the [API docs for `DG::Combinators`](DeferrableGratification/Combinators.html)
56
- for more detail.)
96
+ def complex_operation
97
+ first_result = do_first_thing
98
+ second_result = do_second_thing(first_result)
99
+ third_result = do_third_thing(second_result)
100
+ third_result
101
+ rescue => e
102
+ # ...
103
+ end
57
104
 
58
- def product_names_for_username(username)
59
- DB.query('SELECT id FROM users WHERE username = ?', username).bind! do |user_id|
60
- DB.query('SELECT name FROM products WHERE user_id = ?', user_id)
61
- end.map do |product_names|
62
- product_names.join(', ')
105
+ When the operations are asynchronous, the same sequence is typically
106
+ implemented using nested callbacks:
107
+
108
+ def complex_operation
109
+ result = EM::DefaultDeferrable.new
110
+ first_deferrable = do_first_thing
111
+ first_deferrable.callback do |first_result|
112
+ second_deferrable = do_second_thing(first_result)
113
+ second_deferrable.callback do |second_result|
114
+ third_deferrable = do_third_thing(second_result)
115
+ third_deferrable.callback do |third_result|
116
+ result.succeed(third_result)
117
+ end
118
+ third_deferrable.errback do |third_error|
119
+ result.fail(third_error)
120
+ end
121
+ end
122
+ second_deferrable.errback do |second_error|
123
+ result.fail(second_error)
124
+ end
125
+ end
126
+ first_deferrable.errback do |first_error|
127
+ result.fail(first_error)
63
128
  end
129
+ result
130
+ end
131
+
132
+ Like the synchronous version, this method abstracts the multiple operations
133
+ away from the caller and presents only the result the caller was interested in
134
+ (or details of what went wrong). However, the line count has tripled. Worse,
135
+ the program flow is confusing: the logic of 'do these operations in sequence'
136
+ is obscured; the errbacks read in reverse order; and the way the final result
137
+ makes its way back to the caller is almost invisible. There are also a lot of
138
+ opportunities to create bugs: all of the callbacks must be manually and
139
+ repetitively "wired together", or the method will not work.
140
+
141
+ Deferrable Gratification aims to solve these problems by providing a library of
142
+ composition operators - combinators - which abstract away the boilerplate
143
+ callback wiring and reveal the logic of the code.
144
+
145
+ ## Examples ##
146
+
147
+ ### [Fluent callback syntax](http://samstokes.github.com/deferrable_gratification/doc/DeferrableGratification/Fluent.html): because it feels right ###
148
+
149
+ The return value of
150
+ [`Deferrable#callback`](http://eventmachine.rubyforge.org/EventMachine/Deferrable.html#M000264)
151
+ isn't useful, which means if you want to set a callback on a Deferrable and
152
+ then return it, you have to name the Deferrable first, so you can return it
153
+ explicitly. You also have to mention the name again to set another callback,
154
+ which leads to a lot of noisy repetition:
155
+
156
+ def google_homepage
157
+ request = EM::HttpRequest.new('http://google.com').get(:redirects => 1)
158
+ request.callback {|http| puts http.response }
159
+ request.callback { $call_count = ($call_count || 0) + 1 }
160
+ request.errback { puts "Oh noes!" }
161
+ request
162
+ end
163
+ EM.run { r = google_homepage; r.callback { EM.stop }; r.errback { EM.stop } }
164
+ # prints a lot of HTML
165
+
166
+ DG lets you chain callbacks and errbacks using "fluent syntax" familiar from
167
+ JQuery:
168
+
169
+ DG.enhance! EM::HttpClient
170
+
171
+ def google_homepage
172
+ EM::HttpRequest.new('http://google.com').get(:redirects => 1).
173
+ callback {|http| puts http.response }.
174
+ callback { $call_count = ($call_count || 0) + 1 }.
175
+ errback { puts "Oh noes!" }
176
+ end
177
+ EM.run { google_homepage.callback { EM.stop }.errback { EM.stop } }
178
+ # prints a lot of HTML
179
+
180
+ ### [`bothback`](http://samstokes.github.com/deferrable\_gratification/doc/DeferrableGratification/Bothback.html#bothback-instance\_method): when you absolutely, positively got to... ###
181
+
182
+ Sometimes you need to do something after an asynchronous action completes,
183
+ whether it succeeded or failed: e.g. release a lock, or as in the example
184
+ above, call `EM.stop` to break out of the `EM.run` block. It's annoying to
185
+ have to write that code twice, to make sure it's called both on success and
186
+ failure.
187
+
188
+ `bothback` to the rescue:
189
+
190
+ EM.run { google_homepage.bothback { EM.stop } }
191
+ # prints a lot of HTML
192
+
193
+ ### [`transform`](http://samstokes.github.com/deferrable\_gratification/doc/DeferrableGratification/Combinators.html#transform-instance\_method): receive the callbacks that make sense for you ###
194
+
195
+ The [em-http-request](https://github.com/igrigorik/em-http-request) library is
196
+ great, but it's a bit fiddly to use, because it passes the whole
197
+ [`EM::HttpClient`](http://rdoc.info/github/igrigorik/em-http-request/master/EventMachine/HttpClient)
198
+ instance to its callbacks and errbacks. That means your callbacks can check
199
+ the response code and headers, but it also makes it harder if you just want
200
+ quick access to the response body.
201
+
202
+ EM.run do
203
+ request = EM::HttpRequest.new('http://google.com').get(:redirects => 1)
204
+ request.
205
+ callback do |http|
206
+ # Have to write this code once for each different request
207
+ if http.response_header.status == 200
208
+ puts http.response
209
+ else
210
+ request.fail(http) # triggers the errback as if the request had failed
211
+ end
212
+ end.errback {|http| handle_error(http) }.
213
+ bothback { EM.stop }
214
+ end
215
+ # prints lots of HTML
216
+
217
+ Wouldn't it be great if we could encapsulate the logic of "just give me the
218
+ response if the request was successful", and have callbacks that just act on
219
+ the response body?
220
+
221
+ def fetch_page(url)
222
+ request = EM::HttpRequest.new(url).get(:redirects => 1)
223
+ request.transform do |http|
224
+ if http.response_header.status == 200
225
+ http.response
226
+ else
227
+ request.fail(http)
228
+ end
229
+ end
230
+ end
231
+
232
+ EM.run do
233
+ fetch_page('http://google.com').
234
+ callback {|html| puts html }.
235
+ errback {|http| puts "Oh dear!" }.
236
+ bothback { EM.stop }
64
237
  end
238
+ # prints lots of HTML
239
+
240
+ That looks a lot cleaner. It would be even cooler if instead of passing the
241
+ raw HTML to callbacks, we could parse the HTML using
242
+ [Hpricot](http://hpricot.com) and pass the parsed document instead. No
243
+ problem:
244
+
245
+ require 'hpricot'
246
+
247
+ def fetchpricot(url)
248
+ fetch_page(url).transform {|html| Hpricot(html) }
249
+ end
250
+
251
+ EM.run do
252
+ fetchpricot('http://google.com').
253
+ callback {|doc| puts doc.at(:title) }.
254
+ errback {|http| puts "Oh dear!" }.
255
+ bothback { EM.stop }
256
+ end
257
+ # prints <title>Google</title>
258
+
259
+ ### [`transform_error`](http://samstokes.github.com/deferrable\_gratification/doc/DeferrableGratification/Combinators.html#transform_error-instance\_method): receive the errbacks that make sense to you ###
260
+
261
+ That's cool, but it's a bit annoying that those errbacks receive a `HttpClient`
262
+ object - we have to turn that into a useful error message every time. Let's
263
+ encapsulate that too:
264
+
265
+ def fetchpricot2(url)
266
+ fetchpricot(url).transform_error do |http|
267
+ if http.response_header.status > 0
268
+ "Unexpected response code: #{http.response_header.status}"
269
+ else
270
+ "Unknown error!"
271
+ end
272
+ end
273
+ end
274
+
275
+ EM.run do
276
+ fetchpricot2('http://google.com/page_that_probably_does_not_exist').
277
+ callback {|doc| puts doc.at(:title) }.
278
+ errback {|error| puts "Error: #{error}" }.
279
+ bothback { EM.stop }
280
+ end
281
+ # prints "Error: Unexpected response code: 404"
282
+
283
+ ### [`bind!`](http://samstokes.github.com/deferrable\_gratification/doc/DeferrableGratification/Combinators.html#bind!-instance\_method): for when one thing leads to another ###
284
+
285
+ Say we want to do a simple web crawling task: find the first search result for
286
+ 'deferrable_gratification', follow that link (which should be its Github page),
287
+ and pull down the project website listed on that page. Normally this would
288
+ require some messy nesting of callbacks and errbacks:
289
+
290
+ EM.run do
291
+ fetchpricot2('http://google.com/search?q=deferrable_gratification').callback do |doc1|
292
+ fetchpricot2((doc1 / 'ol' / 'li' / 'a')[0][:href]).callback do |doc2|
293
+ fetchpricot2((doc2 / '#repository_homepage').at(:a)[:href]).callback do |doc3|
294
+ puts doc3.at(:title).inner_text
295
+ # I could also have mistyped 'doc3' as 'doc2' and got the wrong
296
+ # behaviour, but no exception to flag it
297
+ end.errback do |error|
298
+ puts "Error finding homepage link: #{error}"
299
+ end.bothback { EM.stop }
300
+ end.errback do |error|
301
+ puts "Error loading first search result: #{error}"
302
+ EM.stop
303
+ end
304
+ end.errback do |error|
305
+ puts "Error retrieving search results: #{error}"
306
+ EM.stop
307
+ end
308
+ end
309
+ # prints "Deferrable Gratification"
310
+
311
+ With `Deferrable#bind!` we can remove the nesting and write something that
312
+ looks more like the straight-line sequential flow:
313
+
314
+ EM.run do
315
+ fetchpricot2('http://google.com/search?q=deferrable_gratification').bind! do |doc|
316
+ fetchpricot2((doc / 'ol' / 'li' / 'a')[0][:href])
317
+ end.bind! do |doc|
318
+ fetchpricot2((doc / '#repository_homepage').at(:a)[:href])
319
+ end.callback do |doc|
320
+ puts doc.at(:title).inner_text
321
+ # now the previous 'doc's aren't in scope, so I can't accidentally
322
+ # refer to them
323
+ end.errback do |error|
324
+ puts "Error: #{error}"
325
+ end.bothback { EM.stop }
326
+ end
327
+ # prints "Deferrable Gratification"
328
+
329
+ `bind!` also wires up the errbacks so we can just write a single errback that
330
+ will fire if any step in the sequence fails; similarly we don't have to write
331
+ `EM.stop` three times.
332
+
333
+ ## Getting started ##
334
+
335
+ Install the gem:
336
+
337
+ gem install deferrable_gratification
338
+
339
+ In your code:
340
+
341
+ require 'eventmachine'
342
+ require 'deferrable_gratification'
343
+ DG.enhance_all_deferrables!
344
+
345
+ Make sure that the call to
346
+ [`DG.enhance_all_deferrables!`](http://samstokes.github.com/deferrable_gratification/doc/DeferrableGratification.html#enhance_all_deferrables%21-class_method)
347
+ comes *before* you require any library that uses `Deferrable` (e.g.
348
+ [em-http-request](https://github.com/igrigorik/em-http-request)).
349
+
350
+ ### Temporary workaround because `enhance_all_deferrables!` is broken ###
351
+
352
+ You actually need to call
353
+ [`DG.enhance!`](http://samstokes.github.com/deferrable_gratification/doc/DeferrableGratification.html#enhance%21-class_method)
354
+ on each Deferrable class you'll be dealing with. This call needs to come
355
+ *after* that class is defined.
356
+
357
+ ## Documentation ##
358
+
359
+ * [API](http://samstokes.github.com/deferrable_gratification/doc/frames.html)
360
+ * [Behaviour specs](http://samstokes.github.com/deferrable_gratification/doc/spec/index.html)
361
+ (generated from RSpec code examples)
@@ -12,7 +12,7 @@ module DeferrableGratification
12
12
  # def product_names_for_username(username)
13
13
  # DB.query('SELECT id FROM users WHERE username = ?', username).bind! do |user_id|
14
14
  # DB.query('SELECT name FROM products WHERE user_id = ?', user_id)
15
- # end.map do |product_names|
15
+ # end.transform do |product_names|
16
16
  # product_names.join(', ')
17
17
  # end
18
18
  # end
@@ -131,17 +131,39 @@ module DeferrableGratification
131
131
  # Deferrable will also fail.
132
132
  #
133
133
  # @param &block block that transforms the expected result of this
134
- # operation in some way.
134
+ # operation in some way.
135
135
  #
136
136
  # @return [Deferrable] Deferrable that will succeed if this operation did,
137
137
  # after transforming its result.
138
138
  #
139
139
  # @example Retrieve a web page and call back with its title.
140
- # HTTP.request(url).map {|page| Hpricot(page).at(:title).inner_html }
141
- def map(&block)
140
+ # HTTP.request(url).transform {|page| Hpricot(page).at(:title).inner_html }
141
+ def transform(&block)
142
142
  bind!(&block)
143
143
  end
144
144
 
145
+ # Transform the value passed to the errback of this Deferrable by invoking
146
+ # +block+. If this operation succeeds, the returned Deferrable will
147
+ # succeed with the same value. If this operation fails, the returned
148
+ # Deferrable will fail with the transformed error value.
149
+ #
150
+ # @param &block block that transforms the expected error value of this
151
+ # operation in some way.
152
+ #
153
+ # @return [Deferrable] Deferrable that will succeed if this operation did,
154
+ # otherwise fail after transforming the error value with which this
155
+ # operation failed.
156
+ def transform_error(&block)
157
+ errback do |*err|
158
+ self.fail(
159
+ begin
160
+ yield(*err)
161
+ rescue => e
162
+ e
163
+ end)
164
+ end
165
+ end
166
+
145
167
 
146
168
  # Boilerplate hook to extend {ClassMethods}.
147
169
  def self.included(base)
@@ -167,6 +189,93 @@ module DeferrableGratification
167
189
  def chain(*actions)
168
190
  actions.inject(DG.const(nil), &:>>)
169
191
  end
192
+
193
+ # Combinator that waits for all of the supplied asynchronous operations
194
+ # to succeed or fail, then succeeds with the results of all those
195
+ # operations that were successful.
196
+ #
197
+ # This Deferrable will never fail. It may also never succeed, if _any_
198
+ # of the supplied operations does not either succeed or fail.
199
+ #
200
+ # The successful results are guaranteed to be in the same order as the
201
+ # operations were passed in (which may _not_ be the same as the
202
+ # chronological order in which they succeeded).
203
+ #
204
+ # @param [*Deferrable] *operations deferred statuses of asynchronous
205
+ # operations to wait for.
206
+ #
207
+ # @return [Deferrable] a deferred status that will succeed after all the
208
+ # +operations+ have either succeeded or failed; its callbacks will be
209
+ # passed an +Enumerable+ containing the results of those operations
210
+ # that succeeded.
211
+ def join_successes(*operations)
212
+ Join::Successes.setup!(*operations)
213
+ end
214
+
215
+ # Combinator that waits for any of the supplied asynchronous operations
216
+ # to succeed, and succeeds with the result of the first (chronologically)
217
+ # to do so.
218
+ #
219
+ # This Deferrable will fail if all the operations fail. It may never
220
+ # succeed or fail, if one of the operations also does not.
221
+ #
222
+ # @param (see #join_successes)
223
+ #
224
+ # @return [Deferrable] a deferred status that will succeed as soon as any
225
+ # of the +operations+ succeeds; its callbacks will be passed the result
226
+ # of that operation.
227
+ def join_first_success(*operations)
228
+ Join::FirstSuccess.setup!(*operations)
229
+ end
230
+
231
+ # Combinator that repeatedly executes the supplied block until it
232
+ # succeeds, then succeeds itself with the eventual result.
233
+ #
234
+ # This Deferrable may never succeed, if the operation never succeeds.
235
+ # It will fail if an iteration raises an exception.
236
+ #
237
+ # @note this combinator is intended for use inside EventMachine. It will
238
+ # still work outside of EventMachine, _provided_ that the operation is
239
+ # synchronous (although a simple +while+ loop might be preferable in
240
+ # this case!).
241
+ #
242
+ # @param loop_deferrable for internal use only, always omit this.
243
+ # @param &block operation to execute until it succeeds.
244
+ #
245
+ # @yieldreturn [Deferrable] deferred status of the operation. If it
246
+ # fails, the operation will be retried. If it succeeds, the combinator
247
+ # will succeed with the result.
248
+ #
249
+ # @return [Deferrable] a deferred status that will succeed once the
250
+ # supplied operation eventually succeeds.
251
+ def loop_until_success(loop_deferrable = DefaultDeferrable.new, &block)
252
+ if EM.reactor_running?
253
+ EM.next_tick do
254
+ begin
255
+ attempt = yield
256
+ rescue => e
257
+ loop_deferrable.fail(e)
258
+ else
259
+ attempt.callback(&loop_deferrable.method(:succeed))
260
+ attempt.errback { loop_until_success(loop_deferrable, &block) }
261
+ end
262
+ end
263
+ else
264
+ # In the synchronous case, we could simply use the same
265
+ # implementation as in EM, but without the next_tick; unfortunately
266
+ # that means direct recursion, so risks stack overflow. Instead we
267
+ # just reimplement as a loop.
268
+ results = []
269
+ begin
270
+ yield.callback {|*values| results << values } while results.empty?
271
+ rescue => e
272
+ loop_deferrable.fail(e)
273
+ else
274
+ loop_deferrable.succeed(*results[0])
275
+ end
276
+ end
277
+ loop_deferrable
278
+ end
170
279
  end
171
280
  end
172
281
  end
@@ -31,7 +31,7 @@ module DeferrableGratification
31
31
  # However, because Ruby doesn't actually type-check blocks, we can't
32
32
  # enforce that the block really does return a second Deferrable. This
33
33
  # therefore also supports (reasonably) arbitrary blocks. However, it's
34
- # probably clearer (though equivalent) to use {#map} for this case.
34
+ # probably clearer (though equivalent) to use {#transform} for this case.
35
35
  class Bind < DefaultDeferrable
36
36
  # Prepare to bind +block+ to +first+, and create the Deferrable that
37
37
  # will represent the bind.
@@ -84,7 +84,7 @@ module DeferrableGratification
84
84
  second.errback {|*error| self.fail(*error) }
85
85
  else
86
86
  # Not a Deferrable, so we need to "behave sensibly" as alluded to
87
- # above. Just behaving like #map is sensible enough.
87
+ # above. Just behaving like #transform is sensible enough.
88
88
  self.succeed(second)
89
89
  end
90
90
  end
@@ -0,0 +1,129 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. default_deferrable])
2
+
3
+ module DeferrableGratification
4
+ module Combinators
5
+ # Abstract base class for combinators that depend on a number of
6
+ # asynchronous operations (potentially executing in parallel).
7
+ #
8
+ # @abstract Subclasses should override {#done?} to define whether they wait
9
+ # for some or all of the operations to complete, and {#finish} to define
10
+ # what they do when {#done?} returns true.
11
+ class Join < DefaultDeferrable
12
+ # Prepare to wait for the completion of +operations+.
13
+ #
14
+ # Does not actually set up any callbacks or errbacks: call {#setup!} for
15
+ # that.
16
+ #
17
+ # @param [*Deferrable] *operations deferred statuses of asynchronous
18
+ # operations to wait for.
19
+ def initialize(*operations)
20
+ @operations = operations
21
+ @successes = Array.new(@operations.size, Sentinel.new)
22
+ @failures = Array.new(@operations.size, Sentinel.new)
23
+ end
24
+
25
+ # Register callbacks and errbacks on the supplied operations to notify
26
+ # this {Join} of completion.
27
+ def setup!
28
+ finish if done?
29
+
30
+ @operations.each_with_index do |op, index|
31
+ op.callback do |result|
32
+ @successes[index] = result
33
+ finish if done?
34
+ end
35
+ op.errback do |error|
36
+ @failures[index] = error
37
+ finish if done?
38
+ end
39
+ end
40
+ end
41
+
42
+ # Create a {Join} and register the callbacks.
43
+ #
44
+ # @param (see #initialize)
45
+ #
46
+ # @return [Join] Deferrable representing the join operation.
47
+ def self.setup!(*operations)
48
+ new(*operations).tap(&:setup!)
49
+ end
50
+
51
+
52
+ # Combinator that waits for all of the supplied asynchronous operations
53
+ # to succeed or fail, then succeeds with the results of all those
54
+ # operations that were successful.
55
+ #
56
+ # This Deferrable will never fail. It may also never succeed, if _any_
57
+ # of the supplied operations does not either succeed or fail.
58
+ #
59
+ # The successful results are guaranteed to be in the same order as the
60
+ # operations were passed in (which may _not_ be the same as the
61
+ # chronological order in which they succeeded).
62
+ #
63
+ # You probably want to call {ClassMethods#join_successes} rather than
64
+ # using this class directly.
65
+ class Successes < Join
66
+ private
67
+ def done?
68
+ all_completed?
69
+ end
70
+
71
+ def finish
72
+ succeed(successes)
73
+ end
74
+ end
75
+
76
+
77
+ # Combinator that waits for any of the supplied asynchronous operations
78
+ # to succeed, and succeeds with the result of the first (chronologically)
79
+ # to do so.
80
+ #
81
+ # This Deferrable will fail if all the operations fail. It may never
82
+ # succeed or fail, if one of the operations also does not.
83
+ #
84
+ # You probably want to call {ClassMethods#join_first_success} rather than
85
+ # using this class directly.
86
+ class FirstSuccess < Join
87
+ private
88
+ def done?
89
+ successes.length > 0
90
+ end
91
+
92
+ def finish
93
+ succeed(successes.first)
94
+ end
95
+ end
96
+
97
+
98
+ private
99
+ def successes
100
+ without_sentinels(@successes)
101
+ end
102
+
103
+ def failures
104
+ without_sentinels(@failures)
105
+ end
106
+
107
+ def all_completed?
108
+ successes.length + failures.length >= @operations.length
109
+ end
110
+
111
+ def done?
112
+ raise NotImplementedError, 'subclasses should override this'
113
+ end
114
+
115
+ def finish
116
+ raise NotImplementedError, 'subclasses should override this'
117
+ end
118
+
119
+ def without_sentinels(ary)
120
+ ary.reject {|item| item.instance_of? Sentinel }
121
+ end
122
+
123
+ # @private
124
+ # Used internally to distinguish between the absence of a response and
125
+ # a response with the value +nil+.
126
+ class Sentinel; end
127
+ end
128
+ end
129
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deferrable_gratification
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 1
8
+ - 2
9
9
  - 0
10
- version: 0.1.0
10
+ version: 0.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Sam Stokes
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-01-15 00:00:00 -08:00
18
+ date: 2011-01-21 00:00:00 -08:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -91,9 +91,9 @@ dependencies:
91
91
  type: :development
92
92
  version_requirements: *id005
93
93
  description: |
94
- Deferrable Gratification (DG) makes evented code less error-prone and easier to compose, and thus easier to create higher-level abstractions around. It also enhances the API offered by Ruby Deferrables to make them more pleasant to work with.
94
+ Deferrable Gratification (DG) facilitates asynchronous programming in Ruby, by helping create abstractions around complex operations built up from simpler ones. It helps make asynchronous code less error-prone and easier to compose. It also provides some enhancements to the Deferrable API.
95
95
 
96
- Currently consists of the following components:
96
+ Features include:
97
97
 
98
98
  * fluent (aka chainable) syntax for registering multiple callbacks and errbacks to the same Deferrable.
99
99
 
@@ -113,6 +113,7 @@ files:
113
113
  - lib/deferrable_gratification.rb
114
114
  - lib/deferrable_gratification/default_deferrable.rb
115
115
  - lib/deferrable_gratification/combinators/bind.rb
116
+ - lib/deferrable_gratification/combinators/join.rb
116
117
  - lib/deferrable_gratification/combinators.rb
117
118
  - lib/deferrable_gratification/bothback.rb
118
119
  - lib/deferrable_gratification/fluent.rb