reacto 0.0.2 → 0.1.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +60 -9
  4. data/doc/create_and_user_trackables.md +1 -0
  5. data/lib/reacto.rb +2 -0
  6. data/lib/reacto/labeled_trackable.rb +13 -0
  7. data/lib/reacto/operations.rb +4 -0
  8. data/lib/reacto/operations/concat.rb +1 -1
  9. data/lib/reacto/operations/depend_on.rb +103 -0
  10. data/lib/reacto/operations/diff.rb +0 -1
  11. data/lib/reacto/operations/flat_map.rb +1 -1
  12. data/lib/reacto/operations/flat_map_latest.rb +3 -4
  13. data/lib/reacto/operations/label.rb +48 -0
  14. data/lib/reacto/operations/last.rb +0 -1
  15. data/lib/reacto/operations/wrap.rb +33 -0
  16. data/lib/reacto/resources.rb +1 -0
  17. data/lib/reacto/resources/executor_resource.rb +1 -0
  18. data/lib/reacto/resources/shared_resource.rb +14 -0
  19. data/lib/reacto/shared_trackable.rb +56 -0
  20. data/lib/reacto/subscriptions.rb +6 -0
  21. data/lib/reacto/subscriptions/flat_map_subscription.rb +6 -1
  22. data/lib/reacto/subscriptions/inner_subscription.rb +5 -0
  23. data/lib/reacto/subscriptions/simple_subscription.rb +2 -1
  24. data/lib/reacto/subscriptions/tracker_subscription.rb +1 -0
  25. data/lib/reacto/trackable.rb +27 -4
  26. data/lib/reacto/version.rb +1 -1
  27. data/spec/reacto/shared_trackable_spec.rb +38 -0
  28. data/spec/reacto/{create_trackable_spec.rb → trackable/class_level/combine_last_spec.rb} +0 -0
  29. data/spec/reacto/trackable/class_level/combine_spec.rb +26 -0
  30. data/spec/reacto/trackable/class_level/interval_spec.rb +13 -2
  31. data/spec/reacto/trackable/depend_on_spec.rb +70 -0
  32. data/spec/reacto/trackable/label_spec.rb +40 -0
  33. data/spec/reacto/trackable/lift_spec.rb +19 -0
  34. data/spec/reacto/trackable/map_spec.rb +38 -0
  35. data/spec/reacto/trackable/on_spec.rb +22 -0
  36. data/spec/reacto/trackable/prepend_spec.rb +15 -0
  37. data/spec/reacto/trackable/select_spec.rb +26 -0
  38. data/spec/reacto/{executors_and_trackable_spec.rb → trackable/track_on_spec.rb} +1 -0
  39. data/spec/reacto/trackable/wrap_spec.rb +46 -0
  40. metadata +33 -8
  41. data/spec/reacto/trackable_spec.rb +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1ce8f7dd2f7b2c680f62228680fe2dc6fcfbe5b5
4
- data.tar.gz: 5518ae7b29f652786d20016dc3018b3d44975d8a
3
+ metadata.gz: 98fb0c0ff41e6efde1e6c11574dd9be835684b9d
4
+ data.tar.gz: b1f541bb712c8add903faa427b739bf8d8846bdd
5
5
  SHA512:
6
- metadata.gz: 16ea2d612d86393abfdc70c72f78369beb436a9ed14791a907e2a5771cc593bb941779bcef49b2906ad523b65e884d32fc7df6971a4ee9e7bee9da3c2c4f32f7
7
- data.tar.gz: 747d7487a69e971d63fd100db3def9e147e0596b2d337714c3673d849705710e498bacb97ec24064ac5eb68d3cb5d2b60172f0202000e6aee207e7a7444926a7
6
+ metadata.gz: e8492c6d0751362a31a6b2610ad5ceb16b168adf13ef4be8b53aac834fece5761aed037e334035ec2ed150eb5e6fc2fdefe8eb09da0e23ae2d1c72037b8f5e13
7
+ data.tar.gz: 4bb888957bb7241ca42c8955d001c5fd32feb3f047b15c01dac0c28c88757dd055139316be71401da0e17318d58829334857980c182d139015284604c2ca4ce0
data/Gemfile.lock CHANGED
@@ -1,13 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- reacto (0.0.1)
4
+ reacto (0.1.0)
5
5
  concurrent-ruby (~> 1.0.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- concurrent-ruby (1.0.1)
10
+ concurrent-ruby (1.0.2)
11
11
  diff-lcs (1.2.5)
12
12
  rspec (3.3.0)
13
13
  rspec-core (~> 3.3.0)
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # reacto
1
+ # Reacto
2
2
 
3
3
  Reactive Programming for Ruby with some concurrency thrown into the mix!
4
4
 
@@ -10,7 +10,7 @@ If you use `bundler` just add this line to your `Gemfile`:
10
10
  gem 'reacto'
11
11
  ```
12
12
 
13
- Alternatively you can instal the gem with `gem install reacto` or clone this
13
+ Alternatively you can install the gem with `gem install reacto` or clone this
14
14
  repository and play with it!
15
15
 
16
16
  ## Why?
@@ -18,21 +18,72 @@ repository and play with it!
18
18
  Because it is very cool to be reactive these days!
19
19
  Seriously - to be reactive means that your code is able to react on changes
20
20
  happening in various places right away. For example third party data sources or
21
- other parts of the code. It helps writing multy-component apps which could
22
- handle failures and still be availale.
21
+ other parts of the code. It helps writing multi-component apps which could
22
+ handle failures and still be available.
23
23
 
24
- Of course there are other implemenations of reactive programming for ruby:
24
+ Of course there are other implementations of reactive programming for ruby:
25
25
 
26
- * [RxRuby](https://github.com/ReactiveX/RxRuby) : Very powerful implemenation
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
29
  is easier to use it. The goal of `Reacto` is to be competitor to `RxRuby`
30
30
  in the ruby world.
31
- Still the author of reacto is big fan of `RX` especially `RxJava`. He even has
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
- easy to use and simply beautiful. The only drawback - it is a bit limitted :
35
+ easy to use and simply beautiful. The only drawback - it is a bit limited :
36
36
  no concurrency and small set of operators.
37
37
  But if you don't need more complicated operators it is the best.
38
- The author of `Reacto` highy recommends it.
38
+ The author of `Reacto` highly recommends it.
39
+
40
+ ## Usage
41
+
42
+ ### Simple Trackables
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.
49
+ Of course the source can be very simple, for example a single value:
50
+
51
+ ```ruby
52
+ trackable = Reacto::Trackable.value(5)
53
+ ```
54
+
55
+ 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
57
+ anything until necessary.
58
+
59
+ ```ruby
60
+ trackable.on(value: ->(v) { puts v })
61
+
62
+ # => 5
63
+ ```
64
+
65
+ 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
67
+ 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.
69
+
70
+ ### Programming Trackable behavior
71
+
72
+ A `Reacto::Trackable` can have custom behavior, defining what and when should
73
+ be sent:
74
+
75
+ ```ruby
76
+ trackable = Reacto::Trackable.make do |tracker|
77
+ tracker.on_value('You say yes')
78
+ tracker.on_value('I say no')
79
+
80
+ sleep 1
81
+ tracker.on_value('You say stop and I say go go go, oh no')
82
+ tracker.on_close
83
+ end
84
+ ```
85
+
86
+ When a tracker is attached this behavior will become active and the tracker
87
+ will receive the first two sentences as values, then, after one second the third
88
+ one and then a closing notification.
89
+
@@ -0,0 +1 @@
1
+ # Create And Use `Reacto::Trackable` objects.
data/lib/reacto.rb CHANGED
@@ -4,6 +4,8 @@ require 'reacto/constants'
4
4
  require 'reacto/subscriptions'
5
5
  require 'reacto/tracker'
6
6
  require 'reacto/trackable'
7
+ require 'reacto/shared_trackable'
8
+ require 'reacto/labeled_trackable'
7
9
 
8
10
  module Reacto
9
11
 
@@ -0,0 +1,13 @@
1
+ require 'reacto/trackable'
2
+
3
+ module Reacto
4
+ class LabeledTrackable < Trackable
5
+ attr_reader :label
6
+
7
+ def initialize(label, executor = nil, behaviour = NO_ACTION, &block)
8
+ super(behaviour, executor, &block)
9
+
10
+ @label = label
11
+ end
12
+ end
13
+ end
@@ -1,4 +1,5 @@
1
1
  require 'reacto/operations/map'
2
+ require 'reacto/operations/wrap'
2
3
  require 'reacto/operations/select'
3
4
  require 'reacto/operations/inject'
4
5
  require 'reacto/operations/diff'
@@ -17,3 +18,6 @@ require 'reacto/operations/throttle'
17
18
  require 'reacto/operations/flat_map'
18
19
  require 'reacto/operations/flat_map_latest'
19
20
  require 'reacto/operations/cache'
21
+ require 'reacto/operations/depend_on'
22
+ require 'reacto/operations/label'
23
+ require 'reacto/operations/act'
@@ -12,7 +12,7 @@ module Reacto
12
12
  Subscriptions::OperationSubscription.new(
13
13
  tracker,
14
14
  close: -> () { @trackable.send(:do_track, tracker) },
15
- error: -> (e) {
15
+ error: -> (e) {
16
16
  tracker.on_error(e)
17
17
  @trackable.send(:do_track, tracker)
18
18
  }
@@ -0,0 +1,103 @@
1
+ require 'reacto/constants'
2
+ require 'reacto/subscriptions/operation_subscription'
3
+
4
+ module Reacto
5
+ module Operations
6
+ class DependOn
7
+ def initialize(trackable, key: :data, accumulator: nil)
8
+ @key = key
9
+ @trackable =
10
+ if accumulator.nil?
11
+ trackable.first
12
+ else
13
+ trackable.inject(NO_VALUE, accumulator)
14
+ end
15
+
16
+ @lock = Mutex.new
17
+
18
+ @close = false
19
+ @error = nil
20
+ end
21
+
22
+ def buffer
23
+ @buffer ||= []
24
+ end
25
+
26
+ def flush(error = nil)
27
+ unless error.nil?
28
+ @tracker.on_error(error)
29
+ return
30
+ end
31
+
32
+ buffer.each do |val|
33
+ @tracker.on_value(
34
+ OpenStruct.new({ value: val }.merge(@key => @result))
35
+ )
36
+ end
37
+
38
+ @tracker.on_close if @close
39
+ @tracker.on_error(@error) if @error
40
+ end
41
+
42
+ def check_ready_and_track(tracker)
43
+ depend_value = ->(v) { @result = v }
44
+ depend_error = ->(e) { flush(e) }
45
+ depend_close = -> { flush }
46
+
47
+ if @subscription.nil?
48
+ @tracker = tracker
49
+ @subscription = @trackable.on(
50
+ value: depend_value, error: depend_error, close: depend_close
51
+ )
52
+ end
53
+ end
54
+
55
+ def call(tracker)
56
+ value = ->(v) do
57
+ if @result.nil?
58
+ @lock.synchronize do
59
+ buffer << v
60
+
61
+ check_ready_and_track(tracker)
62
+ end
63
+ else
64
+ tracker.on_value(
65
+ OpenStruct.new({ value: v }.merge(@key => @result))
66
+ )
67
+ end
68
+ end
69
+
70
+ error = ->(e) do
71
+ if @result.nil?
72
+ @lock.synchronize do
73
+ @error = e
74
+
75
+ check_ready_and_track(tracker)
76
+ end
77
+ else
78
+ tracker.on_error(e)
79
+ end
80
+ end
81
+
82
+ close = -> do
83
+ if @result.nil?
84
+ @lock.synchronize do
85
+ @close = true
86
+
87
+ check_ready_and_track(tracker)
88
+ end
89
+ else
90
+ tracker.on_close
91
+ end
92
+ end
93
+
94
+ Subscriptions::OperationSubscription.new(
95
+ tracker,
96
+ value: value,
97
+ error: error,
98
+ close: close
99
+ )
100
+ end
101
+ end
102
+ end
103
+ end
@@ -33,4 +33,3 @@ module Reacto
33
33
  end
34
34
  end
35
35
  end
36
-
@@ -17,6 +17,7 @@ module Reacto
17
17
  end
18
18
 
19
19
  close = lambda do
20
+ subscription.source_closed = true
20
21
  return unless subscription.closed?
21
22
 
22
23
  tracker.on_close
@@ -29,4 +30,3 @@ module Reacto
29
30
  end
30
31
  end
31
32
  end
32
-
@@ -13,14 +13,15 @@ module Reacto
13
13
  value = lambda do |v|
14
14
  trackable = @transform.call(v)
15
15
 
16
- sub = subscription.subscription!
17
-
18
16
  @last_active.unsubscribe if @last_active
17
+
18
+ sub = subscription.subscription!
19
19
  trackable.do_track sub
20
20
  @last_active = sub
21
21
  end
22
22
 
23
23
  close = lambda do
24
+ subscription.source_closed = true
24
25
  return unless subscription.closed?
25
26
 
26
27
  tracker.on_close
@@ -33,5 +34,3 @@ module Reacto
33
34
  end
34
35
  end
35
36
  end
36
-
37
-
@@ -0,0 +1,48 @@
1
+ require 'reacto/subscriptions/operation_subscription'
2
+
3
+ module Reacto
4
+ module Operations
5
+ class Label
6
+ def initialize(chose_label, executor = nil)
7
+ @chose_label = chose_label
8
+ @executor = executor
9
+
10
+ @labeled_values = {}
11
+ end
12
+
13
+ def call(tracker)
14
+ value = lambda do |v|
15
+ label, val = @chose_label.call(v)
16
+
17
+ @labeled_values[label] ||= []
18
+ @labeled_values[label] << val
19
+ end
20
+
21
+ close = lambda do
22
+ emit_values(tracker)
23
+ tracker.on_close
24
+ end
25
+
26
+ error = lambda do |err|
27
+ emit_values(tracker)
28
+ tracker.on_error(err)
29
+ end
30
+
31
+ Subscriptions::OperationSubscription.new(
32
+ tracker, value: value, error: error, close: close
33
+ )
34
+ end
35
+
36
+ def emit_values(tracker)
37
+ @labeled_values.each do |label, values|
38
+ trackable =
39
+ LabeledTrackable.new(label, @executor) do |subscriber|
40
+ values.each { |val| subscriber.on_value(val) }
41
+ subscriber.on_close
42
+ end
43
+ tracker.on_value(trackable)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -28,4 +28,3 @@ module Reacto
28
28
  end
29
29
  end
30
30
  end
31
-
@@ -0,0 +1,33 @@
1
+ require 'ostruct'
2
+
3
+ require 'reacto/subscriptions/operation_subscription'
4
+
5
+ module Reacto
6
+ module Operations
7
+ class Wrap
8
+ def initialize(args)
9
+ if args.key?(:value)
10
+ fail ArgumentError, "'value' is not valid key in the wrapping object"
11
+ end
12
+
13
+ if args.key?(:error)
14
+ fail ArgumentError, "'error' is not valid key in the wrapping object"
15
+ end
16
+
17
+ @args = args
18
+ end
19
+
20
+ def call(tracker)
21
+ value = ->(v) do
22
+ data = @args.each_with_object({}) do |(key, val), obj|
23
+ obj[key] = (val.respond_to? :call) ? val.call(v) : val
24
+ end
25
+
26
+ tracker.on_value OpenStruct.new({ value: v }.merge(data))
27
+ end
28
+
29
+ Subscriptions::OperationSubscription.new(tracker, value: value)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1 +1,2 @@
1
1
  require 'reacto/resources/executor_resource'
2
+ require 'reacto/resources/shared_resource'
@@ -5,6 +5,7 @@ module Reacto
5
5
  @executor = executor
6
6
  @threads = threads
7
7
  end
8
+
8
9
  def cleanup
9
10
  @executor.shutdown unless @executor.nil?
10
11
  @executor = nil
@@ -0,0 +1,14 @@
1
+ module Reacto
2
+ module Resources
3
+ class SharedResource
4
+ def initialize(trackable)
5
+ @trackable = trackable
6
+ end
7
+
8
+ def cleanup
9
+ @trackable.off
10
+ @trackable = nil
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,56 @@
1
+ require 'reacto/trackable'
2
+
3
+ module Reacto
4
+ class SharedTrackable < Trackable
5
+ def initialize(
6
+ behaviour = NO_ACTION, executor = nil, activate_on_subscribe = false,
7
+ &block
8
+ )
9
+ super(behaviour, executor, &block)
10
+
11
+ @activate_on_subscribe = activate_on_subscribe
12
+ @active = false
13
+ end
14
+
15
+ def off(notification_tracker = nil)
16
+ shared_subscription.subscriptions.reject! do |subscription|
17
+ !subscription.subscribed?
18
+ end
19
+
20
+ return if shared_subscription.subscriptions.count > 0
21
+
22
+ shared_subscription.unsubscribe
23
+ @shared_subscription = nil
24
+ @active = false
25
+ end
26
+
27
+ def track(notification_tracker)
28
+ subscription =
29
+ Subscriptions::TrackerSubscription.new(notification_tracker, self)
30
+
31
+ shared_subscription.add(subscription)
32
+ activate! if @activate_on_subscribe
33
+
34
+ Subscriptions::SubscriptionWrapper.new(subscription)
35
+ end
36
+
37
+ def activate!
38
+ return if @shared_subscription.nil?
39
+ return if @active
40
+
41
+ @active = true
42
+ do_track(shared_subscription)
43
+
44
+ self
45
+ end
46
+
47
+ def activate_on_subscribe
48
+ end
49
+
50
+ private
51
+
52
+ def shared_subscription
53
+ @shared_subscription ||= Subscriptions.shared_subscription(self)
54
+ end
55
+ end
56
+ end
@@ -9,6 +9,8 @@ require 'reacto/subscriptions/combining_last_subscription'
9
9
  require 'reacto/subscriptions/zipping_subscription'
10
10
  require 'reacto/subscriptions/flat_map_subscription'
11
11
 
12
+ require 'reacto/resources/shared_resource'
13
+
12
14
  module Reacto
13
15
  module Subscriptions
14
16
  class << self
@@ -22,6 +24,10 @@ module Reacto
22
24
  error: -> (_e) { block.call }
23
25
  )
24
26
  end
27
+
28
+ def shared_subscription(trackable)
29
+ SimpleSubscription.new
30
+ end
25
31
  end
26
32
  end
27
33
  end
@@ -4,8 +4,12 @@ require 'reacto/subscriptions/composite_subscription'
4
4
  module Reacto
5
5
  module Subscriptions
6
6
  class FlatMapSubscription < CompositeSubscription
7
+ attr_accessor :source_closed
8
+
7
9
  def initialize(subscriber)
8
10
  super(nil, subscriber)
11
+
12
+ @source_closed = false
9
13
  end
10
14
 
11
15
  def waiting?
@@ -17,8 +21,9 @@ module Reacto
17
21
  end
18
22
 
19
23
  def on_close
24
+ return unless source_closed
20
25
  return unless subscribed?
21
- return unless @subscriptions.any? { |s| !s.closed? }
26
+ return unless @subscriptions.all? { |s| s.closed? }
22
27
 
23
28
  @subscriber.on_close
24
29
  unsubscribe
@@ -35,6 +35,11 @@ module Reacto
35
35
  super(open: open, value: value, error: error, close: close)
36
36
  end
37
37
 
38
+ def unsubscribe
39
+ @closed = true
40
+ super
41
+ end
42
+
38
43
  def active?
39
44
  @active
40
45
  end
@@ -6,6 +6,8 @@ module Reacto
6
6
  class SimpleSubscription
7
7
  include Subscription
8
8
 
9
+ attr_reader :subscriptions
10
+
9
11
  def initialize(
10
12
  open: NO_ACTION,
11
13
  value: NO_ACTION,
@@ -77,4 +79,3 @@ module Reacto
77
79
  end
78
80
  end
79
81
  end
80
-
@@ -20,6 +20,7 @@ module Reacto
20
20
 
21
21
  def unsubscribe
22
22
  @subscriptions.each(&:unsubscribe)
23
+ return unless subscribed?
23
24
 
24
25
  @trackable.off(@notification_tracker)
25
26
 
@@ -1,4 +1,5 @@
1
1
  require 'concurrent'
2
+ require 'ostruct'
2
3
 
3
4
  require 'reacto/constants'
4
5
  require 'reacto/behaviours'
@@ -8,6 +9,7 @@ require 'reacto/operations'
8
9
  require 'reacto/executors'
9
10
  require 'reacto/resources'
10
11
 
12
+ # TODO: Refactor the constructors and the factory methods
11
13
  module Reacto
12
14
  class Trackable
13
15
  TOPICS = [:open, :value, :error, :close]
@@ -89,12 +91,13 @@ module Reacto
89
91
  end
90
92
  else
91
93
  make do |tracker|
94
+ Thread::abort_on_exception = true
95
+
92
96
  queue = Queue.new
93
97
  task = Concurrent::TimerTask.new(execution_interval: interval) do
94
98
  queue.push('ready')
95
99
  end
96
100
 
97
- Thread::abort_on_exception = true
98
101
  thread = Thread.new do
99
102
  begin
100
103
  loop do
@@ -179,7 +182,7 @@ module Reacto
179
182
  track(Tracker.new(trackers))
180
183
  end
181
184
 
182
- def off(notification_tracker)
185
+ def off(notification_tracker = nil)
183
186
  # Clean-up logic
184
187
  end
185
188
 
@@ -194,7 +197,7 @@ module Reacto
194
197
 
195
198
  def lift(operation = nil, &block)
196
199
  operation = block_given? ? block : operation
197
- Trackable.new(nil, @executor) do |tracker_subscription|
200
+ self.class.new(nil, @executor) do |tracker_subscription|
198
201
  begin
199
202
  modified = operation.call(tracker_subscription)
200
203
  lift_behaviour(modified) unless modified == NOTHING
@@ -218,6 +221,10 @@ module Reacto
218
221
  ))
219
222
  end
220
223
 
224
+ def wrap(**args)
225
+ lift(Operations::Wrap.new(args))
226
+ end
227
+
221
228
  def select(filter = nil, &block)
222
229
  lift(Operations::Select.new(block_given? ? block : filter))
223
230
  end
@@ -291,12 +298,28 @@ module Reacto
291
298
  lift(Operations::Cache.new(type: type, **settings))
292
299
  end
293
300
 
301
+ def depend_on(trackable, key: :data, accumulator: nil, &block)
302
+ lift(Operations::DependOn.new(
303
+ trackable, key: key, accumulator: (block_given? ? block : accumulator)
304
+ ))
305
+ end
306
+
307
+ def label(labeling_action = nil, executor: nil, &block)
308
+ lift(Operations::Label.new(
309
+ block_given? ? block : labeling_action, executor
310
+ ))
311
+ end
312
+
313
+ def act(action = NO_ACTION, on: Operations::Act::ALL, &block)
314
+ lift(Operations::Act.new(block_given? ? block : action, on))
315
+ end
316
+
294
317
  def track_on(executor)
295
318
  lift(Operations::TrackOn.new(executor))
296
319
  end
297
320
 
298
321
  def execute_on(executor)
299
- Trackable.new(@behaviour, executor)
322
+ self.class.new(@behaviour, executor)
300
323
  end
301
324
 
302
325
  def await(subscription, timeout = nil)
@@ -1,4 +1,4 @@
1
1
  module Reacto
2
- VERSION = '0.0.2'
2
+ VERSION = '0.1.0'
3
3
  end
4
4
 
@@ -0,0 +1,38 @@
1
+ context Reacto::SharedTrackable do
2
+ subject do
3
+ described_class.interval(0.3).take(10)
4
+ end
5
+
6
+ context '#track' do
7
+ it 'does not activate the behaviour' do
8
+ attach_test_trackers(subject)
9
+
10
+ sleep 1
11
+ expect(test_data).to be_empty
12
+ end
13
+ end
14
+
15
+ context '#activate!' do
16
+ it 'activates the Trackable behavior, all the assigned trackers receive ' \
17
+ 'the same notifications' do
18
+ attach_test_trackers(subject)
19
+
20
+ additional_test_data = []
21
+ subject.on(value: ->(v) { additional_test_data << v })
22
+ subject.activate!
23
+
24
+ sleep 1
25
+ expect(test_data).to be == (0..2).to_a
26
+ expect(additional_test_data).to be == (0..2).to_a
27
+
28
+ yet_another_test_data = []
29
+ subject.on(value: ->(v) { yet_another_test_data << v })
30
+
31
+ sleep 1
32
+
33
+ expect(test_data).to be == (0..5).to_a
34
+ expect(additional_test_data).to be == (0..5).to_a
35
+ expect(yet_another_test_data).to be == (3..5).to_a
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ context Reacto::Trackable do
4
+ context '.combine' do
5
+ it 'combines the notifications of Trackables with different number of ' \
6
+ 'notifications using the passed combinator' do
7
+ trackable1 = described_class.interval(0.3).take(4)
8
+ trackable2 = described_class.interval(0.7, ('a'..'b').each)
9
+ trackable3 = described_class.interval(0.5, ('A'..'C').each)
10
+
11
+ trackable = described_class.combine(
12
+ trackable1, trackable2, trackable3
13
+ ) do |v1, v2, v3|
14
+ "#{v1} : #{v2} : #{v3}"
15
+ end
16
+
17
+ subscription = attach_test_trackers(trackable)
18
+ trackable.await(subscription)
19
+
20
+ expect(test_data).to be == [
21
+ '1 : a : A', '2 : a : A', '2 : a : B', '3 : a : B',
22
+ '3 : b : B', '3 : b : C', '|'
23
+ ]
24
+ end
25
+ end
26
+ end
@@ -3,12 +3,12 @@ require 'spec_helper'
3
3
  context Reacto::Trackable do
4
4
  context '.interval' do
5
5
  it 'emits an infinite sequence of number on every n seconds by default' do
6
- trackable = described_class.interval(0.1)
6
+ trackable = described_class.interval(0.3)
7
7
  subscription = attach_test_trackers(trackable)
8
8
  sleep 1
9
9
  subscription.unsubscribe
10
10
 
11
- expect(test_data).to be == (0..8).to_a
11
+ expect(test_data).to be == (0..2).to_a
12
12
  end
13
13
 
14
14
  it 'can use any enumerator to produce the sequence to emit' do
@@ -20,6 +20,17 @@ context Reacto::Trackable do
20
20
  expect(test_data).to be == ('a'..'i').to_a
21
21
  end
22
22
 
23
+ it 'handles interval of intervals' do
24
+ trackable = described_class.interval(0.5, (5..5).each)
25
+ .map { |v| v * 2 }
26
+ .flat_map { |v| Reacto::Trackable.interval(0.1, (v..15).each) }
27
+ subscription = attach_test_trackers(trackable)
28
+
29
+ trackable.await(subscription)
30
+
31
+ expect(test_data).to be == (10..15).to_a + ['|']
32
+ end
33
+
23
34
  it 'can use the immediate executor to block the current thread while ' \
24
35
  'emitting' do
25
36
  trackable = described_class.interval(
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+
3
+ context Reacto::Trackable do
4
+ context '#depend_on' do
5
+ it 'suspends its caller untill the passed Trackable emits its ' \
6
+ 'notifications and wraps the value of the caller with the accumulated ' \
7
+ 'data of these notifications' do
8
+ trackable = described_class.enumerable([1, 5, 4]).depend_on(
9
+ described_class.value(10)
10
+ )
11
+
12
+ attach_test_trackers(trackable)
13
+
14
+ expect(test_data.size).to be 4
15
+ expect(test_data[0].value).to be 1
16
+ expect(test_data[0].data).to be 10
17
+ expect(test_data[1].value).to be 5
18
+ expect(test_data[1].data).to be 10
19
+ expect(test_data[2].value).to be 4
20
+ expect(test_data[2].data).to be 10
21
+ expect(test_data.last).to be == '|'
22
+ end
23
+
24
+ it 'it uses the first emitted value for the data from the dependency ' \
25
+ 'if no accumulator lambda/block is passed.' do
26
+ trackable = described_class.enumerable([1, 5, 4]).depend_on(
27
+ described_class.interval(0.4, [5, 10].each)
28
+ )
29
+
30
+ subscription = attach_test_trackers(trackable)
31
+ trackable.await(subscription)
32
+
33
+ expect(test_data.size).to be 4
34
+ expect(test_data[0].value).to be 1
35
+ expect(test_data[0].data).to be 5
36
+ expect(test_data[1].value).to be 5
37
+ expect(test_data[1].data).to be 5
38
+ expect(test_data[2].value).to be 4
39
+ expect(test_data[2].data).to be 5
40
+ expect(test_data.last).to be == '|'
41
+ end
42
+
43
+ it 'uses the block passed to accumulate the dependency data' do
44
+ trackable = described_class.enumerable([5, 4]).depend_on(
45
+ described_class.enumerable((1..10))
46
+ ) { |prev, v| prev + v }
47
+
48
+ attach_test_trackers(trackable)
49
+
50
+ expect(test_data.size).to be 3
51
+ expect(test_data[0].value).to be 5
52
+ expect(test_data[0].data).to be 55
53
+ expect(test_data[1].value).to be 4
54
+ expect(test_data[1].data).to be 55
55
+ expect(test_data.last).to be == '|'
56
+ end
57
+
58
+ it 'sends the error if the dependency has an error' do
59
+ error = StandardError.new('Error!')
60
+ trackable = described_class.enumerable([5, 4]).depend_on(
61
+ described_class.value(55).concat(described_class.error(error))
62
+ )
63
+
64
+ attach_test_trackers(trackable)
65
+
66
+ expect(test_data.size).to be 1
67
+ expect(test_data.last).to be == error
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,40 @@
1
+ context Reacto::Trackable do
2
+ context '#label' do
3
+ it 'transforms each of the values emitted by the source into ' \
4
+ 'LabeledTrackable instances, using the passed function to get their ' \
5
+ 'labels and values to emit' do
6
+ trackable =
7
+ Reacto::Trackable.enumerable((1..10).each).label do |value|
8
+ [(value % 3), value]
9
+ end
10
+
11
+ trackable.on(value: test_on_value)
12
+
13
+ expect(test_data.count).to eq(3)
14
+
15
+ one = test_data.first
16
+ expect(one.label).to eq(1)
17
+ one_array = []
18
+ one.on(value: ->(v) { one_array << v })
19
+ expect(one_array).to eq([1, 4, 7, 10])
20
+
21
+ two = test_data[1]
22
+ expect(two.label).to eq(2)
23
+ two_array = []
24
+ two.on(value: ->(v) { two_array << v })
25
+ expect(two_array).to eq([2, 5, 8])
26
+
27
+ two = test_data[1]
28
+ expect(two.label).to eq(2)
29
+ two_array = []
30
+ two.on(value: ->(v) { two_array << v })
31
+ expect(two_array).to eq([2, 5, 8])
32
+
33
+ zero = test_data.last
34
+ expect(zero.label).to eq(0)
35
+ zero_array = []
36
+ zero.on(value: ->(v) { zero_array << v })
37
+ expect(zero_array).to eq([3, 6, 9])
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ context Reacto::Trackable do
4
+ context '#lift' do
5
+ it 'applies a transformation to the trackable behaviour' do
6
+ lifted_trackable = source.lift do |tracker_subscription|
7
+ Reacto::Subscriptions::OperationSubscription.new(
8
+ tracker_subscription,
9
+ value: -> (v) { tracker_subscription.on_value(v * v) }
10
+ )
11
+ end
12
+
13
+ lifted_trackable.on(value: test_on_value)
14
+
15
+ expect(test_data.size).to be(1)
16
+ expect(test_data[0]).to be(25)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ context Reacto::Trackable do
2
+ context '#map' do
3
+ it 'transforms the value of the source Trackable using the passed ' \
4
+ 'transformation' do
5
+ trackable = source.map do |v|
6
+ v * v * v
7
+ end
8
+
9
+ trackable.on(value: test_on_value)
10
+
11
+ expect(test_data.size).to be(1)
12
+ expect(test_data[0]).to be(125)
13
+ end
14
+
15
+ it 'transforms errors, if error transformation is passed' do
16
+ source = described_class.make do |subscriber|
17
+ subscriber.on_value(4)
18
+ subscriber.on_error(StandardError.new('error'))
19
+ end
20
+ trackable = source.map(-> (v) { v }, error: -> (e) { 5 })
21
+
22
+ trackable.on(value: test_on_value, error: test_on_error)
23
+
24
+ expect(test_data.size).to be(2)
25
+ expect(test_data).to be == [4, 5]
26
+ end
27
+
28
+ it 'emits what is produced by the passed `close` function before close' do
29
+ trackable =
30
+ described_class.enumerable((1..5)).map(close: ->() { 10 }) do |v|
31
+ v
32
+ end
33
+
34
+ trackable.on(value: test_on_value, close: test_on_close)
35
+ expect(test_data).to be == (1..5).to_a + [10, '|']
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ context Reacto::Trackable do
4
+ context '#on' do
5
+ it 'returns a Reacto::Subscription' do
6
+ actual = described_class.new(Reacto::NO_ACTION).on
7
+
8
+ expect(actual).to_not be(nil)
9
+ expect(actual).to be_kind_of(Reacto::Subscriptions::Subscription)
10
+ end
11
+
12
+ context('value') do
13
+ it 'the trackable behavior uses a subscription which `on_value` ' \
14
+ 'is the passed value action' do
15
+ described_class.new(test_behaviour).on(value: test_on_value)
16
+
17
+ expect(test_data.size).to be(1)
18
+ expect(test_data[0]).to be(5)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ context Reacto::Trackable do
4
+ context '#prepend' do
5
+ it 'emits the passed enumerable before the values, emited by the caller' do
6
+ source = described_class.enumerable((1..5))
7
+ trackable = source.prepend((-5..0))
8
+
9
+ trackable.on(value: test_on_value)
10
+
11
+ expect(test_data.size).to be(11)
12
+ expect(test_data).to be == (-5..5).to_a
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ context Reacto::Trackable do
4
+ context '#select' do
5
+ it 'doesn\'t notify with values not passing the filter block' do
6
+ trackable = source.select do |v|
7
+ v % 2 == 0
8
+ end
9
+
10
+ trackable.on(value: test_on_value)
11
+
12
+ expect(test_data.size).to be(0)
13
+ end
14
+
15
+ it 'notifies with values passing the filter block' do
16
+ trackable = source.select do |v|
17
+ v % 2 == 1
18
+ end
19
+
20
+ trackable.on(value: test_on_value)
21
+
22
+ expect(test_data.size).to be(1)
23
+ expect(test_data[0]).to be(5)
24
+ end
25
+ end
26
+ end
@@ -25,3 +25,4 @@ context Reacto::Trackable do
25
25
  end
26
26
  end
27
27
  end
28
+
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ context Reacto::Trackable do
4
+ context '#wrap' do
5
+ it 'wraps the incomming value in object with field `value` - the value' \
6
+ ' and the specified fields' do
7
+ now = Time.now
8
+ trackable = described_class.enumerable([1, 2]).wrap(now: now)
9
+
10
+ trackable.on(value: test_on_value)
11
+
12
+ expect(test_data.size).to be 2
13
+ expect(test_data.first.value).to be 1
14
+ expect(test_data.first.now).to be now
15
+ expect(test_data.last.value).to be 2
16
+ expect(test_data.last.now).to be now
17
+ end
18
+
19
+ it 'wraps the incomming value in object with field `value` - the value' \
20
+ ' and the specified fields if some of the field values are lambdas, ' \
21
+ 'they are called with passed parameter - the incoming value' do
22
+ now = Time.now
23
+ trackable = described_class.enumerable([1, 2]).wrap(
24
+ now: now, bau: ->(v) { v * 2 }
25
+ )
26
+
27
+ trackable.on(value: test_on_value)
28
+
29
+ expect(test_data.size).to be 2
30
+ expect(test_data.first.value).to be 1
31
+ expect(test_data.first.now).to be now
32
+ expect(test_data.first.bau).to be 2
33
+ expect(test_data.last.value).to be 2
34
+ expect(test_data.last.now).to be now
35
+ expect(test_data.last.bau).to be 4
36
+ end
37
+
38
+ it 'raises an ArgumentError if the hash passed has a `value` key' do
39
+ expect { described_class.enumerable([1, 2]).wrap(value: 4) }.to(
40
+ raise_error(ArgumentError).with_message(
41
+ "'value' is not valid key in the wrapping object"
42
+ )
43
+ )
44
+ end
45
+ end
46
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reacto
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nickolay Tzvetinov - Meddle
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-20 00:00:00.000000000 Z
11
+ date: 2016-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -64,16 +64,19 @@ files:
64
64
  - Gemfile
65
65
  - Gemfile.lock
66
66
  - README.md
67
+ - doc/create_and_user_trackables.md
67
68
  - lib/reacto.rb
68
69
  - lib/reacto/behaviours.rb
69
70
  - lib/reacto/cache/file.rb
70
71
  - lib/reacto/cache/memory.rb
71
72
  - lib/reacto/constants.rb
72
73
  - lib/reacto/executors.rb
74
+ - lib/reacto/labeled_trackable.rb
73
75
  - lib/reacto/operations.rb
74
76
  - lib/reacto/operations/buffer.rb
75
77
  - lib/reacto/operations/cache.rb
76
78
  - lib/reacto/operations/concat.rb
79
+ - lib/reacto/operations/depend_on.rb
77
80
  - lib/reacto/operations/diff.rb
78
81
  - lib/reacto/operations/drop.rb
79
82
  - lib/reacto/operations/drop_errors.rb
@@ -81,6 +84,7 @@ files:
81
84
  - lib/reacto/operations/flat_map_latest.rb
82
85
  - lib/reacto/operations/flatten.rb
83
86
  - lib/reacto/operations/inject.rb
87
+ - lib/reacto/operations/label.rb
84
88
  - lib/reacto/operations/last.rb
85
89
  - lib/reacto/operations/map.rb
86
90
  - lib/reacto/operations/merge.rb
@@ -90,8 +94,11 @@ files:
90
94
  - lib/reacto/operations/throttle.rb
91
95
  - lib/reacto/operations/track_on.rb
92
96
  - lib/reacto/operations/uniq.rb
97
+ - lib/reacto/operations/wrap.rb
93
98
  - lib/reacto/resources.rb
94
99
  - lib/reacto/resources/executor_resource.rb
100
+ - lib/reacto/resources/shared_resource.rb
101
+ - lib/reacto/shared_trackable.rb
95
102
  - lib/reacto/subscriptions.rb
96
103
  - lib/reacto/subscriptions/buffered_subscription.rb
97
104
  - lib/reacto/subscriptions/combining_last_subscription.rb
@@ -110,11 +117,12 @@ files:
110
117
  - lib/reacto/tracker.rb
111
118
  - lib/reacto/version.rb
112
119
  - reacto.gemspec
113
- - spec/reacto/create_trackable_spec.rb
114
- - spec/reacto/executors_and_trackable_spec.rb
115
120
  - spec/reacto/operations/track_on_spec.rb
121
+ - spec/reacto/shared_trackable_spec.rb
116
122
  - spec/reacto/trackable/buffer_spec.rb
117
123
  - spec/reacto/trackable/cache_spec.rb
124
+ - spec/reacto/trackable/class_level/combine_last_spec.rb
125
+ - spec/reacto/trackable/class_level/combine_spec.rb
118
126
  - spec/reacto/trackable/class_level/enumerable_spec.rb
119
127
  - spec/reacto/trackable/class_level/error_spec.rb
120
128
  - spec/reacto/trackable/class_level/interval_spec.rb
@@ -123,18 +131,26 @@ files:
123
131
  - spec/reacto/trackable/class_level/never_spec.rb
124
132
  - spec/reacto/trackable/class_level/value_spec.rb
125
133
  - spec/reacto/trackable/concat_spec.rb
134
+ - spec/reacto/trackable/depend_on_spec.rb
126
135
  - spec/reacto/trackable/diff_spec.rb
127
136
  - spec/reacto/trackable/drop_errors_spec.rb
128
137
  - spec/reacto/trackable/flat_map_latest_spec.rb
129
138
  - spec/reacto/trackable/flat_map_spec.rb
130
139
  - spec/reacto/trackable/flatten_spec.rb
131
140
  - spec/reacto/trackable/inject_spec.rb
141
+ - spec/reacto/trackable/label_spec.rb
142
+ - spec/reacto/trackable/lift_spec.rb
143
+ - spec/reacto/trackable/map_spec.rb
132
144
  - spec/reacto/trackable/merge_spec.rb
145
+ - spec/reacto/trackable/on_spec.rb
133
146
  - spec/reacto/trackable/positional_filtering_spec.rb
147
+ - spec/reacto/trackable/prepend_spec.rb
148
+ - spec/reacto/trackable/select_spec.rb
134
149
  - spec/reacto/trackable/throttle_spec.rb
150
+ - spec/reacto/trackable/track_on_spec.rb
135
151
  - spec/reacto/trackable/uniq_spec.rb
152
+ - spec/reacto/trackable/wrap_spec.rb
136
153
  - spec/reacto/trackable/zip_spec.rb
137
- - spec/reacto/trackable_spec.rb
138
154
  - spec/spec_helper.rb
139
155
  - spec/support/helpers.rb
140
156
  homepage: https://github.com/meddle0x53/reacto
@@ -162,11 +178,12 @@ signing_key:
162
178
  specification_version: 4
163
179
  summary: Concurrent Reactive Programming for Ruby
164
180
  test_files:
165
- - spec/reacto/create_trackable_spec.rb
166
- - spec/reacto/executors_and_trackable_spec.rb
167
181
  - spec/reacto/operations/track_on_spec.rb
182
+ - spec/reacto/shared_trackable_spec.rb
168
183
  - spec/reacto/trackable/buffer_spec.rb
169
184
  - spec/reacto/trackable/cache_spec.rb
185
+ - spec/reacto/trackable/class_level/combine_last_spec.rb
186
+ - spec/reacto/trackable/class_level/combine_spec.rb
170
187
  - spec/reacto/trackable/class_level/enumerable_spec.rb
171
188
  - spec/reacto/trackable/class_level/error_spec.rb
172
189
  - spec/reacto/trackable/class_level/interval_spec.rb
@@ -175,17 +192,25 @@ test_files:
175
192
  - spec/reacto/trackable/class_level/never_spec.rb
176
193
  - spec/reacto/trackable/class_level/value_spec.rb
177
194
  - spec/reacto/trackable/concat_spec.rb
195
+ - spec/reacto/trackable/depend_on_spec.rb
178
196
  - spec/reacto/trackable/diff_spec.rb
179
197
  - spec/reacto/trackable/drop_errors_spec.rb
180
198
  - spec/reacto/trackable/flat_map_latest_spec.rb
181
199
  - spec/reacto/trackable/flat_map_spec.rb
182
200
  - spec/reacto/trackable/flatten_spec.rb
183
201
  - spec/reacto/trackable/inject_spec.rb
202
+ - spec/reacto/trackable/label_spec.rb
203
+ - spec/reacto/trackable/lift_spec.rb
204
+ - spec/reacto/trackable/map_spec.rb
184
205
  - spec/reacto/trackable/merge_spec.rb
206
+ - spec/reacto/trackable/on_spec.rb
185
207
  - spec/reacto/trackable/positional_filtering_spec.rb
208
+ - spec/reacto/trackable/prepend_spec.rb
209
+ - spec/reacto/trackable/select_spec.rb
186
210
  - spec/reacto/trackable/throttle_spec.rb
211
+ - spec/reacto/trackable/track_on_spec.rb
187
212
  - spec/reacto/trackable/uniq_spec.rb
213
+ - spec/reacto/trackable/wrap_spec.rb
188
214
  - spec/reacto/trackable/zip_spec.rb
189
- - spec/reacto/trackable_spec.rb
190
215
  - spec/spec_helper.rb
191
216
  - spec/support/helpers.rb
@@ -1,16 +0,0 @@
1
- require 'spec_helper'
2
-
3
- context Reacto::Trackable do
4
- context '.new' do
5
- it 'supports behavior invoked on tracking, passed as block' do
6
- trackable = described_class.new do |tracker_subscription|
7
- tracker_subscription.on_value(4)
8
- tracker_subscription.on_close
9
- end
10
-
11
- trackable.on(value: test_on_value)
12
- expect(test_data.size).to be(1)
13
- expect(test_data[0]).to be(4)
14
- end
15
- end
16
- end