reacto 0.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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +32 -0
  5. data/README.md +1 -0
  6. data/lib/reacto/behaviours.rb +50 -0
  7. data/lib/reacto/constants.rb +7 -0
  8. data/lib/reacto/executors.rb +31 -0
  9. data/lib/reacto/operations/buffer.rb +80 -0
  10. data/lib/reacto/operations/concat.rb +23 -0
  11. data/lib/reacto/operations/diff.rb +36 -0
  12. data/lib/reacto/operations/drop.rb +41 -0
  13. data/lib/reacto/operations/drop_errors.rb +15 -0
  14. data/lib/reacto/operations/flat_map.rb +26 -0
  15. data/lib/reacto/operations/flatten.rb +26 -0
  16. data/lib/reacto/operations/inject.rb +43 -0
  17. data/lib/reacto/operations/last.rb +31 -0
  18. data/lib/reacto/operations/map.rb +38 -0
  19. data/lib/reacto/operations/merge.rb +47 -0
  20. data/lib/reacto/operations/prepend.rb +19 -0
  21. data/lib/reacto/operations/select.rb +25 -0
  22. data/lib/reacto/operations/take.rb +38 -0
  23. data/lib/reacto/operations/throttle.rb +60 -0
  24. data/lib/reacto/operations/track_on.rb +18 -0
  25. data/lib/reacto/operations/uniq.rb +22 -0
  26. data/lib/reacto/operations.rb +23 -0
  27. data/lib/reacto/resources/executor_resource.rb +19 -0
  28. data/lib/reacto/resources.rb +1 -0
  29. data/lib/reacto/subscriptions/buffered_subscription.rb +52 -0
  30. data/lib/reacto/subscriptions/combining_last_subscription.rb +22 -0
  31. data/lib/reacto/subscriptions/combining_subscription.rb +8 -0
  32. data/lib/reacto/subscriptions/composite_subscription.rb +82 -0
  33. data/lib/reacto/subscriptions/executor_subscription.rb +63 -0
  34. data/lib/reacto/subscriptions/flat_map_subscription.rb +20 -0
  35. data/lib/reacto/subscriptions/inner_subscription.rb +47 -0
  36. data/lib/reacto/subscriptions/operation_subscription.rb +25 -0
  37. data/lib/reacto/subscriptions/simple_subscription.rb +80 -0
  38. data/lib/reacto/subscriptions/subscription.rb +21 -0
  39. data/lib/reacto/subscriptions/subscription_wrapper.rb +16 -0
  40. data/lib/reacto/subscriptions/tracker_subscription.rb +76 -0
  41. data/lib/reacto/subscriptions/zipping_subscription.rb +43 -0
  42. data/lib/reacto/subscriptions.rb +27 -0
  43. data/lib/reacto/trackable.rb +317 -0
  44. data/lib/reacto/tracker.rb +33 -0
  45. data/lib/reacto/version.rb +4 -0
  46. data/lib/reacto.rb +10 -0
  47. data/reacto.gemspec +27 -0
  48. data/spec/reacto/create_trackable_spec.rb +245 -0
  49. data/spec/reacto/executors_and_trackable_spec.rb +32 -0
  50. data/spec/reacto/operations/track_on_spec.rb +20 -0
  51. data/spec/reacto/trackable/buffer_spec.rb +58 -0
  52. data/spec/reacto/trackable/throttle_spec.rb +15 -0
  53. data/spec/reacto/trackable/zip_spec.rb +25 -0
  54. data/spec/reacto/trackable_spec.rb +417 -0
  55. data/spec/spec_helper.rb +15 -0
  56. data/spec/support/helpers.rb +16 -0
  57. metadata +150 -0
@@ -0,0 +1,43 @@
1
+ require 'reacto/constants'
2
+ require 'reacto/subscriptions/composite_subscription'
3
+ require 'reacto/subscriptions/buffered_subscription'
4
+
5
+ module Reacto
6
+ module Subscriptions
7
+ class ZippingSubscription < CompositeSubscription
8
+ def current_value
9
+ @current_value ||= 0
10
+ end
11
+
12
+ def subscribed?
13
+ @subscriptions.all? { |s| s.subscribed? }
14
+ end
15
+
16
+ def waiting?
17
+ @subscriptions.any? { |sub| sub.buffer[current_value] == NO_VALUE }
18
+ end
19
+
20
+ def on_value_subscriptions(_)
21
+ @subscriber.on_value(
22
+ @combinator.call(
23
+ *@subscriptions.map { |sub| sub.buffer[current_value] }
24
+ )
25
+ )
26
+ @current_value += 1
27
+ end
28
+
29
+ def on_close
30
+ return unless subscribed?
31
+ @subscriber.on_close
32
+ end
33
+
34
+ def subscription!
35
+ subscription = BufferedSubscription.new(self)
36
+ @subscriptions << subscription
37
+
38
+ subscription
39
+ end
40
+ end
41
+ end
42
+ end
43
+
@@ -0,0 +1,27 @@
1
+ require 'reacto/subscriptions/subscription'
2
+ require 'reacto/subscriptions/subscription_wrapper'
3
+ require 'reacto/subscriptions/tracker_subscription'
4
+ require 'reacto/subscriptions/operation_subscription'
5
+ require 'reacto/subscriptions/executor_subscription'
6
+ require 'reacto/subscriptions/simple_subscription'
7
+ require 'reacto/subscriptions/combining_subscription'
8
+ require 'reacto/subscriptions/combining_last_subscription'
9
+ require 'reacto/subscriptions/zipping_subscription'
10
+ require 'reacto/subscriptions/flat_map_subscription'
11
+
12
+ module Reacto
13
+ module Subscriptions
14
+ class << self
15
+ def on_close(&block)
16
+ SimpleSubscription.new(close: block)
17
+ end
18
+
19
+ def on_close_and_error(&block)
20
+ SimpleSubscription.new(
21
+ close: -> () { block.call },
22
+ error: -> (_e) { block.call }
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,317 @@
1
+ require 'concurrent'
2
+
3
+ require 'reacto/behaviours'
4
+ require 'reacto/subscriptions'
5
+ require 'reacto/tracker'
6
+ require 'reacto/operations'
7
+ require 'reacto/executors'
8
+ require 'reacto/resources'
9
+
10
+ module Reacto
11
+ class Trackable
12
+ TOPICS = [:open, :value, :error, :close]
13
+
14
+ class << self
15
+ def never
16
+ self.new
17
+ end
18
+
19
+ def combine(*trackables, &block)
20
+ combine_create(
21
+ Subscriptions::CombiningSubscription, *trackables, &block
22
+ )
23
+ end
24
+
25
+ def combine_last(*trackables, &block)
26
+ combine_create(
27
+ Subscriptions::CombiningLastSubscription, *trackables, &block
28
+ )
29
+ end
30
+
31
+ def zip(*trackables, &block)
32
+ combine_create(Subscriptions::ZippingSubscription, *trackables, &block)
33
+ end
34
+
35
+ def close(executor = nil)
36
+ make(nil, executor) do |subscriber|
37
+ subscriber.on_close
38
+ end
39
+ end
40
+
41
+ def error(err, executor = nil)
42
+ make(nil, executor) do |subscriber|
43
+ subscriber.on_error(err)
44
+ end
45
+ end
46
+
47
+ def make(behaviour = NO_ACTION, executor = nil, &block)
48
+ behaviour = block_given? ? block : behaviour
49
+ self.new(behaviour, executor)
50
+ end
51
+
52
+ def later(secs, value, executor: Reacto::Executors.tasks)
53
+ if executor.is_a?(Concurrent::ImmediateExecutor)
54
+ make do |tracker|
55
+ sleep secs
56
+ Behaviours.single_tracker_value(tracker, value)
57
+ end
58
+ else
59
+ make do |tracker|
60
+ Concurrent::ScheduledTask.execute(secs, executor: executor) do
61
+ Behaviours.single_tracker_value(tracker, value)
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ def interval(
68
+ interval,
69
+ enumerator = Behaviours.integers_enumerator,
70
+ executor: nil
71
+ )
72
+ if executor.is_a?(Concurrent::ImmediateExecutor)
73
+ make do |tracker|
74
+ Behaviours.with_close_and_error(tracker) do |subscriber|
75
+ while subscriber.subscribed?
76
+ sleep interval if subscriber.subscribed?
77
+ if subscriber.subscribed?
78
+ begin
79
+ subscriber.on_value(enumerator.next)
80
+ rescue StopIteration
81
+ break
82
+ end
83
+ else
84
+ break
85
+ end
86
+ end
87
+ end
88
+ end
89
+ else
90
+ make do |tracker|
91
+ queue = Queue.new
92
+ task = Concurrent::TimerTask.new(execution_interval: interval) do
93
+ queue.push('ready')
94
+ end
95
+ thread = Thread.new do
96
+ begin
97
+ loop do
98
+ queue.pop
99
+ break unless tracker.subscribed?
100
+
101
+ begin
102
+ value = enumerator.next
103
+ tracker.on_value(value)
104
+ rescue StopIteration
105
+ tracker.on_close if tracker.subscribed?
106
+ break
107
+ rescue StandardError => error
108
+ tracker.on_error(error) if tracker.subscribed?
109
+ break
110
+ end
111
+ end
112
+ ensure
113
+ task.shutdown
114
+ end
115
+ end
116
+ task.execute
117
+
118
+ tracker.add_resource(Reacto::Resources::ExecutorResource.new(
119
+ task, threads: [thread]
120
+ ))
121
+ end
122
+ end
123
+ end
124
+
125
+ def repeat(array, int: 0.1, executor: nil)
126
+ interval(
127
+ int, Behaviours.array_repeat_enumerator(array), executor: executor
128
+ )
129
+ end
130
+
131
+ def value(value, executor = nil)
132
+ make(Behaviours.single_value(value), executor)
133
+ end
134
+
135
+ def enumerable(enumerable, executor = nil)
136
+ make(nil, executor) do |tracker|
137
+ begin
138
+ enumerable.each do |val|
139
+ break unless tracker.subscribed?
140
+ tracker.on_value(val)
141
+ end
142
+
143
+ tracker.on_close if tracker.subscribed?
144
+ rescue => error
145
+ tracker.on_error(error) if tracker.subscribed?
146
+ end
147
+ end
148
+ end
149
+
150
+ def combine_with(function, *trackables)
151
+ end
152
+
153
+ private
154
+
155
+ def combine_create(type, *trackables, &block)
156
+ make do |subscriber|
157
+ main = type.new(block, subscriber)
158
+ trackables.each do |trackable|
159
+ trackable.do_track main.subscription!
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ def initialize(behaviour = NO_ACTION, executor = nil, &block)
166
+ @behaviour = block_given? ? block : behaviour
167
+ @executor = executor
168
+ end
169
+
170
+ def on(trackers = {})
171
+ unless (trackers.keys - TOPICS).empty?
172
+ raise "This Trackable supports only #{TOPICS}, " \
173
+ "but #{trackers.keys} were passed."
174
+ end
175
+
176
+ track(Tracker.new(trackers))
177
+ end
178
+
179
+ def off(notification_tracker)
180
+ # Clean-up logic
181
+ end
182
+
183
+ def track(notification_tracker)
184
+ subscription =
185
+ Subscriptions::TrackerSubscription.new(notification_tracker, self)
186
+
187
+ do_track(subscription)
188
+
189
+ Subscriptions::SubscriptionWrapper.new(subscription)
190
+ end
191
+
192
+ def lift(operation = nil, &block)
193
+ operation = block_given? ? block : operation
194
+ Trackable.new(nil, @executor) do |tracker_subscription|
195
+ begin
196
+ lift_behaviour(operation.call(tracker_subscription))
197
+ rescue Exception => e
198
+ tracker_subscription.on_error(e)
199
+ end
200
+ end
201
+ end
202
+
203
+ def flat_map(transform = nil, &block)
204
+ lift(Operations::FlatMap.new(block_given? ? block : transform))
205
+ end
206
+
207
+ def map(mapping = nil, error: nil, close: nil, &block)
208
+ lift(Operations::Map.new(
209
+ block_given? ? block : mapping, error: error, close: close
210
+ ))
211
+ end
212
+
213
+ def select(filter = nil, &block)
214
+ lift(Operations::Select.new(block_given? ? block : filter))
215
+ end
216
+
217
+ def inject(initial = NO_VALUE, injector = nil, &block)
218
+ lift(Operations::Inject.new(block_given? ? block : injector, initial))
219
+ end
220
+
221
+ def diff(initial = NO_VALUE, fn = Operations::Diff::DEFAULT_FN, &block)
222
+ lift(Operations::Diff.new(block_given? ? block : fn, initial))
223
+ end
224
+
225
+ def drop(how_many_to_drop)
226
+ lift(Operations::Drop.new(how_many_to_drop))
227
+ end
228
+
229
+ def drop_errors
230
+ lift(Operations::DropErrors.new)
231
+ end
232
+
233
+ def take(how_many_to_take)
234
+ lift(Operations::Take.new(how_many_to_take))
235
+ end
236
+
237
+ def uniq
238
+ lift(Operations::Uniq.new)
239
+ end
240
+
241
+ def flatten
242
+ lift(Operations::Flatten.new)
243
+ end
244
+
245
+ def first
246
+ take(1)
247
+ end
248
+
249
+ def [](x)
250
+ lift(Operations::Drop.new(x, 1))
251
+ end
252
+
253
+ def last
254
+ lift(Operations::Last.new)
255
+ end
256
+
257
+ def prepend(enumerable)
258
+ lift(Operations::Prepend.new(enumerable))
259
+ end
260
+
261
+ def concat(trackable)
262
+ lift(Operations::Concat.new(trackable))
263
+ end
264
+
265
+ def merge(trackable, delay_error: false)
266
+ lift(Operations::Merge.new(trackable, delay_error: delay_error))
267
+ end
268
+
269
+ def buffer(count: nil, delay: nil)
270
+ lift(Operations::Buffer.new(count: count, delay: delay))
271
+ end
272
+
273
+ def delay(delay)
274
+ buffer(delay: delay)
275
+ end
276
+
277
+ def throttle(delay)
278
+ lift(Operations::Throttle.new(delay))
279
+ end
280
+
281
+ def track_on(executor)
282
+ lift(Operations::TrackOn.new(executor))
283
+ end
284
+
285
+ def execute_on(executor)
286
+ Trackable.new(@behaviour, executor)
287
+ end
288
+
289
+ def await(subscription, timeout = nil)
290
+ latch = Concurrent::CountDownLatch.new(1)
291
+ subscription.add(Subscriptions.on_close_and_error { latch.count_down })
292
+ latch.wait(timeout)
293
+ end
294
+
295
+ alias_method :skip, :drop
296
+ alias_method :skip_errors, :drop_errors
297
+
298
+ def do_track(subscription)
299
+ if @executor
300
+ @executor.post(subscription, &@behaviour)
301
+ else
302
+ @behaviour.call(subscription)
303
+ end
304
+ end
305
+
306
+ private
307
+
308
+ def lift_behaviour(lifted_tracker_subscription)
309
+ begin
310
+ lifted_tracker_subscription.on_open
311
+ @behaviour.call(lifted_tracker_subscription)
312
+ rescue Exception => e
313
+ lifted_tracker_subscription.on_error(e)
314
+ end
315
+ end
316
+ end
317
+ end
@@ -0,0 +1,33 @@
1
+ require 'reacto/constants'
2
+
3
+ module Reacto
4
+ class Tracker
5
+ def initialize(
6
+ open: NO_ACTION,
7
+ value: NO_ACTION,
8
+ error: DEFAULT_ON_ERROR,
9
+ close: NO_ACTION
10
+ )
11
+ @open = open
12
+ @value = value
13
+ @error = error
14
+ @close = close
15
+ end
16
+
17
+ def on_open
18
+ @open.call
19
+ end
20
+
21
+ def on_value(v)
22
+ @value.call(v)
23
+ end
24
+
25
+ def on_error(error)
26
+ @error.call(error)
27
+ end
28
+
29
+ def on_close
30
+ @close.call
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,4 @@
1
+ module Reacto
2
+ VERSION = '0.0.0'
3
+ end
4
+
data/lib/reacto.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'reacto/version'
2
+ require 'reacto/constants'
3
+
4
+ require 'reacto/subscriptions'
5
+ require 'reacto/tracker'
6
+ require 'reacto/trackable'
7
+
8
+ module Reacto
9
+
10
+ end
data/reacto.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+
3
+ current_dir = File.dirname(File.realpath(__FILE__))
4
+ $LOAD_PATH.unshift(File.join(current_dir, 'lib'))
5
+
6
+ require 'reacto/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'reacto'
10
+ spec.version = Reacto::VERSION
11
+ spec.authors = ['Nickolay Tzvetinov - Meddle']
12
+ spec.email = ['n.tzvetinov@gmail.com']
13
+ spec.description = 'Concurrent Reactive Programming for Ruby'
14
+ spec.summary = 'Concurrent Reactive Programming for Ruby'
15
+ spec.homepage = 'https://github.com/meddle0x53/reacto'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files`.split($/)
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.3'
23
+ spec.add_development_dependency 'rspec', '~> 3.3.0'
24
+
25
+ spec.add_dependency 'concurrent-ruby', '~> 1.0.0'
26
+ end
27
+
@@ -0,0 +1,245 @@
1
+ require 'spec_helper'
2
+
3
+ context Reacto::Trackable do
4
+
5
+ context '.never' do
6
+ it 'creates a new trackable, which won\'t send any notifications' do
7
+ attach_test_trackers described_class.never
8
+
9
+ expect(test_data).to be_empty
10
+ end
11
+ end
12
+
13
+ context '.error' do
14
+ it 'creates a new trackable emitting only the error passed' do
15
+ err = StandardError.new('Errrr')
16
+ attach_test_trackers described_class.error(err)
17
+
18
+ expect(test_data).to be == [err]
19
+ end
20
+ end
21
+
22
+ context '.make' do
23
+ it 'creates a new trackable with custom behaviour passed as lambda' do
24
+ behaviour = lambda do |tracker|
25
+ (1..10).each do |v|
26
+ tracker.on_value(v)
27
+ end
28
+
29
+ tracker.on_close
30
+ end
31
+
32
+ attach_test_trackers described_class.make(behaviour)
33
+
34
+ expect(test_data.size).to be(11)
35
+ expect(test_data).to be == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, '|']
36
+ end
37
+
38
+ it 'creates a new trackable with custom behaviour passed as block' do
39
+ trackable = described_class.make do |tracker|
40
+ (1..10).each do |v|
41
+ tracker.on_value(v)
42
+ end
43
+
44
+ tracker.on_close
45
+ end
46
+ attach_test_trackers trackable
47
+
48
+ expect(test_data.size).to be(11)
49
+ expect(test_data).to be == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, '|']
50
+ end
51
+
52
+ it 'does not emit anything once it is closed' do
53
+ trackable = described_class.make do |tracker|
54
+ (1..5).each do |v|
55
+ tracker.on_value(v)
56
+ end
57
+
58
+ tracker.on_close
59
+
60
+ (1..5).each do |v|
61
+ tracker.on_value(v)
62
+ end
63
+ end
64
+ attach_test_trackers trackable
65
+
66
+ expect(test_data.size).to be(6)
67
+ expect(test_data).to be == [1, 2, 3, 4, 5, '|']
68
+ end
69
+
70
+ it 'does not emit anything once it has an error' do
71
+ trackable = described_class.make do |tracker|
72
+ (1..5).each do |v|
73
+ tracker.on_value(v)
74
+ end
75
+
76
+ tracker.on_error('error')
77
+
78
+ (1..5).each do |v|
79
+ tracker.on_value(v)
80
+ end
81
+ end
82
+ attach_test_trackers trackable
83
+
84
+ expect(test_data.size).to be(6)
85
+ expect(test_data).to be == [1, 2, 3, 4, 5, 'error']
86
+ end
87
+
88
+ it 'emits the same behaviour for every subscribe' do
89
+ trackable = described_class.make do |tracker|
90
+ (1..5).each do |v|
91
+ tracker.on_value(v)
92
+ end
93
+
94
+ tracker.on_close
95
+ end
96
+ attach_test_trackers trackable
97
+ attach_test_trackers trackable
98
+
99
+ expect(test_data.size).to be(12)
100
+ expect(test_data).to be == [1, 2, 3, 4, 5, '|', 1, 2, 3, 4, 5, '|']
101
+ end
102
+ end
103
+
104
+ context '.later' do
105
+ it 'emits the passed value after the passed time runs out and then emits ' \
106
+ 'a close notification' do
107
+ trackable = described_class.later(0.2, 5)
108
+ subscription = attach_test_trackers(trackable)
109
+
110
+ expect(test_data).to be_empty
111
+ trackable.await(subscription)
112
+ expect(test_data).to be == [5, '|']
113
+ end
114
+
115
+ it 'it can use a specific executor' do
116
+ trackable = described_class.later(
117
+ 0.2, 5, executor: Reacto::Executors.immediate
118
+ )
119
+ subscription = attach_test_trackers(trackable)
120
+ expect(test_data).to be == [5, '|']
121
+ end
122
+ end
123
+
124
+ context '.interval' do
125
+ it 'emits an infinite sequence of number on every n seconds by default' do
126
+ trackable = described_class.interval(0.1)
127
+ subscription = attach_test_trackers(trackable)
128
+ sleep 1
129
+ subscription.unsubscribe
130
+
131
+ expect(test_data).to be == (0..8).to_a
132
+ end
133
+
134
+ it 'can use any enumerator to produce the sequence to emit' do
135
+ trackable = described_class.interval(0.1, ('a'..'z').each)
136
+ subscription = attach_test_trackers(trackable)
137
+ sleep 1
138
+ subscription.unsubscribe
139
+
140
+ expect(test_data).to be == ('a'..'i').to_a
141
+ end
142
+
143
+ it 'can use the immediate executor to block the current thread while ' \
144
+ 'emitting' do
145
+ trackable = described_class.interval(
146
+ 0.1, (1..5).each, executor: Reacto::Executors.immediate
147
+ )
148
+ subscription = attach_test_trackers(trackable)
149
+
150
+ expect(test_data).to be == (1..5).to_a + ['|']
151
+ end
152
+ end
153
+
154
+ context '.value' do
155
+ it 'emits only the passed value and then closes' do
156
+ trackable = described_class.value(5)
157
+ subscription = attach_test_trackers(trackable)
158
+
159
+ expect(test_data).to be == [5, '|']
160
+ end
161
+ end
162
+
163
+ context '.enumerable' do
164
+ it 'emits the whole enumerable one-by-one and then closes' do
165
+ trackable = described_class.enumerable([1, 3, 5, 6, 7, 8])
166
+ subscription = attach_test_trackers(trackable)
167
+
168
+ expect(test_data).to be == [1, 3, 5, 6, 7, 8, '|']
169
+ end
170
+
171
+ it 'on error it emits all til the error and the error' do
172
+ class PositiveArray
173
+ include Enumerable
174
+
175
+ def error
176
+ @error ||= StandardError.new('Bad.')
177
+ end
178
+
179
+ def initialize(*members)
180
+ @members = members
181
+ end
182
+
183
+ def each(&block)
184
+ @members.each do |member|
185
+ raise error if member < 0
186
+ block.call(member)
187
+ end
188
+ end
189
+ end
190
+
191
+ enumerable = PositiveArray.new(2, 3, -4, 3, 6)
192
+ trackable = described_class.enumerable(enumerable)
193
+ subscription = attach_test_trackers(trackable)
194
+
195
+ expect(test_data).to be == [2, 3, enumerable.error]
196
+
197
+ end
198
+ end
199
+
200
+ context '.combine' do
201
+ it 'combines the notifications of Trackables with different number of ' \
202
+ 'notifications using the passed combinator' do
203
+ trackable1 = described_class.interval(0.3).take(4)
204
+ trackable2 = described_class.interval(0.7, ('a'..'b').each)
205
+ trackable3 = described_class.interval(0.5, ('A'..'C').each)
206
+
207
+ trackable = described_class.combine(
208
+ trackable1, trackable2, trackable3
209
+ ) do |v1, v2, v3|
210
+ "#{v1} : #{v2} : #{v3}"
211
+ end
212
+
213
+ subscription = attach_test_trackers(trackable)
214
+ trackable.await(subscription)
215
+
216
+ expect(test_data).to be == [
217
+ '1 : a : A', '2 : a : A', '2 : a : B', '3 : a : B',
218
+ '3 : b : B', '3 : b : C', '|'
219
+ ]
220
+ end
221
+ end
222
+
223
+ context '.combine_last' do
224
+ it 'combines the notifications of Trackables based on their ' \
225
+ 'sequence number - the first notification of the sources, then ' \
226
+ 'the next ones and in the end closes if any of the sources closes' do
227
+ trackable1 = described_class.interval(0.3).take(4)
228
+ trackable2 = described_class.interval(0.7, ('a'..'b').each)
229
+ trackable3 = described_class.interval(0.5, ('A'..'C').each)
230
+
231
+ trackable = described_class.combine_last(
232
+ trackable1, trackable2, trackable3
233
+ ) do |v1, v2, v3|
234
+ "#{v1} : #{v2} : #{v3}"
235
+ end
236
+
237
+ subscription = attach_test_trackers(trackable)
238
+ trackable.await(subscription)
239
+
240
+ expect(test_data).to be == [
241
+ '1 : a : A', '3 : b : B', '|'
242
+ ]
243
+ end
244
+ end
245
+ end