reacto 0.1.0 → 1.0.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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/README.md +932 -11
  4. data/doc/reactive_programming_with_reacto.md +238 -0
  5. data/lib/reacto.rb +70 -0
  6. data/lib/reacto/behaviours.rb +24 -1
  7. data/lib/reacto/constants.rb +4 -1
  8. data/lib/reacto/executors.rb +8 -10
  9. data/lib/reacto/labeled_trackable.rb +14 -2
  10. data/lib/reacto/operations.rb +23 -2
  11. data/lib/reacto/operations/act.rb +69 -0
  12. data/lib/reacto/operations/append.rb +45 -0
  13. data/lib/reacto/operations/blocking_enumerable.rb +40 -0
  14. data/lib/reacto/operations/buffer.rb +1 -4
  15. data/lib/reacto/operations/chunk.rb +81 -0
  16. data/lib/reacto/operations/chunk_while.rb +56 -0
  17. data/lib/reacto/operations/cycle.rb +27 -0
  18. data/lib/reacto/operations/delay_each.rb +75 -0
  19. data/lib/reacto/operations/depend_on.rb +4 -5
  20. data/lib/reacto/operations/diff.rb +8 -10
  21. data/lib/reacto/operations/drop.rb +6 -8
  22. data/lib/reacto/operations/drop_while.rb +23 -0
  23. data/lib/reacto/operations/each_collect.rb +57 -0
  24. data/lib/reacto/operations/each_with_object.rb +31 -0
  25. data/lib/reacto/operations/extremums.rb +54 -0
  26. data/lib/reacto/operations/find_index.rb +28 -0
  27. data/lib/reacto/operations/flat_map.rb +2 -2
  28. data/lib/reacto/operations/flatten.rb +2 -7
  29. data/lib/reacto/operations/flatten_labeled.rb +44 -0
  30. data/lib/reacto/operations/{label.rb → group_by_label.rb} +1 -1
  31. data/lib/reacto/operations/include.rb +40 -0
  32. data/lib/reacto/operations/inject.rb +15 -9
  33. data/lib/reacto/operations/map.rb +15 -13
  34. data/lib/reacto/operations/merge.rb +17 -16
  35. data/lib/reacto/operations/operation_on_labeled.rb +29 -0
  36. data/lib/reacto/operations/partition.rb +52 -0
  37. data/lib/reacto/operations/prepend.rb +0 -3
  38. data/lib/reacto/operations/rescue_and_replace_error.rb +21 -0
  39. data/lib/reacto/operations/retry.rb +26 -0
  40. data/lib/reacto/operations/retry_when.rb +30 -0
  41. data/lib/reacto/operations/select.rb +2 -6
  42. data/lib/reacto/operations/slice.rb +50 -0
  43. data/lib/reacto/operations/slice_when.rb +41 -0
  44. data/lib/reacto/operations/split_labeled.rb +32 -0
  45. data/lib/reacto/operations/take.rb +9 -14
  46. data/lib/reacto/operations/take_while.rb +28 -0
  47. data/lib/reacto/operations/throttle.rb +2 -3
  48. data/lib/reacto/operations/track_on.rb +1 -3
  49. data/lib/reacto/shared_trackable.rb +2 -5
  50. data/lib/reacto/subscriptions/buffered_subscription.rb +10 -9
  51. data/lib/reacto/subscriptions/executor_subscription.rb +12 -4
  52. data/lib/reacto/subscriptions/tracker_subscription.rb +0 -4
  53. data/lib/reacto/subscriptions/zipping_subscription.rb +0 -1
  54. data/lib/reacto/trackable.rb +429 -64
  55. data/lib/reacto/version.rb +1 -1
  56. data/reacto.gemspec +2 -3
  57. data/spec/reacto/labeled_trackable_spec.rb +17 -0
  58. data/spec/reacto/trackable/act_spec.rb +15 -0
  59. data/spec/reacto/trackable/all_spec.rb +38 -0
  60. data/spec/reacto/trackable/any_spec.rb +39 -0
  61. data/spec/reacto/trackable/append_spec.rb +38 -0
  62. data/spec/reacto/trackable/buffer_spec.rb +11 -15
  63. data/spec/reacto/trackable/chunk_spec.rb +86 -0
  64. data/spec/reacto/trackable/chunk_while_spec.rb +22 -0
  65. data/spec/reacto/trackable/class_level/combine_last_spec.rb +1 -3
  66. data/spec/reacto/trackable/class_level/interval_spec.rb +4 -6
  67. data/spec/reacto/trackable/class_level/make_spec.rb +0 -15
  68. data/spec/reacto/trackable/{zip_spec.rb → class_level/zip_spec.rb} +0 -2
  69. data/spec/reacto/trackable/concat_spec.rb +12 -12
  70. data/spec/reacto/trackable/count_spec.rb +38 -0
  71. data/spec/reacto/trackable/cycle_spec.rb +14 -0
  72. data/spec/reacto/trackable/delay_each_spec.rb +18 -0
  73. data/spec/reacto/trackable/depend_on_spec.rb +6 -9
  74. data/spec/reacto/trackable/diff_spec.rb +3 -5
  75. data/spec/reacto/trackable/drop_errors_spec.rb +1 -3
  76. data/spec/reacto/trackable/drop_while_spec.rb +15 -0
  77. data/spec/reacto/trackable/each_cons_spec.rb +53 -0
  78. data/spec/reacto/trackable/each_slice_spec.rb +37 -0
  79. data/spec/reacto/trackable/each_with_index_spec.rb +33 -0
  80. data/spec/reacto/trackable/each_with_object_spec.rb +26 -0
  81. data/spec/reacto/trackable/entries_spec.rb +25 -0
  82. data/spec/reacto/trackable/execute_on_spec.rb +33 -0
  83. data/spec/reacto/trackable/find_index_spec.rb +31 -0
  84. data/spec/reacto/trackable/find_spec.rb +34 -0
  85. data/spec/reacto/trackable/first_spec.rb +36 -0
  86. data/spec/reacto/trackable/flat_map_latest_spec.rb +5 -5
  87. data/spec/reacto/trackable/flat_map_spec.rb +25 -0
  88. data/spec/reacto/trackable/flatten_labeled_spec.rb +48 -0
  89. data/spec/reacto/trackable/grep_spec.rb +29 -0
  90. data/spec/reacto/trackable/grep_v_spec.rb +23 -0
  91. data/spec/reacto/trackable/{label_spec.rb → group_by_label_spec.rb} +4 -11
  92. data/spec/reacto/trackable/include_spec.rb +23 -0
  93. data/spec/reacto/trackable/inject_spec.rb +30 -4
  94. data/spec/reacto/trackable/lift_spec.rb +1 -3
  95. data/spec/reacto/trackable/map_spec.rb +17 -3
  96. data/spec/reacto/trackable/max_by_spec.rb +12 -0
  97. data/spec/reacto/trackable/max_spec.rb +19 -0
  98. data/spec/reacto/trackable/merge_spec.rb +6 -7
  99. data/spec/reacto/trackable/min_by_spec.rb +12 -0
  100. data/spec/reacto/trackable/min_spec.rb +19 -0
  101. data/spec/reacto/trackable/minmax_by_spec.rb +12 -0
  102. data/spec/reacto/trackable/minmax_spec.rb +19 -0
  103. data/spec/reacto/trackable/none_spec.rb +38 -0
  104. data/spec/reacto/trackable/on_spec.rb +11 -4
  105. data/spec/reacto/trackable/one_spec.rb +38 -0
  106. data/spec/reacto/trackable/partition_spec.rb +23 -0
  107. data/spec/reacto/trackable/prepend_spec.rb +1 -3
  108. data/spec/reacto/trackable/reject_spec.rb +21 -0
  109. data/spec/reacto/trackable/rescue_and_replace_error_spec.rb +48 -0
  110. data/spec/reacto/trackable/rescue_and_replace_error_with_spec.rb +26 -0
  111. data/spec/reacto/trackable/retry_spec.rb +50 -0
  112. data/spec/reacto/trackable/retry_when_spec.rb +33 -0
  113. data/spec/reacto/trackable/select_spec.rb +18 -7
  114. data/spec/reacto/trackable/slice_after_spec.rb +38 -0
  115. data/spec/reacto/trackable/slice_before_spec.rb +38 -0
  116. data/spec/reacto/trackable/slice_when_spec.rb +26 -0
  117. data/spec/reacto/trackable/sort_by_spec.rb +16 -0
  118. data/spec/reacto/trackable/sort_spec.rb +23 -0
  119. data/spec/reacto/trackable/split_labeled_spec.rb +37 -0
  120. data/spec/reacto/trackable/take_while_spec.rb +16 -0
  121. data/spec/reacto/trackable/throttle_spec.rb +2 -3
  122. data/spec/reacto/trackable/track_on_spec.rb +2 -3
  123. data/spec/reacto/trackable/uniq_spec.rb +2 -4
  124. data/spec/support/helpers.rb +9 -1
  125. metadata +135 -25
  126. data/Gemfile.lock +0 -32
  127. data/lib/reacto/operations/cache.rb +0 -53
  128. data/spec/reacto/trackable/cache_spec.rb +0 -64
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 98fb0c0ff41e6efde1e6c11574dd9be835684b9d
4
- data.tar.gz: b1f541bb712c8add903faa427b739bf8d8846bdd
3
+ metadata.gz: 72cb45d79605d42cd093b0bfaa2a84d605da0e52
4
+ data.tar.gz: abf52dd63f1d2f5b78558e607965e7ab2fb0c687
5
5
  SHA512:
6
- metadata.gz: e8492c6d0751362a31a6b2610ad5ceb16b168adf13ef4be8b53aac834fece5761aed037e334035ec2ed150eb5e6fc2fdefe8eb09da0e23ae2d1c72037b8f5e13
7
- data.tar.gz: 4bb888957bb7241ca42c8955d001c5fd32feb3f047b15c01dac0c28c88757dd055139316be71401da0e17318d58829334857980c182d139015284604c2ca4ce0
6
+ metadata.gz: e9a3c9de5616c0bfda47ac8c7f7e192abe298dd4e5413a94f8b594b9d71964550ba975b6d67f64c89437c66c44a26d972147b0aa1f476edc4db8fb9c75c5290c
7
+ data.tar.gz: e3b42cdcaace06dd7716048b43ca58ef61852e3577a116e3c7d618615fbca4024f2d8be3e894ae83128e4a4de08fd8f3a4fd38dfbc9fcc8f661272fbc7830f65
data/.gitignore CHANGED
@@ -1 +1,3 @@
1
1
  *.gem
2
+ Gemfile.lock
3
+ .ruby-version
data/README.md CHANGED
@@ -26,26 +26,30 @@ Of course there are other implementations of reactive programming for ruby:
26
26
  * [RxRuby](https://github.com/ReactiveX/RxRuby) : Very powerful implementation
27
27
  of RX. Handles concurrency and as a whole has more features than `Reacto`.
28
28
  So why `Reacto`? `Reacto` has simpler interface it is native Ruby lib and
29
- is easier to use it. The goal of `Reacto` is to be competitor to `RxRuby`
29
+ is easier to use it. The goal of `Reacto` is to be alternative to `RxRuby`
30
30
  in the ruby world.
31
31
  Still the author of Reacto is big fan of `RX` especially `RxJava`. He even has
32
32
  a book on the topic using `RxJava` :
33
33
  [Learning Reactive Programming with Java 8](https://www.packtpub.com/application-development/learning-reactive-programming-java-8)
34
34
  * [Frappuccino](https://github.com/steveklabnik/frappuccino) : Very cool lib,
35
35
  easy to use and simply beautiful. The only drawback - it is a bit limited :
36
- no concurrency and small set of operators.
37
- But if you don't need more complicated operators it is the best.
36
+ no concurrency and small set of operations.
37
+ But if you don't need more complicated operations it works.
38
38
  The author of `Reacto` highly recommends it.
39
39
 
40
40
  ## Usage
41
41
 
42
42
  ### Simple Trackables
43
43
 
44
- The main entry point to the lib is the `Reacto::Trackable` class.
45
- It is something you can track for notifications. Usually A `Reacto::Trackable`
46
- implemenation is pushing notifications to some notification tracker.
47
- It depends on the source. We can have some remote streaming service as a source,
48
- or an synchronous HTTP request or some process pushing updates to another.
44
+ The main entry point to the library is the `Reacto::Trackable` class.
45
+ It represents something you can track for notifications.
46
+ Usually a `Reacto::Trackable` implemenation is pushing notifications to
47
+ some notification tracker. It depends on the source. We can have some remote
48
+ streaming service as a source or an asynchronous HTTP request or some process
49
+ pushing updates to another.
50
+
51
+ #### value
52
+
49
53
  Of course the source can be very simple, for example a single value:
50
54
 
51
55
  ```ruby
@@ -53,7 +57,7 @@ Of course the source can be very simple, for example a single value:
53
57
  ```
54
58
 
55
59
  This value won't be emitted as notification until there is no tracker (listener)
56
- attached to the `trackable` - so this `trackable` instance is lazy - won't do
60
+ attached to `trackable` - so this `Trackable` instance is lazy - won't do
57
61
  anything until necessary.
58
62
 
59
63
  ```ruby
@@ -63,12 +67,90 @@ anything until necessary.
63
67
  ```
64
68
 
65
69
  This line attaches a notification tracker to the `trackable` - a lambda that
66
- should be called when the `trackable` emits any value. This example is very
70
+ should be called when `trackable` emits any value. This example is very
67
71
  simple and the `trackable` emits only one value - `5` when a tracker is attached
68
- to it so the lambda will be called and the value will be printed.
72
+ to it so the lambda will be called and the value will be printed. Shortcuts
73
+ for `Reacto::Trackable.value(val)` are `Reacto.value(val)` and `Reacto[val]`.
74
+
75
+ #### error
76
+
77
+ If we want to emit only an error notification we can do it with
78
+ `Trackable.error`. It works the same way as `Trackable.value`,
79
+ but the notification is of type _error_:
80
+
81
+ ```ruby
82
+ trackable = Reacto::Trackable.error(StandardError.new('Some error!'))
83
+
84
+ trackable.on(error: ->(e) { raise e })
85
+ ```
86
+
87
+ Shorcuts for `Reacto::Trackable.error(err)` are `Reacto.error(err)` and
88
+ `Reacto[err]`. Notice that `Reacto[simple_vlue]` is like `Reacto.value`, but
89
+ `Reacto[some_standart_error]` is like calling `Reacto.error`.
90
+
91
+ #### close
92
+
93
+ There is a way to create a `Reacto::Trackable` emitting only _close_
94
+ notification too:
95
+
96
+ ```ruby
97
+ trackable = Reacto::Trackable.error(StandardError.new('Some error!'))
98
+
99
+ trackable.on(close: ->() { p 'closed' })
100
+ ```
101
+
102
+ Shorcuts are `Reacto.close` and `Reacto[:close]`.
103
+
104
+ #### enumerable
105
+
106
+ Another example is `Trackable` with source an Enumerable instance:
107
+
108
+ ```ruby
109
+ trackable = Reacto::Trackable.enumerable([1, 3, 4])
110
+ ```
111
+
112
+ Again we'll have to call `#on` on it in order to push its values to a tracker.
113
+ Shorcuts are `Reacto.enumerable(enumerable)` and `Reacto[enumerable]`.
114
+
115
+ #### interval
116
+
117
+ A neat way to create `Trackable` emitting the values of some _Enumerable_ on
118
+ every second, for example is `Reacto::Trackable.interval`:
119
+
120
+ ```ruby
121
+ trackable = described_class.interval(0.3)
122
+ ```
123
+
124
+ This one emits the natural numbers on every _0.3_ seconds.
125
+ The second argument can be an `Enumerator` - limited or unlimited, for example:
126
+
127
+ ```ruby
128
+ trackable = described_class.interval(2, ('a'..'z').each)
129
+ ```
130
+
131
+ Emits the letters _a to z_ on every two seconds. We can create a custom
132
+ enumerator and use it.
133
+ Note that `.interval` creates `Reacto::Trackable` which emits in a special
134
+ thread, so calling `#on` on it won't block the current thread.
135
+ Shortcut is `Reacto.interval`.
136
+
137
+ #### never
138
+
139
+ It is possible that a Trackable which never emits anything is needed. Some
140
+ operations behave according to `Trackable` instances returned, so a way to have
141
+ such a `Reacto::Trackable` is:
142
+
143
+ ```ruby
144
+ trackable = Reacto::Trackable.never
145
+ ```
146
+
147
+ Shortcuts for this one are `Reacto.never` and `Reacto[:never]`
148
+
69
149
 
70
150
  ### Programming Trackable behavior
71
151
 
152
+ #### make
153
+
72
154
  A `Reacto::Trackable` can have custom behavior, defining what and when should
73
155
  be sent:
74
156
 
@@ -86,4 +168,843 @@ be sent:
86
168
  When a tracker is attached this behavior will become active and the tracker
87
169
  will receive the first two sentences as values, then, after one second the third
88
170
  one and then a closing notification.
171
+ Shorcut is `Reacto.make`.
172
+
173
+ #### SharedTrackable
174
+
175
+ Every time a tracker is attached with call to `on`, this behavior will be
176
+ executed for the given tracker. If we want to have a shared behavior for all
177
+ the trackers we can create a `Reacto::SharedTrackable` instance:
178
+
179
+ ```ruby
180
+ require 'socket'
181
+
182
+ trackable = Reacto::SharedTrackable.make do |subscriber|
183
+ hostname = 'localhost'
184
+ port = 3555
185
+
186
+ return unless subscriber.subscribed?
187
+
188
+ socket = nil
189
+ begin
190
+ socket = TCPSocket.open(hostname, port)
191
+
192
+ while line = socket.gets
193
+ break unless subscriber.subscribed?
194
+
195
+ subscriber.on_value(line)
196
+ end
197
+
198
+ subscriber.on_close if subscriber.subscribed?
199
+ rescue StandardError => error
200
+ subscriber.on_error(error) if subscriber.subscribed?
201
+ ensure
202
+ socket.close unless socket.nil?
203
+ end
204
+
205
+ end
206
+
207
+ trackable.on(value: -> (v) { puts v })
208
+ trackable.on do |v|
209
+ puts v
210
+ end
211
+
212
+ # The above calls of `on` are identical. And the two will the same data.
213
+ # Nothing happens on calling `on` though, the `trackable` has to be activated:
214
+
215
+ trackable.activate!
216
+ ```
217
+
218
+ ### Tracking for notifications
219
+
220
+ #### on
221
+
222
+ The easiest way to listen a `Reacto::Trackable` is to call `#on` on it:
223
+
224
+ ```ruby
225
+ consumer = -> (value) do
226
+ # Consume the incoming value
227
+ end
228
+
229
+ trackable.on(value: consumer)
230
+ ```
231
+
232
+ Calling it like that will trigger the behavior of `trackable` and
233
+ all the values it emits, will be passed to the consumer. A block can be passed
234
+ to `#on` and it will have the same effect:
235
+
236
+ ```ruby
237
+ trackable.on do |value|
238
+ # Consume the incoming value
239
+ end
240
+ ```
241
+
242
+ If we want to listen for errors we can call `#on` like that:
243
+
244
+ ```ruby
245
+ error_consumer = -> (error) do
246
+ # Consume the incoming error
247
+ end
248
+
249
+ trackable.on(error: error_consumer)
250
+ ```
251
+
252
+ Only one error can be emitted by a `Trackable` for subscription and that will
253
+ close the `Reacto::Trackable`. If there is no error, the normal closing
254
+ notification should be emitted. We can fetch it like this:
255
+
256
+ ```ruby
257
+ to_be_called_on_close = -> () do
258
+ # Fnalize?
259
+ end
260
+
261
+ trackable.on(close: to_be_called_on_cloe)
262
+ ```
263
+
264
+ #### track
265
+
266
+ Under the hood `#on` creates a new `Reacto::Tracker` instance with the right
267
+ methods. If we want to create our own tracker, we can always call `#track` on
268
+ the trackable with the given instance:
269
+
270
+ ```ruby
271
+ consumer = -> (value) do
272
+ # Consume the incoming value
273
+ end
274
+ error_consumer = -> (error) do
275
+ # Consume the incoming error
276
+ end
277
+
278
+ trackable.track(Reacto::Trackable.new(
279
+ value: consumer, error: error_consumer, close: -> () { p 'Closing!' }
280
+ ))
281
+ ```
282
+
283
+ All of these keyword parameters have default values - for example if we don't
284
+ pass a `value:` action, a _no-action_ will be used, doing nothing with the
285
+ value, the same is right about not passing `close:` action. Be aware that
286
+ the default `error:` action is raising the error.
287
+
288
+
289
+ ### Subscriptions
290
+
291
+ Calling `#on` or `#track` will create and return a `Reacto::Subscription`.
292
+ We can unsubscribe form the `Trackable` with it by calling `#unsubscribe`:
293
+
294
+ ```ruby
295
+ subscription = trackable.on(value: consumer)
296
+
297
+ subscription.unsubscribe
298
+ ```
299
+
300
+ This way our notification tracker won't receive notification anymore.
301
+ Checking if a `Subscription` is subscribed can be done by calling `subscribed?`
302
+ on it.
303
+
304
+ ```ruby
305
+ subscription = trackable.on(value: consumer)
306
+
307
+ subscription.subscribed? # true
308
+ ```
309
+
310
+ Subscriptions can be used for other things, adding additional subscriptions
311
+ to them, adding resources, which should be closed on receiving the `close`
312
+ notification and waiting for a `Trackable` operating on background to finish.
313
+
314
+ ### Operations
315
+
316
+ Operations are methods which can be invoked on a `Reacto::Trackable` instance,
317
+ and always return a new `Reacto::Trackable` instance. The new trackable has
318
+ emits all or some of the notifications of the source, somewhat changed by
319
+ the operation. Let's look at an example:
320
+
321
+ #### map
322
+
323
+ The `map` operation is a transformation, it transforms every value
324
+ (and not only), emitted by the source using the _block_ given.
325
+
326
+ ```ruby
327
+ source_trackable = Reacto.enumerable((1..100))
328
+
329
+ trackable = source_trackable.map { |value| value - 1 }
330
+
331
+ trackable.on(value: -> (val) { puts val })
332
+ # the numbers from 0 to 99 will be printed
333
+ ```
334
+
335
+ The `map` operation is able to transform errors as well, it can transform
336
+ the stream of notifications itself and add new notification before the
337
+ close notification for example.
338
+
339
+ #### select
340
+
341
+ The `select` operation filters values using a predicate block:
342
+
343
+ ```ruby
344
+ source_trackable = Reacto.enumerable((1..100))
345
+
346
+ trackable = source_trackable.select { |value| value % 5 == 0 }
347
+
348
+ trackable.on(value: -> (val) { puts val })
349
+ # the numbers printed will be 5, 10, 15, ... 95, 100
350
+ ```
351
+
352
+ There are more filtering operations - `drop`, `take`, `first`, `last`, etc.
353
+ Look at the specs for examples of them.
354
+
355
+ #### inject
356
+
357
+ Using `inject` is a way to accumulate and emit data based on the current
358
+ incoming value and the accumulated data from the previous ones. A better way
359
+ to explain it is an example:
360
+
361
+ ```ruby
362
+ source_trackable = Reacto.enumerable((1..100))
363
+
364
+ trackable = source.inject(0) { |prev, v| prev + v }
365
+
366
+ trackable.on(value: -> (val) { puts val })
367
+ # Will print a sequesnce of sums 0+1 then 1+2=3, then 3+4=7, etc, the last
368
+ # value will be the sum of all the source values
369
+ ```
370
+
371
+ Operation similar to `inject` is `diff`, which calls a given block for every
372
+ two emitted in sequence values and the `Reacto::Trackable` resulting from it,
373
+ emits this the block's return value. Another one is `each_with_object`, which
374
+ calls a given block for each value emitted by the source with an arbitrary
375
+ object given, and emits the initially given object.
376
+
377
+ #### flat_map
378
+
379
+ This operation takes a block which will be called for every emitted value
380
+ by the source. The block has to return a `Reacto::Trackable` instance for
381
+ every value. So if the source emits 10 values, ten Trackable instances will
382
+ be created, all of which will emit values. All these values are flattened
383
+ and emitted by the `Reacto::Trackable` created by calling `flat_map`.
384
+
385
+ ```ruby
386
+ source = Reacto.enumerable([(-10..-1), [0], (1..10)])
387
+ trackable = source.flat_map { |v| Reacto[v] }
388
+
389
+ trackable.on(value: -> (val) { puts val })
390
+ # Will print all the numbers from -10 to 10
391
+ ```
392
+
393
+ It is a very powerful operation, which allows us to create `Reacto::Trackable`
394
+ instances from incoming data and write logic using operations on them.
395
+
396
+ #### ... and even more operations
397
+
398
+ *Reacto* is in continuous development more and more _operations_ are being added
399
+ to it and there are even more to come.
400
+ So soon there will be a documentation page for all of the available operations,
401
+ which will be updated when new ones are added, or existing ones are modified.
402
+ Keep in mind that `Reacto::Trackable` mirrors `Enumerable`, it even includes
403
+ it in itself. This means that for every method in `Enumerable`, there is a
404
+ corresponding operation or method in `Reacto::Trackable`.
405
+
406
+ TODO
407
+
408
+ ### Interacting Trackables
409
+
410
+ Trackables can interact with one another, for example one `Reacto::Trackable`
411
+ instance can be merged with another to produce a new one - emitting the
412
+ notifications of the two sources.
413
+
414
+ #### merge
415
+
416
+ This is done by calling `merge` on one of the Trackables and passing to it
417
+ the other. `merge` is an operation - it produces a new `Reacto::Trackable`
418
+ instance:
419
+
420
+ ```ruby
421
+ trackable = Reacto.interval(2).map { |v| v.to_s + 'a'}.take(5)
422
+ to_be_merged = Reacto.interval(3.5).map { |v| v.to_s + 'b'}.take(4)
423
+
424
+ subscription = trackable.merge(to_be_merged).on(value: -> (val) { puts val })
425
+ trackable.await(subscription)
426
+
427
+ # Something like '0a', '0b', '1a', '2a', '1b', '3a', '4a', '2b', '3b' will
428
+ # be printed
429
+ ```
430
+
431
+ As mentioned before, interval is executed in the background by default, so
432
+ adding trackers to either of the sources won't block the current thread.
433
+ This means that the `Reacto::Trackable` created by `merge` will emit the
434
+ source notifications in the order they are coming and that doesn't depend on
435
+ which source they are coming from. We call `#await` to it passing the
436
+ _subscription_ because we don't want the current thread to terminate, we want
437
+ it to wait for the threads of the two sources to finish. More on that later.
438
+
439
+ #### zip
440
+
441
+ Zip combines the notifications emitted by multiple `Reacto::Trackable` instances
442
+ into one, using a combinator function. The first notifications of all the
443
+ trackables are combined, then the second notifications and when one of the
444
+ sources emits close/error notification, the one produced by `zip` emits it and
445
+ closes.
446
+
447
+ ```ruby
448
+ source1 = Reacto.interval(3).drop(1).take(4)
449
+ source2 = Reacto.interval(7, ('a'..'b').each)
450
+ source3 = Reacto.interval(5, ('A'..'C').each)
451
+
452
+ trackable = Reacto::Trackable.zip(source1, source2, source3) do |v1, v2, v3|
453
+ "#{v1} : #{v2} : #{v3}"
454
+ end
455
+
456
+ subscription = trackable.on(
457
+ value: -> (val) { puts val }, close: -> () { puts 'Bye!' }
458
+ )
459
+ trackable.await(subscription)
460
+
461
+ # '1 : a : A' and '2 : b : B' will be printed, then 'Bye!', because the second
462
+ # source will emit the close notification after emitting 'b'.
463
+ ```
464
+
465
+ Here the first source - `source1` is emitting the numbers from `0` to infinity
466
+ on every 3 seconds, we want to drop the `0` and start by emitting `1`, so we
467
+ drop the first emitted value with `drop(1)`, then we don't want to emit to
468
+ infinity, so we `take(4)` - only the first `4` numbers, so `1`, `2`, `3` and `4`
469
+ . This is an example of how to use the positional filtering operations.
470
+ A shortcut for this one is `Reacto.zip`.
471
+
472
+ #### combine
473
+
474
+ There is the `combine` operation which behaves in a fashion similar to `zip`
475
+ but combines the last emitted values with its combinator function on every new
476
+ value incoming from any source and closes when all of the sources have closed.
477
+
478
+ ```ruby
479
+ source1 = Reacto.interval(3).take(4)
480
+ source2 = Reacto.interval(7, ('a'..'b').each)
481
+ source3 = Reacto.interval(5, ('A'..'C').each)
482
+
483
+ trackable = Reacto::Trackable.combine(source1, source2, source3) do |v1, v2, v3|
484
+ "#{v1} : #{v2} : #{v3}"
485
+ end
486
+
487
+ subscription = trackable.on(
488
+ value: -> (val) { puts val }, close: -> () { puts 'Bye!' }
489
+ )
490
+ trackable.await(subscription)
491
+
492
+ # '1 : a : A', '2 : a : A', '2 : a : B', '3 : a : B', '3 : b : B',
493
+ # '3 : b : C' and then 'Bye!' will be printed.
494
+ ```
495
+
496
+ All of these values will be emitted on the right intervals. For example
497
+ `'1 : a : A'` will be emitted `7` seconds after the subscription, because `a`
498
+ takes the most time and the first notification of the combined trackable have
499
+ to include data from all of the sources. Then the second - `'2 : a : A'` will be
500
+ emitted the `9th` second from the start, because `2` is emitted on the `9th`
501
+ second, etc. In the beginning the `0` emitted by the first source is silently
502
+ skipped.
503
+ Shortcut for this one is `Reacto.combine`; `Reacto::Trackable.combine_latest`
504
+ is an alias.
505
+
506
+ #### concat
507
+
508
+ Concatenating one `Reacto::Trackable` to another, basically means that the
509
+ resulting `Trackable` will emit the values of the first one, then the values
510
+ of the second one:
511
+
512
+ ```ruby
513
+ source1 = Reacto.enumerable((1..5))
514
+ source2 = Reacto.enumerable((6..10))
515
+
516
+ trackable = source1.concat(source2)
517
+
518
+ trackable.on(
519
+ value: -> (val) { puts val }, close: -> () { puts 'Bye!' }
520
+ )
521
+
522
+ # The values from 1 to 10 will be printed, then 'Bye!'
523
+ ```
524
+
525
+ Another way to use it would be
526
+ `Reacto::Trackable.concat(trackable1, trackable2, ... , trackableN)` with
527
+ shortcut `Reacto.concat`.
528
+
529
+ #### depend_on
530
+
531
+ One `Reacto::Trackable`'s notifications can depend on another's accumulated
532
+ notifications. The `depend_on` operation, expects a `Trackable` and a block,
533
+ the block is used in the same manner as `inject` uses its block on this passed
534
+ `Trackable`. When the passed `Trackable` closes, the accumulated data by the
535
+ block (the last value) is emitted with every notification of the caller:
536
+
537
+ ```ruby
538
+ dependency = Reacto.enumerable((1..10))
539
+ trackable = Reacto.enumerable([5, 4]).depend_on(dependency, &:+)
540
+
541
+ trackable.on(
542
+ value: -> (val) { puts val }, close: -> () { puts 'Bye!' }
543
+ )
544
+
545
+ # The emitted notifications will printed:
546
+ # Value notification : notification.value: 5, notification.data: 55
547
+ # Value notification : notification.value: 4, notification.data: 55
548
+ # Close notification : prints 'Bye!'
549
+ ```
550
+
551
+ Without passed block, the first emitted value of the dependency is used as data.
552
+ If there is an error from the dependency, it is emitted by the caller. The key
553
+ of the dependency, can be changed from `data` to something else by passing a
554
+ `key:` to the operation.
555
+
556
+ ### Concurency
557
+
558
+ Aside from factory methods like `.interval` or `.later`, `Reacto::Trackable`
559
+ has two other ways of emitting its notification concurrently to the thread
560
+ that created it (or some other thread). Every trackable can do that by using
561
+ the two dedicated operations `execute_on` and `track_on`.
562
+
563
+ #### execute_on
564
+
565
+ This operation returns a `Reacto::Trackable` which operations will be executed
566
+ on the given `executor` plus the operations of its source will be executed on
567
+ that `executor` as well. Basically this means that the whole logic - the
568
+ behavior of the first `Trackable` in the chain of operations and all subsequent
569
+ operations will be executed on the given `executor`.
570
+ By `executor`, we mean the ones provided by the `Reacto::Executors`'s methods or
571
+ a custom implementation complying to `concurrent-ruby`'s
572
+ `Concurrent::ExecutorService`. These executors menage threads for us, some of
573
+ them are thread pools, which allows us to reuse unused threads from the pool,
574
+ others provide always new threads on demand or just a single thread. In the
575
+ following example the `Reacto::Executors.io`
576
+ executor is used (passed as just `:io`) :
577
+
578
+ ```ruby
579
+ require 'net/http'
580
+ require 'uri'
581
+ require 'json'
582
+
583
+ request_url_behavior = -> (url) do
584
+ -> (subscriber) do
585
+ begin
586
+ response = Net::HTTP.get_response(URI.parse(url))
587
+
588
+ if response.code == '200'
589
+ subscriber.on_value(response.body)
590
+ subscriber.on_close
591
+ else
592
+ subscriber.on_error(StandardError.new(response))
593
+ end
594
+ rescue StandardError => e
595
+ subscriber.on_error(e)
596
+ end
597
+ end
598
+ end
599
+
600
+ trackable = Reacto.make(
601
+ &request_url_behavior.call('https://api.github.com/repos/meddle0x53/reacto')
602
+ )
603
+
604
+ trackable = trackable.map { |response| JSON.parse(response) }
605
+
606
+ star_count = trackable.map { |val| val['stargazers_count'] }.map do |val|
607
+ "#{val} star(s) on Reacto's github page!"
608
+ end
609
+
610
+ star_count = star_count.execute_on(:io)
611
+
612
+ star_count_subscription = star_count.on(
613
+ value: -> (val) { puts val }, close: -> () { puts '---------' }
614
+ )
615
+
616
+ star_gazers = trackable.map { |val| val['stargazers_url'] }.flat_map do |url|
617
+ Reacto.make(&request_url_behavior.call(url)).map do |response|
618
+ JSON.parse(response)
619
+ end.flat_map do |array|
620
+ Reacto.enumerable(array).map { |data| data['login'] }
621
+ end
622
+ end
623
+
624
+ star_gazers = star_gazers.execute_on(:io)
625
+
626
+ star_gazers_subscription = star_gazers.on(
627
+ value: -> (val) { puts val }, close: -> () { puts 'Thank you all!' }
628
+ )
629
+
630
+ star_gazers.await(star_gazers_subscription, 60)
631
+ star_count.await(star_count_subscription, 60)
632
+
633
+ ```
634
+
635
+ This example is a bit silly because it makes two requests to the same URL,
636
+ but they are concurrent thanks to `execute_on` and that's the important thing.
637
+ The first trackable reads the number of the stars of this repository and its
638
+ consumer prints them, and the second one requests the _stargazers_ list, using
639
+ `flat_map` and prints the names of the star gazers. The two chains are
640
+ executed concurrently.
641
+ The `IO` executor is a cached thread pool, which means that threads are reused
642
+ if available, otherwise new are created, the thread pool does not have fixed
643
+ size.
644
+
645
+ #### track_on
646
+
647
+ The difference between `execute_on` and `track_on` is that `track_on` executes
648
+ on the given executor only the operations positioned after it in the chain.
649
+
650
+ ```ruby
651
+ trackable = Reacto.enumerable((1..100)).map { |v| v * 10 }.track_on(:tasks)
652
+ trackable = trackable.inject(&:+).last
653
+
654
+ subscription = trackable.on(
655
+ value: -> (val) { puts val }, close: -> () { puts 'DONE.' }
656
+ )
657
+
658
+ trackable.await(subscription, 10)
659
+ ```
660
+
661
+ Only the sum and `last` will happen on the `tasks` executor - a thread pool
662
+ with fixed number of threads. The `map` will execute on the current thread.
663
+
664
+ #### Executors and factory methods
665
+
666
+ Executors can be passed to most of the methods which create `Reacto::Trackable`
667
+ instances. The methods can receive a keyword argument - `executor:` and will
668
+ execute the whole trackable chain on it. The same way if it was passed to
669
+ `execute_on`.
670
+
671
+ ```ruby
672
+ trackable = Reacto.enumerable((1..1000), executor: :new_thread)
673
+ ```
674
+
675
+ All the operations on called on this `Trackable` and its derivative
676
+ `Trackable`s will be executed in the `new_thread` executor. This executor
677
+ creates a new thread always.
678
+
679
+ The available executors are:
680
+
681
+ * `IO` - can be passed as `:io` or `Executors.io` - an unlimited cached thread
682
+ pool.
683
+ * `Tasks` - can be passed as `:tasks`, `:background` and `Executors.tasks` -
684
+ a thread pool with fixed size.
685
+ * `New thread` - can be passed as `:new_thread` ot `Executors.new_thread` -
686
+ always creates a new thread.
687
+ * `Current` - can be passed as `:immediate`, `:current`, `:now`,
688
+ `Executors.current` and `Executors.immediate` - uses the current thread to
689
+ execute operations, does not create a new thread at all.
690
+
691
+ ### Buffering, delaying and skipping
692
+
693
+ There are a few special operations related to buffering incoming notifications
694
+ and emit notifications consisting of the buffered ones.
695
+
696
+ #### buffer
697
+
698
+ It is possible to buffer values using a count and then emit them as one array
699
+ of values.
700
+
701
+ ```ruby
702
+ trackable = Reacto.enumerable((1..20)).buffer(count: 5)
703
+
704
+ trackable.on(value: -> (val) { p val })
705
+
706
+ # Will print [1, 2, 3, 4, 5], then [6, 7, 8, 9, 10], then [11, 12, 13, 14, 15]
707
+ # and in the end [16, 17, 18 , 19, 20]
708
+ ```
709
+
710
+ Buffering helps lowering the number of value notification, when the source is
711
+ emitting too many of them, too fast.
712
+
713
+ #### delay
714
+
715
+ Notifications can be buffered using a delay too, for example : don't emit
716
+ anything from the source for 5 seconds, then emit everything received until
717
+ now and repeat.
718
+
719
+ ```ruby
720
+ trackable = Reacto.interval(1).take(20).buffer(delay: 5)
721
+
722
+ subscription = trackable.on(value: -> (val) { p val })
723
+ trackable.await(subscription)
724
+
725
+ # Will print on each 5 seconds something like
726
+ # [0, 1, 2, 3]
727
+ # [4, 5, 6, 7, 8]
728
+ # [9, 10, 11, 12, 13]
729
+ # [14, 15, 16, 17, 18]
730
+ # [19]
731
+ ```
732
+
733
+ Instead of using `buffer(delay: 5)`, we can use the shortcut `delay(5)`.
734
+ We can buffer by both count and delay using the `buffer` operation.
735
+
736
+ #### throttle
737
+
738
+ If too many notifications are received too fast, sometimes it is better to
739
+ skip some of them and emit only the last one. That can be done with `throttleb`.
740
+
741
+ ```ruby
742
+ trackable = Reacto.interval(1).take(30).throttle(5)
743
+
744
+ values = []
745
+ subscription = trackable.on(value: -> (val) { values << val })
746
+ trackable.await(subscription)
747
+
748
+ puts values.size # just 6
749
+ ```
750
+
751
+ ### Grouping
752
+
753
+ Incoming notifications can be grouped by some common property they have.
754
+ The resulting `Reacto::Trackable` emits special `LabeledTrackable` instances
755
+ which are just trackables with additional property - `label` - the name of
756
+ the group.
757
+
758
+ #### group_by_label
759
+
760
+ The most basic operation which groups values into sub-trackables is
761
+ `group_by_label` or just `group_by`:
762
+
763
+ ```ruby
764
+ trackable = Reacto.enumerable((1..10)).group_by_label do |value|
765
+ [(value % 3), value]
766
+ end
767
+
768
+ trackable.on do |labeled_trackable|
769
+ p "Label: #{labeled_trackable.label}"
770
+ p "Values: #{labeled_trackable.to_a.join(',')}"
771
+ end
772
+
773
+ # This produces:
774
+ # Label: 1
775
+ # Values: 1,4,7,10
776
+ # Label: 2
777
+ # Values: 2,5,8
778
+ # Label: 0
779
+ # Values: 3,6,9
780
+ ```
781
+
782
+ This example prints the label of every `Reacto::LabeledTrackable` emitted and
783
+ its values. It uses the `#to_a` method, which blocks and waits for every value
784
+ to be received, then produces an array with all the values in the order they
785
+ were received.
786
+
787
+ #### chunk
788
+
789
+ With `chunk` we can create `LabeledTrackable` instances emitting chunks based
790
+ on the return value of a block called on an emitted value. The difference with
791
+ `group_by` is that there can be multiple trackables with the same key.
792
+
793
+ ```ruby
794
+ source = Reacto.enumerable([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5])
795
+ trackable = source.chunk { |val| val.even? }
796
+
797
+ trackable.on do |labeled_trackable|
798
+ p "Label: #{labeled_trackable.label}"
799
+ p "Values: #{labeled_trackable.to_a.join(',')}"
800
+ end
801
+
802
+ # This produces:
803
+ # Label: false
804
+ # Values: 3,1
805
+ # Label: true
806
+ # Values: 4
807
+ # Label: false
808
+ # Values: 1,5,9
809
+ # Label: true
810
+ # Values: 2,6
811
+ # Label: false
812
+ # Values: 5,3,5
813
+ ```
814
+
815
+ The first chunk consists of `3` and `1` - odd values, then on the first even
816
+ value - `4` we get another chink, then we've got 3 sequential odd values, so
817
+ a `false` chunk of `1`, `5` and `9`, then one with `2` and `6` with label
818
+ `true`, because the values are even. The last chunk is an odd one.
819
+
820
+ #### operating only on given group
821
+
822
+ The `map` operator is able to operate only on a given group by passing it
823
+ a `label:` argument:
824
+
825
+ ```ruby
826
+ source = Reacto.enumerable((1..10)).group_by_label do |value|
827
+ [(value % 3), value]
828
+ end
829
+ trackable = source.map(label: 0) { |value| value / 3 }
830
+
831
+ trackable.on do |labeled_trackable|
832
+ p "Label: #{labeled_trackable.label}"
833
+ p "Values: #{labeled_trackable.to_a.join(',')}"
834
+ end
835
+
836
+ # This produces:
837
+ # Label: 1
838
+ # Values: 1,4,7,10
839
+ # Label: 2
840
+ # Values: 2,5,8
841
+ # Label: 0
842
+ # Values: 1,2,3
843
+ ```
844
+
845
+ As we can see only the values emitted by the `Trackable` with label `0`, the
846
+ ones that can be divided by 3 without remainder are affected by the `map`
847
+ operation. The operations `select`, `inject` and `flat_map` have a `label:`
848
+ argument too and can be applied only to sub-trackables with the passed label.
849
+
850
+ #### flatten_labeled
851
+
852
+ The `Reacto::LabeledTrackable` instances emitted by a `Trackable` after grouping
853
+ can be turned to simple value notifications by using the `flatten_labeled`
854
+ operation. It turns every sub-trackable into an object with two fields
855
+ label and value. The label is the same as the label of the sub-trackable the
856
+ object represents, and the value is accumulated with a block passed to
857
+ `flatten_labeled` from the notifications emitted by the sub-trackable.
858
+ It is the same as using inject:
859
+
860
+ ```ruby
861
+ source = Reacto.enumerable((1..10)).group_by_label do |value|
862
+ [(value % 3), value]
863
+ end
864
+ trackable = source.flatten_labeled { |prev, curr| prev + curr }
865
+
866
+ trackable.on { |object| puts "#{object.label} : #{object.value}"}
867
+
868
+ # This produces:
869
+ # 1 : 22
870
+ # 2 : 15
871
+ # 0 : 18
872
+ ```
873
+
874
+ Prints the original label and the sums of the values emitted by the original
875
+ `Reacto::LabeledTrackable`.
876
+
877
+ ### Error handling
878
+
879
+ Sometimes we want to handle incoming error notification before actually going
880
+ out of the operation chain and in the error consumer code. This can be achieved
881
+ with some special operations, designed to work with errors.
882
+
883
+ #### retrying
884
+
885
+ The `retry` operator will execute the source's behavior when there is an error
886
+ notification instead of emitting it and closing the `Reacto::Trackable`.
887
+
888
+ ```ruby
889
+ source = Reacto.make do |subscriber|
890
+ subscriber.on_value('Test your luck!')
891
+ number = Random.new.rand(1..10)
892
+
893
+ if number <= 5
894
+ subscriber.on_error(
895
+ StandardError.new("Bad luck, last number was : #{number}")
896
+ )
897
+ else
898
+ subscriber.on_value("Lucky number #{number}!")
899
+ subscriber.on_close
900
+ end
901
+ end
902
+
903
+ trackable = source.retry(5)
904
+ trackable.on(
905
+ value: -> (v) { puts v },
906
+ close: -> () { puts 'Done' },
907
+ error: -> (e) { puts e.message }
908
+ )
909
+ ```
910
+
911
+ This piece of code will retry up to 5 times when the number is `5` or smaller.
912
+ On the sixth time if we don't have luck the error will be emitted. The default
913
+ retry count (when a value is not passed) is just `1`. There is a `retry_when`
914
+ operation, which uses a block to determine if the error should be emitted, or
915
+ the source should be retried. For example:
916
+
917
+ ```ruby
918
+ source = Reacto.make do |subscriber|
919
+ subscriber.on_value('Test your luck!')
920
+ number = Random.new.rand(1..10)
921
+
922
+ if number <= 5
923
+ subscriber.on_error(
924
+ StandardError.new("Bad luck, last number was : #{number}")
925
+ )
926
+ else
927
+ subscriber.on_value("Lucky number #{number}!")
928
+ subscriber.on_close
929
+ end
930
+ end
931
+
932
+ trackable = source.retry_when do |error, retries|
933
+ retries < 5 && !error.message.include?('3')
934
+ end
935
+
936
+ trackable.on(
937
+ value: -> (v) { puts v },
938
+ close: -> () { puts 'Done' },
939
+ error: -> (e) { puts e.message }
940
+ )
941
+ ```
942
+
943
+ In this example we use the block to say the that we retry at most 5 times
944
+ again, but this time if the unlucky number was `3` we should not retry.
945
+
946
+ #### how to rescue from errors
947
+
948
+ The simples way to not emit an error but to continue emitting something else
949
+ is by using the `rescue_and_replace_error_with` operation. This one accepts
950
+ a `Reacto::Trackable` instance as its sole argument.
951
+ When an error notification is emitted by its source `Trackable`, it is not
952
+ emitted by the trackable it returns. Instead the notifications of the argument
953
+ are emitted.
954
+
955
+ ```ruby
956
+ source = Reacto.enumerable([1, 2, 3, 0, 7, 8, 9]).map do |val|
957
+ 10 / val
958
+ end
959
+
960
+ trackable = source.rescue_and_replace_error_with(
961
+ Reacto::Trackable.enumerable((4..6)).map { |val| 10 / val }
962
+ )
963
+
964
+ trackable.on(
965
+ value: -> (v) { puts v },
966
+ close: -> () { puts 'Done' },
967
+ error: -> (e) { puts e.message }
968
+ )
969
+ ```
970
+
971
+ We want see the error cause the by division by `0`, instead after the emission
972
+ of `10/1` -> `10`, `10/2` -> `5` and `10/3` -> `3`, the values `2`, `2` and `1`
973
+ will be emitted -> that's `10/4`, `10/5` and `10/6`.
974
+
975
+ Another more precise way to do that is to use the `rescue_and_replace_error`
976
+ operation which receives a block returning a `Reacto::Trackable` -
977
+ the replacement. The block has as an argument the original error, so some logic
978
+ can be written around that.
979
+
980
+ ```ruby
981
+ source = Reacto.enumerable([1, 2, 3, 0, 7, 8, 9]).map do |val|
982
+ 10 / val
983
+ end
984
+
985
+ trackable = source.rescue_and_replace_error do |error|
986
+ if error.is_a?(ArgumentError)
987
+ Reacto.error(error)
988
+ else
989
+ Reacto.value(1)
990
+ end
991
+ end
992
+
993
+ trackable.on(
994
+ value: -> (v) { puts v },
995
+ close: -> () { puts 'Done' },
996
+ error: -> (e) { puts e.message }
997
+ )
998
+ ```
999
+
1000
+ The number `1` will be emitted instead of the `ZeroDivisionError` because it is
1001
+ not an `ArgumentError`.
1002
+
1003
+ ## Dependencies
1004
+
1005
+ Reacto is powered by [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby)
1006
+
1007
+ ## Tested with
89
1008
 
1009
+ * Ruby 2.0.0+
1010
+ * JRuby 9.1.2.0