deferrable_gratification 0.1.0 → 0.2.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.
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