rrx 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +40 -0
- data/Rakefile +7 -0
- data/examples/all.rb +36 -0
- data/lib/Reactive/observable.rb +58 -0
- data/lib/Reactive/observable/base.rb +88 -0
- data/lib/Reactive/observable/composite.rb +34 -0
- data/lib/Reactive/observable/double_wrapper.rb +17 -0
- data/lib/Reactive/observable/empty.rb +7 -0
- data/lib/Reactive/observable/first.rb +26 -0
- data/lib/Reactive/observable/from_proc.rb +17 -0
- data/lib/Reactive/observable/generate.rb +41 -0
- data/lib/Reactive/observable/grep.rb +35 -0
- data/lib/Reactive/observable/map.rb +20 -0
- data/lib/Reactive/observable/merge.rb +26 -0
- data/lib/Reactive/observable/push.rb +47 -0
- data/lib/Reactive/observable/skip.rb +25 -0
- data/lib/Reactive/observable/wrapper.rb +10 -0
- data/lib/Reactive/observer.rb +36 -0
- data/lib/Reactive/scheduler.rb +23 -0
- data/lib/Reactive/version.rb +3 -0
- data/lib/rrx.rb +125 -0
- data/rrx.gemspec +25 -0
- data/spec/observable/count_spec.rb +30 -0
- data/spec/observable/first_spec.rb +47 -0
- data/spec/observable/interval_spec.rb +47 -0
- data/spec/observable/push_spec.rb +82 -0
- data/spec/observable/skip_spec.rb +30 -0
- data/spec/observable/timer_spec.rb +41 -0
- data/spec/spec_helper.rb +177 -0
- metadata +149 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Reactive::Observable
|
|
2
|
+
class Map < Wrapper
|
|
3
|
+
|
|
4
|
+
add_attributes :mapper
|
|
5
|
+
|
|
6
|
+
#TODO need to handle array returned from @map.call ?
|
|
7
|
+
observer_on_next do |value|
|
|
8
|
+
begin
|
|
9
|
+
new_value = @mapper.call(value)
|
|
10
|
+
rescue Exception => e
|
|
11
|
+
on_error(e)
|
|
12
|
+
else
|
|
13
|
+
#new_values.each {|v| @target.on_next(v) }
|
|
14
|
+
@target.on_next(new_value)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Reactive
|
|
2
|
+
module Observable
|
|
3
|
+
class Merge < DoubleWrapper
|
|
4
|
+
|
|
5
|
+
class Observer < ObserverWrapper
|
|
6
|
+
attr_accessor :num_completed
|
|
7
|
+
|
|
8
|
+
def initialize(*args)
|
|
9
|
+
@num_completed = 0
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def on_complete
|
|
14
|
+
if num_completed == 0
|
|
15
|
+
self.num_completed = 1
|
|
16
|
+
else
|
|
17
|
+
self.target.on_complete
|
|
18
|
+
unwrap
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Reactive::Observable
|
|
2
|
+
|
|
3
|
+
class Push < Composite
|
|
4
|
+
add_attributes :o1, :o2
|
|
5
|
+
|
|
6
|
+
#def initialize(o1, o2)
|
|
7
|
+
# @o1, @o2 = o1, o2
|
|
8
|
+
#end
|
|
9
|
+
|
|
10
|
+
def initial_subscriptions
|
|
11
|
+
[@o1]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def observer_args(observer, parent)
|
|
15
|
+
[observer, parent, @o2]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Observer < Reactive::ObserverWrapper
|
|
19
|
+
attr_reader :next_observable
|
|
20
|
+
|
|
21
|
+
def initialize(observer, parent, ob)
|
|
22
|
+
@next_observable = ob
|
|
23
|
+
super(observer, parent)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def on_complete
|
|
27
|
+
if @next_observable
|
|
28
|
+
next_observable = @next_observable
|
|
29
|
+
@next_observable = nil
|
|
30
|
+
disposable = next_observable.subscribe_observer(self)
|
|
31
|
+
wrap_with_parent(disposable) if @target
|
|
32
|
+
else
|
|
33
|
+
@target.on_complete
|
|
34
|
+
unwrap
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def unwrap
|
|
39
|
+
@next_observable = nil
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Reactive::Observable
|
|
2
|
+
|
|
3
|
+
class Skip < Wrapper
|
|
4
|
+
add_attributes :count
|
|
5
|
+
|
|
6
|
+
class Observer < Reactive::ObserverWrapper
|
|
7
|
+
|
|
8
|
+
def initialize(*args)
|
|
9
|
+
@skipped = 0
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def on_next(value)
|
|
14
|
+
if @skipped == @count
|
|
15
|
+
@target.on_next(value)
|
|
16
|
+
else
|
|
17
|
+
@skipped += 1
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Reactive
|
|
2
|
+
class Observer
|
|
3
|
+
|
|
4
|
+
def initialize(handlers)
|
|
5
|
+
@handlers = handlers
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def on_next(value)
|
|
9
|
+
@handlers[:on_next].call(value)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def on_complete
|
|
13
|
+
@handlers[:on_complete].call()
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def on_error(error)
|
|
17
|
+
@handlers[:on_error].call(error)
|
|
18
|
+
unwrap
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def unwrap
|
|
22
|
+
@handlers = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class EmptyObserver
|
|
29
|
+
|
|
30
|
+
def on_next(value); end
|
|
31
|
+
def on_complete; end
|
|
32
|
+
def on_error(error); end
|
|
33
|
+
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Reactive
|
|
2
|
+
class Scheduler
|
|
3
|
+
|
|
4
|
+
def schedule_periodic(at, action, wrapper = Disposable::Wrapper.new)
|
|
5
|
+
return unless wrapper
|
|
6
|
+
callback = lambda do
|
|
7
|
+
new_at = action.()
|
|
8
|
+
if new_at #defined ~
|
|
9
|
+
schedule_periodic(new_at, action, wrapper)
|
|
10
|
+
else
|
|
11
|
+
wrapper.unwrap
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
wrapper.target = schedule_once(at, callback)
|
|
15
|
+
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def schedule_once(at, action)
|
|
19
|
+
EventMachine.add_timer(at / 1000.0, action)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/rrx.rb
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
require "reactive/version"
|
|
2
|
+
require 'weakref'
|
|
3
|
+
require 'pp'
|
|
4
|
+
require 'active_support'
|
|
5
|
+
require 'eventmachine'
|
|
6
|
+
|
|
7
|
+
require 'active_support/core_ext/module/delegation'
|
|
8
|
+
require 'active_support/core_ext/module/introspection'
|
|
9
|
+
require 'active_support/core_ext/class/attribute'
|
|
10
|
+
|
|
11
|
+
module Reactive
|
|
12
|
+
|
|
13
|
+
autoload :Observer, 'reactive/observer'
|
|
14
|
+
autoload :Scheduler, 'reactive/scheduler'
|
|
15
|
+
autoload :Observable, 'reactive/observable'
|
|
16
|
+
|
|
17
|
+
class AmbivalentRef < WeakRef
|
|
18
|
+
|
|
19
|
+
def self.create(value)
|
|
20
|
+
case value
|
|
21
|
+
when NilClass, TrueClass, FalseClass, Fixnum, Symbol
|
|
22
|
+
value
|
|
23
|
+
else
|
|
24
|
+
new(value)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def __getobj__
|
|
29
|
+
super rescue nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module Disposable
|
|
34
|
+
class Wrapper
|
|
35
|
+
attr_reader :target
|
|
36
|
+
|
|
37
|
+
def initialize(target = nil, parent = nil)
|
|
38
|
+
@parent = AmbivalentRef.create(parent)
|
|
39
|
+
self.target = target
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def target=(v)
|
|
43
|
+
@target = AmbivalentRef.create(v)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def unwrap
|
|
47
|
+
self.target = nil #return previous target?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class Closure
|
|
53
|
+
def initialize(&cleanup)
|
|
54
|
+
ObjectSpace.define_finalizer(self, cleanup)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ObserverWrapper
|
|
61
|
+
attr_reader :target, :parent
|
|
62
|
+
|
|
63
|
+
def initialize(target, parent, opts = {})
|
|
64
|
+
attributes.each do |name, default|
|
|
65
|
+
instance_variable_set(:"@#{name}", opts[name])
|
|
66
|
+
end
|
|
67
|
+
@target = target
|
|
68
|
+
@parent = AmbivalentRef.new(parent)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def attributes
|
|
72
|
+
self.class.parent.attributes
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def on_next(value)
|
|
76
|
+
@target.on_next(value)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def on_complete
|
|
80
|
+
@target.on_complete
|
|
81
|
+
unwrap
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def wrap_with_parent(child)
|
|
85
|
+
@parent.target = child
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def unwrap
|
|
89
|
+
attributes.each {|name, default| instance_variable_set(:"@#{name}", nil) }
|
|
90
|
+
@target = EmptyObserver.new
|
|
91
|
+
unwrap_parent if active?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def active?
|
|
95
|
+
@parent
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def unwrap_parent(*args)
|
|
99
|
+
@parent.unwrap(*args)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
#class ReplaySubject
|
|
104
|
+
#
|
|
105
|
+
#
|
|
106
|
+
# def subject
|
|
107
|
+
# @subject ||= Subject.new
|
|
108
|
+
# end
|
|
109
|
+
#
|
|
110
|
+
# def run(observer)
|
|
111
|
+
# wrapper = subject.run(observer)
|
|
112
|
+
# @queue.each {|q| q.accept(observer) }
|
|
113
|
+
# return wrapper
|
|
114
|
+
# end
|
|
115
|
+
#
|
|
116
|
+
# def on_next(value)
|
|
117
|
+
# @queue << value
|
|
118
|
+
# value.accept(subject)
|
|
119
|
+
# end
|
|
120
|
+
#
|
|
121
|
+
#end
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
end
|
|
125
|
+
|
data/rrx.gemspec
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'reactive/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |gem|
|
|
7
|
+
gem.name = "rrx"
|
|
8
|
+
gem.version = Rrx::VERSION
|
|
9
|
+
gem.authors = ["Daniel Davis"]
|
|
10
|
+
gem.email = ["shoefish@gmail.com"]
|
|
11
|
+
gem.description = %q{Reactive Extensions provide an enumerable-like interface to process temporal data(events) with the eash usually reserved for structural Enumerables }
|
|
12
|
+
gem.summary = %q{Ruby Reactive Extensions}
|
|
13
|
+
gem.homepage = "https://github.com/emirikol/rrx"
|
|
14
|
+
|
|
15
|
+
gem.files = `git ls-files`.split($/)
|
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
18
|
+
gem.require_paths = ["lib"]
|
|
19
|
+
|
|
20
|
+
gem.add_development_dependency 'rake'
|
|
21
|
+
gem.add_development_dependency 'rspec'
|
|
22
|
+
|
|
23
|
+
gem.add_runtime_dependency 'eventmachine'
|
|
24
|
+
gem.add_runtime_dependency 'active_support'
|
|
25
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Reactive::Observable::Count do
|
|
4
|
+
|
|
5
|
+
include Lets
|
|
6
|
+
|
|
7
|
+
context '#count from interval(1000)' do
|
|
8
|
+
before do
|
|
9
|
+
@s = Reactive::Observable.interval(1000).count
|
|
10
|
+
@s.subscribe(observer)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'fires with 1 after one interval' do
|
|
14
|
+
observer[:on_next].should_receive(:call).with(1)
|
|
15
|
+
advance_by(1001)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'fires with 2 after two intervals' do
|
|
19
|
+
observer[:on_next].should_receive(:call).with(2)
|
|
20
|
+
advance_by 2001
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'fires with 7 after 7 intervals' do
|
|
24
|
+
observer[:on_next].should_receive(:call).with(7)
|
|
25
|
+
advance_by 7001
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Reactive::Observable::First do
|
|
4
|
+
include Lets
|
|
5
|
+
|
|
6
|
+
context '#first(3) from interval' do
|
|
7
|
+
before do
|
|
8
|
+
@s = Reactive::Observable::First.new(target: Reactive::Observable.interval(1000), count: 3)
|
|
9
|
+
@s.subscribe(observer)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
context 'after one interval' do
|
|
13
|
+
advance_by(1001) {
|
|
14
|
+
should send_call.with(0).to(observer, :on_next)
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
context 'after 3 intervals' do
|
|
19
|
+
advance_by(3001, ignore: :on_complete) {
|
|
20
|
+
should send_call.exactly(3).times.with(kind_of(Fixnum)).to(observer, :on_next)
|
|
21
|
+
}
|
|
22
|
+
advance_by(3001, ignore: :on_next) {
|
|
23
|
+
should send_call.with(no_args).to(observer, :on_complete)
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
context 'after 4 intervals' do
|
|
29
|
+
advance_by(4001, ignore: :on_complete) {
|
|
30
|
+
should send_call.exactly(3).times.with(kind_of(Fixnum)).to(observer, :on_next)
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
context do
|
|
37
|
+
it 'take 1 from 1', :focus => true do
|
|
38
|
+
@s = Reactive::Observable::First.new(target: Reactive::Observable.once(1), count: 1 )
|
|
39
|
+
observer[:on_next].should_receive(:call).with(1)
|
|
40
|
+
observer[:on_complete].should_receive(:call).with(no_args())
|
|
41
|
+
@s.subscribe(observer)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Reactive::Observable do
|
|
4
|
+
include Lets
|
|
5
|
+
|
|
6
|
+
context '#interval(400)' do
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
@s = Reactive::Observable.interval(400)
|
|
10
|
+
@s.subscribe(observer)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "does not fire on subscribe" do
|
|
14
|
+
observer[:on_next].should_not_receive(:call)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "does not fire before interval has passed" do
|
|
18
|
+
scheduler.advance_by(200)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "fires once interval was reached" do
|
|
22
|
+
observer[:on_next].should_receive(:call).with(0)
|
|
23
|
+
scheduler.advance_by(400)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "fires for each interval passed" do
|
|
27
|
+
[0,1,2,3].each do |i|
|
|
28
|
+
observer[:on_next].should_receive(:call).times.with(i).ordered
|
|
29
|
+
end
|
|
30
|
+
scheduler.advance_by(1600)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context "when it has been destroyed" do
|
|
34
|
+
#didn't manage to garbage collect without using before block.
|
|
35
|
+
before do
|
|
36
|
+
remove_instance_variable(:@s)
|
|
37
|
+
ObjectSpace.garbage_collect
|
|
38
|
+
end
|
|
39
|
+
it 'does not fire' do
|
|
40
|
+
scheduler.advance_by(400)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
end
|