rrx 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|