weak_observable 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.
- data/.gitignore +17 -0
- data/.rspec +5 -0
- data/CHANGELOG +5 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +67 -0
- data/Rakefile +15 -0
- data/lib/weak_observable.rb +89 -0
- data/lib/weak_observable/hub.rb +98 -0
- data/lib/weak_observable/mixin.rb +7 -0
- data/lib/weak_observable/version.rb +3 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/weak_observable/hub_spec.rb +177 -0
- data/spec/weak_observable/mixin_spec.rb +14 -0
- data/spec/weak_observable/weak_observable_spec.rb +110 -0
- data/weak_observable.gemspec +25 -0
- metadata +106 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/CHANGELOG
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Kim Burgestrand
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# WeakObservable
|
2
|
+
|
3
|
+
WeakObservable is very similar to Observable from ruby’ standard library, but
|
4
|
+
with the very important difference in that it allows it’s subscribers to be
|
5
|
+
garbage collected.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
First, install the gems with rubygems.
|
10
|
+
|
11
|
+
```shell
|
12
|
+
gem install weak_observable
|
13
|
+
```
|
14
|
+
|
15
|
+
Now include it onto any object, set up your subscribers and play away!
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
require 'weak_observable'
|
19
|
+
|
20
|
+
class Observer
|
21
|
+
def update(event, *args, &block)
|
22
|
+
puts "[#{event}]: #{args.inspect}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Playlist
|
27
|
+
include WeakObservable::Mixin
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create an observer to wait for notifications.
|
31
|
+
observer = Observer.new
|
32
|
+
|
33
|
+
playlist = Playlist.new
|
34
|
+
playlist.observers << observer
|
35
|
+
playlist.observers.count # => 1
|
36
|
+
|
37
|
+
# Notify all observers. Any arguments and given block goes out to them all.
|
38
|
+
playlist.observers.notify(:ping)
|
39
|
+
# ^ the above ends up calling observer.update(:ping)
|
40
|
+
|
41
|
+
# Unassign the variable, allowing the observer to be garbage collected.
|
42
|
+
observer = nil
|
43
|
+
|
44
|
+
# Some time passes, ruby garbage collection eventually kicks in, and now…
|
45
|
+
playlist.observers.count # => 0
|
46
|
+
playlist.observers.notify(:ping) # nothing happens, we have no observers.
|
47
|
+
```
|
48
|
+
|
49
|
+
## Contributing
|
50
|
+
|
51
|
+
Please fork the repository and clone it. To get started with development you’ll
|
52
|
+
need to install the development dependencies and make sure you can run the
|
53
|
+
tests. You can easily install all development dependencies using bundler.
|
54
|
+
|
55
|
+
```shell
|
56
|
+
$> gem install bundler
|
57
|
+
$> bundle install # => installs development dependencies
|
58
|
+
```
|
59
|
+
|
60
|
+
Once you have the dependencies installed you should be able to run the tests.
|
61
|
+
|
62
|
+
```shell
|
63
|
+
$> rake
|
64
|
+
```
|
65
|
+
|
66
|
+
They should be green. Now go ahead and add your tests, add your changes, and push
|
67
|
+
it back up to GitHub. Once you feel you’re done create a pull request. Thank you!
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
begin
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'yard'
|
8
|
+
require 'yard/rake/yardoc_task'
|
9
|
+
YARD::Rake::YardocTask.new
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'rspec/core/rake_task'
|
13
|
+
RSpec::Core::RakeTask.new
|
14
|
+
|
15
|
+
task :default => :spec
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "ref"
|
2
|
+
require "weak_observable/version"
|
3
|
+
require "weak_observable/mixin"
|
4
|
+
require "weak_observable/hub"
|
5
|
+
|
6
|
+
# WeakObservable is like the Observable from standard library,
|
7
|
+
# with a slightly different API and one important difference:
|
8
|
+
# it does not keep any strong references to it’s observers.
|
9
|
+
#
|
10
|
+
# This is useful in case you want observers, but don’t really
|
11
|
+
# care if they are garbage collected or not. Mostly useful for
|
12
|
+
# asynchronous callbacks in C extensions or FFI bindings.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# observable = WeakObservable.new
|
16
|
+
# observer = Object.new
|
17
|
+
#
|
18
|
+
# observable.add(observer)
|
19
|
+
# observable.notify # notifies observable
|
20
|
+
#
|
21
|
+
# observer = nil
|
22
|
+
# GC.start # this might garbage collect observer
|
23
|
+
# observable.notify # if observer is garbage collected, this does nothing now
|
24
|
+
class WeakObservable
|
25
|
+
def initialize
|
26
|
+
@monitor = Ref::SafeMonitor.new
|
27
|
+
@observers = Ref::WeakValueMap.new
|
28
|
+
@observers.extend(Enumerable)
|
29
|
+
@callbacks = Ref::WeakKeyMap.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# Add an observer.
|
33
|
+
#
|
34
|
+
# @note You cannot add the same observer multiple times. The
|
35
|
+
# most recent call overrides any earlier calls; because
|
36
|
+
# of this you can change the method for an observer by
|
37
|
+
# calling add with a new method.
|
38
|
+
#
|
39
|
+
# @param [#hash] observer
|
40
|
+
# @param [String, Symbol] method
|
41
|
+
# @return observer, as passed in.
|
42
|
+
# @raise ArgumentError if observer does not respodn to method.
|
43
|
+
def add(observer, method = :update)
|
44
|
+
unless observer.respond_to?(method)
|
45
|
+
raise ArgumentError, "#{observer} does not respond to #{method}"
|
46
|
+
end
|
47
|
+
|
48
|
+
synchronize do
|
49
|
+
@callbacks[observer] = method
|
50
|
+
@observers[key(observer)] = observer
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Remove an observer.
|
55
|
+
#
|
56
|
+
# @param [#hash] observer
|
57
|
+
# @return [Object, nil] the observer, or nil if it was not an observer.
|
58
|
+
def delete(observer)
|
59
|
+
synchronize do
|
60
|
+
@callbacks.delete(observer)
|
61
|
+
@observers.delete(key(observer))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Notify all observers, passing along all arguments and given block.
|
66
|
+
#
|
67
|
+
# @return [Array] all return values.
|
68
|
+
def notify(*args, &block)
|
69
|
+
synchronize do
|
70
|
+
@observers.map do |_, object|
|
71
|
+
callback = @callbacks[object]
|
72
|
+
# rare, but could happen if @callbacks is touched by finalizer
|
73
|
+
# before the @observers
|
74
|
+
next if callback.nil?
|
75
|
+
object.send(callback, *args, &block)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def key(observer)
|
83
|
+
observer.hash
|
84
|
+
end
|
85
|
+
|
86
|
+
def synchronize
|
87
|
+
@monitor.synchronize { return yield }
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
class WeakObservable
|
4
|
+
# The Hub is like a hash, mapping a key to an observable.
|
5
|
+
#
|
6
|
+
# Hub holds no strong references. Objects added as observers
|
7
|
+
# for a given key can be garbage collected. When all observers
|
8
|
+
# for a given key has been garbage collected, the observable
|
9
|
+
# for that key is also (eventually) garbage collected.
|
10
|
+
#
|
11
|
+
# This class is useful for having observers for a given key
|
12
|
+
# without necessarily protecting them from being collected by
|
13
|
+
# the garbage collector. Say, if you were mapping a pointer
|
14
|
+
# address to a bunch of ruby objects for FFI callbacks.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# $hub = WeakObservable::Hub.new
|
18
|
+
#
|
19
|
+
# def some_callback(address, some_arg)
|
20
|
+
# $hub[address].notify(some_arg)
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# object = Object.new
|
24
|
+
# $hub[pointer.address] = object
|
25
|
+
# SomeFFIBinding.register_callback(object.pointer, :some_callback)
|
26
|
+
class Hub
|
27
|
+
def initialize
|
28
|
+
@mapping = Ref::WeakValueMap.new
|
29
|
+
@monitor = Ref::SafeMonitor.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# Add an object as an observer for updates related to the given key.
|
33
|
+
#
|
34
|
+
# @param [#hash, #eql?] key
|
35
|
+
# @param object
|
36
|
+
# @param [Symbol, String] method
|
37
|
+
# @return object
|
38
|
+
def add(key, object, method = :update)
|
39
|
+
synchronize do
|
40
|
+
observable = (@mapping[key] ||= WeakObservable.new)
|
41
|
+
backrefs_of(object) << observable
|
42
|
+
observable.add(object, method)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Remove an object as an observer from updates related to the given key.
|
47
|
+
#
|
48
|
+
# @param [#hash, #eql?] key
|
49
|
+
# @param object
|
50
|
+
# @return [Object, nil] object, or nil if it was not an observer for that key
|
51
|
+
def delete(key, object)
|
52
|
+
synchronize do
|
53
|
+
if observable = @mapping[key]
|
54
|
+
backrefs_of(object).delete(observable)
|
55
|
+
observable.delete(object)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Notify all observers listening to events on the given key.
|
61
|
+
# All arguments are passed, as well as the given block.
|
62
|
+
#
|
63
|
+
# @param [#hash, #eql?] key
|
64
|
+
# @param args
|
65
|
+
# @return [Array] all return values
|
66
|
+
def notify(key, *args, &block)
|
67
|
+
synchronize do
|
68
|
+
if observable = @mapping[key]
|
69
|
+
observable.notify(*args, &block)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
protected
|
75
|
+
|
76
|
+
def synchronize
|
77
|
+
@monitor.synchronize { return yield }
|
78
|
+
end
|
79
|
+
|
80
|
+
# The observers retain their observable, but the hub does not.
|
81
|
+
# This way we don’t need to garbage collect our mapping, since
|
82
|
+
# when all observers are gone, the observables for the address
|
83
|
+
# they are registered to is also eligible for garbage collection.
|
84
|
+
def backrefs_of(object)
|
85
|
+
# The object may be attached to several hubs, so make sure
|
86
|
+
# we don’t fuck up by mapping our own object_id first.
|
87
|
+
ivar = '@__observable__hubs__'
|
88
|
+
back_hash = object.instance_variable_get(ivar)
|
89
|
+
back_hash ||= Hash.new
|
90
|
+
object.instance_variable_set(ivar, back_hash)
|
91
|
+
|
92
|
+
# An object may also be attached to several addresses within
|
93
|
+
# this hub. We want to allow this, but we don’t care about
|
94
|
+
# the addresses so we just keep a list of observables.
|
95
|
+
back_hash[object_id] ||= Set.new
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'weak_observable'
|
@@ -0,0 +1,177 @@
|
|
1
|
+
describe WeakObservable::Hub do
|
2
|
+
let(:observable) { WeakObservable::Hub.new }
|
3
|
+
let(:observer) { stub(:update => 1) }
|
4
|
+
let(:key) { 1337 }
|
5
|
+
let(:other_key) { 0xDEADBEEF }
|
6
|
+
|
7
|
+
describe "#add and #notify" do
|
8
|
+
it "adds an observer for given key" do
|
9
|
+
observer.should_receive(:update)
|
10
|
+
|
11
|
+
observable.add(key, observer)
|
12
|
+
observable.notify(key)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "allows us to specify callback method" do
|
16
|
+
observer.should_receive(:trigger)
|
17
|
+
|
18
|
+
observable.add(key, observer, :trigger)
|
19
|
+
observable.notify(key)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "allows the same observer for multiple keys" do
|
23
|
+
observer.should_receive(:update).twice
|
24
|
+
|
25
|
+
observable.add(key, observer)
|
26
|
+
observable.add(other_key, observer)
|
27
|
+
|
28
|
+
observable.notify(key)
|
29
|
+
observable.notify(other_key)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "allows different methods for different keys" do
|
33
|
+
observer.should_receive(:update).once
|
34
|
+
observer.should_receive(:trigger).once
|
35
|
+
|
36
|
+
observable.add(key, observer)
|
37
|
+
observable.add(other_key, observer, :trigger)
|
38
|
+
|
39
|
+
observable.notify(key)
|
40
|
+
observable.notify(other_key)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "does not allow duplicates for the same key" do
|
44
|
+
observer.should_receive(:update).once
|
45
|
+
|
46
|
+
observable.add(key, observer)
|
47
|
+
observable.notify(key)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "raises an error if observer does not respond to method" do
|
51
|
+
expect { observable.add(key, stub) }.to raise_error(ArgumentError)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "#notify" do
|
56
|
+
it "notifies observers only with the same key" do
|
57
|
+
observer.should_not_receive(:update)
|
58
|
+
|
59
|
+
observable.add(key, observer)
|
60
|
+
observable.notify(other_key)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "passes along any args and given block" do
|
64
|
+
observer.should_receive(:update).with(1, 2).and_return do |x, y, &block|
|
65
|
+
block.call(x, y)
|
66
|
+
end
|
67
|
+
|
68
|
+
observable.add(key, observer)
|
69
|
+
observable.notify(key, 1, 2) { |x, y| x + y }.should eq [3]
|
70
|
+
end
|
71
|
+
|
72
|
+
it "returns all return values" do
|
73
|
+
observer1 = stub(:update => 1)
|
74
|
+
observer2 = stub(:update => 2)
|
75
|
+
|
76
|
+
observable.add(key, observer1)
|
77
|
+
observable.add(key, observer2)
|
78
|
+
|
79
|
+
observable.notify(key).should eq [1, 2]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "#delete" do
|
84
|
+
it "deletes the observer with the given key" do
|
85
|
+
observer.should_not_receive(:update)
|
86
|
+
|
87
|
+
observable.add(key, observer)
|
88
|
+
observable.delete(key, observer)
|
89
|
+
observable.notify(key).should eq []
|
90
|
+
end
|
91
|
+
|
92
|
+
it "does not crash trying to remove a non-existing observer" do
|
93
|
+
observable.add(key, observer)
|
94
|
+
expect { observable.delete(key, stub) }.to_not raise_error
|
95
|
+
end
|
96
|
+
|
97
|
+
it "does not crash trying to remove an observer with a non-existing key" do
|
98
|
+
expect { observable.delete(key, observer) }.to_not raise_error
|
99
|
+
end
|
100
|
+
|
101
|
+
it "keeps the observer for other keys" do
|
102
|
+
observer1 = stub(:update => 1)
|
103
|
+
observer2 = stub(:update => 2)
|
104
|
+
|
105
|
+
observer2.should_not_receive(:update)
|
106
|
+
|
107
|
+
observable.add(key, observer1)
|
108
|
+
observable.add(other_key, observer2)
|
109
|
+
observable.delete(other_key, observer2)
|
110
|
+
|
111
|
+
observable.notify(key).should eq [1]
|
112
|
+
observable.notify(other_key).should eq []
|
113
|
+
end
|
114
|
+
|
115
|
+
it "does not remove any other observer for a given key" do
|
116
|
+
observer1 = stub(:update => 1)
|
117
|
+
observer2 = stub(:update => 2)
|
118
|
+
|
119
|
+
observer2.should_not_receive(:update)
|
120
|
+
|
121
|
+
observable.add(key, observer1)
|
122
|
+
observable.add(key, observer2)
|
123
|
+
observable.delete(key, observer2)
|
124
|
+
|
125
|
+
observable.notify(key).should eq [1]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
specify "observers can be garbage collected" do
|
130
|
+
counter = 0
|
131
|
+
finalizer = lambda { |_| counter += 1 }
|
132
|
+
|
133
|
+
threshold = 5
|
134
|
+
stretches = 5
|
135
|
+
|
136
|
+
threshold.times do
|
137
|
+
# stub with return value for method cannot be garbage collected.
|
138
|
+
observer = OpenStruct.new(:update => nil)
|
139
|
+
observable.add(key, observer)
|
140
|
+
ObjectSpace.define_finalizer(observer, finalizer)
|
141
|
+
end
|
142
|
+
|
143
|
+
(threshold * stretches).times do
|
144
|
+
GC.start
|
145
|
+
sleep 0.001
|
146
|
+
end
|
147
|
+
|
148
|
+
# if one is garbage collected, we can expect all to eventually be
|
149
|
+
# as well
|
150
|
+
counter.should be > 1
|
151
|
+
end
|
152
|
+
|
153
|
+
specify "entire keys can be garbage collected" do
|
154
|
+
counter = 0
|
155
|
+
finalizer = lambda { |_| counter += 1 }
|
156
|
+
|
157
|
+
threshold = 5
|
158
|
+
stretches = 5
|
159
|
+
|
160
|
+
threshold.times do |i|
|
161
|
+
# stub with return value for method cannot be garbage collected.
|
162
|
+
observer = OpenStruct.new(:update => nil)
|
163
|
+
observable.add(key + i, observer)
|
164
|
+
ObjectSpace.define_finalizer(observer, finalizer)
|
165
|
+
end
|
166
|
+
|
167
|
+
(threshold * stretches).times do
|
168
|
+
GC.start
|
169
|
+
sleep 0.001
|
170
|
+
end
|
171
|
+
|
172
|
+
# some very intimate introspection here
|
173
|
+
observable.instance_eval do
|
174
|
+
@mapping.to_a.length.should < threshold
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
describe WeakObservable::Mixin do
|
2
|
+
let(:klass) { Class.new { include WeakObservable::Mixin } }
|
3
|
+
let(:observable) { klass.new }
|
4
|
+
|
5
|
+
describe "#observers" do
|
6
|
+
it "is a weak observable" do
|
7
|
+
observable.observers.should be_a WeakObservable
|
8
|
+
end
|
9
|
+
|
10
|
+
it "is memoized" do
|
11
|
+
observable.observers.should eql observable.observers
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
describe WeakObservable do
|
4
|
+
let(:observable) { WeakObservable.new }
|
5
|
+
let(:observer) { stub(:update => nil) }
|
6
|
+
|
7
|
+
it "has a version" do
|
8
|
+
WeakObservable::VERSION.should be_a String
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#add and #notify" do
|
12
|
+
it "adds an observer" do
|
13
|
+
observer.should_receive(:update)
|
14
|
+
|
15
|
+
observable.add(observer)
|
16
|
+
observable.notify
|
17
|
+
end
|
18
|
+
|
19
|
+
it "allows us to specify callback method" do
|
20
|
+
observer.should_receive(:trigger)
|
21
|
+
|
22
|
+
observable.add(observer, :trigger)
|
23
|
+
observable.notify
|
24
|
+
end
|
25
|
+
|
26
|
+
it "does not allow duplicates" do
|
27
|
+
observer.should_receive(:update).once
|
28
|
+
|
29
|
+
observable.add(observer)
|
30
|
+
observable.add(observer)
|
31
|
+
observable.notify
|
32
|
+
end
|
33
|
+
|
34
|
+
it "raises an error if observer does not respond to method" do
|
35
|
+
expect { observable.add(stub) }.to raise_error(ArgumentError)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#notify" do
|
40
|
+
it "passes along any args given block" do
|
41
|
+
observer.should_receive(:update).with(1, 2).and_return do |&block|
|
42
|
+
block.call(2)
|
43
|
+
end
|
44
|
+
|
45
|
+
observable.add(observer)
|
46
|
+
observable.notify(1, 2) { |x| x * 2 }.should eq [4]
|
47
|
+
end
|
48
|
+
|
49
|
+
it "returns all return values" do
|
50
|
+
observer1 = stub(:update => 1)
|
51
|
+
observer2 = stub(:update => 2)
|
52
|
+
|
53
|
+
observable.add(observer1)
|
54
|
+
observable.add(observer2)
|
55
|
+
|
56
|
+
observable.notify.should eq [1, 2]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "#delete" do
|
61
|
+
it "removes the observer" do
|
62
|
+
observer.should_not_receive(:update)
|
63
|
+
|
64
|
+
observable.add(observer)
|
65
|
+
observable.delete(observer)
|
66
|
+
observable.notify.should eq []
|
67
|
+
end
|
68
|
+
|
69
|
+
it "does not remove any other observer" do
|
70
|
+
observer1 = stub(:update => 1)
|
71
|
+
observer2 = stub(:update => 2)
|
72
|
+
|
73
|
+
observer2.should_not_receive(:update)
|
74
|
+
|
75
|
+
observable.add(observer1)
|
76
|
+
observable.add(observer2)
|
77
|
+
observable.delete(observer2)
|
78
|
+
|
79
|
+
observable.notify.should eq [1]
|
80
|
+
end
|
81
|
+
|
82
|
+
it "does not crash removing a non-existing observer" do
|
83
|
+
expect { observable.delete(observer) }.to_not raise_error
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
specify "observers can be garbage collected" do
|
88
|
+
counter = 0
|
89
|
+
finalizer = lambda { |_| counter += 1 }
|
90
|
+
|
91
|
+
threshold = 5
|
92
|
+
stretches = 5
|
93
|
+
|
94
|
+
threshold.times do
|
95
|
+
# stub with return value for method cannot be garbage collected.
|
96
|
+
observer = OpenStruct.new(:update => nil)
|
97
|
+
observable.add(observer)
|
98
|
+
ObjectSpace.define_finalizer(observer, finalizer)
|
99
|
+
end
|
100
|
+
|
101
|
+
(threshold * stretches).times do
|
102
|
+
GC.start
|
103
|
+
sleep 0.001
|
104
|
+
end
|
105
|
+
|
106
|
+
# if one is garbage collected, we can expect all to eventually be
|
107
|
+
# as well
|
108
|
+
counter.should be > 1
|
109
|
+
end
|
110
|
+
end
|
@@ -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 'weak_observable/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "weak_observable"
|
8
|
+
gem.version = WeakObservable::VERSION
|
9
|
+
gem.authors = ["Kim Burgestrand"]
|
10
|
+
gem.email = ["kim@burgestrand.se"]
|
11
|
+
gem.summary = "Like Observer from the standard library, but allows the subscribers to be garbage collected."
|
12
|
+
gem.description = <<-DESC.gsub(/ {2,}/, '')
|
13
|
+
Observable::Weak is very similar to Observable from ruby’ standard library, but
|
14
|
+
with the very important difference in that it allows it’s subscribers to be
|
15
|
+
garbage collected.
|
16
|
+
DESC
|
17
|
+
gem.homepage = "http://github.com/Burgestrand/weak_observable"
|
18
|
+
|
19
|
+
gem.files = `git ls-files`.split($/)
|
20
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
21
|
+
gem.require_paths = ["lib"]
|
22
|
+
|
23
|
+
gem.add_dependency 'ref', '~> 1.0.0'
|
24
|
+
gem.add_development_dependency 'rspec', '~> 2.11.0'
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: weak_observable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kim Burgestrand
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-28 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: ref
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.0.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 2.11.0
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 2.11.0
|
46
|
+
description: ! 'Observable::Weak is very similar to Observable from ruby’ standard
|
47
|
+
library, but
|
48
|
+
|
49
|
+
with the very important difference in that it allows it’s subscribers to be
|
50
|
+
|
51
|
+
garbage collected.
|
52
|
+
|
53
|
+
'
|
54
|
+
email:
|
55
|
+
- kim@burgestrand.se
|
56
|
+
executables: []
|
57
|
+
extensions: []
|
58
|
+
extra_rdoc_files: []
|
59
|
+
files:
|
60
|
+
- .gitignore
|
61
|
+
- .rspec
|
62
|
+
- CHANGELOG
|
63
|
+
- Gemfile
|
64
|
+
- LICENSE.txt
|
65
|
+
- README.md
|
66
|
+
- Rakefile
|
67
|
+
- lib/weak_observable.rb
|
68
|
+
- lib/weak_observable/hub.rb
|
69
|
+
- lib/weak_observable/mixin.rb
|
70
|
+
- lib/weak_observable/version.rb
|
71
|
+
- spec/spec_helper.rb
|
72
|
+
- spec/weak_observable/hub_spec.rb
|
73
|
+
- spec/weak_observable/mixin_spec.rb
|
74
|
+
- spec/weak_observable/weak_observable_spec.rb
|
75
|
+
- weak_observable.gemspec
|
76
|
+
homepage: http://github.com/Burgestrand/weak_observable
|
77
|
+
licenses: []
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 1.8.24
|
97
|
+
signing_key:
|
98
|
+
specification_version: 3
|
99
|
+
summary: Like Observer from the standard library, but allows the subscribers to be
|
100
|
+
garbage collected.
|
101
|
+
test_files:
|
102
|
+
- spec/spec_helper.rb
|
103
|
+
- spec/weak_observable/hub_spec.rb
|
104
|
+
- spec/weak_observable/mixin_spec.rb
|
105
|
+
- spec/weak_observable/weak_observable_spec.rb
|
106
|
+
has_rdoc:
|